UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

507 lines (506 loc) • 22.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getYarnLockfileNodes = getYarnLockfileNodes; exports.getYarnLockfileDependencies = getYarnLockfileDependencies; exports.stringifyYarnLockfile = stringifyYarnLockfile; const package_json_1 = require("./utils/package-json"); const project_graph_builder_1 = require("../../../project-graph/project-graph-builder"); const semver_1 = require("semver"); const project_graph_1 = require("../../../config/project-graph"); const file_hasher_1 = require("../../../hasher/file-hasher"); const object_sort_1 = require("../../../utils/object-sort"); const get_workspace_packages_from_graph_1 = require("../utils/get-workspace-packages-from-graph"); let currentLockFileHash; let cachedParsedLockFile; // we use key => node map to avoid duplicate work when parsing keys let keyMap = new Map(); function parseLockFile(lockFileContent, lockFileHash) { if (currentLockFileHash === lockFileHash) { return cachedParsedLockFile; } const { parseSyml } = require('@yarnpkg/parsers'); keyMap.clear(); const result = parseSyml(lockFileContent); cachedParsedLockFile = result; currentLockFileHash = lockFileHash; return result; } function getYarnLockfileNodes(lockFileContent, lockFileHash, packageJson) { const { __metadata, ...dependencies } = parseLockFile(lockFileContent, lockFileHash); const isBerry = !!__metadata; // yarn classic splits keys when parsing so we need to stich them back together const groupedDependencies = groupDependencies(dependencies, isBerry); return getNodes(groupedDependencies, packageJson, isBerry); } function getYarnLockfileDependencies(lockFileContent, lockFileHash, ctx) { const { __metadata, ...dependencies } = parseLockFile(lockFileContent, lockFileHash); const isBerry = !!__metadata; // yarn classic splits keys when parsing so we need to stich them back together const groupedDependencies = groupDependencies(dependencies, isBerry); return getDependencies(groupedDependencies, ctx); } function getPackageNameKeyPairs(keys) { const result = new Map(); keys.split(', ').forEach((key) => { const packageName = key.slice(0, key.indexOf('@', 1)); if (result.has(packageName)) { result.get(packageName).add(key); } else { result.set(packageName, new Set([key])); } }); return result; } function getNodes(dependencies, packageJson, isBerry) { const nodes = new Map(); const combinedDeps = { ...packageJson.dependencies, ...packageJson.devDependencies, ...packageJson.peerDependencies, ...packageJson.optionalDependencies, }; Object.entries(dependencies).forEach(([keys, snapshot]) => { // ignore workspace projects & patches if (snapshot.linkType === 'soft' || keys.includes('@patch:')) { return; } const nameKeyPairs = getPackageNameKeyPairs(keys); nameKeyPairs.forEach((keySet, packageName) => { const keysArray = Array.from(keySet); // use key relevant to the package name const [version, isAlias] = findVersion(packageName, keysArray[0], snapshot, isBerry); // use keys linked to the extracted package name keysArray.forEach((key) => { // we don't need to keep duplicates, we can just track the keys const existingNode = nodes.get(packageName)?.get(version); if (existingNode) { keyMap.set(key, existingNode); return; } const node = { type: 'npm', name: version && !isAlias ? `npm:${packageName}@${version}` : `npm:${packageName}`, data: { version, packageName, hash: snapshot.integrity || snapshot.checksum || (0, file_hasher_1.hashArray)([packageName, version]), }, }; keyMap.set(key, node); // use actual version so we can detect it later based on npm package's version const mapKey = snapshot.version && version !== snapshot.version ? snapshot.version : version; if (!nodes.has(packageName)) { nodes.set(packageName, new Map([[mapKey, node]])); } else { nodes.get(packageName).set(mapKey, node); } }); }); }); const externalNodes = {}; for (const [packageName, versionMap] of nodes.entries()) { const hoistedNode = findHoistedNode(packageName, versionMap, combinedDeps); if (hoistedNode) { hoistedNode.name = `npm:${packageName}`; } versionMap.forEach((node) => { externalNodes[node.name] = node; }); } return externalNodes; } function findHoistedNode(packageName, versionMap, combinedDeps) { const hoistedVersion = getHoistedVersion(packageName); if (hoistedVersion) { return versionMap.get(hoistedVersion); } const rootVersionSpecifier = combinedDeps[packageName]; if (!rootVersionSpecifier) { return; } const versions = Array.from(versionMap.keys()).sort((a, b) => (0, semver_1.gt)(a, b) ? -1 : 1); // take the highest version found if (rootVersionSpecifier === '*') { return versionMap.get(versions[0]); } // take version that satisfies the root version specifier let version = versions.find((v) => (0, semver_1.satisfies)(v, rootVersionSpecifier)); if (!version) { // try to find alias version version = versions.find((v) => versionMap.get(v).name === `npm:${packageName}@${rootVersionSpecifier}`); } if (!version) { // try to find tarball package version = versions.find((v) => versionMap.get(v).data.version !== v); } if (version) { return versionMap.get(version); } } function findVersion(packageName, key, snapshot, isBerry) { const versionRange = key.slice(key.indexOf('@', 1) + 1); // check for alias packages const isAlias = isBerry ? snapshot.resolution && !snapshot.resolution.startsWith(`${packageName}@`) : versionRange.startsWith('npm:'); if (isAlias) { return [versionRange, true]; } // check for berry tarball packages if (isBerry && snapshot.resolution && // different registry would yield suffix following '::' which we don't need snapshot.resolution.split('::')[0] !== `${packageName}@npm:${snapshot.version}`) { return [snapshot.resolution.slice(packageName.length + 1)]; } if (!isBerry && isTarballPackage(versionRange, snapshot)) { return [snapshot.resolved]; } // otherwise it's a standard version return [snapshot.version]; } // check if snapshot represents tarball package function isTarballPackage(versionRange, snapshot) { // if resolved is missing it's internal link if (!snapshot.resolved) { return false; } // tarballs have no integrity if (snapshot.integrity) { return false; } try { new semver_1.Range(versionRange); // range is a valid semver return false; } catch { // range is not a valid semver, it can be an npm tag or url part of a tarball return snapshot.version && !snapshot.resolved.includes(snapshot.version); } } function getHoistedVersion(packageName) { const version = (0, package_json_1.getHoistedPackageVersion)(packageName); if (version) { return version; } } function getDependencies(dependencies, ctx) { const projectGraphDependencies = []; Object.keys(dependencies).forEach((keys) => { const snapshot = dependencies[keys]; keys.split(', ').forEach((key) => { if (keyMap.has(key)) { const node = keyMap.get(key); [snapshot.dependencies, snapshot.optionalDependencies].forEach((section) => { if (section) { Object.entries(section).forEach(([name, versionRange]) => { let target = keyMap.get(`${name}@npm:${versionRange}`) || keyMap.get(`${name}@${versionRange}`); if (!target) { const shortRange = versionRange.replace(/^npm:/, ''); // for range like 'npm:*' the above will not be a match if (shortRange === '*') { const foundKey = Array.from(keyMap.keys()).find((k) => k.startsWith(`${name}@`)); if (foundKey) { target = keyMap.get(foundKey); } } else if (shortRange.includes('||')) { // when range is a union of ranges, we need to treat it as an array const ranges = shortRange.split('||').map((r) => r.trim()); target = Object.values(keyMap).find((n) => { return (n.data.packageName === name && ranges.some((r) => (0, semver_1.satisfies)(n.data.version, r))); })?.[1]; } } if (target) { const dep = { source: node.name, target: target.name, type: project_graph_1.DependencyType.static, }; (0, project_graph_builder_1.validateDependency)(dep, ctx); projectGraphDependencies.push(dep); } }); } }); } }); }); return projectGraphDependencies; } function stringifyYarnLockfile(graph, rootLockFileContent, packageJson) { const { parseSyml, stringifySyml } = require('@yarnpkg/parsers'); const { __metadata, ...dependencies } = parseSyml(rootLockFileContent); const isBerry = !!__metadata; const workspaceModules = (0, get_workspace_packages_from_graph_1.getWorkspacePackagesFromGraph)(graph); const snapshots = mapSnapshots(dependencies, graph.externalNodes, packageJson, workspaceModules, isBerry); if (isBerry) { // add root workspace package const workspacePackage = generateRootWorkspacePackage(packageJson); snapshots[workspacePackage.resolution] = workspacePackage; return (BERRY_LOCK_FILE_DISCLAIMER + stringifySyml({ __metadata, ...(0, object_sort_1.sortObjectByKeys)(snapshots), })); } else { const { stringify } = require('@yarnpkg/lockfile'); return stringify((0, object_sort_1.sortObjectByKeys)(snapshots)); } } function groupDependencies(dependencies, isBerry) { if (isBerry) { return dependencies; } let groupedDependencies; const resolutionMap = new Map(); const snapshotMap = new Map(); Object.entries(dependencies).forEach(([key, snapshot]) => { const resolutionKey = `${snapshot.resolved}${snapshot.integrity}`; if (resolutionMap.has(resolutionKey)) { const existingSnapshot = resolutionMap.get(resolutionKey); snapshotMap.get(existingSnapshot).add(key); } else { resolutionMap.set(resolutionKey, snapshot); snapshotMap.set(snapshot, new Set([key])); } }); groupedDependencies = {}; snapshotMap.forEach((keys, snapshot) => { groupedDependencies[Array.from(keys).join(', ')] = snapshot; }); return groupedDependencies; } function addPackageVersion(packageName, version, collection, isBerry) { if (!collection.has(packageName)) { collection.set(packageName, new Set()); } collection.get(packageName).add(`${packageName}@${version}`); if (isBerry && !version.startsWith('npm:') && !version.startsWith('patch:')) { collection.get(packageName).add(`${packageName}@npm:${version}`); } } function mapSnapshots(rootDependencies, nodes, packageJson, workspaceModules, isBerry) { // map snapshot to set of keys (e.g. `eslint@^7.0.0, eslint@npm:^7.0.0`) const snapshotMap = new Map(); // track all existing dependencies's keys const existingKeys = new Map(); const combinedDependencies = { ...packageJson.dependencies, ...packageJson.devDependencies, ...packageJson.optionalDependencies, ...packageJson.peerDependencies, }; const resolutions = { ...packageJson.resolutions, }; // yarn classic splits keys when parsing so we need to stich them back together const groupedDependencies = groupDependencies(rootDependencies, isBerry); // collect snapshots and their matching keys Object.values(nodes).forEach((node) => { const foundOriginalKeys = findOriginalKeys(groupedDependencies, node, workspaceModules); if (!foundOriginalKeys) { throw new Error(`Original key(s) not found for "${node.data.packageName}@${node.data.version}" while pruning yarn.lock.`); } const [matchedKeys, snapshot] = foundOriginalKeys; snapshotMap.set(snapshot, new Set(matchedKeys)); // separately save keys that still exist [snapshot.dependencies, snapshot.optionalDependencies].forEach((section) => { Object.entries(section || {}).forEach(([name, versionSpec]) => addPackageVersion(name, versionSpec, existingKeys, isBerry)); }); // add package.json requested version to keys const requestedVersion = getPackageJsonVersion(combinedDependencies, node, workspaceModules); if (requestedVersion) { addPackageVersion(node.data.packageName, requestedVersion, existingKeys, isBerry); const requestedKey = isBerry ? reverseMapBerryKey(node, requestedVersion, snapshot) : `${node.data.packageName}@${requestedVersion}`; if (!snapshotMap.get(snapshot).has(requestedKey)) { snapshotMap.get(snapshot).add(requestedKey); } } const requestedResolutionsVersion = getPackageJsonVersion(resolutions, node, workspaceModules); if (requestedResolutionsVersion) { addPackageVersion(node.data.packageName, requestedResolutionsVersion, existingKeys, isBerry); const requestedKey = isBerry ? reverseMapBerryKey(node, requestedResolutionsVersion, snapshot) : `${node.data.packageName}@${requestedResolutionsVersion}`; if (!snapshotMap.get(snapshot).has(requestedKey)) { snapshotMap.get(snapshot).add(requestedKey); } } if (isBerry) { // look for patched versions const patch = findPatchedKeys(groupedDependencies, node, resolutions[node.data.packageName]); if (patch) { const [matchedKeys, snapshot] = patch; snapshotMap.set(snapshot, new Set(matchedKeys)); } } }); // remove keys that match version ranges that have been pruned away snapshotMap.forEach((snapshotValue, snapshot) => { for (const key of snapshotValue.values()) { const packageName = key.slice(0, key.indexOf('@', 1)); let normalizedKey = key; if (isBerry && key.includes('@patch:') && key.includes('#')) { const regEx = new RegExp(`@patch:${packageName}@(npm%3A)?(.*)$`); normalizedKey = key .slice(0, key.indexOf('#')) .replace(regEx, '@npm:$2'); } if (!existingKeys.get(packageName) || !existingKeys.get(packageName).has(normalizedKey)) { snapshotValue.delete(key); } } }); // join mapped snapshots to lock json file const result = {}; snapshotMap.forEach((keysSet, snapshot) => { if (isBerry) { result[Array.from(keysSet).sort().join(', ')] = snapshot; } else { for (const key of keysSet.values()) { result[key] = snapshot; } } }); return result; } function reverseMapBerryKey(node, version, snapshot) { // alias packages already have version if (version.startsWith('npm:') || version.startsWith('patch:')) { return `${node.data.packageName}@${version}`; } // check for berry tarball packages if (snapshot.resolution && snapshot.resolution === `${node.data.packageName}@${version}`) { return snapshot.resolution; } return `${node.data.packageName}@npm:${version}`; } function getPackageJsonVersion(dependencies, node, workspaceModules) { const { packageName, version } = node.data; if (workspaceModules.has(packageName)) { return `file:./workspace_modules/${packageName}`; } if (dependencies[packageName]) { const patchRegex = new RegExp(`^patch:${packageName}@(.*)|#.*$`); // extract the version from the patch or use the full version const versionRange = dependencies[packageName].match(patchRegex)?.[1] || dependencies[packageName]; if (versionRange === version || (0, semver_1.satisfies)(version, versionRange)) { return dependencies[packageName]; } } } function isStandardPackage(snapshot, version) { return snapshot.version === version; } function isBerryAlias(snapshot, version) { return snapshot.resolution && `npm:${snapshot.resolution}` === version; } function isClassicAlias(node, keys) { return (node.data.version.startsWith('npm:') && keys.some((k) => k === `${node.data.packageName}@${node.data.version}`)); } function findOriginalKeys(dependencies, node, workspaceModules) { for (const keyExpr of Object.keys(dependencies)) { const snapshot = dependencies[keyExpr]; const keys = keyExpr.split(', '); if (!keys.some((k) => k.startsWith(`${node.data.packageName}@`))) { continue; } if (keys.some((k) => workspaceModules.has(k) || workspaceModules.has(k.split('@file:')[0]))) { const packageName = keys[0].split('@file:')[0]; return [ [`${packageName}@file:./workspace_modules/${packageName}`], snapshot, ]; } if (isStandardPackage(snapshot, node.data.version) || isBerryAlias(snapshot, node.data.version) || isClassicAlias(node, keys)) { return [keys, snapshot]; } // tarball package if (snapshot.resolved === node.data.version || snapshot.resolution === `${node.data.packageName}@${node.data.version}`) { return [keys, snapshot]; } } } function findPatchedKeys(dependencies, node, resolutionVersion) { for (const keyExpr of Object.keys(dependencies)) { const snapshot = dependencies[keyExpr]; const keys = keyExpr.split(', '); if (!keys[0].startsWith(`${node.data.packageName}@patch:`)) { continue; } if (keyExpr.includes('.yarn/patches')) { if (!resolutionVersion) { continue; } const key = `${node.data.packageName}@${resolutionVersion}`; // local patches can have different location from than the root lock file // use the one from local package.json as the source of truth as long as the rest of the patch matches // this obviously doesn't cover the case of patch over a patch, but that's a super rare case and one can argue one can just join those two patches if (key.split('::locator')[0] !== keyExpr.split('::locator')[0]) { continue; } else { return [[key], { ...snapshot, resolution: key }]; } } if (snapshot.version === node.data.version) { return [keys, snapshot]; } } } const BERRY_LOCK_FILE_DISCLAIMER = `# This file is generated by running "yarn install" inside your project.\n# Manual changes might be lost - proceed with caution!\n\n`; function generateRootWorkspacePackage(packageJson) { let isVersion4 = false; if (!!packageJson.packageManager) { const [_, version] = packageJson.packageManager.split('@'); isVersion4 = !!version && (0, semver_1.satisfies)(version, '>=4.0.0'); } const reducer = (acc, [name, version]) => { acc[name] = isVersion4 ? `npm:${version}` : version; return acc; }; return { version: '0.0.0-use.local', resolution: `${packageJson.name}@workspace:.`, ...(packageJson.dependencies && { dependencies: Object.entries(packageJson.dependencies).reduce(reducer, {}), }), ...(packageJson.peerDependencies && { peerDependencies: Object.entries(packageJson.peerDependencies).reduce(reducer, {}), }), ...(packageJson.devDependencies && { devDependencies: Object.entries(packageJson.devDependencies).reduce(reducer, {}), }), ...(packageJson.optionalDependencies && { optionalDependencies: Object.entries(packageJson.optionalDependencies).reduce(reducer, {}), }), languageName: 'unknown', linkType: 'soft', }; }