UNPKG

@nodesecure/tree-walker

Version:
217 lines 9.91 kB
// Import Node.js Dependencies import os from "node:os"; // Import Third-party Dependencies import * as iter from "itertools"; import combineAsyncIterators from "combine-async-iterators"; import pacote from "pacote"; import Arborist from "@npmcli/arborist"; // @ts-ignore import pickManifest from "npm-pick-manifest"; import { getNpmRegistryURL } from "@nodesecure/npm-registry-sdk"; // Import Internal Dependencies import * as utils from "../utils/index.js"; import { LocalDependencyTreeLoader } from "./LocalDependencyTreeLoader.js"; import { Dependency } from "../Dependency.class.js"; export class TreeWalker { /** * A Map of Children Spec -> Parent Spec (as a unique list) */ relationsMap = new Map(); registry; providers = Object.seal({ pacote, localTreeLoader: new LocalDependencyTreeLoader() }); constructor(options = {}) { const { providers = {} } = options; this.registry = options?.registry ?? getNpmRegistryURL(); Object.assign(this.providers, providers); } get registryOptions() { return { ...utils.NPM_TOKEN, registry: this.registry, cache: `${os.homedir()}/.npm` }; } addTreeRelation(key, value) { if (!this.relationsMap.has(key)) { return; } this.relationsMap .get(key) .add(value); } async resolveDependencyVersion(dependency) { const [dependencyName, range] = dependency; try { const packument = await this.providers.pacote.packument(dependencyName, this.registryOptions); const manifest = pickManifest(packument, range); return { rangedSpec: `${dependencyName}@${range}`, spec: `${dependencyName}@${manifest.version}`, isLatest: manifest.version === packument["dist-tags"].latest }; } catch { return { rangedSpec: `${dependencyName}@${range}`, spec: `${dependencyName}@${utils.cleanRange(range)}`, isLatest: true }; } } async *walkRemoteDependency(spec, options) { const { currDepth = 1, parent, maxDepth, gitURL } = options; const { name, version, deprecated, ...pkg } = await this.providers.pacote.manifest(gitURL ?? spec, this.registryOptions); const { dependencies, customResolvers, alias } = utils.mergeDependencies(pkg); const current = new Dependency(name, version, { parent, alias: Object.fromEntries(alias) }); if (gitURL !== null && gitURL !== undefined) { current.isGit(gitURL); try { await this.providers.pacote.manifest(`${name}@${version}`, this.registryOptions); } catch { current.existOnRemoteRegistry = false; } } current.addFlag("isDeprecated", deprecated === true); current.addFlag("hasCustomResolver", customResolvers.size > 0); current.addFlag("hasDependencies", dependencies.size > 0); if (currDepth < maxDepth) { const config = { currDepth: currDepth + 1, parent: current, maxDepth }; const gitDependencies = iter.filter(customResolvers.entries(), ([, valueStr]) => utils.isGitDependency(valueStr)); for (const [depName, gitURL] of gitDependencies) { yield* this.walkRemoteDependency(depName, { ...config, gitURL }); } const resolvedDependencies = await Promise.all(iter.map(dependencies.entries(), this.resolveDependencyVersion.bind(this))); for (const { rangedSpec, spec, isLatest } of resolvedDependencies) { if (!isLatest) { current.addFlag("hasOutdatedDependency"); } if (this.relationsMap.has(spec)) { current.addChildren(); this.relationsMap.get(spec).add(current.spec); } else { this.relationsMap.set(spec, new Set([current.spec])); yield* this.walkRemoteDependency(rangedSpec, config); } } } yield current; } async *walkLocalDependency(packageName, node, options) { const { currDepth = 1, maxDepth, parent, fetchManifest = false, includeDevDeps = false } = options; const { version, integrity = node.integrity } = node.package; const updatedVersion = version === "*" || typeof version === "undefined" ? "latest" : version; const current = new Dependency(packageName, updatedVersion, { parent, isDevDependency: node.dev }); if (fetchManifest && !includeDevDeps) { const { _integrity, ...pkg } = await this.providers.pacote.manifest(`${packageName}@${updatedVersion}`, this.registryOptions); const { customResolvers, alias } = utils.mergeDependencies(pkg); current.alias = Object.fromEntries(alias); current.addFlag("hasValidIntegrity", _integrity === integrity); current.addFlag("isDeprecated"); current.addFlag("hasCustomResolver", customResolvers.size > 0); if (node.resolved && utils.isGitDependency(node.resolved)) { current.isGit(node.resolved); } } current.addFlag("hasDependencies", node.edgesOut.size > 0); if (currDepth < maxDepth) { const config = { currDepth: currDepth + 1, parent: current, maxDepth, fetchManifest, includeDevDeps }; for (const [packageName, { to: toNode }] of node.edgesOut) { if (toNode === null || (!includeDevDeps && toNode.dev)) { continue; } const spec = `${packageName}@${toNode.package.version}`; if (this.relationsMap.has(spec)) { current.addChildren(); this.relationsMap.get(spec).add(current.spec); } else { this.relationsMap.set(spec, new Set([current.spec])); yield* this.walkLocalDependency(packageName, toNode, config); } } } yield current; } async *walk(manifest, options = {}) { this.relationsMap.clear(); const { maxDepth = Infinity, packageLock = null, includeDevDeps = false } = options; const { dependencies, customResolvers, alias } = utils.mergeDependencies(manifest); const rootDependency = new Dependency(manifest?.name ?? "workspace", manifest?.version ?? "1.0.0", { alias: Object.fromEntries(alias) }); try { await this.providers.pacote.manifest(`${manifest.name}@${manifest.version}`, this.registryOptions); } catch { rootDependency.existOnRemoteRegistry = false; } rootDependency.addFlag("hasCustomResolver", customResolvers.size > 0); rootDependency.addFlag("hasDependencies", dependencies.size > 0); if (packageLock === null) { const walkRemoteOptions = { maxDepth, parent: rootDependency }; const iterators = [ ...iter .filter(customResolvers.entries(), ([, version]) => utils.isGitDependency(version)) .map(([name, gitURL]) => this.walkRemoteDependency(name, { ...walkRemoteOptions, gitURL })), ...iter .map(dependencies.entries(), ([name, ver]) => this.walkRemoteDependency(`${name}@${ver}`, walkRemoteOptions)) ]; for await (const dep of combineAsyncIterators({}, ...iterators)) { yield dep.exportAsPlainObject(); } /** * Add root dependencies to the relations Map because the parent is not handled by walkRemoteDependency. * If we skip this step, the code will fail to properly re-link dependencies in the subsequent steps. */ const resolvedDependencies = await Promise.all(iter.map(dependencies.entries(), this.resolveDependencyVersion.bind(this))); for (const { spec, isLatest } of resolvedDependencies) { if (!isLatest) { rootDependency.addFlag("hasOutdatedDependency"); } this.addTreeRelation(spec, rootDependency.spec); } } else { const { location, ...packageLockOptions } = packageLock; const { edgesOut } = await this.providers.localTreeLoader.load(location, this.registry); const iterators = [ ...iter .filter(edgesOut.entries(), ([, { to }]) => to !== null && (includeDevDeps ? true : (!to.dev || to.isWorkspace))) .map(([packageName, { to }]) => [packageName, to.isWorkspace ? to.target : to]) .map(([packageName, to]) => this.walkLocalDependency(packageName, to, { maxDepth, parent: rootDependency, includeDevDeps, ...packageLockOptions })) ]; for await (const dep of combineAsyncIterators({}, ...iterators)) { yield dep.exportAsPlainObject(); } for (const [packageName, { to: toNode }] of edgesOut) { if (toNode === null || (!includeDevDeps && toNode.dev)) { continue; } this.addTreeRelation(`${packageName}@${toNode.package.version}`, rootDependency.spec); } } yield rootDependency.exportAsPlainObject(0); } } //# sourceMappingURL=walker.js.map