snyk-nodejs-lockfile-parser
Version:
Generate a dep tree given a lockfile
221 lines • 8.85 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getChildNode = exports.getGraphDependencies = exports.getTopLevelDeps = exports.addPkgNodeToGraph = exports.createNodeInfo = void 0;
exports.parsePkgJson = parsePkgJson;
const errors_1 = require("../errors");
const utils_1 = require("../utils");
const parsers_1 = require("../parsers");
const pkgJson_1 = require("../aliasesPreprocessors/pkgJson");
const createNodeInfo = (options, scope = 'unknown') => {
if (options.showNpmScope) {
return {
labels: {
'npm:scope': scope,
},
};
}
return;
};
exports.createNodeInfo = createNodeInfo;
const addPkgNodeToGraph = (depGraphBuilder, node, options) => {
return depGraphBuilder.addPkgNode({ name: node.name, version: node.version }, node.id, {
labels: {
scope: node.isDev ? 'dev' : 'prod',
...(options.showNpmScope && {
'npm:scope': node.isDev ? 'dev' : 'prod',
}),
...(options.isCyclic && { pruned: 'cyclic' }),
...(options.isWorkspacePkg && { pruned: 'true' }),
...(node.missingLockFileEntry && { missingLockFileEntry: 'true' }),
...(node.alias && {
alias: `${node.alias.aliasName}=>${node.alias.aliasTargetDepName}@${node.version}`,
}),
},
});
};
exports.addPkgNodeToGraph = addPkgNodeToGraph;
/**
* Get top level dependencies from the given package json object which is parsed from a package.json file.
* This includes both prod dependencies and dev dependencies supposing includeDevDeps is supported.
*/
const getTopLevelDeps = (pkgJson, options) => {
const prodDeps = (0, exports.getGraphDependencies)(pkgJson.dependencies || {}, {
isDev: false,
});
const devDeps = (0, exports.getGraphDependencies)(pkgJson.devDependencies || {}, {
isDev: true,
});
const optionalDeps = options.includeOptionalDeps
? (0, exports.getGraphDependencies)(pkgJson.optionalDependencies || {}, {
isDev: false,
isOptional: true,
})
: {};
const peerDeps = options.includePeerDeps
? (0, exports.getGraphDependencies)(pkgJson.peerDependencies || {}, { isDev: false })
: {};
const deps = { ...prodDeps, ...optionalDeps, ...peerDeps };
if (pkgJson.aliases) {
for (const alias of Object.keys(pkgJson.aliases)) {
// Only add alias metadata to dependencies that are actually in deps
if (deps[alias]) {
deps[alias] = {
...deps[alias],
...{ alias: { ...pkgJson.aliases[alias] } },
};
}
}
}
if (options.includeDevDeps) {
// Ensure dev dependency 'isDev' flags are correctly set.
// Dev dependencies are applied last to override shared keys with regular dependencies.
return { ...deps, ...devDeps };
}
// For includeDevDeps option set to false, simulate pnpm install --prod
// by excluding all devDependencies,
// ignoring potential duplicates in other dependency lists.
// https://pnpm.io/cli/install#--prod--p
return Object.keys(deps)
.filter((packageName) => !devDeps.hasOwnProperty(packageName))
.reduce((result, packageName) => {
result[packageName] = deps[packageName];
return result;
}, {});
};
exports.getTopLevelDeps = getTopLevelDeps;
/**
* Validates if a package name is valid for inclusion in dependency graph.
* Invalid cases include:
* - Empty names
* - Scoped packages with empty names after the scope (e.g., "@types/")
*/
function isValidPackageName(name) {
if (!name || name.trim() === '') {
return false;
}
// Check for scoped packages with empty names after the scope
// e.g., "@types/" is invalid (ends with slash after scope)
if (name.startsWith('@')) {
const slashIndex = name.indexOf('/');
if (slashIndex !== -1) {
// Get the part after the scope
const afterScope = name.substring(slashIndex + 1);
// Invalid if nothing after the slash or only whitespace
if (!afterScope || afterScope.trim() === '') {
return false;
}
}
}
return true;
}
/**
* Converts dependencies parsed from the a lock file to a dependencies object required by the graph.
* For example, { 'mime-db': '~1.12.0' } will be converted to { 'mime-db': { version: '~1.12.0', isDev: true/false } }.
*/
const getGraphDependencies = (dependencies, options) => {
return Object.entries(dependencies).reduce((pnpmDeps, [name, semver]) => {
// Skip invalid package names to prevent downstream errors
if (!isValidPackageName(name)) {
return pnpmDeps;
}
pnpmDeps[name] = {
version: semver,
isDev: options.isDev,
isOptional: options.isOptional || false,
};
return pnpmDeps;
}, {});
};
exports.getGraphDependencies = getGraphDependencies;
function parsePkgJson(pkgJsonContent) {
const parsedPkgJson = (0, utils_1.parseJsonFile)(pkgJsonContent, 'package.json');
if (!parsedPkgJson.name) {
parsedPkgJson.name = 'package.json';
}
return parsedPkgJson;
}
const getChildNode = (name, depInfo, pkgs, strictOutOfSync, includeOptionalDeps) => {
const childNodeKey = `${name}@${depInfo.version}`;
let childNode;
// Check if this lockfile entry is for an aliased package
// by looking for a corresponding npm: entry in the lockfile
let aliasInfo = depInfo.alias;
if (!aliasInfo && pkgs[childNodeKey]) {
// Look for any key in pkgs that matches the pattern: name@npm:*
// and has the same version as our current entry
for (const key in pkgs) {
if (key.startsWith(`${name}@npm:`)) {
const pkgEntry = pkgs[key];
if (pkgEntry.version === pkgs[childNodeKey].version) {
// Extract the npm: portion and parse it using the shared helper
const npmPortion = key.substring(name.length + 1); // Remove "name@" prefix
const parsed = (0, pkgJson_1.parseNpmAlias)(npmPortion);
if (parsed) {
const targetPkgName = parsed.packageName;
// Only add alias info if the alias name is different from the target name
if (targetPkgName !== name) {
aliasInfo = {
aliasName: name,
aliasTargetDepName: targetPkgName,
semver: parsed.version,
version: parsed.version,
};
}
break;
}
}
}
}
}
if (!pkgs[childNodeKey]) {
// Handle optional dependencies that don't have separate package entries
if (depInfo.isOptional) {
childNode = {
id: childNodeKey,
name: aliasInfo?.aliasTargetDepName ?? name,
version: depInfo.version,
dependencies: {},
isDev: depInfo.isDev,
missingLockFileEntry: true,
alias: aliasInfo,
};
}
else if (strictOutOfSync && !/^file:/.test(depInfo.version)) {
throw new errors_1.OutOfSyncError(childNodeKey, parsers_1.LockfileType.yarn);
}
else {
childNode = {
id: childNodeKey,
name: aliasInfo?.aliasTargetDepName ?? name,
version: depInfo.version,
dependencies: {},
isDev: depInfo.isDev,
missingLockFileEntry: true,
alias: aliasInfo,
};
}
}
else {
const depData = pkgs[childNodeKey];
const dependencies = (0, exports.getGraphDependencies)(depData.dependencies || {}, {
isDev: depInfo.isDev,
});
const optionalDependencies = includeOptionalDeps
? (0, exports.getGraphDependencies)(depData.optionalDependencies || {}, {
isDev: depInfo.isDev,
isOptional: true,
})
: {};
childNode = {
id: `${name}@${depData.version}`,
name: aliasInfo?.aliasTargetDepName ?? name,
version: depData.version,
dependencies: { ...dependencies, ...optionalDependencies },
isDev: depInfo.isDev,
alias: aliasInfo,
};
}
return childNode;
};
exports.getChildNode = getChildNode;
//# sourceMappingURL=util.js.map