UNPKG

@pnpm/core

Version:

Fast, disk space efficient installation engine

970 lines (969 loc) 61.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.install = install; exports.mutateModulesInSingleProject = mutateModulesInSingleProject; exports.mutateModules = mutateModules; exports.addDependenciesToPackage = addDependenciesToPackage; const path_1 = __importDefault(require("path")); const build_modules_1 = require("@pnpm/build-modules"); const builder_policy_1 = require("@pnpm/builder.policy"); const catalogs_protocol_parser_1 = require("@pnpm/catalogs.protocol-parser"); const constants_1 = require("@pnpm/constants"); const core_loggers_1 = require("@pnpm/core-loggers"); const crypto_object_hasher_1 = require("@pnpm/crypto.object-hasher"); const lockfile_settings_checker_1 = require("@pnpm/lockfile.settings-checker"); const error_1 = require("@pnpm/error"); const get_context_1 = require("@pnpm/get-context"); const headless_1 = require("@pnpm/headless"); const lifecycle_1 = require("@pnpm/lifecycle"); const link_bins_1 = require("@pnpm/link-bins"); const lockfile_fs_1 = require("@pnpm/lockfile.fs"); const lockfile_to_pnp_1 = require("@pnpm/lockfile-to-pnp"); const lockfile_utils_1 = require("@pnpm/lockfile.utils"); const lockfile_verification_1 = require("@pnpm/lockfile.verification"); const lockfile_preferred_versions_1 = require("@pnpm/lockfile.preferred-versions"); const logger_1 = require("@pnpm/logger"); const manifest_utils_1 = require("@pnpm/manifest-utils"); const modules_yaml_1 = require("@pnpm/modules-yaml"); const patching_config_1 = require("@pnpm/patching.config"); const read_project_manifest_1 = require("@pnpm/read-project-manifest"); const resolve_dependencies_1 = require("@pnpm/resolve-dependencies"); const is_subdir_1 = __importDefault(require("is-subdir")); const p_limit_1 = __importDefault(require("p-limit")); const map_1 = __importDefault(require("ramda/src/map")); const clone_1 = __importDefault(require("ramda/src/clone")); const isEmpty_1 = __importDefault(require("ramda/src/isEmpty")); const pipeWith_1 = __importDefault(require("ramda/src/pipeWith")); const props_1 = __importDefault(require("ramda/src/props")); const parseWantedDependencies_1 = require("../parseWantedDependencies"); const removeDeps_1 = require("../uninstall/removeDeps"); const extendInstallOptions_1 = require("./extendInstallOptions"); const link_1 = require("./link"); const reportPeerDependencyIssues_1 = require("./reportPeerDependencyIssues"); const validateModules_1 = require("./validateModules"); const ci_info_1 = require("ci-info"); class LockfileConfigMismatchError extends error_1.PnpmError { constructor(outdatedLockfileSettingName) { super('LOCKFILE_CONFIG_MISMATCH', `Cannot proceed with the frozen installation. The current "${outdatedLockfileSettingName}" configuration doesn't match the value found in the lockfile`, { hint: 'Update your lockfile using "pnpm install --no-frozen-lockfile"', }); } } const BROKEN_LOCKFILE_INTEGRITY_ERRORS = new Set([ 'ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE', 'ERR_PNPM_TARBALL_INTEGRITY', ]); const DEV_PREINSTALL = 'pnpm:devPreinstall'; async function install(manifest, opts) { const rootDir = (opts.dir ?? process.cwd()); const { updatedProjects: projects, ignoredBuilds } = await mutateModules([ { mutation: 'install', pruneDirectDependencies: opts.pruneDirectDependencies, rootDir, update: opts.update, updateMatching: opts.updateMatching, updateToLatest: opts.updateToLatest, updatePackageManifest: opts.updatePackageManifest, }, ], { ...opts, allProjects: [{ buildIndex: 0, manifest, rootDir, binsDir: opts.binsDir, }], }); return { updatedManifest: projects[0].manifest, ignoredBuilds }; } async function mutateModulesInSingleProject(project, maybeOpts) { const result = await mutateModules([ { ...project, update: maybeOpts.update, updateToLatest: maybeOpts.updateToLatest, updateMatching: maybeOpts.updateMatching, updatePackageManifest: maybeOpts.updatePackageManifest, }, ], { ...maybeOpts, allProjects: [{ buildIndex: 0, ...project, }], }); return { updatedProject: result.updatedProjects[0], ignoredBuilds: result.ignoredBuilds }; } async function mutateModules(projects, maybeOpts) { const reporter = maybeOpts?.reporter; if ((reporter != null) && typeof reporter === 'function') { logger_1.streamParser.on('data', reporter); } const opts = (0, extendInstallOptions_1.extendOptions)(maybeOpts); if (!opts.include.dependencies && opts.include.optionalDependencies) { throw new error_1.PnpmError('OPTIONAL_DEPS_REQUIRE_PROD_DEPS', 'Optional dependencies cannot be installed without production dependencies'); } const installsOnly = allMutationsAreInstalls(projects); if (!installsOnly) opts.strictPeerDependencies = false; const rootProjectManifest = opts.allProjects.find(({ rootDir }) => rootDir === opts.lockfileDir)?.manifest ?? // When running install/update on a subset of projects, the root project might not be included, // so reading its manifest explicitly here. await (0, read_project_manifest_1.safeReadProjectManifestOnly)(opts.lockfileDir); let ctx = await (0, get_context_1.getContext)(opts); if (!opts.lockfileOnly && ctx.modulesFile != null) { const { purged } = await (0, validateModules_1.validateModules)(ctx.modulesFile, Object.values(ctx.projects), { forceNewModules: installsOnly, include: opts.include, lockfileDir: opts.lockfileDir, modulesDir: opts.modulesDir ?? 'node_modules', registries: opts.registries, storeDir: opts.storeDir, virtualStoreDir: ctx.virtualStoreDir, virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength, confirmModulesPurge: opts.confirmModulesPurge && !ci_info_1.isCI, forceHoistPattern: opts.forceHoistPattern, hoistPattern: opts.hoistPattern, currentHoistPattern: ctx.currentHoistPattern, forcePublicHoistPattern: opts.forcePublicHoistPattern, publicHoistPattern: opts.publicHoistPattern, currentPublicHoistPattern: ctx.currentPublicHoistPattern, global: opts.global, }); if (purged) { ctx = await (0, get_context_1.getContext)(opts); } } if (opts.hooks.preResolution) { await opts.hooks.preResolution({ currentLockfile: ctx.currentLockfile, wantedLockfile: ctx.wantedLockfile, existsCurrentLockfile: ctx.existsCurrentLockfile, existsNonEmptyWantedLockfile: ctx.existsNonEmptyWantedLockfile, lockfileDir: ctx.lockfileDir, storeDir: ctx.storeDir, registries: ctx.registries, }); } const pruneVirtualStore = ctx.modulesFile?.prunedAt && opts.modulesCacheMaxAge > 0 ? cacheExpired(ctx.modulesFile.prunedAt, opts.modulesCacheMaxAge) : true; if (!maybeOpts.ignorePackageManifest) { for (const { manifest, rootDir } of Object.values(ctx.projects)) { if (!manifest) { throw new Error(`No package.json found in "${rootDir}"`); } } } const result = await _install(); // @ts-expect-error if (global['verifiedFileIntegrity'] > 1000) { // @ts-expect-error (0, logger_1.globalInfo)(`The integrity of ${global['verifiedFileIntegrity']} files was checked. This might have caused installation to take longer.`); } if ((reporter != null) && typeof reporter === 'function') { logger_1.streamParser.removeListener('data', reporter); } if (opts.mergeGitBranchLockfiles) { await (0, lockfile_fs_1.cleanGitBranchLockfiles)(ctx.lockfileDir); } return { updatedProjects: result.updatedProjects, stats: result.stats ?? { added: 0, removed: 0, linkedToRoot: 0 }, depsRequiringBuild: result.depsRequiringBuild, ignoredBuilds: result.ignoredBuilds, }; async function _install() { const scriptsOpts = { extraBinPaths: opts.extraBinPaths, extraNodePaths: ctx.extraNodePaths, extraEnv: opts.extraEnv, preferSymlinkedExecutables: opts.preferSymlinkedExecutables, rawConfig: opts.rawConfig, resolveSymlinksInInjectedDirs: opts.resolveSymlinksInInjectedDirs, scriptsPrependNodePath: opts.scriptsPrependNodePath, scriptShell: opts.scriptShell, shellEmulator: opts.shellEmulator, stdio: opts.ownLifecycleHooksStdio, storeController: opts.storeController, unsafePerm: opts.unsafePerm || false, prepareExecutionEnv: opts.prepareExecutionEnv, }; if (!opts.ignoreScripts && !opts.ignorePackageManifest && rootProjectManifest?.scripts?.[DEV_PREINSTALL]) { await (0, lifecycle_1.runLifecycleHook)(DEV_PREINSTALL, rootProjectManifest, { ...scriptsOpts, depPath: opts.lockfileDir, pkgRoot: opts.lockfileDir, rootModulesDir: ctx.rootModulesDir, }); } const packageExtensionsChecksum = (0, crypto_object_hasher_1.hashObjectNullableWithPrefix)(opts.packageExtensions); const pnpmfileChecksum = await opts.hooks.calculatePnpmfileChecksum?.(); const patchedDependencies = opts.ignorePackageManifest ? ctx.wantedLockfile.patchedDependencies : (opts.patchedDependencies ? await (0, lockfile_settings_checker_1.calcPatchHashes)(opts.patchedDependencies, opts.lockfileDir) : {}); const patchedDependenciesWithResolvedPath = patchedDependencies ? (0, map_1.default)((patchFile) => ({ hash: patchFile.hash, path: path_1.default.join(opts.lockfileDir, patchFile.path), }), patchedDependencies) : undefined; const patchGroups = patchedDependenciesWithResolvedPath && (0, patching_config_1.groupPatchedDependencies)(patchedDependenciesWithResolvedPath); const frozenLockfile = opts.frozenLockfile || opts.frozenLockfileIfExists && ctx.existsNonEmptyWantedLockfile; let outdatedLockfileSettings = false; const overridesMap = (0, lockfile_settings_checker_1.createOverridesMapFromParsed)(opts.parsedOverrides); if (!opts.ignorePackageManifest) { const outdatedLockfileSettingName = (0, lockfile_settings_checker_1.getOutdatedLockfileSetting)(ctx.wantedLockfile, { autoInstallPeers: opts.autoInstallPeers, injectWorkspacePackages: opts.injectWorkspacePackages, excludeLinksFromLockfile: opts.excludeLinksFromLockfile, peersSuffixMaxLength: opts.peersSuffixMaxLength, overrides: overridesMap, ignoredOptionalDependencies: opts.ignoredOptionalDependencies?.sort(), packageExtensionsChecksum, patchedDependencies, pnpmfileChecksum, }); outdatedLockfileSettings = outdatedLockfileSettingName != null; if (frozenLockfile && outdatedLockfileSettings) { throw new LockfileConfigMismatchError(outdatedLockfileSettingName); } } const _isWantedDepPrefSame = isWantedDepPrefSame.bind(null, ctx.wantedLockfile.catalogs, opts.catalogs); const upToDateLockfileMajorVersion = ctx.wantedLockfile.lockfileVersion.toString().startsWith(`${constants_1.LOCKFILE_MAJOR_VERSION}.`); let needsFullResolution = outdatedLockfileSettings || opts.fixLockfile || !upToDateLockfileMajorVersion || opts.forceFullResolution; if (needsFullResolution) { ctx.wantedLockfile.settings = { autoInstallPeers: opts.autoInstallPeers, excludeLinksFromLockfile: opts.excludeLinksFromLockfile, peersSuffixMaxLength: opts.peersSuffixMaxLength, injectWorkspacePackages: opts.injectWorkspacePackages, }; ctx.wantedLockfile.overrides = overridesMap; ctx.wantedLockfile.packageExtensionsChecksum = packageExtensionsChecksum; ctx.wantedLockfile.ignoredOptionalDependencies = opts.ignoredOptionalDependencies; ctx.wantedLockfile.pnpmfileChecksum = pnpmfileChecksum; ctx.wantedLockfile.patchedDependencies = patchedDependencies; } else if (!frozenLockfile) { ctx.wantedLockfile.settings = { autoInstallPeers: opts.autoInstallPeers, excludeLinksFromLockfile: opts.excludeLinksFromLockfile, peersSuffixMaxLength: opts.peersSuffixMaxLength, injectWorkspacePackages: opts.injectWorkspacePackages, }; } const frozenInstallResult = await tryFrozenInstall({ frozenLockfile, needsFullResolution, patchGroups, upToDateLockfileMajorVersion, }); if (frozenInstallResult !== null) { if ('needsFullResolution' in frozenInstallResult) { needsFullResolution = frozenInstallResult.needsFullResolution; } else { return frozenInstallResult; } } const projectsToInstall = []; let preferredSpecs = null; // TODO: make it concurrent /* eslint-disable no-await-in-loop */ for (const project of projects) { const projectOpts = { ...project, ...ctx.projects[project.rootDir], }; switch (project.mutation) { case 'uninstallSome': projectsToInstall.push({ pruneDirectDependencies: false, ...projectOpts, removePackages: project.dependencyNames, updatePackageManifest: true, wantedDependencies: [], }); break; case 'install': { await installCase({ ...projectOpts, updatePackageManifest: projectOpts.updatePackageManifest ?? projectOpts.update, }); break; } case 'installSome': { await installSome({ ...projectOpts, updatePackageManifest: projectOpts.updatePackageManifest !== false, }); break; } } } /* eslint-enable no-await-in-loop */ async function installCase(project) { const wantedDependencies = (0, resolve_dependencies_1.getWantedDependencies)(project.manifest, { autoInstallPeers: opts.autoInstallPeers, includeDirect: opts.includeDirect, updateWorkspaceDependencies: project.update, nodeExecPath: opts.nodeExecPath, }) .map((wantedDependency) => ({ ...wantedDependency, updateSpec: true, preserveNonSemverVersionSpec: true })); if (ctx.wantedLockfile?.importers) { forgetResolutionsOfPrevWantedDeps(ctx.wantedLockfile.importers[project.id], wantedDependencies, _isWantedDepPrefSame); } if (opts.ignoreScripts && project.manifest?.scripts && (project.manifest.scripts.preinstall || project.manifest.scripts.install || project.manifest.scripts.postinstall || project.manifest.scripts.prepare)) { ctx.pendingBuilds.push(project.id); } projectsToInstall.push({ pruneDirectDependencies: false, ...project, wantedDependencies, }); } async function installSome(project) { const currentPrefs = opts.ignoreCurrentPrefs ? {} : (0, manifest_utils_1.getAllDependenciesFromManifest)(project.manifest); const optionalDependencies = project.targetDependenciesField ? {} : project.manifest.optionalDependencies || {}; const devDependencies = project.targetDependenciesField ? {} : project.manifest.devDependencies || {}; if (preferredSpecs == null) { const manifests = []; for (const versions of ctx.workspacePackages.values()) { for (const { manifest } of versions.values()) { manifests.push(manifest); } } preferredSpecs = (0, manifest_utils_1.getAllUniqueSpecs)(manifests); } const wantedDeps = (0, parseWantedDependencies_1.parseWantedDependencies)(project.dependencySelectors, { allowNew: project.allowNew !== false, currentPrefs, defaultTag: opts.tag, dev: project.targetDependenciesField === 'devDependencies', devDependencies, optional: project.targetDependenciesField === 'optionalDependencies', optionalDependencies, updateWorkspaceDependencies: project.update, preferredSpecs, overrides: opts.overrides, defaultCatalog: opts.catalogs?.default, }); projectsToInstall.push({ pruneDirectDependencies: false, ...project, wantedDependencies: wantedDeps.map(wantedDep => ({ ...wantedDep, isNew: !currentPrefs[wantedDep.alias], updateSpec: true, nodeExecPath: opts.nodeExecPath })), }); } // Unfortunately, the private lockfile may differ from the public one. // A user might run named installations on a project that has a pnpm-lock.yaml file before running a noop install const makePartialCurrentLockfile = !installsOnly && (ctx.existsNonEmptyWantedLockfile && !ctx.existsCurrentLockfile || !ctx.currentLockfileIsUpToDate); const result = await installInContext(projectsToInstall, ctx, { ...opts, currentLockfileIsUpToDate: !ctx.existsNonEmptyWantedLockfile || ctx.currentLockfileIsUpToDate, makePartialCurrentLockfile, needsFullResolution, pruneVirtualStore, scriptsOpts, updateLockfileMinorVersion: true, patchedDependencies: patchGroups, }); return { updatedProjects: result.projects, stats: result.stats, depsRequiringBuild: result.depsRequiringBuild, ignoredBuilds: result.ignoredBuilds, }; } /** * Attempt to perform a "frozen install". * * A "frozen install" will be performed if: * * 1. The --frozen-lockfile flag was explicitly specified or evaluates to * true based on conditions like running on CI. * 2. No workspace modifications have been made that would invalidate the * pnpm-lock.yaml file. In other words, the pnpm-lock.yaml file is * known to be "up-to-date". * * A frozen install is significantly faster since the pnpm-lock.yaml file * can treated as immutable, skipping expensive lookups to acquire new * dependencies. For this reason, a frozen install should be performed even * if --frozen-lockfile wasn't explicitly specified. This allows users to * benefit from the increased performance of a frozen install automatically. * * If a frozen install is not possible, this function will return null. * This indicates a standard mutable install needs to be performed. * * Note this function may update the pnpm-lock.yaml file if the lockfile was * on a different major version, needs to be merged due to git conflicts, * etc. These changes update the format of the pnpm-lock.yaml file, but do * not change recorded dependency resolutions. */ async function tryFrozenInstall({ frozenLockfile, needsFullResolution, patchGroups, upToDateLockfileMajorVersion, }) { const isFrozenInstallPossible = // A frozen install is never possible when any of these are true: !ctx.lockfileHadConflicts && !opts.fixLockfile && !opts.dedupe && installsOnly && ( // If the user explicitly requested a frozen lockfile install, attempt // to perform one. An error will be thrown if updates are required. frozenLockfile || // Otherwise, check if a frozen-like install is possible for // performance. This will be the case if all projects are up-to-date. opts.ignorePackageManifest || !needsFullResolution && opts.preferFrozenLockfile && (!opts.pruneLockfileImporters || Object.keys(ctx.wantedLockfile.importers).length === Object.keys(ctx.projects).length) && ctx.existsNonEmptyWantedLockfile && ctx.wantedLockfile.lockfileVersion === constants_1.LOCKFILE_VERSION && await (0, lockfile_verification_1.allProjectsAreUpToDate)(Object.values(ctx.projects), { catalogs: opts.catalogs, autoInstallPeers: opts.autoInstallPeers, excludeLinksFromLockfile: opts.excludeLinksFromLockfile, linkWorkspacePackages: opts.linkWorkspacePackagesDepth >= 0, wantedLockfile: ctx.wantedLockfile, workspacePackages: ctx.workspacePackages, lockfileDir: opts.lockfileDir, })); if (!isFrozenInstallPossible) { return null; } if (needsFullResolution) { throw new error_1.PnpmError('FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE', 'Cannot perform a frozen installation because the version of the lockfile is incompatible with this version of pnpm', { hint: `Try either: 1. Aligning the version of pnpm that generated the lockfile with the version that installs from it, or 2. Migrating the lockfile so that it is compatible with the newer version of pnpm, or 3. Using "pnpm install --no-frozen-lockfile". Note that in CI environments, this setting is enabled by default.`, }); } if (!opts.ignorePackageManifest) { const _satisfiesPackageManifest = lockfile_verification_1.satisfiesPackageManifest.bind(null, { autoInstallPeers: opts.autoInstallPeers, excludeLinksFromLockfile: opts.excludeLinksFromLockfile, }); for (const { id, manifest, rootDir } of Object.values(ctx.projects)) { const { satisfies, detailedReason } = _satisfiesPackageManifest(ctx.wantedLockfile.importers[id], manifest); if (!satisfies) { if (!ctx.existsWantedLockfile) { throw new error_1.PnpmError('NO_LOCKFILE', `Cannot install with "frozen-lockfile" because ${constants_1.WANTED_LOCKFILE} is absent`, { hint: 'Note that in CI environments this setting is true by default. If you still need to run install in such cases, use "pnpm install --no-frozen-lockfile"', }); } throw new error_1.PnpmError('OUTDATED_LOCKFILE', `Cannot install with "frozen-lockfile" because ${constants_1.WANTED_LOCKFILE} is not up to date with ` + path_1.default.join('<ROOT>', path_1.default.relative(opts.lockfileDir, path_1.default.join(rootDir, 'package.json'))), { hint: `Note that in CI environments this setting is true by default. If you still need to run install in such cases, use "pnpm install --no-frozen-lockfile" Failure reason: ${detailedReason ?? ''}`, }); } } } if (opts.lockfileOnly) { // The lockfile will only be changed if the workspace will have new projects with no dependencies. await (0, lockfile_fs_1.writeWantedLockfile)(ctx.lockfileDir, ctx.wantedLockfile); return { updatedProjects: projects.map((mutatedProject) => ctx.projects[mutatedProject.rootDir]), ignoredBuilds: undefined, }; } if (!ctx.existsNonEmptyWantedLockfile) { if (Object.values(ctx.projects).some((project) => pkgHasDependencies(project.manifest))) { throw new Error(`Headless installation requires a ${constants_1.WANTED_LOCKFILE} file`); } return null; } if (maybeOpts.ignorePackageManifest) { logger_1.logger.info({ message: 'Importing packages to virtual store', prefix: opts.lockfileDir }); } else { logger_1.logger.info({ message: 'Lockfile is up to date, resolution step is skipped', prefix: opts.lockfileDir }); } try { const { stats, ignoredBuilds } = await (0, headless_1.headlessInstall)({ ...ctx, ...opts, currentEngine: { nodeVersion: opts.nodeVersion, pnpmVersion: opts.packageManager.name === 'pnpm' ? opts.packageManager.version : '', }, currentHoistedLocations: ctx.modulesFile?.hoistedLocations, patchedDependencies: patchGroups, selectedProjectDirs: projects.map((project) => project.rootDir), allProjects: ctx.projects, prunedAt: ctx.modulesFile?.prunedAt, pruneVirtualStore, wantedLockfile: maybeOpts.ignorePackageManifest ? undefined : ctx.wantedLockfile, useLockfile: opts.useLockfile && ctx.wantedLockfileIsModified, }); if (opts.useLockfile && opts.saveLockfile && opts.mergeGitBranchLockfiles || !upToDateLockfileMajorVersion && !opts.frozenLockfile) { await (0, lockfile_fs_1.writeLockfiles)({ currentLockfile: ctx.currentLockfile, currentLockfileDir: ctx.virtualStoreDir, wantedLockfile: ctx.wantedLockfile, wantedLockfileDir: ctx.lockfileDir, useGitBranchLockfile: opts.useGitBranchLockfile, mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles, }); } return { updatedProjects: projects.map((mutatedProject) => { const project = ctx.projects[mutatedProject.rootDir]; return { ...project, manifest: project.originalManifest ?? project.manifest, }; }), stats, ignoredBuilds, }; } catch (error) { // eslint-disable-line if (frozenLockfile || (error.code !== 'ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY' && !BROKEN_LOCKFILE_INTEGRITY_ERRORS.has(error.code)) || (!ctx.existsNonEmptyWantedLockfile && !ctx.existsCurrentLockfile)) throw error; if (BROKEN_LOCKFILE_INTEGRITY_ERRORS.has(error.code)) { needsFullResolution = true; // Ideally, we would not update but currently there is no other way to redownload the integrity of the package for (const project of projects) { project.update = true; } } // A broken lockfile may be caused by a badly resolved Git conflict logger_1.logger.warn({ error, message: error.message, prefix: ctx.lockfileDir, }); logger_1.logger.error(new error_1.PnpmError(error.code, 'The lockfile is broken! Resolution step will be performed to fix it.')); return { needsFullResolution }; } } } function cacheExpired(prunedAt, maxAgeInMinutes) { return ((Date.now() - new Date(prunedAt).valueOf()) / (1000 * 60)) > maxAgeInMinutes; } function pkgHasDependencies(manifest) { return Boolean((Object.keys(manifest.dependencies ?? {}).length > 0) || Object.keys(manifest.devDependencies ?? {}).length || Object.keys(manifest.optionalDependencies ?? {}).length); } // If the specifier is new, the old resolution probably does not satisfy it anymore. // By removing these resolutions we ensure that they are resolved again using the new specs. function forgetResolutionsOfPrevWantedDeps(importer, wantedDeps, isWantedDepPrefSame) { if (!importer.specifiers) return; importer.dependencies = importer.dependencies ?? {}; importer.devDependencies = importer.devDependencies ?? {}; importer.optionalDependencies = importer.optionalDependencies ?? {}; for (const { alias, pref } of wantedDeps) { if (alias && !isWantedDepPrefSame(alias, importer.specifiers[alias], pref)) { if (!importer.dependencies[alias]?.startsWith('link:')) { delete importer.dependencies[alias]; } delete importer.devDependencies[alias]; delete importer.optionalDependencies[alias]; } } } function forgetResolutionsOfAllPrevWantedDeps(wantedLockfile) { // Similar to the forgetResolutionsOfPrevWantedDeps function above, we can // delete existing resolutions in importers to make sure they're resolved // again. if ((wantedLockfile.importers != null) && !(0, isEmpty_1.default)(wantedLockfile.importers)) { wantedLockfile.importers = (0, map_1.default)(({ dependencies, devDependencies, optionalDependencies, ...rest }) => rest, wantedLockfile.importers); } // The resolveDependencies function looks at previous PackageSnapshot // dependencies/optionalDependencies blocks and merges them with new resolved // deps. Clear the previous PackageSnapshot fields so the newly resolved deps // are always used. if ((wantedLockfile.packages != null) && !(0, isEmpty_1.default)(wantedLockfile.packages)) { wantedLockfile.packages = (0, map_1.default)(({ dependencies, optionalDependencies, ...rest }) => rest, wantedLockfile.packages); } } /** * Check if a wanted pref is the same. * * It would be different if the user modified a dependency in package.json or a * catalog entry in pnpm-workspace.yaml. This is normally a simple check to see * if the specifier strings match, but catalogs make this more involved since we * also have to check if the catalog config in pnpm-workspace.yaml is the same. */ function isWantedDepPrefSame(prevCatalogs, catalogsConfig, alias, prevPref, nextPref) { if (prevPref !== nextPref) { return false; } // When pnpm catalogs are used, the specifiers can be the same (e.g. // "catalog:default"), but the wanted versions for the dependency can be // different after resolution if the catalog config was just edited. const catalogName = (0, catalogs_protocol_parser_1.parseCatalogProtocol)(prevPref); // If there's no catalog name, the catalog protocol was not used and we // can assume the pref is the same since prevPref and nextPref match. if (catalogName === null) { return true; } const prevCatalogEntrySpec = prevCatalogs?.[catalogName]?.[alias]?.specifier; const nextCatalogEntrySpec = catalogsConfig?.[catalogName]?.[alias]; return prevCatalogEntrySpec === nextCatalogEntrySpec; } async function addDependenciesToPackage(manifest, dependencySelectors, opts) { const rootDir = (opts.dir ?? process.cwd()); const { updatedProjects: projects, ignoredBuilds } = await mutateModules([ { allowNew: opts.allowNew, dependencySelectors, mutation: 'installSome', peer: opts.peer, pinnedVersion: opts.pinnedVersion, rootDir, targetDependenciesField: opts.targetDependenciesField, update: opts.update, updateMatching: opts.updateMatching, updatePackageManifest: opts.updatePackageManifest, updateToLatest: opts.updateToLatest, }, ], { ...opts, lockfileDir: opts.lockfileDir ?? opts.dir, allProjects: [ { buildIndex: 0, binsDir: opts.bin, manifest, rootDir, }, ], }); return { updatedManifest: projects[0].manifest, ignoredBuilds }; } const _installInContext = async (projects, ctx, opts) => { // The wanted lockfile is mutated during installation. To compare changes, a // deep copy before installation is needed. This copy should represent the // original wanted lockfile on disk as close as possible. // // This object can be quite large. Intentionally avoiding an expensive copy // if no lockfileCheck option was passed in. const originalLockfileForCheck = opts.lockfileCheck != null ? (0, clone_1.default)(ctx.wantedLockfile) : null; // Aliasing for clarity in boolean expressions below. const isInstallationOnlyForLockfileCheck = opts.lockfileCheck != null; ctx.wantedLockfile.importers = ctx.wantedLockfile.importers || {}; for (const { id } of projects) { if (!ctx.wantedLockfile.importers[id]) { ctx.wantedLockfile.importers[id] = { specifiers: {} }; } } if (opts.pruneLockfileImporters) { const projectIds = new Set(projects.map(({ id }) => id)); for (const wantedImporter of Object.keys(ctx.wantedLockfile.importers)) { if (!projectIds.has(wantedImporter)) { delete ctx.wantedLockfile.importers[wantedImporter]; } } } await Promise.all(projects .map(async (project) => { if (project.mutation !== 'uninstallSome') return; const _removeDeps = async (manifest) => (0, removeDeps_1.removeDeps)(manifest, project.dependencyNames, { prefix: project.rootDir, saveType: project.targetDependenciesField }); project.manifest = await _removeDeps(project.manifest); if (project.originalManifest != null) { project.originalManifest = await _removeDeps(project.originalManifest); } })); core_loggers_1.stageLogger.debug({ prefix: ctx.lockfileDir, stage: 'resolution_started', }); const update = projects.some((project) => project.update); const preferredVersions = opts.preferredVersions ?? (!update ? (0, lockfile_preferred_versions_1.getPreferredVersionsFromLockfileAndManifests)(ctx.wantedLockfile.packages, Object.values(ctx.projects).map(({ manifest }) => manifest)) : undefined); const forceFullResolution = ctx.wantedLockfile.lockfileVersion !== constants_1.LOCKFILE_VERSION || !opts.currentLockfileIsUpToDate || opts.force || opts.needsFullResolution || ctx.lockfileHadConflicts || opts.dedupePeerDependents; // Ignore some fields when fixing lockfile, so these fields can be regenerated // and make sure it's up to date if (opts.fixLockfile && (ctx.wantedLockfile.packages != null) && !(0, isEmpty_1.default)(ctx.wantedLockfile.packages)) { ctx.wantedLockfile.packages = (0, map_1.default)(({ dependencies, optionalDependencies, resolution }) => ({ // These fields are needed to avoid losing information of the locked dependencies if these fields are not broken // If these fields are broken, they will also be regenerated dependencies, optionalDependencies, resolution, }), ctx.wantedLockfile.packages); } if (opts.dedupe) { // Deleting recorded version resolutions from importers and packages. These // fields will be regenerated using the preferred versions computed above. // // This is a bit different from a "full resolution", which completely // ignores preferred versions from the lockfile. forgetResolutionsOfAllPrevWantedDeps(ctx.wantedLockfile); } let { dependenciesGraph, dependenciesByProjectId, linkedDependenciesByProjectId, newLockfile, outdatedDependencies, peerDependencyIssuesByProjects, wantedToBeSkippedPackageIds, waitTillAllFetchingsFinish, } = await (0, resolve_dependencies_1.resolveDependencies)(projects, { allowedDeprecatedVersions: opts.allowedDeprecatedVersions, allowUnusedPatches: opts.allowUnusedPatches, autoInstallPeers: opts.autoInstallPeers, autoInstallPeersFromHighestMatch: opts.autoInstallPeersFromHighestMatch, catalogs: opts.catalogs, currentLockfile: ctx.currentLockfile, defaultUpdateDepth: opts.depth, dedupeDirectDeps: opts.dedupeDirectDeps, dedupeInjectedDeps: opts.dedupeInjectedDeps, dedupePeerDependents: opts.dedupePeerDependents, dryRun: opts.lockfileOnly, engineStrict: opts.engineStrict, excludeLinksFromLockfile: opts.excludeLinksFromLockfile, force: opts.force, forceFullResolution, ignoreScripts: opts.ignoreScripts, hooks: { readPackage: opts.readPackageHook, }, linkWorkspacePackagesDepth: opts.linkWorkspacePackagesDepth ?? (opts.saveWorkspaceProtocol ? 0 : -1), lockfileDir: opts.lockfileDir, nodeVersion: opts.nodeVersion, pnpmVersion: opts.packageManager.name === 'pnpm' ? opts.packageManager.version : '', preferWorkspacePackages: opts.preferWorkspacePackages, preferredVersions, preserveWorkspaceProtocol: opts.preserveWorkspaceProtocol, registries: ctx.registries, resolutionMode: opts.resolutionMode, saveWorkspaceProtocol: opts.saveWorkspaceProtocol, storeController: opts.storeController, tag: opts.tag, virtualStoreDir: ctx.virtualStoreDir, virtualStoreDirMaxLength: ctx.virtualStoreDirMaxLength, wantedLockfile: ctx.wantedLockfile, workspacePackages: ctx.workspacePackages, patchedDependencies: opts.patchedDependencies, lockfileIncludeTarballUrl: opts.lockfileIncludeTarballUrl, resolvePeersFromWorkspaceRoot: opts.resolvePeersFromWorkspaceRoot, supportedArchitectures: opts.supportedArchitectures, peersSuffixMaxLength: opts.peersSuffixMaxLength, injectWorkspacePackages: opts.injectWorkspacePackages, }); if (!opts.include.optionalDependencies || !opts.include.devDependencies || !opts.include.dependencies) { linkedDependenciesByProjectId = (0, map_1.default)((linkedDeps) => linkedDeps.filter((linkedDep) => !(linkedDep.dev && !opts.include.devDependencies || linkedDep.optional && !opts.include.optionalDependencies || !linkedDep.dev && !linkedDep.optional && !opts.include.dependencies)), linkedDependenciesByProjectId ?? {}); for (const { id, manifest } of projects) { for (const [alias, depPath] of dependenciesByProjectId[id].entries()) { let include; const dep = dependenciesGraph[depPath]; if (!dep) { include = false; } else { const isDev = Boolean(manifest.devDependencies?.[dep.name]); const isOptional = Boolean(manifest.optionalDependencies?.[dep.name]); include = !(isDev && !opts.include.devDependencies || isOptional && !opts.include.optionalDependencies || !isDev && !isOptional && !opts.include.dependencies); } if (!include) { dependenciesByProjectId[id].delete(alias); } } } } core_loggers_1.stageLogger.debug({ prefix: ctx.lockfileDir, stage: 'resolution_done', }); newLockfile = ((opts.hooks?.afterAllResolved) != null) ? await (0, pipeWith_1.default)(async (f, res) => f(await res), opts.hooks.afterAllResolved)(newLockfile) // eslint-disable-line : newLockfile; if (opts.updateLockfileMinorVersion) { newLockfile.lockfileVersion = constants_1.LOCKFILE_VERSION; } const depsStateCache = {}; const lockfileOpts = { useGitBranchLockfile: opts.useGitBranchLockfile, mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles, }; let stats; const allowBuild = (0, builder_policy_1.createAllowBuildFunction)(opts); let ignoredBuilds; if (!opts.lockfileOnly && !isInstallationOnlyForLockfileCheck && opts.enableModulesDir) { const result = await (0, link_1.linkPackages)(projects, dependenciesGraph, { allowBuild, currentLockfile: ctx.currentLockfile, dedupeDirectDeps: opts.dedupeDirectDeps, dependenciesByProjectId, depsStateCache, disableRelinkLocalDirDeps: opts.disableRelinkLocalDirDeps, extraNodePaths: ctx.extraNodePaths, force: opts.force, hoistedDependencies: ctx.hoistedDependencies, hoistedModulesDir: ctx.hoistedModulesDir, hoistPattern: ctx.hoistPattern, ignoreScripts: opts.ignoreScripts, include: opts.include, linkedDependenciesByProjectId, lockfileDir: opts.lockfileDir, makePartialCurrentLockfile: opts.makePartialCurrentLockfile, outdatedDependencies, pruneStore: opts.pruneStore, pruneVirtualStore: opts.pruneVirtualStore, publicHoistPattern: ctx.publicHoistPattern, registries: ctx.registries, rootModulesDir: ctx.rootModulesDir, sideEffectsCacheRead: opts.sideEffectsCacheRead, symlink: opts.symlink, skipped: ctx.skipped, storeController: opts.storeController, virtualStoreDir: ctx.virtualStoreDir, virtualStoreDirMaxLength: ctx.virtualStoreDirMaxLength, wantedLockfile: newLockfile, wantedToBeSkippedPackageIds, hoistWorkspacePackages: opts.hoistWorkspacePackages, }); stats = result.stats; if (opts.enablePnp) { const importerNames = Object.fromEntries(projects.map(({ manifest, id }) => [id, manifest.name ?? id])); await (0, lockfile_to_pnp_1.writePnpFile)(result.currentLockfile, { importerNames, lockfileDir: ctx.lockfileDir, virtualStoreDir: ctx.virtualStoreDir, virtualStoreDirMaxLength: ctx.virtualStoreDirMaxLength, registries: ctx.registries, }); } ctx.pendingBuilds = ctx.pendingBuilds .filter((relDepPath) => !result.removedDepPaths.has(relDepPath)); if (result.newDepPaths?.length) { if (opts.ignoreScripts) { // we can use concat here because we always only append new packages, which are guaranteed to not be there by definition ctx.pendingBuilds = ctx.pendingBuilds .concat(result.newDepPaths.filter((depPath) => dependenciesGraph[depPath].requiresBuild)); } if (!opts.ignoreScripts || Object.keys(opts.patchedDependencies ?? {}).length > 0) { // postinstall hooks const depPaths = Object.keys(dependenciesGraph); const rootNodes = depPaths.filter((depPath) => dependenciesGraph[depPath].depth === 0); let extraEnv = opts.scriptsOpts.extraEnv; if (opts.enablePnp) { extraEnv = { ...extraEnv, ...(0, lifecycle_1.makeNodeRequireOption)(path_1.default.join(opts.lockfileDir, '.pnp.cjs')), }; } ignoredBuilds = (await (0, build_modules_1.buildModules)(dependenciesGraph, rootNodes, { allowBuild, ignorePatchFailures: opts.ignorePatchFailures, ignoredBuiltDependencies: opts.ignoredBuiltDependencies, childConcurrency: opts.childConcurrency, depsStateCache, depsToBuild: new Set(result.newDepPaths), extraBinPaths: ctx.extraBinPaths, extraNodePaths: ctx.extraNodePaths, extraEnv, ignoreScripts: opts.ignoreScripts || opts.ignoreDepScripts, lockfileDir: ctx.lockfileDir, optional: opts.include.optionalDependencies, preferSymlinkedExecutables: opts.preferSymlinkedExecutables, rawConfig: opts.rawConfig, rootModulesDir: ctx.virtualStoreDir, scriptsPrependNodePath: opts.scriptsPrependNodePath, scriptShell: opts.scriptShell, shellEmulator: opts.shellEmulator, sideEffectsCacheWrite: opts.sideEffectsCacheWrite, storeController: opts.storeController, unsafePerm: opts.unsafePerm, userAgent: opts.userAgent, })).ignoredBuilds; if (ignoredBuilds == null && ctx.modulesFile?.ignoredBuilds?.length) { ignoredBuilds = ctx.modulesFile.ignoredBuilds; core_loggers_1.ignoredScriptsLogger.debug({ packageNames: ignoredBuilds }); } } } const binWarn = (prefix, message) => { logger_1.logger.info({ message, prefix }); }; if (result.newDepPaths?.length) { const newPkgs = (0, props_1.default)(result.newDepPaths, dependenciesGraph); await linkAllBins(newPkgs, dependenciesGraph, { extraNodePaths: ctx.extraNodePaths, optional: opts.include.optionalDependencies, warn: binWarn.bind(null, opts.lockfileDir), }); } await Promise.all(projects.map(async (project, index) => { let linkedPackages; if (ctx.publicHoistPattern?.length && path_1.default.relative(project.rootDir, opts.lockfileDir) === '') { const nodeExecPathByAlias = {}; for (const alias in project.manifest.dependenciesMeta) { const { node } = project.manifest.dependenciesMeta[alias]; if (node) { nodeExecPathByAlias[alias] = node; } } linkedPackages = await (0, link_bins_1.linkBins)(project.modulesDir, project.binsDir, { allowExoticManifests: true, preferSymlinkedExecutables: opts.preferSymlinkedExecutables, projectManifest: project.manifest, nodeExecPathByAlias, extraNodePaths: ctx.extraNodePaths, warn: binWarn.bind(null, project.rootDir), }); } else { const directPkgs = [ ...(0, props_1.default)(Array.from(dependenciesByProjectId[project.id].values()).filter((depPath) => !ctx.skipped.has(depPath)), dependenciesGraph), ...linkedDependenciesByProjectId[project.id].map(({ pkgId }) => ({ dir: path_1.default.join(project.rootDir, pkgId.substring(5)), fetching: undefined, })), ]; linkedPackages = await (0, link_bins_1.linkBinsOfPackages)((await Promise.all(directPkgs.map(async (dep) => { const manifest = (await dep.fetching?.())?.bundledManifest ?? await (0, read_project_manifest_1.safeReadProjectManifestOnly)(dep.dir); let nodeExecPath; if (manifest?.name) { nodeExecPath = project.manifest.dependenciesMeta?.[manifest.name]?.node; } return { location: dep.dir, manifest, nodeExecPath, }; }))) .filter(({ manifest }) => manifest != null), project.binsDir, { extraNodePaths: ctx.extraNodePaths, preferSymlinkedExecutables: opts.preferSymlinkedExecutables, }); } const projectToInstall = projects[index]; if (opts.global && projectToInstall.mutation.includes('install')) { for (const pkg of projectToInstall.wantedDependencies) { // This warning is never printed currently during "pnpm link --global" // due to the following issue: https://github.com/pnpm/pnpm/issues/4761 if (pkg.alias && !linkedPackages?.includes(pkg.alias)) { logger_1.logger.warn({ message: `${pkg.alias} has