UNPKG

nx

Version:

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

443 lines (442 loc) • 20.9 kB
"use strict"; /** * This is the main API for accessing the lock file functionality. * It encapsulates the package manager specific logic and implementation details. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.AUTO_AFFECTED_LOCK_FILES = exports.LOCKFILES = void 0; exports.getLockFileNodes = getLockFileNodes; exports.getLockFileNodesForName = getLockFileNodesForName; exports.getLockFileDependencies = getLockFileDependencies; exports.lockFileExists = lockFileExists; exports.getLockFileName = getLockFileName; exports.getLockFilePath = getLockFilePath; exports.createLockFile = createLockFile; exports.generatePrunedDeployOutput = generatePrunedDeployOutput; const node_child_process_1 = require("node:child_process"); const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const semver_1 = require("semver"); const fileutils_1 = require("../../../utils/fileutils"); const output_1 = require("../../../utils/output"); const pruned_output_1 = require("./pruned-output"); const package_manager_1 = require("../../../utils/package-manager"); const workspace_root_1 = require("../../../utils/workspace-root"); const get_workspace_packages_from_graph_1 = require("../utils/get-workspace-packages-from-graph"); const bun_parser_1 = require("./bun-parser"); const npm_parser_1 = require("./npm-parser"); const pnpm_parser_1 = require("./pnpm-parser"); const project_graph_pruning_1 = require("./project-graph-pruning"); const package_json_1 = require("./utils/package-json"); const yarn_parser_1 = require("./yarn-parser"); const YARN_LOCK_FILE = 'yarn.lock'; const NPM_LOCK_FILE = 'package-lock.json'; const PNPM_LOCK_FILE = 'pnpm-lock.yaml'; const PNPM_LOCK_FILE_LEGACY = 'pnpm-lock.yml'; exports.LOCKFILES = [ YARN_LOCK_FILE, NPM_LOCK_FILE, PNPM_LOCK_FILE, bun_parser_1.BUN_LOCK_FILE, bun_parser_1.BUN_TEXT_LOCK_FILE, ]; exports.AUTO_AFFECTED_LOCK_FILES = [ YARN_LOCK_FILE, NPM_LOCK_FILE, PNPM_LOCK_FILE, PNPM_LOCK_FILE_LEGACY, bun_parser_1.BUN_LOCK_FILE, bun_parser_1.BUN_TEXT_LOCK_FILE, ]; const YARN_LOCK_PATH = (0, node_path_1.join)(workspace_root_1.workspaceRoot, YARN_LOCK_FILE); const NPM_LOCK_PATH = (0, node_path_1.join)(workspace_root_1.workspaceRoot, NPM_LOCK_FILE); const PNPM_LOCK_PATH = (0, node_path_1.join)(workspace_root_1.workspaceRoot, PNPM_LOCK_FILE); const BUN_LOCK_PATH = (0, node_path_1.join)(workspace_root_1.workspaceRoot, bun_parser_1.BUN_LOCK_FILE); const BUN_TEXT_LOCK_PATH = (0, node_path_1.join)(workspace_root_1.workspaceRoot, bun_parser_1.BUN_TEXT_LOCK_FILE); /** * Parses lock file and maps dependencies and metadata to {@link LockFileGraph} */ function getLockFileNodes(packageManager, contents, lockFileHash, context) { try { const packageJson = packageManager === 'yarn' || packageManager === 'bun' ? (0, fileutils_1.readJsonFile)((0, node_path_1.join)(context.workspaceRoot, 'package.json')) : undefined; return getLockFileNodesForName(getLockFileName(packageManager), contents, lockFileHash, packageJson); } catch (e) { if (!isPostInstallProcess()) { output_1.output.error({ title: `Failed to parse ${packageManager} lockfile`, bodyLines: errorBodyLines(e), }); } throw e; } throw new Error(`Unknown package manager: ${packageManager}`); } function getLockFileNodesForName(lockFile, contents, lockFileHash, packageJson) { if (lockFile === YARN_LOCK_FILE || lockFile === bun_parser_1.BUN_LOCK_FILE) { // yarn-parser only reads optional fields plus an unused `name` for the // synthetic root workspace node, which is identical across base/head and // therefore irrelevant for affected diffing. return (0, yarn_parser_1.getYarnLockfileNodes)(contents, lockFileHash, packageJson ?? {}); } if (lockFile === PNPM_LOCK_FILE || lockFile === PNPM_LOCK_FILE_LEGACY) { return (0, pnpm_parser_1.getPnpmLockfileNodes)(contents, lockFileHash); } if (lockFile === NPM_LOCK_FILE) { return (0, npm_parser_1.getNpmLockfileNodes)(contents, lockFileHash); } if (lockFile === bun_parser_1.BUN_TEXT_LOCK_FILE) { const nodes = (0, bun_parser_1.getBunTextLockfileNodes)(contents, lockFileHash); return { nodes, keyMap: new Map() }; } throw new Error(`Unknown lock file: ${lockFile}`); } /** * Parses lock file and maps dependencies and metadata to {@link LockFileGraph} */ function getLockFileDependencies(packageManager, contents, lockFileHash, context, keyMap) { try { if (packageManager === 'yarn') { return (0, yarn_parser_1.getYarnLockfileDependencies)(contents, lockFileHash, context, keyMap); } if (packageManager === 'pnpm') { return (0, pnpm_parser_1.getPnpmLockfileDependencies)(contents, lockFileHash, context, keyMap); } if (packageManager === 'npm') { return (0, npm_parser_1.getNpmLockfileDependencies)(contents, lockFileHash, context, keyMap); } if (packageManager === 'bun') { const lockFilePath = getLockFilePath(packageManager); if (lockFilePath.endsWith(bun_parser_1.BUN_TEXT_LOCK_FILE)) { // Bun parser doesn't use keyMap return (0, bun_parser_1.getBunTextLockfileDependencies)(contents, lockFileHash, context); } else { // Fallback to yarn parser for binary format return (0, yarn_parser_1.getYarnLockfileDependencies)(contents, lockFileHash, context, keyMap); } } } catch (e) { if (!isPostInstallProcess()) { output_1.output.error({ title: `Failed to parse ${packageManager} lockfile`, bodyLines: errorBodyLines(e), }); } throw e; } throw new Error(`Unknown package manager: ${packageManager}`); } function lockFileExists(packageManager) { if (packageManager === 'yarn') { return (0, node_fs_1.existsSync)(YARN_LOCK_PATH); } if (packageManager === 'pnpm') { return (0, node_fs_1.existsSync)(PNPM_LOCK_PATH); } if (packageManager === 'npm') { return (0, node_fs_1.existsSync)(NPM_LOCK_PATH); } if (packageManager === 'bun') { return (0, node_fs_1.existsSync)(BUN_LOCK_PATH) || (0, node_fs_1.existsSync)(BUN_TEXT_LOCK_PATH); } throw new Error(`Unknown package manager ${packageManager} or lock file missing`); } /** * Returns lock file name based on the detected package manager in the root * @param packageManager * @returns */ function getLockFileName(packageManager) { if (packageManager === 'yarn') { return YARN_LOCK_FILE; } if (packageManager === 'pnpm') { return PNPM_LOCK_FILE; } if (packageManager === 'npm') { return NPM_LOCK_FILE; } if (packageManager === 'bun') { const lockFilePath = getLockFilePath(packageManager); return lockFilePath.endsWith(bun_parser_1.BUN_TEXT_LOCK_FILE) ? bun_parser_1.BUN_TEXT_LOCK_FILE : bun_parser_1.BUN_LOCK_FILE; } throw new Error(`Unknown package manager: ${packageManager}`); } function getLockFilePath(packageManager) { if (packageManager === 'yarn') { return YARN_LOCK_PATH; } if (packageManager === 'pnpm') { return PNPM_LOCK_PATH; } if (packageManager === 'npm') { return NPM_LOCK_PATH; } if (packageManager === 'bun') { try { // Check if text format exists first (prefer over binary) if ((0, node_fs_1.existsSync)(BUN_TEXT_LOCK_PATH)) { return BUN_TEXT_LOCK_PATH; } // Fall back to binary format if ((0, node_fs_1.existsSync)(BUN_LOCK_PATH)) { return BUN_LOCK_PATH; } const bunVersion = (0, node_child_process_1.execSync)('bun --version', { windowsHide: true }) .toString() .trim(); // Version-based fallback if ((0, semver_1.gte)(bunVersion, '1.2.0')) { return BUN_TEXT_LOCK_PATH; } return BUN_LOCK_PATH; } catch { return BUN_LOCK_PATH; } } throw new Error(`Unknown package manager: ${packageManager}`); } /** * Create lock file based on the root level lock file and (pruned) package.json * * A pruned pnpm lockfile no longer declares the resolution-time pnpm config it * bakes into its snapshots, so the config is dropped from `packageJson` too: * pnpm 10 and below validate the manifest against the lockfile and abort a * frozen install with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH when the two disagree. * An inherited `pnpm.patchedDependencies` goes with it, since the prune scopes * the lockfile's patches to the packages that survive it and rewrites their * paths onto the output. * The manifest is left alone for npm and yarn, which never read that block. * Mutating it means callers must write or emit the manifest after this returns. * * The lockfile alone does not make a complete pnpm output. A workspace * declaring build-script approvals, patches or vendored local paths also needs * the artifacts `generatePrunedDeployOutput` ships, and this warns when that is * the case. * * On a pruning error the root lockfile is returned as a fail-open fallback, * with the manifest left as authored. * * @deprecated Use `generatePrunedDeployOutput` instead. This will be removed in Nx 25. */ function createLockFile(packageJson, graph, packageManager = (0, package_manager_1.detectPackageManager)(workspace_root_1.workspaceRoot)) { let pruned = true; const lockFileContent = buildLockFile(packageJson, graph, packageManager, { onPruneFallback: () => { pruned = false; }, }); if (pruned && packageManager === 'pnpm') { (0, pruned_output_1.stripPrunedLockfilePnpmConfig)(packageJson); (0, pruned_output_1.dropInheritedPnpmPatchedDependencies)(packageJson); (0, pruned_output_1.warnIncompletePrunedPnpmOutput)(lockFileContent); } return lockFileContent; } /** * `createLockFile` without the manifest reconciliation, for callers that own * that step themselves. `options.onPruneFallback` fires just before the * root-lockfile fallback is returned, so a caller can skip work that only makes * sense for an actually pruned lockfile (e.g. link-closure validation and * local-path artifact shipping). Every root-relative read resolves from * `options.workspaceRootPath`, so a caller passing one cannot end up with the * lockfile read from one root and the catalogs resolved from another. */ function buildLockFile(packageJson, graph, packageManager = (0, package_manager_1.detectPackageManager)(workspace_root_1.workspaceRoot), options) { const workspaceRootPath = options?.workspaceRootPath ?? workspace_root_1.workspaceRoot; const normalizedPackageJson = (0, package_json_1.normalizePackageJson)(packageJson); const content = (0, node_fs_1.readFileSync)((0, node_path_1.join)(workspaceRootPath, getLockFileName(packageManager)), 'utf8'); try { if (packageManager === 'bun') { output_1.output.log({ title: "Unable to create bun lock files. Run bun install it's just as quick", }); return ''; } const prunedGraph = (0, project_graph_pruning_1.pruneProjectGraph)(graph, packageJson, workspaceRootPath, packageManager); if (packageManager === 'yarn') { return (0, yarn_parser_1.stringifyYarnLockfile)(prunedGraph, content, normalizedPackageJson); } if (packageManager === 'pnpm') { return (0, pnpm_parser_1.stringifyPnpmLockfile)(prunedGraph, content, normalizedPackageJson, workspaceRootPath); } if (packageManager === 'npm') { return (0, npm_parser_1.stringifyNpmLockfile)(prunedGraph, content, normalizedPackageJson); } } catch (e) { options?.onPruneFallback?.(e); if (!isPostInstallProcess()) { const additionalInfo = [ 'To prevent the build from breaking we are returning the root lock file.', ]; if (packageManager === 'npm') { additionalInfo.push('If you run `npm install --package-lock-only` in your output folder it will regenerate the correct pruned lockfile.'); } if (packageManager === 'pnpm') { additionalInfo.push('If you run `pnpm install --lockfile-only` in your output folder it will regenerate the correct pruned lockfile.'); } output_1.output.error({ title: 'An error occurred while creating pruned lockfile', bodyLines: errorBodyLines(e, additionalInfo), }); } return content; } } /** * Creates the pruned lockfile for a generate-package-json flow, running the * steps such a flow needs around `createLockFile`. For pnpm, the manifest's * `file:`/`link:` local-path specifiers are relocated to their shipped location * first (pnpm re-resolves them on a non-frozen install, and the lockfile copies * the manifest's form), and the local-path dependency closure is validated * after pruning so a shipped `link:` target that requires an unresolvable * dependency fails the build instead of the deploy. After a successful prune, * the manifest's pnpm config block is stripped for every package manager: * re-declaring config a pruned pnpm lockfile bakes into its snapshots trips * ERR_PNPM_LOCKFILE_CONFIG_MISMATCH, and npm and yarn never read the block at * install time, so dropping it does not change their installs. An inherited * `pnpm.patchedDependencies` is dropped on both paths, since the sinks below * declare the patches the output actually ships. * * `pruned` is false when the prune failed and the root lockfile was returned on a * pruning error: the fallback's importer describes the whole workspace, so the * manifest mutations are rolled back (the root lockfile matches the manifest as * authored: original local-path specifiers, the rest of the pnpm config kept), * the closure validation is skipped, and the remaining install-time pieces must * not ship local-path artifacts for it (see `getPrunedPnpmInstallArtifacts`). * * Mutates `packageJson` (the pnpm-only specifier relocation and the config * strip), so write or emit the manifest after calling this. Not for bun, which * has no lockfile generation. */ function createPrunedLockfile(packageJson, graph, projectRoot, workspaceRootPath = workspace_root_1.workspaceRoot, packageManager = (0, package_manager_1.detectPackageManager)(workspaceRootPath)) { const originalPackageJson = structuredClone(packageJson); if (packageManager === 'pnpm') { (0, pruned_output_1.rewritePrunedLocalPathSpecifiers)(packageJson, projectRoot, workspaceRootPath, new Set((0, get_workspace_packages_from_graph_1.getWorkspacePackagesFromGraph)(graph).keys())); } let pruneError; const lockFileContent = buildLockFile(packageJson, graph, packageManager, { onPruneFallback: (error) => { pruneError = error; }, workspaceRootPath, }); const pruned = pruneError === undefined; if (pruned) { (0, pruned_output_1.stripPrunedLockfilePnpmConfig)(packageJson); if (packageManager === 'pnpm') { (0, pruned_output_1.validatePrunedLocalPathClosure)(packageJson, workspaceRootPath, lockFileContent); } } else { // The root lockfile matches the manifest as authored, so undo the // specifier relocation and keep the pnpm config it still declares. for (const key of Object.keys(packageJson)) { delete packageJson[key]; } Object.assign(packageJson, originalPackageJson); // The pruning error output is suppressed under a postinstall, so // this is the only signal there naming the cause and what the fallback // output is missing. const bodyLines = [`The lockfile pruning failed: ${pruneError?.message}`]; if (packageManager === 'pnpm') { bodyLines.push('The emitted package.json keeps its resolution-time pnpm config (`overrides`, `packageExtensions`), its vendored local-path specifiers point at their original workspace locations, and no local-path artifacts are shipped for it.'); } bodyLines.push(packageManager === 'npm' ? '`npm ci` in the output will fail; run `npm install` instead.' : packageManager === 'yarn' ? 'An immutable install of the output (`--immutable`, or `--frozen-lockfile` on yarn 1) may fail; run an install without immutability instead (yarn 2+ turns it on by default in CI).' : 'A `--frozen-lockfile` install of the output will fail; run a regular install instead.'); output_1.output.warn({ title: 'The pruned output falls back to the root lockfile', bodyLines, }); } (0, pruned_output_1.dropInheritedPnpmPatchedDependencies)(packageJson); return { lockFileContent, pruned }; } /** * Generates the standalone deploy output a generate-package-json flow ships * alongside its manifest: the pruned lockfile and, for pnpm, the install-time * artifacts that lockfile needs (the settings-only pnpm-workspace.yaml, the * `pnpm patch` files, and the vendored non-workspace local-path dependencies). * `options` carries either an `outputDirectory` to write into or an `emit` sink * for a bundler's asset pipeline, never both. * * Mutates `packageJson` into the form the output must ship (the relocated * local-path specifiers, the pnpm config strip, the pnpm <=10 build settings), * so write or emit the manifest after this returns. * * Bun has no lockfile generation, so it warns and ships nothing, leaving the * manifest as authored. */ function generatePrunedDeployOutput(packageJson, graph, projectRoot, options) { const workspaceRootPath = options.workspaceRoot ?? workspace_root_1.workspaceRoot; const { packageManager } = options; if (packageManager === 'bun') { output_1.output.warn({ title: 'Bun lockfile generation is not supported', bodyLines: [ 'Only the package.json is generated. Run `bun install` in the output directory if needed.', ], }); return; } const { lockFileContent, pruned } = createPrunedLockfile(packageJson, graph, projectRoot, workspaceRootPath, packageManager); const artifacts = [ { path: getLockFileName(packageManager), content: lockFileContent }, ]; if (packageManager === 'pnpm') { artifacts.push(...(0, pruned_output_1.getPrunedPnpmInstallArtifacts)(workspaceRootPath, lockFileContent, packageJson, { includeLocalPathArtifacts: pruned })); } // Discriminate on the value, not key presence: options built by spread can // carry an explicitly-present `outputDirectory: undefined` beside `emit`. if (options.outputDirectory !== undefined) { const { outputDirectory } = options; // Directory trees flow through here one file at a time, so dedupe the // recursive mkdir per destination directory. const createdDirs = new Set(); for (const artifact of artifacts) { const destination = (0, node_path_1.join)(outputDirectory, artifact.path); const destinationDir = (0, node_path_1.dirname)(destination); if (!createdDirs.has(destinationDir)) { (0, node_fs_1.mkdirSync)(destinationDir, { recursive: true }); createdDirs.add(destinationDir); } if ('sourcePath' in artifact) { (0, node_fs_1.copyFileSync)(artifact.sourcePath, destination); } else { (0, node_fs_1.writeFileSync)(destination, artifact.content); } } } else { for (const artifact of artifacts) { options.emit(artifact.path, 'sourcePath' in artifact ? (0, node_fs_1.readFileSync)(artifact.sourcePath) : artifact.content); } } } // generate body lines for error message function errorBodyLines(originalError, additionalInfo = []) { return [ 'Please open an issue at `https://github.com/nrwl/nx/issues/new?template=1-bug.yml` and provide a reproduction.', ...additionalInfo, `\nOriginal error: ${originalError.message}\n\n`, originalError.stack, ]; } function isPostInstallProcess() { return (process.env.npm_command === 'install' && process.env.npm_lifecycle_event === 'postinstall'); }