diff --git a/public/js/components/package.info.js b/public/js/components/package.info.js
deleted file mode 100644
index 3935c66b..00000000
--- a/public/js/components/package.info.js
+++ /dev/null
@@ -1,806 +0,0 @@
-// Import Third-party Dependencies
-import prettyBytes from "pretty-bytes";
-import { getFlagsEmojisInlined, getJSON } from "@nodesecure/vis-network";
-import { locationToString } from "@nodesecure/utils";
-
-// Import Internal Dependencies
-import * as utils from "../utils.js";
-import { Bundlephobia } from "./bundlephobia.js";
-import { UnpkgCodeFetcher } from "./unpkgCodeFetcher.js";
-
-const kSocketDevLink = 'https://socket.dev/npm/package/';
-const kSnykAdvisorLink = 'https://snyk.io/advisor/npm-package/';
-const kScorecardVisualizer = (repo) => `https://kooltheba.github.io/openssf-scorecard-api-visualizer/#/projects/github.com/${repo}`;
-
-export class PackageInfo {
- static DOMElementName = "package-info";
- static StopSimulationTimeout = null;
-
- static close() {
- const domElement = document.getElementById(PackageInfo.DOMElementName);
- if (domElement.classList.contains("slide-in")) {
- domElement.setAttribute("class", "slide-out");
- }
- }
-
- /**
- * @param {*} dependencyVersionData
- * @param {*} dependency
- * @param {*} nsn
- */
- constructor(dependencyVersionData, currentNode, dependency, nsn) {
- this.codeCache = new Map();
- this.nsn = nsn;
- this.currentNode = currentNode;
- this.dom = document.getElementById(PackageInfo.DOMElementName);
- this.dom.innerHTML = "";
- this.menus = new Map();
-
- const { name, version } = dependencyVersionData;
- this.dependencyVersion = dependencyVersionData;
- this.dependency = dependency;
-
- const template = document.getElementById("package-info-template");
- /** @type {HTMLTemplateElement} */
- const clone = document.importNode(template.content, true);
-
- this.activeNavigation = clone.querySelector(".package-navigation > div.active");
- this.setupNavigation(clone);
- this.hydrateAndGenerate(clone);
-
- for (const domElement of clone.querySelectorAll(".open-wiki")) {
- domElement.addEventListener("click", () => {
- window.wiki.header.setNewActiveView("warnings");
- window.wiki.open();
- });
- }
-
- this.dom.appendChild(clone);
- this.enableNavigation(window.settings.config.defaultPackageMenu);
- this.open();
-
- // Fetch Github stats
- if (this.links.github.href !== null) {
- this.fetchGithubStats().catch(console.error);
- }
-
- // Fetch bundlephobia size stats
- new Bundlephobia(name, version)
- .fetchDataOnHttpServer()
- .catch(console.error);
- }
-
- get isLocalProject() {
- return this.currentNode === 0 || this.dependencyVersion.flags.includes("isGit");
- }
-
- open() {
- this.dom.setAttribute("class", "slide-in");
- }
-
- async fetchGithubStats() {
- const github = new URL(this.links.github.href);
- const repoName = github.pathname.slice(1, github.pathname.includes(".git") ? -4 : github.pathname.length);
-
- const { stargazers_count, open_issues_count, forks_count } = await fetch(`https://api.github.com/repos/${repoName}`)
- .then((value) => value.json());
-
- document.querySelector(".github-stars").innerHTML = ` ${stargazers_count}`;
- document.querySelector(".github-issues").textContent = open_issues_count;
- document.querySelector(".github-forks").textContent = forks_count;
- }
-
- /**
- * @param {!HTMLTemplateElement} clone
- */
- setupNavigation(clone) {
- for (const div of clone.querySelectorAll(".package-navigation > div")) {
- const dataMenu = div.getAttribute("data-menu");
- this.menus.set(dataMenu, div);
-
- div.addEventListener("click", () => this.enableNavigation(dataMenu));
- }
- }
-
- setupNavigationSignal(navElement, count = 0) {
- if (count === 0) {
- navElement.classList.add("disabled");
- }
- else {
- const counter = navElement.querySelector(".signal");
- counter.style.display = "flex";
- counter.appendChild(document.createTextNode(count));
- }
- }
-
- enableNavigation(name) {
- const div = this.menus.has(name) ? this.menus.get(name) : this.menus.get("info");
-
- const isActive = div.classList.contains("active");
- const isDisabled = div.classList.contains("disabled");
- const dataTitle = div.getAttribute("data-title");
-
- if (isActive || isDisabled) {
- return;
- }
-
- div.classList.add("active");
- this.activeNavigation.classList.remove("active");
-
- const targetPan = document.getElementById(`pan-${name}`);
- const currentPan = document.getElementById(`pan-${this.activeNavigation.getAttribute("data-menu")}`);
- targetPan.classList.remove("hidden");
- currentPan.classList.add("hidden");
- document.querySelector(".container-title").textContent = dataTitle;
-
- this.activeNavigation = div;
- }
-
- get lastUpdateAt() {
- return Intl.DateTimeFormat("en-GB", {
- day: "2-digit", month: "short", year: "numeric", hour: "numeric", minute: "numeric", second: "numeric"
- }).format(new Date(this.dependency.metadata.lastUpdateAt));
- }
-
- get author() {
- const author = this.dependencyVersion.author;
- const flatAuthorFullname = typeof author === "string" ? author : (author?.name ?? "Unknown");
-
- return flatAuthorFullname.length > 26 ? `${flatAuthorFullname.slice(0, 26)}...` : flatAuthorFullname;
- }
-
- /**
- * @param {!HTMLTemplateElement} clone
- */
- hydrateAndGenerate(clone) {
- const { name, version, size, composition, warnings, usedBy, engines, flags } = this.dependencyVersion;
- const { metadata, vulnerabilities } = this.dependency;
-
- const [maintainersDomElement, licensesDomElement, warningsDomElement, scriptsDomElement, vulnDomElement, ossfScorecardDomElement] = [
- clone.querySelector(".package-maintainers"),
- clone.getElementById("pan-licenses"),
- clone.getElementById("pan-warnings"),
- clone.querySelector(".package-scripts"),
- clone.querySelector(".packages-vuln"),
- clone.getElementById("pan-scorecard")
- ];
- const [fieldsDefault, fieldsReleases] = [
- clone.querySelector(".fields"),
- clone.querySelector(".fields.releases")
- ];
-
- this.generateHeader(clone);
- if (flags.includes("hasScript")) {
- this.setupNavigationSignal(clone.getElementById("dependencies-nav-menu"), "!");
- }
- {
- const warningsLength = warnings.filter((warning) => !window.settings.warnings.has(warning.kind)).length;
- this.setupNavigationSignal(clone.getElementById("warnings-nav-menu"), warningsLength);
- }
-
- this.setupNavigationSignal(clone.getElementById("vulnerabilities-nav-menu"), vulnerabilities.length);
-
- {
- const doc = document.createDocumentFragment();
-
- if (this.links.homepage.href !== null) {
- doc.appendChild(utils.createLiField("Homepage", this.links.homepage.href, { isLink: true }));
- }
- doc.appendChild(utils.createLiField("Author", this.author));
- doc.appendChild(utils.createLiField("Size on system", prettyBytes(size)));
- doc.appendChild(utils.createLiField("Number of dependencies", metadata.dependencyCount));
- doc.appendChild(utils.createLiField("Number of files", composition.files.length));
- doc.appendChild(utils.createLiField("README.md", composition.files.some((file) => /README\.md/gi.test(file)) ? "✔️" : "❌"));
- doc.appendChild(utils.createLiField("TS Typings", composition.files.some((file) => /d\.ts/gi.test(file)) ? "✔️" : "❌"));
- if ("node" in engines) {
- doc.appendChild(utils.createLiField("Node.js compatibility", engines.node));
- }
- if ("npm" in engines) {
- doc.appendChild(utils.createLiField("NPM compatibility", engines.npm));
- }
-
- fieldsDefault.appendChild(doc);
- }
- {
- const doc = document.createDocumentFragment();
-
- doc.appendChild(utils.createLiField("Last release version", metadata.lastVersion));
- doc.appendChild(utils.createLiField("Last release date", this.lastUpdateAt));
- doc.appendChild(utils.createLiField("Number of published releases", metadata.publishedCount));
- doc.appendChild(utils.createLiField("Number of publisher(s)", metadata.publishers.length));
-
- fieldsReleases.appendChild(doc);
- }
-
- utils.createItemsList(clone.getElementById("nodedep"), composition.required_nodejs, {
- hideItemsLength: 8,
- onclick: (event, coreLib) => {
- const lib = coreLib.startsWith('node:') ? coreLib.slice(5) : coreLib;
-
- window.open(`https://nodejs.org/dist/latest/docs/api/${lib}.html`, "_blank").focus();
- }
- });
-
- const onclick = (_, fileName) => {
- if (fileName === "../" || fileName === "./") {
- return;
- }
- const cleanedFile = fileName.startsWith("./") ? fileName.slice(2) : fileName;
- window.open(`https://unpkg.com/${name}@${version}/${cleanedFile}`, "_blank").focus();
- };
-
- utils.createItemsList(clone.getElementById("extensions"), composition.extensions);
- utils.createItemsList(clone.getElementById("tarballfiles"), composition.files, {
- onclick, hideItems: true, hideItemsLength: 3
- });
-
- utils.createItemsList(clone.getElementById("minifiedfiles"), composition.minified, {
- onclick, hideItems: true
- });
- utils.createItemsList(clone.getElementById("unuseddep"), composition.unused);
- utils.createItemsList(clone.getElementById("missingdep"), composition.missing);
-
- const onclickFocusNetworkNode = (_, packageName) => this.nsn.focusNodeByName(packageName);
- utils.createItemsList(clone.getElementById("requireddep"), composition.required_thirdparty, {
- onclick: onclickFocusNetworkNode,
- hideItems: true
- });
- utils.createItemsList(clone.getElementById("usedby"), Object.keys(usedBy), { onclick: onclickFocusNetworkNode, hideItems: true });
- utils.createItemsList(clone.getElementById("internaldep"), composition.required_files, {
- onclick,
- hideItems: true,
- hideItemsLength: 3
- });
- licensesDomElement.appendChild(this.generateLicenses());
- maintainersDomElement.appendChild(this.generateMaintainers());
- warningsDomElement.appendChild(this.generateWarnings());
- scriptsDomElement.appendChild(this.generateScripts());
- vulnDomElement.appendChild(this.generateVulnerabilities());
-
- this.generateOssfScorecard(name).then(
- (ossfScorecardElementChildren) => {
- if (ossfScorecardElementChildren) {
- ossfScorecardDomElement.appendChild(ossfScorecardElementChildren);
- document.getElementById('scorecard-menu').style.display = 'flex';
- }
- }
- );
-
- const strategy = window.vulnerabilityStrategy;
- clone.querySelector(".vuln-strategy .name").textContent = strategy;
-
- /** @type {HTMLImageElement} */
- const strategyLogo = clone.querySelector(".vuln-strategy img");
- if (strategy === "none") {
- strategyLogo.style.display = "none";
- }
- else {
- strategyLogo.src = strategy === "npm" ? "npm-icon.svg" : `${strategy}.png`;
- }
-
- const btnShow = clone.getElementById("show-hide-dependency");
- if (this.currentNode === 0) {
- btnShow.classList.add("disabled");
-
- return;
- }
- btnShow.innerHTML = this.dependencyVersion.hidden ? " show" : " hide";
-
- if (this.dependency.metadata.dependencyCount === 0) {
- btnShow.classList.add("disabled");
- }
- else {
- btnShow.addEventListener("click", () => {
- const currBtn = document.getElementById("show-hide-dependency");
- currBtn.classList.toggle("active");
- const hidden = !this.dependencyVersion.hidden;
-
- currBtn.innerHTML = hidden ? " show" : " hide";
-
- this.nsn.highlightNodeNeighbour(this.currentNode, hidden);
- if (PackageInfo.StopSimulationTimeout !== null) {
- clearTimeout(PackageInfo.StopSimulationTimeout);
- }
- PackageInfo.StopSimulationTimeout = setTimeout(() => {
- this.nsn.network.stopSimulation();
- PackageInfo.StopSimulationTimeout = null;
- }, 500);
- this.dependencyVersion.hidden = !this.dependencyVersion.hidden;
- });
- }
- }
-
- /**
- * @param {!HTMLTemplateElement} clone
- */
- generateHeader(clone) {
- const { license } = this.dependencyVersion;
- const [nameDomElement, versionDomElement, descriptionDomElement, linksDomElement, flagsDomElement] = [
- clone.querySelector(".name"),
- clone.querySelector(".version"),
- clone.querySelector(".package-description"),
- clone.querySelector(".package-links"),
- clone.querySelector(".package-flags")
- ]
-
- // Name and Version
- nameDomElement.textContent = this.dependencyVersion.name;
- if (this.dependencyVersion.name.length >= 18) {
- nameDomElement.classList.add("lowsize");
- }
- versionDomElement.textContent = `v${this.dependencyVersion.version}`;
-
- // Description
- const description = this.dependencyVersion.description.trim();
- if (description === "") {
- descriptionDomElement.style.display = "none";
- }
- else {
- descriptionDomElement.textContent = description;
- }
-
- // Links
- const packageHomePage = this.dependency.metadata.homepage || null;
- const packageGithubPage = utils.parseRepositoryUrl(
- this.dependencyVersion.repository,
- packageHomePage !== null && new URL(packageHomePage).hostname === "github.com" ? packageHomePage : null
- );
-
- const hasNoLicense = license === "unkown license";
- this.links = {
- npm: {
- href: `https://www.npmjs.com/package/${this.dependencyVersion.name}/v/${this.dependencyVersion.version}`,
- text: "NPM",
- image: "npm-icon.svg",
- showInHeader: true
- },
- homepage: {
- href: packageHomePage,
- showInHeader: false
- },
- github: {
- href: packageGithubPage,
- text: "GitHub",
- image: "github-mark.png",
- showInHeader: true
- },
- unpkg: {
- href: `https://unpkg.com/${this.dependencyVersion.name}@${this.dependencyVersion.version}/`,
- text: "Unpkg",
- icon: "icon-cubes",
- showInHeader: true
- },
- license: {
- href: hasNoLicense ? "#" : (license.licenses[0]?.spdxLicenseLinks[0] ?? "#"),
- text: hasNoLicense ? "unkown" : license.uniqueLicenseIds.join(", ").toUpperCase(),
- icon: "icon-vcard",
- showInHeader: true
- },
- thirdParty: {
- menu: this.externalToolsMenu(),
- text: 'Tools',
- icon: 'icon-link',
- showInHeader: true
- }
- };
-
- {
- const linksFragment = document.createDocumentFragment();
- for (const [linkName, linkAttributes] of Object.entries(this.links)) {
- if (!linkAttributes.showInHeader || linkAttributes.href === null) {
- continue;
- }
- const linkImageOrIcon = linkAttributes.icon ?
- utils.createDOMElement("i", { classList: [linkAttributes.icon] }) :
- utils.createDOMElement("img", {
- attributes: { src: linkAttributes.image, alt: linkName }
- });
-
- const linksChildren = [
- linkImageOrIcon,
- ];
- if (linkAttributes.menu) {
- linksChildren.push(
- utils.createDOMElement("div", {
- classList: ['package-info-header-menu'],
- childs: linkAttributes.menu
- })
- );
- } else {
- linksChildren.push(
- utils.createDOMElement("a", {
- text: linkAttributes.text,
- attributes: {
- href: linkAttributes.href,
- target: "_blank",
- rel: "noopener noreferrer"
- }
- })
- );
- }
-
- linksFragment.appendChild(utils.createDOMElement("div", {
- className: "link", childs: linksChildren
- }));
- }
-
- linksDomElement.appendChild(linksFragment);
- }
-
- // Flags
- {
- const textContent = getFlagsEmojisInlined(this.dependencyVersion.flags, new Set(window.settings.config.ignore.flags));
-
- if (textContent === "") {
- flagsDomElement.style.display = "none";
- }
- else {
- const flagsMap = new Map(
- Object.entries(this.nsn.secureDataSet.FLAGS).map(([name, row]) => [row.emoji, { ...row, name }])
- );
-
- const flagsFragment = document.createDocumentFragment();
- for (const icon of textContent) {
- if (flagsMap.has(icon)) {
- const tooltipElement = utils.createTooltip(icon, flagsMap.get(icon).tooltipDescription);
- tooltipElement.addEventListener("click", () => {
- const { name } = flagsMap.get(icon);
-
- wiki.header.setNewActiveView("flags");
- wiki.navigation.flags.setNewActiveMenu(name);
- wiki.open();
- });
-
- flagsFragment.appendChild(tooltipElement);
- }
- }
- flagsDomElement.appendChild(flagsFragment);
- }
- }
- }
-
- generateLicenses() {
- const licensesFragment = document.createDocumentFragment();
- if (typeof this.dependencyVersion.license === "string") {
- return licensesFragment;
- }
-
- for (const license of this.dependencyVersion.license.licenses) {
- const [licenseName] = license.uniqueLicenseIds;
- const [licenseLink] = license.spdxLicenseLinks;
-
- const spdx = Object.entries(license.spdx)
- .map(([key, value]) => `${value ? "✔️" : "❌"} ${key}`);
-
- const boxContainer = utils.createDOMElement("div", {
- classList: ["box-container-licenses"],
- childs: spdx.map((text) => utils.createDOMElement("div", { text }))
- });
-
- const box = utils.createFileBox({
- title: licenseName,
- fileName: license.from,
- childs: [boxContainer],
- titleHref: licenseLink,
- fileHref: `${this.links.unpkg.href}${license.from}`
- });
- licensesFragment.appendChild(box);
- }
-
- return licensesFragment;
- }
-
- generateMaintainers() {
- const maintainersFragment = document.createDocumentFragment();
- for (const author of this.dependency.metadata.maintainers) {
- const img = utils.createAvatarImageElement(author.email);
- maintainersFragment.appendChild(utils.createDOMElement("div", { childs: [img] }));
- }
-
- return maintainersFragment;
- }
-
- generateWarnings() {
- const warningsFragment = document.createDocumentFragment();
- const codeFetcher = new UnpkgCodeFetcher(this.links.unpkg.href);
-
- for (const warning of this.dependencyVersion.warnings) {
- if (window.settings.warnings.has(warning.kind)) {
- continue;
- }
- const multipleLocation = warning.kind === "encoded-literal" ?
- warning.location.map((loc) => locationToString(loc)).join(" // ") :
- locationToString(warning.location);
-
- const id = Math.random().toString(36).slice(2);
- const hasNoInspection =
- warning.file.includes(".min") &&
- warning.kind === "short-identifiers" &&
- warning.kind === "obfuscated-code";
-
- const viewMoreElement = utils.createDOMElement("div", {
- className: "view-more",
- childs: [
- utils.createDOMElement("i", { className: "icon-code" })
- ]
- });
-
- if (this.isLocalProject || hasNoInspection) {
- viewMoreElement.style.display = "none";
- }
- else {
- const location = warning.kind === "encoded-literal" ? warning.location[0] : warning.location;
-
- viewMoreElement.addEventListener("click", (event) => {
- codeFetcher.fetchCodeLine(event, { file: warning.file, location, id });
- });
- }
-
- const boxContainer = utils.createDOMElement("div", {
- classList: ["box-container-warning"],
- childs: [
- utils.createDOMElement("div", {
- className: "info",
- childs: [
- utils.createDOMElement("p", {
- className: "title",
- text: "incrimined value"
- }),
- utils.createDOMElement("p", {
- className: "value",
- text: warning.value && warning.value.length > 200 ? `${warning.value.slice(0, 200)}...` : warning.value
- })
- ]
- }),
- viewMoreElement
- ]
- });
- const boxPosition = utils.createDOMElement("div", {
- className: "box-source-code-position",
- childs: [
- utils.createDOMElement("p", { text: multipleLocation })
- ]
- });
-
- const box = utils.createFileBox({
- title: warning.kind,
- fileName: warning.file.length > 20 ? `${warning.file.slice(0, 20)}...` : warning.file,
- childs: [boxContainer, boxPosition],
- titleHref: `https://github.com/NodeSecure/js-x-ray/blob/master/docs/${warning.kind}.md`,
- fileHref: `${this.links.unpkg.href}${warning.file}`,
- severity: warning.severity ?? "Information"
- })
- warningsFragment.appendChild(box);
- }
-
- return warningsFragment;
- }
-
- generateScripts() {
- const fragment = document.createDocumentFragment();
- const createPElement = (className, text) => utils.createDOMElement("p", { className, text });
-
- const scripts = Object.entries(this.dependencyVersion.scripts);
- const hideItemsLength = 4;
- const hideItems = scripts.length > hideItemsLength;
-
- for (let id = 0; id < scripts.length; id++) {
- const [key, value] = scripts[id];
-
- const script = utils.createDOMElement("div", {
- className: "script",
- childs: [
- createPElement("name", key),
- createPElement("value", value)
- ]
- });
- if (hideItems && id >= hideItemsLength) {
- script.classList.add("hidden");
- }
-
- fragment.appendChild(script);
- }
-
- if (hideItems) {
- fragment.appendChild(utils.createExpandableSpan(hideItemsLength));
- }
-
- return fragment;
- }
-
- generateVulnerabilities() {
- const fragment = document.createDocumentFragment();
- const defaultHrefProperties = { target: "_blank", rel: "noopener noreferrer" };
-
- for (const vuln of this.dependency.vulnerabilities) {
- const severity = vuln.severity ?? "info";
- const vulnerableSemver = vuln.vulnerableRanges[0] ?? "N/A";
-
- const header = utils.createDOMElement("div", {
- childs: [
- utils.createDOMElement("div", {
- classList: ["severity", severity],
- text: severity.charAt(0).toUpperCase()
- }),
- utils.createDOMElement("p", { className: "name", text: vuln.package }),
- utils.createDOMElement("span", { text: vulnerableSemver })
- ]
- });
- const description = utils.createDOMElement("div", {
- className: "description",
- childs: [utils.createDOMElement("p", { text: vuln.title })]
- });
- const links = utils.createDOMElement("div", {
- className: "links",
- childs: [
- utils.createDOMElement("i", { className: "icon-link" }),
- utils.createDOMElement("a", {
- text: vuln.url,
- attributes: { href: vuln.url, ...defaultHrefProperties }
- })
- ]
- });
-
- const vulnDomElement = utils.createDOMElement("div", {
- classList: ["vuln", severity],
- childs: [
- header,
- description,
- links
- ]
- });
- fragment.appendChild(vulnDomElement);
- }
-
- return fragment;
- }
-
- async generateOssfScorecard() {
- if (!this.links.github.href) {
- const scorecardMenu = document.getElementById('scorecard-menu');
- if (scorecardMenu) {
- scorecardMenu.style.display = 'none';
- }
- return;
- }
-
- const github = new URL(this.links.github.href);
- const repoName = github.pathname.slice(1, github.pathname.includes(".git") ? -4 : github.pathname.length);
-
- let data;
-
- try {
- data = (await getJSON(`/scorecard/${repoName}`)).data;
- }
- catch (error) {
- console.error(error);
- document.getElementById('scorecard-menu').style.display = 'none';
-
- return null;
- }
-
- if (!data) {
- return;
- }
-
- const { score, checks } = data;
- const checksContainerElement = utils.createDOMElement('div', {
- classList: ['checks'],
- });
-
- function generateCheckElement(check) {
- if (!check.score || check.score < 0) {
- check.score = 0;
- }
-
- const fragment = document.createDocumentFragment();
- fragment.appendChild(
- utils.createDOMElement('div', {
- classList: ['check'],
- childs: [
- utils.createDOMElement('span', {
- classList: ['name'],
- text: check.name,
- }),
- utils.createDOMElement('div', {
- classList: ['score'],
- text: `${check.score}/10`,
- }),
- utils.createDOMElement('div', {
- classList: ['info'],
- childs: [
- utils.createDOMElement('div', {
- classList: ['description'],
- text: check.documentation.short,
- }),
- utils.createDOMElement('div', {
- classList: ['reason'],
- childs: [
- utils.createDOMElement('p', {
- childs: [
- utils.createDOMElement('strong', {
- text: "Reasoning",
- }),
- ],
- }),
- utils.createDOMElement('span', {
- text: check.reason,
- }),
- ],
- }),
- ],
- }),
- ],
- })
- );
-
- for (const detail of check.details ?? []) {
- fragment.querySelector('.info').appendChild(
- utils.createDOMElement('div', {
- classList: ['detail'],
- text: detail,
- }),
- );
- }
-
- return fragment;
- }
-
- for (const check of checks) {
- checksContainerElement.append(generateCheckElement(check));
- }
-
- document.getElementById('ossf-score').innerText = score;
- document.getElementById('head-score').innerText = score;
- document.querySelector(".score-header .visualizer a").setAttribute('href', kScorecardVisualizer(repoName));
-
- const checksNodes = checksContainerElement.childNodes;
- checksNodes.forEach((check, checkKey) => {
- check.addEventListener('click', () => {
- if (check.children[2].classList.contains('visible')) {
- check.children[2].classList.remove('visible');
- check.classList.remove('visible')
-
- return;
- }
-
- check.classList.add('visible');
- check.children[2].classList.add('visible');
-
- checksNodes.forEach((check, key) => {
- if (checkKey !== key) {
- check.classList.remove('visible');
- check.children[2].classList.remove('visible');
- }
- });
- });
- });
-
- return checksContainerElement;
- }
-
- externalToolsMenu() {
- return [
- utils.createDOMElement('span', { text: 'Tools' }),
- utils.createDOMElement('div', {
- classList: ['tools-menu'],
- childs: [
- utils.createDOMElement('a', {
- text: 'Snyk',
- attributes: {
- href: kSnykAdvisorLink + this.dependencyVersion.name,
- target: "_blank",
- }
- }),
- utils.createDOMElement('a', {
- text: 'Socket.dev',
- attributes: {
- href: kSocketDevLink + this.dependencyVersion.name,
- target: "_blank",
- }
- })
- ]
- })
- ]
- }
-}
diff --git a/public/js/components/bundlephobia.js b/public/js/components/package/bundlephobia.js
similarity index 100%
rename from public/js/components/bundlephobia.js
rename to public/js/components/package/bundlephobia.js
diff --git a/public/js/components/package/header.js b/public/js/components/package/header.js
new file mode 100644
index 00000000..01c9de3b
--- /dev/null
+++ b/public/js/components/package/header.js
@@ -0,0 +1,220 @@
+// Import Third-party Dependencies
+import { getFlagsEmojisInlined } from "@nodesecure/vis-network";
+
+// Import Internal Dependencies
+import * as utils from "../../utils.js";
+
+export class PackageHeader {
+ static ExternalLinks = {
+ socket: "https://socket.dev/npm/package/",
+ snykAdvisor: "https://snyk.io/advisor/npm-package/"
+ };
+
+ constructor(pkg) {
+ this.package = pkg;
+ this.nsn = this.package.nsn;
+ }
+
+ /**
+ * @param {!HTMLTemplateElement} clone
+ */
+ generate(clone) {
+ const {
+ name: packageName,
+ version: packageVersion,
+ description: packageDescription,
+ license,
+ repository,
+ flags
+ } = this.package.dependencyVersion;
+
+ const [nameDomElement, versionDomElement, descriptionDomElement, linksDomElement, flagsDomElement] = [
+ clone.querySelector(".name"),
+ clone.querySelector(".version"),
+ clone.querySelector(".package-description"),
+ clone.querySelector(".package-links"),
+ clone.querySelector(".package-flags")
+ ]
+
+ // Name and Version
+ nameDomElement.textContent = packageName;
+ if (packageName.length >= 18) {
+ nameDomElement.classList.add("lowsize");
+ }
+ versionDomElement.textContent = `v${packageVersion}`;
+
+ // Description
+ const description = packageDescription.trim();
+ if (description === "") {
+ descriptionDomElement.style.display = "none";
+ }
+ else {
+ descriptionDomElement.textContent = description;
+ }
+
+ // Links
+ const packageHomePage = this.package.dependency.metadata.homepage || null;
+ const packageGithubPage = utils.parseRepositoryUrl(
+ repository,
+ packageHomePage !== null && new URL(packageHomePage).hostname === "github.com" ? packageHomePage : null
+ );
+
+ const hasNoLicense = license === "unkown license";
+ const links = {
+ npm: {
+ href: `https://www.npmjs.com/package/${packageName}/v/${packageVersion}`,
+ text: "NPM",
+ image: "npm-icon.svg",
+ showInHeader: true
+ },
+ homepage: {
+ href: packageHomePage,
+ showInHeader: false
+ },
+ github: {
+ href: packageGithubPage,
+ text: "GitHub",
+ image: "github-mark.png",
+ showInHeader: true
+ },
+ unpkg: {
+ href: `https://unpkg.com/${packageName}@${packageVersion}/`,
+ text: "Unpkg",
+ icon: "icon-cubes",
+ showInHeader: true
+ },
+ license: {
+ href: hasNoLicense ? "#" : (license.licenses[0]?.spdxLicenseLinks[0] ?? "#"),
+ text: hasNoLicense ? "unkown" : license.uniqueLicenseIds.join(", ").toUpperCase(),
+ icon: "icon-vcard",
+ showInHeader: true
+ },
+ thirdParty: {
+ menu: this.renderToolsMenu(packageName),
+ text: 'Tools',
+ icon: 'icon-link',
+ showInHeader: true
+ }
+ };
+ linksDomElement.appendChild(this.renderLinks(links));
+
+ // Flags
+ const flagFragment = this.renderFlags(flags);
+ if (flagFragment) {
+ flagsDomElement.appendChild(flagFragment);
+ }
+ else {
+ flagsDomElement.style.display = "none";
+ }
+
+ return links;
+ }
+
+ renderLinks(links) {
+ const fragment = document.createDocumentFragment();
+ for (const [linkName, linkAttributes] of Object.entries(links)) {
+ if (!linkAttributes.showInHeader || linkAttributes.href === null) {
+ continue;
+ }
+
+ const linkImageOrIcon = linkAttributes.icon ?
+ utils.createDOMElement("i", { classList: [linkAttributes.icon] }) :
+ utils.createDOMElement("img", {
+ attributes: { src: linkAttributes.image, alt: linkName }
+ });
+
+ const linksChildren = [
+ linkImageOrIcon,
+ ];
+ if (linkAttributes.menu) {
+ linksChildren.push(
+ utils.createDOMElement("div", {
+ classList: ['package-info-header-menu'],
+ childs: linkAttributes.menu
+ })
+ );
+ }
+ else {
+ linksChildren.push(
+ utils.createDOMElement("a", {
+ text: linkAttributes.text,
+ attributes: {
+ href: linkAttributes.href,
+ target: "_blank",
+ rel: "noopener noreferrer"
+ }
+ })
+ );
+ }
+
+ fragment.appendChild(utils.createDOMElement("div", {
+ className: "link", childs: linksChildren
+ }));
+ }
+
+ return fragment;
+ }
+
+ /**
+ * @param {!string} packageName
+ * @returns {HTMLElement[]}
+ */
+ renderToolsMenu(packageName) {
+ const { snykAdvisor, socket } = PackageHeader.ExternalLinks;
+
+ return [
+ utils.createDOMElement('span', { text: 'Tools' }),
+ utils.createDOMElement('div', {
+ classList: ['tools-menu'],
+ childs: [
+ utils.createDOMElement('a', {
+ text: 'Snyk',
+ attributes: {
+ href: snykAdvisor + packageName,
+ target: "_blank",
+ }
+ }),
+ utils.createDOMElement('a', {
+ text: 'Socket.dev',
+ attributes: {
+ href: socket + packageName,
+ target: "_blank",
+ }
+ })
+ ]
+ })
+ ]
+ }
+
+ renderFlags(flags) {
+ const textContent = getFlagsEmojisInlined(flags, new Set(window.settings.config.ignore.flags));
+
+ if (textContent === "") {
+ return null;
+ }
+
+ const flagsMap = new Map(
+ Object
+ .entries(this.package.nsn.secureDataSet.FLAGS)
+ .map(([name, row]) => [row.emoji, { ...row, name }])
+ );
+
+ const fragment = document.createDocumentFragment();
+ for (const icon of textContent) {
+ if (flagsMap.has(icon)) {
+ const tooltipElement = utils.createTooltip(icon, flagsMap.get(icon).tooltipDescription);
+ tooltipElement.addEventListener("click", () => {
+ const { name } = flagsMap.get(icon);
+
+ wiki.header.setNewActiveView("flags");
+ wiki.navigation.flags.setNewActiveMenu(name);
+ wiki.open();
+ });
+
+ fragment.appendChild(tooltipElement);
+ }
+ }
+
+ return fragment;
+ }
+}
diff --git a/public/js/components/package/package.js b/public/js/components/package/package.js
new file mode 100644
index 00000000..0bfd2039
--- /dev/null
+++ b/public/js/components/package/package.js
@@ -0,0 +1,120 @@
+// Import Internal Dependencies
+import { Bundlephobia } from "./bundlephobia.js";
+import { PackageHeader } from "./header.js";
+import * as Pannels from "./pannels/index.js";
+
+export class PackageInfo {
+ static DOMElementName = "package-info";
+
+ static close() {
+ const domElement = document.getElementById(PackageInfo.DOMElementName);
+ if (domElement.classList.contains("slide-in")) {
+ domElement.setAttribute("class", "slide-out");
+ }
+ }
+
+ /**
+ * @param {*} dependencyVersionData
+ * @param {*} dependency
+ * @param {*} nsn
+ */
+ constructor(
+ dependencyVersionData,
+ currentNode,
+ dependency,
+ nsn
+ ) {
+ this.codeCache = new Map();
+ this.menus = new Map();
+ this.nsn = nsn;
+ this.currentNode = currentNode;
+ this.dependencyVersion = dependencyVersionData;
+ this.dependency = dependency;
+
+ this.initialize();
+ }
+
+ initialize() {
+ const packageHTMLElement = document.getElementById(PackageInfo.DOMElementName);
+ packageHTMLElement.innerHTML = "";
+ packageHTMLElement.appendChild(
+ this.render()
+ );
+ this.enableNavigation(
+ window.settings.config.defaultPackageMenu
+ );
+ packageHTMLElement.setAttribute("class", "slide-in");
+
+ new Bundlephobia(this.dependencyVersion.name, this.dependencyVersion.version)
+ .fetchDataOnHttpServer()
+ .catch(console.error);
+ }
+
+ /**
+ * @param {HTMLElement} navElement
+ * @param {number} [count=0]
+ */
+ addNavigationSignal(navElement, count = 0) {
+ if (count === 0) {
+ navElement.classList.add("disabled");
+ }
+ else {
+ const counter = navElement.querySelector(".signal");
+ counter.style.display = "flex";
+ counter.appendChild(document.createTextNode(count));
+ }
+ }
+
+ /**
+ * @param {!string} name
+ * @returns {void}
+ */
+ enableNavigation(name) {
+ const div = this.menus.has(name) ? this.menus.get(name) : this.menus.get("info");
+
+ const isActive = div.classList.contains("active");
+ const isDisabled = div.classList.contains("disabled");
+ const dataTitle = div.getAttribute("data-title");
+
+ if (isActive || isDisabled) {
+ return;
+ }
+
+ div.classList.add("active");
+ this.activeNavigation.classList.remove("active");
+
+ const targetPan = document.getElementById(`pan-${name}`);
+ const currentPan = document.getElementById(`pan-${this.activeNavigation.getAttribute("data-menu")}`);
+ targetPan.classList.remove("hidden");
+ currentPan.classList.add("hidden");
+ document.querySelector(".container-title").textContent = dataTitle;
+
+ this.activeNavigation = div;
+ }
+
+ render() {
+ const template = document.getElementById("package-info-template");
+ /** @type {HTMLTemplateElement} */
+ const clone = document.importNode(template.content, true);
+
+ this.activeNavigation = clone.querySelector(".package-navigation > div.active");
+ for (const div of clone.querySelectorAll(".package-navigation > div")) {
+ const dataMenu = div.getAttribute("data-menu");
+ this.menus.set(dataMenu, div);
+
+ div.addEventListener("click", () => this.enableNavigation(dataMenu));
+ }
+
+ this.links = new PackageHeader(this).generate(clone);
+
+ new Pannels.Overview(this).generate(clone);
+ new Pannels.Licenses(this).generate(clone)
+ new Pannels.Warnings(this).generate(clone)
+ new Pannels.Scripts(this).generate(clone)
+ new Pannels.Vulnerabilities(this).generate(clone)
+ new Pannels.Scorecard(this).generate(clone);
+ new Pannels.Files(this).generate(clone);
+
+ return clone;
+ }
+}
diff --git a/public/js/components/package/pannels/files.js b/public/js/components/package/pannels/files.js
new file mode 100644
index 00000000..5cf7b021
--- /dev/null
+++ b/public/js/components/package/pannels/files.js
@@ -0,0 +1,53 @@
+// Import Internal Dependencies
+import * as utils from "../../../utils.js";
+
+export class Files {
+ constructor(pkg) {
+ this.package = pkg;
+ }
+
+ /**
+ * @param {!HTMLTemplateElement} clone
+ */
+ generate(clone) {
+ const { name, version, composition } = this.package.dependencyVersion;
+
+ const onclick = (_, fileName) => {
+ if (fileName === "../" || fileName === "./") {
+ return;
+ }
+
+ const cleanedFile = fileName.startsWith("./") ? fileName.slice(2) : fileName;
+ window
+ .open(`https://unpkg.com/${name}@${version}/${cleanedFile}`, "_blank")
+ .focus();
+ };
+
+ utils.createItemsList(
+ clone.getElementById("extensions"),
+ composition.extensions
+ );
+
+ utils.createItemsList(
+ clone.getElementById("tarballfiles"),
+ composition.files,
+ { onclick, hideItems: true, hideItemsLength: 3 }
+ );
+
+ utils.createItemsList(
+ clone.getElementById("minifiedfiles"),
+ composition.minified,
+ { onclick, hideItems: true }
+ );
+
+ utils.createItemsList(
+ clone.getElementById("internaldep"),
+ composition.required_files,
+ {
+ onclick,
+ hideItems: true,
+ hideItemsLength: 3
+ }
+ );
+ }
+}
diff --git a/public/js/components/package/pannels/index.js b/public/js/components/package/pannels/index.js
new file mode 100644
index 00000000..dd06bd41
--- /dev/null
+++ b/public/js/components/package/pannels/index.js
@@ -0,0 +1,7 @@
+export * from "./licenses.js";
+export * from "./warnings.js";
+export * from "./vulnerabilities.js";
+export * from "./overview.js";
+export * from "./scripts.js";
+export * from "./scorecard.js";
+export * from "./files.js";
diff --git a/public/js/components/package/pannels/licenses.js b/public/js/components/package/pannels/licenses.js
new file mode 100644
index 00000000..3100cd65
--- /dev/null
+++ b/public/js/components/package/pannels/licenses.js
@@ -0,0 +1,50 @@
+// Import Internal Dependencies
+import * as utils from "../../../utils.js";
+
+export class Licenses {
+ constructor(pkg) {
+ this.package = pkg;
+ }
+
+ /**
+ * @param {!HTMLTemplateElement} clone
+ */
+ generate(clone) {
+ clone.getElementById("pan-licenses")
+ .appendChild(this.renderLicenses());
+ }
+
+ renderLicenses() {
+ const { license: packageLicense } = this.package.dependencyVersion;
+
+ const fragment = document.createDocumentFragment();
+ if (typeof packageLicense === "string") {
+ return fragment;
+ }
+
+ const unpkgRoot = this.package.links.unpkg.href;
+ for (const license of packageLicense.licenses) {
+ const [licenseName] = license.uniqueLicenseIds;
+ const [licenseLink] = license.spdxLicenseLinks;
+
+ const spdx = Object.entries(license.spdx)
+ .map(([key, value]) => `${value ? "✔️" : "❌"} ${key}`);
+
+ const boxContainer = utils.createDOMElement("div", {
+ classList: ["box-container-licenses"],
+ childs: spdx.map((text) => utils.createDOMElement("div", { text }))
+ });
+
+ const box = utils.createFileBox({
+ title: licenseName,
+ fileName: license.from,
+ childs: [boxContainer],
+ titleHref: licenseLink,
+ fileHref: `${unpkgRoot}${license.from}`
+ });
+ fragment.appendChild(box);
+ }
+
+ return fragment;
+ }
+}
diff --git a/public/js/components/package/pannels/overview.js b/public/js/components/package/pannels/overview.js
new file mode 100644
index 00000000..c7209129
--- /dev/null
+++ b/public/js/components/package/pannels/overview.js
@@ -0,0 +1,127 @@
+// Import Third-party Dependencies
+import prettyBytes from "pretty-bytes";
+
+// Import Internal Dependencies
+import * as utils from "../../../utils.js";
+
+// CONSTANTS
+const kEnGBDateFormat = Intl.DateTimeFormat("en-GB", {
+ day: "2-digit", month: "short", year: "numeric", hour: "numeric", minute: "numeric", second: "numeric"
+});
+
+export class Overview {
+ constructor(pkg) {
+ this.package = pkg;
+ }
+
+ get author() {
+ const author = this.package.dependencyVersion.author;
+ const flatAuthorFullname = typeof author === "string" ? author : (author?.name ?? "Unknown");
+
+ return flatAuthorFullname.length > 26 ? `${flatAuthorFullname.slice(0, 26)}...` : flatAuthorFullname;
+ }
+
+ /**
+ * @param {!HTMLTemplateElement} clone
+ */
+ generate(clone) {
+ const { usedBy } = this.package.dependencyVersion;
+
+ clone.querySelector(".fields")
+ .appendChild(this.renderTopFields());
+ clone.querySelector(".fields.releases")
+ .appendChild(this.renderReleases());
+
+ utils.createItemsList(
+ clone.getElementById("usedby"),
+ Object.keys(usedBy),
+ {
+ onclick: (_, packageName) => this.package.nsn.focusNodeByName(packageName),
+ hideItems: true
+ }
+ );
+
+ // Fetch Github stats
+ const githubLink = this.package.links.github;
+ if (githubLink.href !== null) {
+ this.fetchGithubStats(githubLink.href)
+ .catch(console.error);
+ }
+
+ clone.querySelector(".package-maintainers")
+ .appendChild(this.renderMaintainers());
+ }
+
+ async fetchGithubStats(githubLink) {
+ const github = new URL(githubLink);
+ const repoName = github.pathname.slice(
+ 1,
+ github.pathname.includes(".git") ? -4 : github.pathname.length
+ );
+
+ const {
+ stargazers_count,
+ open_issues_count,
+ forks_count
+ } = await fetch(`https://api.github.com/repos/${repoName}`)
+ .then((value) => value.json());
+
+ document.querySelector(".github-stars").innerHTML = ` ${stargazers_count}`;
+ document.querySelector(".github-issues").textContent = open_issues_count;
+ document.querySelector(".github-forks").textContent = forks_count;
+ }
+
+ renderTopFields() {
+ const { size, composition, engines } = this.package.dependencyVersion;
+ const { metadata } = this.package.dependency;
+
+ const fragment = document.createDocumentFragment();
+
+ const { homepage } = this.package.links;
+ if (homepage.href !== null) {
+ fragment.appendChild(utils.createLiField("Homepage", homepage.href, { isLink: true }));
+ }
+ fragment.appendChild(utils.createLiField("Author", this.author));
+ fragment.appendChild(utils.createLiField("Size on system", prettyBytes(size)));
+ fragment.appendChild(utils.createLiField("Number of dependencies", metadata.dependencyCount));
+ fragment.appendChild(utils.createLiField("Number of files", composition.files.length));
+ fragment.appendChild(utils.createLiField("README.md", composition.files.some((file) => /README\.md/gi.test(file)) ? "✔️" : "❌"));
+ fragment.appendChild(utils.createLiField("TS Typings", composition.files.some((file) => /d\.ts/gi.test(file)) ? "✔️" : "❌"));
+ if ("node" in engines) {
+ fragment.appendChild(utils.createLiField("Node.js compatibility", engines.node));
+ }
+ if ("npm" in engines) {
+ fragment.appendChild(utils.createLiField("NPM compatibility", engines.npm));
+ }
+
+ return fragment;
+ }
+
+ renderReleases() {
+ const { metadata } = this.package.dependency;
+ const fragment = document.createDocumentFragment();
+
+ const lastUpdatedAt = kEnGBDateFormat.format(
+ new Date(this.package.dependency.metadata.lastUpdateAt)
+ );
+
+ fragment.appendChild(utils.createLiField("Last release version", metadata.lastVersion));
+ fragment.appendChild(utils.createLiField("Last release date", lastUpdatedAt));
+ fragment.appendChild(utils.createLiField("Number of published releases", metadata.publishedCount));
+ fragment.appendChild(utils.createLiField("Number of publisher(s)", metadata.publishers.length));
+
+ return fragment;
+ }
+
+ renderMaintainers() {
+ const { metadata } = this.package.dependency;
+ const fragment = document.createDocumentFragment();
+
+ for (const author of metadata.maintainers) {
+ const img = utils.createAvatarImageElement(author.email);
+ fragment.appendChild(utils.createDOMElement("div", { childs: [img] }));
+ }
+
+ return fragment;
+ }
+}
diff --git a/public/js/components/package/pannels/scorecard.js b/public/js/components/package/pannels/scorecard.js
new file mode 100644
index 00000000..597a4d81
--- /dev/null
+++ b/public/js/components/package/pannels/scorecard.js
@@ -0,0 +1,163 @@
+// Import Third-party Dependencies
+import { getJSON } from "@nodesecure/vis-network";
+
+// Import Internal Dependencies
+import * as utils from "../../../utils.js";
+
+export class Scorecard {
+ static ExternalLinks = {
+ visualizer: "https://kooltheba.github.io/openssf-scorecard-api-visualizer/#/projects/github.com/"
+ }
+
+ constructor(pkg) {
+ this.package = pkg;
+ }
+
+ async fetchScorecardData(repoName) {
+ try {
+ const { data } = (await getJSON(`/scorecard/${repoName}`));
+ if (!data) {
+ return null;
+ }
+
+ return data;
+ }
+ catch (error) {
+ console.error(error);
+
+ return null;
+ }
+ }
+
+ hide() {
+ const scorecardMenu = document.getElementById('scorecard-menu');
+ if (scorecardMenu) {
+ scorecardMenu.style.display = 'none';
+ }
+ }
+
+ /**
+ * @param {!HTMLTemplateElement} clone
+ */
+ generate(clone) {
+ const githubURL = this.package.links.github;
+ if (!githubURL.href) {
+ return this.hide();
+ }
+
+ const github = new URL(githubURL.href);
+ const repoName = github.pathname.slice(
+ 1,
+ github.pathname.includes(".git") ? -4 : github.pathname.length
+ );
+
+ const pannel = clone.getElementById("pan-scorecard");
+ this.fetchScorecardData(repoName).then((data) => {
+ if (!data) {
+ return this.hide();
+ }
+
+ pannel.appendChild(this.renderScorecard(data, repoName));
+ document.getElementById('scorecard-menu').style.display = 'flex';
+ });
+ }
+
+ renderScorecard(data, repoName) {
+ const { score, checks } = data;
+
+ const container = utils.createDOMElement('div', {
+ classList: ['checks'],
+ });
+
+ for (const check of checks) {
+ container.append(generateCheckElement(check));
+ }
+
+ document.getElementById('ossf-score').innerText = score;
+ document.getElementById('head-score').innerText = score;
+ document
+ .querySelector(".score-header .visualizer a")
+ .setAttribute('href', Scorecard.ExternalLinks.visualizer + repoName);
+
+ container.childNodes.forEach((check, checkKey) => {
+ check.addEventListener('click', () => {
+ if (check.children[2].classList.contains('visible')) {
+ check.children[2].classList.remove('visible');
+ check.classList.remove('visible')
+
+ return;
+ }
+
+ check.classList.add('visible');
+ check.children[2].classList.add('visible');
+
+ container.childNodes.forEach((check, key) => {
+ if (checkKey !== key) {
+ check.classList.remove('visible');
+ check.children[2].classList.remove('visible');
+ }
+ });
+ });
+ });
+
+ return container;
+ }
+}
+
+function generateCheckElement(check) {
+ if (!check.score || check.score < 0) {
+ check.score = 0;
+ }
+
+ const fragment = document.createDocumentFragment();
+ fragment.appendChild(
+ utils.createDOMElement('div', {
+ classList: ['check'],
+ childs: [
+ utils.createDOMElement('span', {
+ classList: ['name'],
+ text: check.name,
+ }),
+ utils.createDOMElement('div', {
+ classList: ['score'],
+ text: `${check.score}/10`,
+ }),
+ utils.createDOMElement('div', {
+ classList: ['info'],
+ childs: [
+ utils.createDOMElement('div', {
+ classList: ['description'],
+ text: check.documentation.short,
+ }),
+ utils.createDOMElement('div', {
+ classList: ['reason'],
+ childs: [
+ utils.createDOMElement('p', {
+ childs: [
+ utils.createDOMElement('strong', {
+ text: "Reasoning",
+ }),
+ ],
+ }),
+ utils.createDOMElement('span', {
+ text: check.reason,
+ }),
+ ],
+ }),
+ ],
+ }),
+ ],
+ })
+ );
+
+ for (const detail of check.details ?? []) {
+ fragment.querySelector('.info').appendChild(
+ utils.createDOMElement('div', {
+ classList: ['detail'],
+ text: detail,
+ }),
+ );
+ }
+
+ return fragment;
+}
diff --git a/public/js/components/package/pannels/scripts.js b/public/js/components/package/pannels/scripts.js
new file mode 100644
index 00000000..9d42f02f
--- /dev/null
+++ b/public/js/components/package/pannels/scripts.js
@@ -0,0 +1,145 @@
+// Import Internal Dependencies
+import * as utils from "../../../utils.js";
+
+export class Scripts {
+ static SimulationTimeout = null;
+
+ constructor(pkg) {
+ this.package = pkg;
+ }
+
+ /**
+ * @param {!HTMLTemplateElement} clone
+ */
+ generate(clone) {
+ this.setupSignal(clone);
+
+ clone.querySelector(".package-scripts")
+ .appendChild(this.renderScripts());
+ this.renderDependencies(clone);
+ this.showHideDependenciesInTree(clone);
+ }
+
+ /**
+ * @param {!HTMLTemplateElement} clone
+ */
+ setupSignal(clone) {
+ const { flags } = this.package.dependencyVersion;
+
+ if (flags.includes("hasScript")) {
+ this.package.addNavigationSignal(
+ clone.getElementById("dependencies-nav-menu"),
+ "!"
+ );
+ }
+ }
+
+ renderScripts() {
+ const fragment = document.createDocumentFragment();
+ const createPElement = (className, text) => utils.createDOMElement("p", { className, text });
+
+ const scripts = Object.entries(this.package.dependencyVersion.scripts);
+ const hideItemsLength = 4;
+ const hideItems = scripts.length > hideItemsLength;
+
+ for (let id = 0; id < scripts.length; id++) {
+ const [key, value] = scripts[id];
+
+ const script = utils.createDOMElement("div", {
+ className: "script",
+ childs: [
+ createPElement("name", key),
+ createPElement("value", value)
+ ]
+ });
+ if (hideItems && id >= hideItemsLength) {
+ script.classList.add("hidden");
+ }
+
+ fragment.appendChild(script);
+ }
+
+ if (hideItems) {
+ fragment.appendChild(utils.createExpandableSpan(hideItemsLength));
+ }
+
+ return fragment;
+ }
+
+ /**
+ * @param {!HTMLTemplateElement} clone
+ */
+ renderDependencies(clone) {
+ const { composition } = this.package.dependencyVersion;
+
+ utils.createItemsList(
+ clone.getElementById("nodedep"),
+ composition.required_nodejs,
+ {
+ hideItemsLength: 8,
+ onclick: (_, coreModuleName) => this.openNodeDocumentation(coreModuleName)
+ }
+ );
+
+ utils.createItemsList(
+ clone.getElementById("unuseddep"),
+ composition.unused
+ );
+ utils.createItemsList(
+ clone.getElementById("missingdep"),
+ composition.missing
+ );
+
+ utils.createItemsList(
+ clone.getElementById("requireddep"),
+ composition.required_thirdparty,
+ {
+ onclick: (_, packageName) => this.package.nsn.focusNodeByName(packageName),
+ hideItems: true
+ }
+ );
+ }
+
+ openNodeDocumentation(coreModuleName) {
+ const name = coreModuleName.startsWith('node:') ?
+ coreModuleName.slice(5) : coreModuleName;
+
+ window
+ .open(`https://nodejs.org/dist/latest/docs/api/${name}.html`, "_blank")
+ .focus();
+ }
+
+ showHideDependenciesInTree(clone) {
+ const btnShow = clone.getElementById("show-hide-dependency");
+ if (this.package.currentNode === 0) {
+ btnShow.classList.add("disabled");
+
+ return;
+ }
+ btnShow.innerHTML = this.package.dependencyVersion.hidden ?
+ " show" : " hide";
+
+ if (this.package.dependency.metadata.dependencyCount === 0) {
+ btnShow.classList.add("disabled");
+ }
+ else {
+ btnShow.addEventListener("click", () => {
+ const currBtn = document.getElementById("show-hide-dependency");
+ currBtn.classList.toggle("active");
+ const hidden = !this.package.dependencyVersion.hidden;
+
+ currBtn.innerHTML = hidden ? " show" : " hide";
+
+ this.package.nsn.highlightNodeNeighbour(this.package.currentNode, hidden);
+ if (Scripts.SimulationTimeout !== null) {
+ clearTimeout(Scripts.SimulationTimeout);
+ }
+ Scripts.SimulationTimeout = setTimeout(() => {
+ this.package.nsn.network.stopSimulation();
+ Scripts.SimulationTimeout = null;
+ }, 500);
+ this.package.dependencyVersion.hidden = !this.package.dependencyVersion.hidden;
+ });
+ }
+ }
+}
diff --git a/public/js/components/package/pannels/vulnerabilities.js b/public/js/components/package/pannels/vulnerabilities.js
new file mode 100644
index 00000000..dc776295
--- /dev/null
+++ b/public/js/components/package/pannels/vulnerabilities.js
@@ -0,0 +1,96 @@
+// Import Internal Dependencies
+import * as utils from "../../../utils.js";
+
+export class Vulnerabilities {
+ static href = { target: "_blank", rel: "noopener noreferrer" };
+
+ constructor(pkg) {
+ this.package = pkg;
+ }
+
+ /**
+ * @param {!HTMLTemplateElement} clone
+ */
+ setStrategy(clone) {
+ const strategy = window.vulnerabilityStrategy;
+ clone.querySelector(".vuln-strategy .name").textContent = strategy;
+
+ /** @type {HTMLImageElement} */
+ const strategyLogo = clone.querySelector(".vuln-strategy img");
+ if (strategy === "none") {
+ strategyLogo.style.display = "none";
+ }
+ else {
+ strategyLogo.src = strategy === "npm" ? "npm-icon.svg" : `${strategy}.png`;
+ }
+ }
+
+ /**
+ * @param {!HTMLTemplateElement} clone
+ */
+ generate(clone) {
+ this.setupSignal(clone);
+ this.setStrategy(clone);
+
+ clone.querySelector(".packages-vuln")
+ .appendChild(this.renderVulnerabilies());
+ }
+
+ /**
+ * @param {!HTMLTemplateElement} clone
+ */
+ setupSignal(clone) {
+ const { vulnerabilities } = this.package.dependency;
+ this.package.addNavigationSignal(
+ clone.getElementById("vulnerabilities-nav-menu"),
+ vulnerabilities.length
+ );
+ }
+
+ renderVulnerabilies() {
+ const { vulnerabilities } = this.package.dependency;
+
+ const fragment = document.createDocumentFragment();
+ for (const vuln of vulnerabilities) {
+ const severity = vuln.severity ?? "info";
+ const vulnerableSemver = vuln.vulnerableRanges[0] ?? "N/A";
+
+ const header = utils.createDOMElement("div", {
+ childs: [
+ utils.createDOMElement("div", {
+ classList: ["severity", severity],
+ text: severity.charAt(0).toUpperCase()
+ }),
+ utils.createDOMElement("p", { className: "name", text: vuln.package }),
+ utils.createDOMElement("span", { text: vulnerableSemver })
+ ]
+ });
+ const description = utils.createDOMElement("div", {
+ className: "description",
+ childs: [utils.createDOMElement("p", { text: vuln.title })]
+ });
+ const links = utils.createDOMElement("div", {
+ className: "links",
+ childs: [
+ utils.createDOMElement("i", { className: "icon-link" }),
+ utils.createDOMElement("a", {
+ text: vuln.url,
+ attributes: { href: vuln.url, ...Vulnerabilities.href }
+ })
+ ]
+ });
+
+ const vulnDomElement = utils.createDOMElement("div", {
+ classList: ["vuln", severity],
+ childs: [
+ header,
+ description,
+ links
+ ]
+ });
+ fragment.appendChild(vulnDomElement);
+ }
+
+ return fragment;
+ }
+}
diff --git a/public/js/components/package/pannels/warnings.js b/public/js/components/package/pannels/warnings.js
new file mode 100644
index 00000000..5e179c67
--- /dev/null
+++ b/public/js/components/package/pannels/warnings.js
@@ -0,0 +1,125 @@
+// Import Third-party Dependencies
+import { locationToString } from "@nodesecure/utils";
+
+// Import Internal Dependencies
+import { UnpkgCodeFetcher } from "../unpkgCodeFetcher.js";
+import * as utils from "../../../utils.js";
+
+export class Warnings {
+ constructor(pkg) {
+ this.package = pkg;
+ }
+
+ get isLocalProject() {
+ return this.package.currentNode === 0 ||
+ this.package.dependencyVersion.flags.includes("isGit");
+ }
+
+ /**
+ * @param {!HTMLTemplateElement} clone
+ */
+ generate(clone) {
+ this.setupSignal(clone);
+ clone.getElementById("pan-warnings")
+ .appendChild(this.renderWarnings());
+
+ clone.querySelectorAll(".open-wiki")
+ .forEach((element) => element.addEventListener("click", () => this.openWiki()));
+ }
+
+ openWiki() {
+ window.wiki.header.setNewActiveView("warnings");
+ window.wiki.open();
+ }
+
+ /**
+ * @param {!HTMLTemplateElement} clone
+ */
+ setupSignal(clone) {
+ const { warnings } = this.package.dependencyVersion;
+ this.package.addNavigationSignal(
+ clone.getElementById("warnings-nav-menu"),
+ warnings.filter((warning) => !window.settings.warnings.has(warning.kind)).length
+ );
+ }
+
+ renderWarnings() {
+ const { warnings } = this.package.dependencyVersion;
+
+ const fragment = document.createDocumentFragment();
+ const unpkgRoot = this.package.links.unpkg.href;
+
+ const codeFetcher = new UnpkgCodeFetcher(unpkgRoot);
+
+ for (const warning of warnings) {
+ if (window.settings.warnings.has(warning.kind)) {
+ continue;
+ }
+ const multipleLocation = warning.kind === "encoded-literal" ?
+ warning.location.map((loc) => locationToString(loc)).join(" // ") :
+ locationToString(warning.location);
+
+ const id = Math.random().toString(36).slice(2);
+ const hasNoInspection =
+ warning.file.includes(".min") &&
+ warning.kind === "short-identifiers" &&
+ warning.kind === "obfuscated-code";
+
+ const viewMoreElement = utils.createDOMElement("div", {
+ className: "view-more",
+ childs: [
+ utils.createDOMElement("i", { className: "icon-code" })
+ ]
+ });
+
+ if (this.isLocalProject || hasNoInspection) {
+ viewMoreElement.style.display = "none";
+ }
+ else {
+ const location = warning.kind === "encoded-literal" ? warning.location[0] : warning.location;
+
+ viewMoreElement.addEventListener("click", (event) => {
+ codeFetcher.fetchCodeLine(event, { file: warning.file, location, id });
+ });
+ }
+
+ const boxContainer = utils.createDOMElement("div", {
+ classList: ["box-container-warning"],
+ childs: [
+ utils.createDOMElement("div", {
+ className: "info",
+ childs: [
+ utils.createDOMElement("p", {
+ className: "title",
+ text: "incrimined value"
+ }),
+ utils.createDOMElement("p", {
+ className: "value",
+ text: warning.value && warning.value.length > 200 ? `${warning.value.slice(0, 200)}...` : warning.value
+ })
+ ]
+ }),
+ viewMoreElement
+ ]
+ });
+ const boxPosition = utils.createDOMElement("div", {
+ className: "box-source-code-position",
+ childs: [
+ utils.createDOMElement("p", { text: multipleLocation })
+ ]
+ });
+
+ const box = utils.createFileBox({
+ title: warning.kind,
+ fileName: warning.file.length > 20 ? `${warning.file.slice(0, 20)}...` : warning.file,
+ childs: [boxContainer, boxPosition],
+ titleHref: `https://github.com/NodeSecure/js-x-ray/blob/master/docs/${warning.kind}.md`,
+ fileHref: `${unpkgRoot}${warning.file}`,
+ severity: warning.severity ?? "Information"
+ })
+ fragment.appendChild(box);
+ }
+
+ return fragment;
+ }
+}
diff --git a/public/js/components/unpkgCodeFetcher.js b/public/js/components/package/unpkgCodeFetcher.js
similarity index 100%
rename from public/js/components/unpkgCodeFetcher.js
rename to public/js/components/package/unpkgCodeFetcher.js
diff --git a/public/js/master.js b/public/js/master.js
index d93d7930..d0cdfbf7 100644
--- a/public/js/master.js
+++ b/public/js/master.js
@@ -5,7 +5,7 @@ import { NodeSecureDataSet, NodeSecureNetwork } from "@nodesecure/vis-network";
// Import UI Components
import { ViewNavigation } from "./components/navigation.js";
-import { PackageInfo } from "./components/package.info.js";
+import { PackageInfo } from "./components/package/package.js";
import { Wiki } from "./components/wiki.js";
import { SearchBar } from "./components/searchbar.js";
import { Settings } from "./components/settings.js";