@nodesecure/tree-walker
Version:
NodeSecure tree walker
217 lines • 9.91 kB
JavaScript
// 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