UNPKG

snyk-nodejs-lockfile-parser

Version:
301 lines 14.3 kB
"use strict"; 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; /** * Yarn Berry merges a workspace package's dependencies, devDependencies and peerDependencies * into a single `dependencies` block in yarn.lock, so the dev marker is lost. When such a * workspace package is consumed as a production dependency, walking that merged block would * promote its dev-only tooling (e.g. webpack, babel) into the production graph. * * Given the consumed workspace member's own package.json (via `workspaceManifest`), drop the * dev-only entries: names that appear in devDependencies but not in dependencies / * optionalDependencies / peerDependencies (prod wins on overlap). */ const pruneWorkspaceDevDependencies = (deps, workspaceManifest) => { const nonDev = new Set([ ...Object.keys(workspaceManifest.dependencies || {}), ...Object.keys(workspaceManifest.optionalDependencies || {}), ...Object.keys(workspaceManifest.peerDependencies || {}), ]); const dev = new Set(Object.keys(workspaceManifest.devDependencies || {})); return Object.entries(deps).reduce((acc, [name, depInfo]) => { // Drop only names that are exclusively devDependencies of the workspace member. if (dev.has(name) && !nonDev.has(name)) { return acc; } acc[name] = depInfo; return acc; }, {}); }; const getYarnLockV2ChildNode = (name, depInfo, pkgs, strictOutOfSync, includeOptionalDeps, resolutions, parentNode, includeDevDeps = false, workspacePackages) => { // 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 { id: childNodeKeyFromResolution, name: depInfo.alias ? depInfo.alias.aliasTargetDepName : name, version: decodedResolution, dependencies: {}, isDev: depInfo.isDev, missingLockFileEntry: true, ...(depInfo.alias ? { alias: { ...depInfo.alias, version: decodedResolution, }, } : {}), }; } } const pkgData = pkgs[childNodeKeyFromResolution]; const { version: versionFromResolution, dependencies, optionalDependencies, } = pkgData; let formattedDependencies = (0, util_1.getGraphDependencies)(dependencies || {}, { isDev: depInfo.isDev, }); const formattedOptionalDependencies = includeOptionalDeps ? (0, util_1.getGraphDependencies)(optionalDependencies || {}, { isDev: depInfo.isDev, isOptional: true, }) : {}; // Key by the resolved package name, not the parent's name, so npm aliases // (e.g. "alias": "npm:@scope/real-pkg@1") still match the workspace manifest. const workspaceManifestFromResolution = workspacePackages?.[depInfo.alias ? depInfo.alias.aliasTargetDepName : name]; if (!includeDevDeps && workspaceManifestFromResolution && pkgData.resolution?.includes('@workspace:')) { formattedDependencies = pruneWorkspaceDevDependencies(formattedDependencies, workspaceManifestFromResolution); } return { id: `${name}@${versionFromResolution}`, name: depInfo.alias ? depInfo.alias.aliasTargetDepName : name, version: versionFromResolution, dependencies: { ...formattedOptionalDependencies, ...formattedDependencies, }, isDev: depInfo.isDev, ...(depInfo.alias ? { alias: { ...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 { id: childNodeKey, name: depInfo.alias ? depInfo.alias.aliasTargetDepName : name, version: depInfo.version, dependencies: {}, isDev: depInfo.isDev, missingLockFileEntry: true, ...(depInfo.alias ? { alias: { ...depInfo.alias, version: depInfo.version } } : {}), }; } } else { const depData = pkgs[childNodeKey]; let dependencies = (0, util_1.getGraphDependencies)(depData.dependencies || {}, { isDev: depInfo.isDev, }); const optionalDependencies = includeOptionalDeps ? (0, util_1.getGraphDependencies)(depData.optionalDependencies || {}, { isDev: depInfo.isDev, isOptional: true, }) : {}; // Key by the resolved package name, not the parent's name, so npm aliases // (e.g. "alias": "npm:@scope/real-pkg@1") still match the workspace manifest. const workspaceManifest = workspacePackages?.[depInfo.alias ? depInfo.alias.aliasTargetDepName : name]; if (!includeDevDeps && workspaceManifest && depData.resolution?.includes('@workspace:')) { dependencies = pruneWorkspaceDevDependencies(dependencies, workspaceManifest); } return { id: `${name}@${depData.version}`, name: depInfo.alias ? depInfo.alias.aliasTargetDepName : name, version: depData.version, dependencies: { ...dependencies, ...optionalDependencies }, isDev: depInfo.isDev, ...(depInfo.alias ? { alias: { ...depInfo.alias, version: depData.version } } : {}), }; } }; exports.getYarnLockV2ChildNode = getYarnLockV2ChildNode; //# sourceMappingURL=utils.js.map