snyk-nodejs-lockfile-parser
Version:
Generate a dep tree given a lockfile
223 lines • 11.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getYarnLockV2ChildNode = exports.yarnLockFileKeyNormalizer = void 0;
const core_1 = require("@yarnpkg/core");
const _flatMap = require("lodash.flatmap");
const errors_1 = require("../../errors");
const parsers_1 = require("../../parsers");
const util_1 = require("../util");
const semver = require("semver");
const debugModule = require("debug");
const debug = debugModule('snyk-nodejs-plugin');
const BUILTIN_PLACEHOLDER = 'builtin';
const MULTIPLE_KEYS_REGEXP = / *, */g;
const keyNormalizer = (parseDescriptor, parseRange) => (rawDescriptor) => {
// See https://yarnpkg.com/features/protocols
const descriptors = [rawDescriptor];
const descriptor = parseDescriptor(rawDescriptor);
const name = `${descriptor.scope ? '@' + descriptor.scope + '/' : ''}${descriptor.name}`;
const range = parseRange(descriptor.range);
const protocol = range.protocol;
switch (protocol) {
case 'npm:':
case 'file:':
// This is space inneficient but will be kept for now,
// Due to how we wish to index using the dependencies map
// we want the keys to match name@version but this is handled different
// for npm alias and normal install.
descriptors.push(`${name}@${range.selector}`);
descriptors.push(`${name}@${protocol}${range.selector}`);
break;
case 'git:':
case 'git+ssh:':
case 'git+http:':
case 'git+https:':
case 'github:':
if (range.source) {
descriptors.push(`${name}@${protocol}${range.source}${range.selector ? '#' + range.selector : ''}`);
}
else {
descriptors.push(`${name}@${protocol}${range.selector}`);
}
break;
case 'patch:':
if (range.source && range.selector.indexOf(BUILTIN_PLACEHOLDER) === 0) {
descriptors.push(range.source);
}
else {
descriptors.push(`${name}@${protocol}${range.source}${range.selector ? '#' + range.selector : ''}`);
}
break;
case null:
case undefined:
if (range.source) {
descriptors.push(`${name}@${range.source}#${range.selector}`);
}
else {
descriptors.push(`${name}@${range.selector}`);
}
break;
case 'http:':
case 'https:':
case 'link:':
case 'portal:':
case 'exec:':
case 'workspace:':
case 'virtual:':
default:
// For user defined plugins
descriptors.push(`${name}@${protocol}${range.selector}`);
break;
}
return descriptors;
};
const yarnLockFileKeyNormalizer = (parseDescriptor, parseRange) => (fullDescriptor) => {
const allKeys = fullDescriptor
.split(MULTIPLE_KEYS_REGEXP)
.map(keyNormalizer(parseDescriptor, parseRange));
return new Set(_flatMap(allKeys));
};
exports.yarnLockFileKeyNormalizer = yarnLockFileKeyNormalizer;
const getYarnLockV2ChildNode = (name, depInfo, pkgs, strictOutOfSync, includeOptionalDeps, resolutions, parentNode) => {
// First, check if a resolution would be used
const resolvedVersionFromResolution = (() => {
// Check for scoped resolution (e.g., "parentPackageName/dependencyName")
const scopedKey = `${parentNode.name}/${name}`;
if (resolutions[scopedKey]) {
return resolutions[scopedKey];
}
// Check for scoped + versioned resolution (e.g., "parentPkg@npm:version/depName")
// These have the format: parentPackageName@versionOrProtocol/dependencyName
// The dep name suffix could be scoped (e.g., "@scope/dep"), so we check
// if the key ends with `/${name}` to correctly split parent from dep.
const suffix = `/${name}`;
for (const resKey in resolutions) {
if (Object.prototype.hasOwnProperty.call(resolutions, resKey)) {
if (!resKey.endsWith(suffix))
continue;
const parentPart = resKey.substring(0, resKey.length - suffix.length);
// Skip if parentPart is just a plain name (handled by simple scoped check above)
if (!parentPart.includes('@') || parentPart === parentNode.name) {
continue;
}
try {
const descriptor = core_1.structUtils.parseDescriptor(parentPart);
const parentPkgName = core_1.structUtils.stringifyIdent(descriptor);
if (parentPkgName !== parentNode.name)
continue;
// If the resolution key includes a version/range for the parent,
// verify the parent's resolved version satisfies it
if (descriptor.range && descriptor.range !== 'unknown') {
const rangeWithoutProtocol = descriptor.range.replace(/^[a-z]+:/, '');
if (parentNode.version !== rangeWithoutProtocol &&
!(semver.valid(parentNode.version) &&
semver.validRange(rangeWithoutProtocol) &&
semver.satisfies(parentNode.version, rangeWithoutProtocol))) {
continue;
}
}
return resolutions[resKey];
}
catch (e) {
debug(`Error parsing scoped-versioned resolution key(${resKey}): ${e}`);
}
}
}
// Check for resolutions matching "packageName@versionOrRangeToOverride"
for (const resKey in resolutions) {
if (Object.prototype.hasOwnProperty.call(resolutions, resKey)) {
try {
const descriptor = core_1.structUtils.parseDescriptor(resKey);
const resKeyPkgName = core_1.structUtils.stringifyIdent(descriptor);
// Check if the resolution key targets the current package name
if (resKeyPkgName === name) {
if (descriptor.range && descriptor.range !== 'unknown') {
// Strip protocol prefix from both sides (e.g., 'npm:^3.5.4' -> '^3.5.4')
const versionWithoutProtocol = depInfo.version.replace(/^[a-z]+:/, '');
const rangeWithoutProtocol = descriptor.range.replace(/^[a-z]+:/, '');
// Check if the current dependency's version/range matches or satisfies
// the version/range specified in the resolution key.
// If the dependency version is a concrete version (e.g., '3.5.4'),
// check if it satisfies the resolution range.
// If the dependency version is a range (e.g., '^3.5.4'), check for equality.
if (versionWithoutProtocol === rangeWithoutProtocol ||
(semver.valid(versionWithoutProtocol) &&
semver.satisfies(versionWithoutProtocol, rangeWithoutProtocol))) {
return resolutions[resKey];
}
}
}
}
catch (e) {
debug(`Error parsing resolution key(${resKey}): ${e}$`);
}
}
}
// Check for global resolution by package name (e.g., "packageName": "version")
if (resolutions[name]) {
return resolutions[name];
}
return ''; // No resolution applies
})();
if (resolvedVersionFromResolution) {
// Decode URL-encoded characters in resolution values (e.g., npm%3A -> npm:)
// to match the keys extracted from yarn.lock
const decodedResolution = decodeURIComponent(resolvedVersionFromResolution);
const childNodeKeyFromResolution = `${name}@${decodedResolution}`;
if (!pkgs[childNodeKeyFromResolution]) {
if (strictOutOfSync && !/^file:/.test(decodedResolution)) {
throw new errors_1.OutOfSyncError(childNodeKeyFromResolution, parsers_1.LockfileType.yarn2);
}
else {
return Object.assign({ id: childNodeKeyFromResolution, name: depInfo.alias ? depInfo.alias.aliasTargetDepName : name, version: decodedResolution, dependencies: {}, isDev: depInfo.isDev, missingLockFileEntry: true }, (depInfo.alias
? {
alias: Object.assign(Object.assign({}, depInfo.alias), { version: decodedResolution }),
}
: {}));
}
}
const pkgData = pkgs[childNodeKeyFromResolution];
const { version: versionFromResolution, dependencies, optionalDependencies, } = pkgData;
const formattedDependencies = (0, util_1.getGraphDependencies)(dependencies || {}, {
isDev: depInfo.isDev,
});
const formattedOptionalDependencies = includeOptionalDeps
? (0, util_1.getGraphDependencies)(optionalDependencies || {}, {
isDev: depInfo.isDev,
isOptional: true,
})
: {};
return Object.assign({ id: `${name}@${versionFromResolution}`, name: depInfo.alias ? depInfo.alias.aliasTargetDepName : name, version: versionFromResolution, dependencies: Object.assign(Object.assign({}, formattedOptionalDependencies), formattedDependencies), isDev: depInfo.isDev }, (depInfo.alias
? { alias: Object.assign(Object.assign({}, depInfo.alias), { version: versionFromResolution }) }
: {}));
}
// No resolutions
const childNodeKey = `${name}@${depInfo.version}`;
if (!pkgs[childNodeKey]) {
if (strictOutOfSync && !/^file:/.test(depInfo.version)) {
throw new errors_1.OutOfSyncError(childNodeKey, parsers_1.LockfileType.yarn2);
}
else {
return Object.assign({ id: childNodeKey, name: depInfo.alias ? depInfo.alias.aliasTargetDepName : name, version: depInfo.version, dependencies: {}, isDev: depInfo.isDev, missingLockFileEntry: true }, (depInfo.alias
? { alias: Object.assign(Object.assign({}, depInfo.alias), { version: depInfo.version }) }
: {}));
}
}
else {
const depData = pkgs[childNodeKey];
const dependencies = (0, util_1.getGraphDependencies)(depData.dependencies || {}, {
isDev: depInfo.isDev,
});
const optionalDependencies = includeOptionalDeps
? (0, util_1.getGraphDependencies)(depData.optionalDependencies || {}, {
isDev: depInfo.isDev,
isOptional: true,
})
: {};
return Object.assign({ id: `${name}@${depData.version}`, name: depInfo.alias ? depInfo.alias.aliasTargetDepName : name, version: depData.version, dependencies: Object.assign(Object.assign({}, dependencies), optionalDependencies), isDev: depInfo.isDev }, (depInfo.alias
? { alias: Object.assign(Object.assign({}, depInfo.alias), { version: depData.version }) }
: {}));
}
};
exports.getYarnLockV2ChildNode = getYarnLockV2ChildNode;
//# sourceMappingURL=utils.js.map