UNPKG

nx

Version:

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

291 lines (290 loc) • 14.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.pruneProjectGraph = pruneProjectGraph; exports.isLocalPathSpecifier = isLocalPathSpecifier; exports.findLocalPathNode = findLocalPathNode; exports.findNodeMatchingVersion = findNodeMatchingVersion; exports.addNodesAndDependencies = addNodesAndDependencies; exports.rehoistNodes = rehoistNodes; const semver_1 = require("semver"); const operators_1 = require("../../../project-graph/operators"); const project_graph_builder_1 = require("../../../project-graph/project-graph-builder"); const catalog_1 = require("../../../utils/catalog"); const workspace_root_1 = require("../../../utils/workspace-root"); const get_workspace_packages_from_graph_1 = require("../utils/get-workspace-packages-from-graph"); const pruned_output_1 = require("./pruned-output"); /** * Prune project graph's external nodes and their dependencies * based on the pruned package.json */ function pruneProjectGraph(graph, prunedPackageJson, workspaceRootPath = workspace_root_1.workspaceRoot, packageManager) { const builder = new project_graph_builder_1.ProjectGraphBuilder(); const workspacePackages = (0, get_workspace_packages_from_graph_1.getWorkspacePackagesFromGraph)(graph); const { combinedDependencies, localPathNodes } = normalizeDependencies(prunedPackageJson, graph, workspacePackages, workspaceRootPath, packageManager); addNodesAndDependencies(graph, combinedDependencies, workspacePackages, builder); // A local-path dependency is keyed in the lockfile by the target's real // package name, which an aliased one does not share with its manifest entry, // so the name-based lookup above cannot reach its node. Add the nodes matched // by target path rather than repeating that match here. for (const node of localPathNodes) { traverseNode(graph, builder, node); } for (const project of workspacePackages.values()) { const node = graph.nodes[project.name]; builder.addNode(node); } // for NPM (as well as the graph consistency) // we need to distinguish between hoisted and non-hoisted dependencies rehoistNodes(graph, combinedDependencies, builder); return builder.getUpdatedProjectGraph(); } // ensure that dependency ranges from package.json (e.g. ^1.0.0) // are replaced with the actual version based on the available nodes (e.g. 1.0.1) // Also returns the external nodes matched for local-path dependencies, whose // names the caller cannot re-derive from the returned map. function normalizeDependencies(packageJson, graph, workspacePackages, workspaceRootPath, packageManager) { const { dependencies, devDependencies, optionalDependencies, peerDependencies, } = packageJson; const combinedDependencies = { ...dependencies, ...devDependencies, ...optionalDependencies, ...peerDependencies, }; const localPathNodes = []; const manager = (0, catalog_1.getCatalogManager)(workspaceRootPath); Object.entries(combinedDependencies).forEach(([packageName, versionRange]) => { let resolvedVersionRange = versionRange; if (manager?.isCatalogReference(versionRange)) { resolvedVersionRange = manager.resolveCatalogReference(workspaceRootPath, packageName, versionRange); if (!resolvedVersionRange) { throw new Error(`Could not resolve catalog reference for ${packageName}@${versionRange}.`); } } if (graph.externalNodes[`npm:${packageName}@${resolvedVersionRange}`]) { combinedDependencies[packageName] = resolvedVersionRange; return; } if (graph.externalNodes[`npm:${packageName}`] && graph.externalNodes[`npm:${packageName}`].data.version === resolvedVersionRange) { combinedDependencies[packageName] = resolvedVersionRange; return; } // otherwise we need to find the correct version const node = findNodeMatchingVersion(graph, packageName, resolvedVersionRange); // A file:/link: local-path dependency (e.g. a vendored tarball) records a // path where a version would go, so the version-based lookups above never // match it; findLocalPathNode matches it on that path instead (pnpm-only, // matching the rest of the local-path handling). const localPathNode = !node && packageManager === 'pnpm' && !workspacePackages.has(packageName) && isLocalPathSpecifier(resolvedVersionRange) ? findLocalPathNode(graph, packageName, resolvedVersionRange) : undefined; if (node) { combinedDependencies[packageName] = node.data.version; } else if (workspacePackages.has(packageName)) { // workspace module, leave as is combinedDependencies[packageName] = resolvedVersionRange; } else if (localPathNode) { combinedDependencies[packageName] = localPathNode.data.version; localPathNodes.push(localPathNode); } else if (packageManager === 'pnpm' && resolvedVersionRange.startsWith('link:')) { // Only a link: is valid importer-only in a pnpm lockfile (see // mapRootSnapshot); a nodeless file: (stale lockfile) or npm/yarn still throw. combinedDependencies[packageName] = resolvedVersionRange; } else { throw new Error(`Pruned lock file creation failed. The following package was not found in the root lock file: ${packageName}@${resolvedVersionRange}`); } }); return { combinedDependencies, localPathNodes }; } /** * A `file:` (local tarball or directory) or `link:` specifier resolves to a * single local package. pnpm records its path relative to the workspace root in * the lockfile, while the manifest records it relative to the declaring package, * so the two never match by string. */ function isLocalPathSpecifier(versionExpr) { return versionExpr.startsWith('file:') || versionExpr.startsWith('link:'); } /** * The external node for a `file:`/`link:` local-path dependency. * * The target path is what identifies one: an aliased dependency (`"alias": * "file:libs/x"`) is keyed in the lockfile by the target's real package name, so * the manifest's own name matches nothing. A manifest the pruned output already * rewrote carries a workspace-root-relative path, relocated under the shipped * output directory, which strips back to the path the lockfile records. * * A manifest that was not rewritten records the path relative to the declaring * package instead, and this has no way to resolve that against the workspace * root, so it falls back to the package name. Two local-path packages sharing a * name cannot be told apart by it, so that throws rather than risking a match to * the wrong one. */ function findLocalPathNode(graph, packageName, versionExpr) { const localPathNodes = Object.values(graph.externalNodes).filter((node) => isLocalPathSpecifier(node.data.version)); // Only the manifest side is read back from its shipped location; the lock // file records the source path, which relocation never touched. const sourceSpec = (0, pruned_output_1.uncontainLocalPathSpec)(versionExpr); const targetMatch = localPathNodes.find((node) => (0, pruned_output_1.normalizeLocalPathSpec)(node.data.version) === sourceSpec); if (targetMatch) { return targetMatch; } const matches = localPathNodes.filter((node) => node.data.packageName === packageName); if (matches.length > 1) { throw new Error(`Pruned lock file creation failed. Multiple local-path packages named "${packageName}" were found in the lock file (${matches .map((node) => node.data.version) .join(', ')}), so the manifest's "${packageName}" dependency cannot be matched to one of them. Rename the packages so their names are unique.`); } return matches[0]; } function findNodeMatchingVersion(graph, packageName, versionExpr) { if (versionExpr === '*') { return graph.externalNodes[`npm:${packageName}`]; } const nodes = Object.values(graph.externalNodes) .filter((n) => n.data.packageName === packageName) .sort((a, b) => ((0, semver_1.gte)(b.data.version, a.data.version) ? 1 : -1)); if (versionExpr === 'latest') { return nodes.sort((a, b) => +(0, semver_1.gte)(b.data.version, a.data.version))[0]; } if (graph.externalNodes[`npm:${packageName}`] && (0, semver_1.satisfies)(graph.externalNodes[`npm:${packageName}`].data.version, versionExpr)) { return graph.externalNodes[`npm:${packageName}`]; } return nodes.find((n) => (0, semver_1.satisfies)(n.data.version, versionExpr)); } function addNodesAndDependencies(graph, packageJsonDeps, workspacePackages, builder) { Object.entries(packageJsonDeps).forEach(([name, version]) => { const node = graph.externalNodes[`npm:${name}@${version}`] || graph.externalNodes[`npm:${name}`]; if (node) { traverseNode(graph, builder, node); } else if (workspacePackages.has(name)) { // Workspace Node const workspaceNode = workspacePackages.get(name); if (workspaceNode) { traverseWorkspaceNode(graph, builder, workspaceNode); } } }); } function traverseNode(graph, builder, node) { if (builder.graph.externalNodes[node.name]) { return; } builder.addExternalNode(node); graph.dependencies[node.name]?.forEach((dep) => { const depNode = graph.externalNodes[dep.target]; traverseNode(graph, builder, depNode); builder.addStaticDependency(node.name, dep.target); }); } function traverseWorkspaceNode(graph, builder, node, visited = new Set()) { if (visited.has(node.name)) return; visited.add(node.name); graph.dependencies[node.name]?.forEach((dep) => { const externalDepNode = graph.externalNodes[dep.target]; if (externalDepNode) { traverseNode(graph, builder, externalDepNode); return; } const workspaceDepNode = graph.nodes[dep.target]; if (workspaceDepNode) { traverseWorkspaceNode(graph, builder, workspaceDepNode, visited); } }); } function rehoistNodes(graph, packageJsonDeps, builder) { const packagesToRehoist = new Map(); // find all packages that need to be rehoisted Object.values(graph.externalNodes).forEach((node) => { if (node.name === `npm:${node.data.packageName}` && !builder.graph.externalNodes[node.name]) { const nestedNodes = Object.values(builder.graph.externalNodes).filter((n) => n.data.packageName === node.data.packageName); if (nestedNodes.length > 0) { packagesToRehoist.set(node.data.packageName, nestedNodes); } } }); if (!packagesToRehoist.size) { return; } // invert dependencies for easier traversal back const invertedGraph = (0, operators_1.reverse)(builder.graph); const invBuilder = new project_graph_builder_1.ProjectGraphBuilder(invertedGraph, {}); // find new hoisted version packagesToRehoist.forEach((nestedNodes) => { if (nestedNodes.length === 1) { switchNodeToHoisted(nestedNodes[0], builder, invBuilder); } else { let minDistance = Infinity; let closest; nestedNodes.forEach((node) => { const distance = pathLengthToIncoming(node, packageJsonDeps, builder, invertedGraph); if (distance < minDistance) { minDistance = distance; closest = node; } }); if (closest) { switchNodeToHoisted(closest, builder, invBuilder); } } }); } function switchNodeToHoisted(node, builder, invBuilder) { // make a copy of current name, all the dependencies and dependents const previousName = node.name; const targets = (builder.graph.dependencies[node.name] || []).map((d) => d.target); const sources = Object.keys(builder.graph.dependencies).filter((name) => builder.graph.dependencies[name].some((d) => d.target === previousName)); builder.removeNode(node.name); invBuilder.removeNode(node.name); // Re-add under the hoisted name as a new object. The node is shared by // reference with the caller's graph, so renaming it in place would leave that // graph with a node keyed `npm:<pkg>@<version>` but named `npm:<pkg>`, and // every later prune of the same graph would fail to resolve the edge. const hoistedNode = { ...node, name: `npm:${node.data.packageName}`, }; builder.addExternalNode(hoistedNode); invBuilder.addExternalNode(hoistedNode); targets.forEach((target) => { builder.addStaticDependency(hoistedNode.name, target); invBuilder.addStaticDependency(target, hoistedNode.name); }); sources.forEach((source) => { builder.addStaticDependency(source, hoistedNode.name); invBuilder.addStaticDependency(hoistedNode.name, source); }); } // BFS to find the shortest path to a dependency specified in package.json // package version with the shortest path is the one that should be hoisted function pathLengthToIncoming(node, packageJsonDeps, builder, invertedGraph) { const visited = new Set([node.name]); const queue = [[node, 0]]; while (queue.length > 0) { const [current, distance] = queue.shift(); if (packageJsonDeps[current.data.packageName] === current.data.version) { return distance; } for (let { target } of invertedGraph.dependencies[current.name] || []) { if (!visited.has(target)) { visited.add(target); queue.push([builder.graph.externalNodes[target], distance + 1]); } } } }