From 9ea8fa457b339548afe4cf5448d74b48295815e3 Mon Sep 17 00:00:00 2001 From: Jordan Date: Fri, 25 Oct 2024 02:45:49 +1100 Subject: [PATCH] fix: only validate modules directory if required (e.g. for install) (#8657) --- .changeset/itchy-horses-wink.md | 5 + .changeset/long-peaches-applaud.md | 5 + .changeset/many-cooks-develop.md | 5 + .changeset/perfect-spoons-call.md | 5 + .changeset/pretty-houses-refuse.md | 5 + .changeset/spicy-apricots-beam.md | 5 + .changeset/tiny-terms-help.md | 5 + pkg-manager/core/package.json | 2 + pkg-manager/core/src/index.ts | 4 +- .../checkCompatibility/BreakingChangeError.ts | 0 .../checkCompatibility/ErrorRelatedSources.ts | 0 .../ModulesBreakingChangeError.ts | 0 .../UnexpectedStoreError.ts | 0 .../UnexpectedVirtualStoreDirError.ts | 0 .../src/install}/checkCompatibility/index.ts | 0 pkg-manager/core/src/install/index.ts | 32 +- .../core/src/install/validateModules.ts | 202 ++++++++++++ pkg-manager/get-context/package.json | 3 - pkg-manager/get-context/src/index.ts | 289 ++---------------- pkg-manager/get-context/tsconfig.json | 3 - pnpm-lock.yaml | 19 +- 21 files changed, 303 insertions(+), 286 deletions(-) create mode 100644 .changeset/itchy-horses-wink.md create mode 100644 .changeset/long-peaches-applaud.md create mode 100644 .changeset/many-cooks-develop.md create mode 100644 .changeset/perfect-spoons-call.md create mode 100644 .changeset/pretty-houses-refuse.md create mode 100644 .changeset/spicy-apricots-beam.md create mode 100644 .changeset/tiny-terms-help.md rename pkg-manager/{get-context/src => core/src/install}/checkCompatibility/BreakingChangeError.ts (100%) rename pkg-manager/{get-context/src => core/src/install}/checkCompatibility/ErrorRelatedSources.ts (100%) rename pkg-manager/{get-context/src => core/src/install}/checkCompatibility/ModulesBreakingChangeError.ts (100%) rename pkg-manager/{get-context/src => core/src/install}/checkCompatibility/UnexpectedStoreError.ts (100%) rename pkg-manager/{get-context/src => core/src/install}/checkCompatibility/UnexpectedVirtualStoreDirError.ts (100%) rename pkg-manager/{get-context/src => core/src/install}/checkCompatibility/index.ts (100%) create mode 100644 pkg-manager/core/src/install/validateModules.ts diff --git a/.changeset/itchy-horses-wink.md b/.changeset/itchy-horses-wink.md new file mode 100644 index 0000000000..a95ea01fa2 --- /dev/null +++ b/.changeset/itchy-horses-wink.md @@ -0,0 +1,5 @@ +--- +"@pnpm/get-context": major +--- + +Don't validate (and possibly purge) modules directory as a side effect of `getContext` and `getContextForSingleImporter` [#8657](https://github.com/pnpm/pnpm/pull/8657). diff --git a/.changeset/long-peaches-applaud.md b/.changeset/long-peaches-applaud.md new file mode 100644 index 0000000000..371109f539 --- /dev/null +++ b/.changeset/long-peaches-applaud.md @@ -0,0 +1,5 @@ +--- +"pnpm": patch +--- + +Don't validate (and possibly purge) `node_modules` in commands which should not modify it (e.g. `pnpm install --lockfile-only`) [#8657](https://github.com/pnpm/pnpm/pull/8657). diff --git a/.changeset/many-cooks-develop.md b/.changeset/many-cooks-develop.md new file mode 100644 index 0000000000..486d70d539 --- /dev/null +++ b/.changeset/many-cooks-develop.md @@ -0,0 +1,5 @@ +--- +"@pnpm/get-context": major +--- + +`PnpmContext.hoistPattern` and `PnpmContext.publicHoistPattern` are no longer affected by modules directory state [#8657](https://github.com/pnpm/pnpm/pull/8657). Prior behavior can be recreated with the new properties `PnpmContext.currentHoistPattern` (`_.currentHoistPattern ?? _.hoistPattern`) and `PnpmContext.currentPublicHoistPattern` (`_.currentPublicHoistPattern ?? _.publicHoistPattern`). diff --git a/.changeset/perfect-spoons-call.md b/.changeset/perfect-spoons-call.md new file mode 100644 index 0000000000..1157d32485 --- /dev/null +++ b/.changeset/perfect-spoons-call.md @@ -0,0 +1,5 @@ +--- +"@pnpm/get-context": major +--- + +`PnpmSingleContext.hoistPattern` and `PnpmSingleContext.publicHoistPattern` are no longer affected by modules directory state [#8657](https://github.com/pnpm/pnpm/pull/8657). diff --git a/.changeset/pretty-houses-refuse.md b/.changeset/pretty-houses-refuse.md new file mode 100644 index 0000000000..ac38a45f67 --- /dev/null +++ b/.changeset/pretty-houses-refuse.md @@ -0,0 +1,5 @@ +--- +"@pnpm/core": patch +--- + +Don't validate (and possibly purge) modules directory in operations that do not mutate the structure (e.g. `mutateModules({ ... }, { ..., lockfileOnly: true })`) [#8657](https://github.com/pnpm/pnpm/pull/8657). diff --git a/.changeset/spicy-apricots-beam.md b/.changeset/spicy-apricots-beam.md new file mode 100644 index 0000000000..5a439ccab0 --- /dev/null +++ b/.changeset/spicy-apricots-beam.md @@ -0,0 +1,5 @@ +--- +"@pnpm/get-context": major +--- + +`UnexpectedStoreError` and `UnexpectedVirtualStoreDirError` are no longer exported [#8657](https://github.com/pnpm/pnpm/pull/8657). They can be imported from `@pnpm/core` instead. diff --git a/.changeset/tiny-terms-help.md b/.changeset/tiny-terms-help.md new file mode 100644 index 0000000000..7f1200a12b --- /dev/null +++ b/.changeset/tiny-terms-help.md @@ -0,0 +1,5 @@ +--- +"@pnpm/get-context": major +--- + +Argument `alreadyPurged` removed from `getContextForSingleImporter` [#8657](https://github.com/pnpm/pnpm/pull/8657). diff --git a/pkg-manager/core/package.json b/pkg-manager/core/package.json index 2223faf40d..f87683e9db 100644 --- a/pkg-manager/core/package.json +++ b/pkg-manager/core/package.json @@ -64,6 +64,8 @@ "@pnpm/types": "workspace:*", "@pnpm/which-version-is-pinned": "workspace:*", "@zkochan/rimraf": "catalog:", + "ci-info": "catalog:", + "enquirer": "catalog:", "is-inner-link": "catalog:", "is-subdir": "catalog:", "load-json-file": "catalog:", diff --git a/pkg-manager/core/src/index.ts b/pkg-manager/core/src/index.ts index 1dd92c3b90..9ced591287 100644 --- a/pkg-manager/core/src/index.ts +++ b/pkg-manager/core/src/index.ts @@ -9,7 +9,9 @@ export type { export type { HoistingLimits } from '@pnpm/headless' export * from './api' -export { type ProjectOptions, UnexpectedStoreError, UnexpectedVirtualStoreDirError } from '@pnpm/get-context' +export { type ProjectOptions } from '@pnpm/get-context' +export { UnexpectedStoreError } from './install/checkCompatibility/UnexpectedStoreError' +export { UnexpectedVirtualStoreDirError } from './install/checkCompatibility/UnexpectedVirtualStoreDirError' export type { InstallOptions } from './install/extendInstallOptions' export type { WorkspacePackages } from '@pnpm/resolver-base' diff --git a/pkg-manager/get-context/src/checkCompatibility/BreakingChangeError.ts b/pkg-manager/core/src/install/checkCompatibility/BreakingChangeError.ts similarity index 100% rename from pkg-manager/get-context/src/checkCompatibility/BreakingChangeError.ts rename to pkg-manager/core/src/install/checkCompatibility/BreakingChangeError.ts diff --git a/pkg-manager/get-context/src/checkCompatibility/ErrorRelatedSources.ts b/pkg-manager/core/src/install/checkCompatibility/ErrorRelatedSources.ts similarity index 100% rename from pkg-manager/get-context/src/checkCompatibility/ErrorRelatedSources.ts rename to pkg-manager/core/src/install/checkCompatibility/ErrorRelatedSources.ts diff --git a/pkg-manager/get-context/src/checkCompatibility/ModulesBreakingChangeError.ts b/pkg-manager/core/src/install/checkCompatibility/ModulesBreakingChangeError.ts similarity index 100% rename from pkg-manager/get-context/src/checkCompatibility/ModulesBreakingChangeError.ts rename to pkg-manager/core/src/install/checkCompatibility/ModulesBreakingChangeError.ts diff --git a/pkg-manager/get-context/src/checkCompatibility/UnexpectedStoreError.ts b/pkg-manager/core/src/install/checkCompatibility/UnexpectedStoreError.ts similarity index 100% rename from pkg-manager/get-context/src/checkCompatibility/UnexpectedStoreError.ts rename to pkg-manager/core/src/install/checkCompatibility/UnexpectedStoreError.ts diff --git a/pkg-manager/get-context/src/checkCompatibility/UnexpectedVirtualStoreDirError.ts b/pkg-manager/core/src/install/checkCompatibility/UnexpectedVirtualStoreDirError.ts similarity index 100% rename from pkg-manager/get-context/src/checkCompatibility/UnexpectedVirtualStoreDirError.ts rename to pkg-manager/core/src/install/checkCompatibility/UnexpectedVirtualStoreDirError.ts diff --git a/pkg-manager/get-context/src/checkCompatibility/index.ts b/pkg-manager/core/src/install/checkCompatibility/index.ts similarity index 100% rename from pkg-manager/get-context/src/checkCompatibility/index.ts rename to pkg-manager/core/src/install/checkCompatibility/index.ts diff --git a/pkg-manager/core/src/install/index.ts b/pkg-manager/core/src/install/index.ts index bd429fc181..eaf39944b4 100644 --- a/pkg-manager/core/src/install/index.ts +++ b/pkg-manager/core/src/install/index.ts @@ -84,6 +84,8 @@ import { } from './extendInstallOptions' import { linkPackages } from './link' import { reportPeerDependencyIssues } from './reportPeerDependencyIssues' +import { validateModules } from './validateModules' +import { isCI } from 'ci-info' class LockfileConfigMismatchError extends PnpmError { constructor (outdatedLockfileSettingName: string) { @@ -234,14 +236,38 @@ export async function mutateModules ( const installsOnly = allMutationsAreInstalls(projects) if (!installsOnly) opts.strictPeerDependencies = false - // @ts-expect-error - opts['forceNewModules'] = installsOnly 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 safeReadProjectManifestOnly(opts.lockfileDir) - const ctx = await getContext(opts) + let ctx = await getContext(opts) + + if (!opts.lockfileOnly && ctx.modulesFile != null) { + const { purged } = await 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 && !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 getContext(opts) + } + } if (opts.hooks.preResolution) { await opts.hooks.preResolution({ diff --git a/pkg-manager/core/src/install/validateModules.ts b/pkg-manager/core/src/install/validateModules.ts new file mode 100644 index 0000000000..8fd641918d --- /dev/null +++ b/pkg-manager/core/src/install/validateModules.ts @@ -0,0 +1,202 @@ +import { promises as fs } from 'fs' +import path from 'path' +import { PnpmError } from '@pnpm/error' +import { logger } from '@pnpm/logger' +import { + type IncludedDependencies, + type Modules, +} from '@pnpm/modules-yaml' +import { + DEPENDENCIES_FIELDS, + type Registries, + type ProjectRootDir, +} from '@pnpm/types' +import rimraf from '@zkochan/rimraf' +import enquirer from 'enquirer' +import equals from 'ramda/src/equals' +import { checkCompatibility } from './checkCompatibility' + +interface ImporterToPurge { + modulesDir: string + rootDir: ProjectRootDir +} + +export async function validateModules ( + modules: Modules, + projects: Array<{ + modulesDir: string + id: string + rootDir: ProjectRootDir + }>, + opts: { + currentHoistPattern?: string[] + currentPublicHoistPattern?: string[] + forceNewModules: boolean + include?: IncludedDependencies + lockfileDir: string + modulesDir: string + registries: Registries + storeDir: string + virtualStoreDir: string + virtualStoreDirMaxLength: number + confirmModulesPurge?: boolean + + hoistPattern?: string[] | undefined + forceHoistPattern?: boolean + + publicHoistPattern?: string[] | undefined + forcePublicHoistPattern?: boolean + global?: boolean + } +): Promise<{ purged: boolean }> { + const rootProject = projects.find(({ id }) => id === '.') + if (opts.virtualStoreDirMaxLength !== modules.virtualStoreDirMaxLength) { + if (opts.forceNewModules && (rootProject != null)) { + await purgeModulesDirsOfImporter(opts, rootProject) + return { purged: true } + } + throw new PnpmError( + 'VIRTUAL_STORE_DIR_MAX_LENGTH_DIFF', + 'This modules directory was created using a different virtual-store-dir-max-length value.' + + ' Run "pnpm install" to recreate the modules directory.' + ) + } + if ( + opts.forcePublicHoistPattern && + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + !equals(modules.publicHoistPattern, opts.publicHoistPattern || undefined) + ) { + if (opts.forceNewModules && (rootProject != null)) { + await purgeModulesDirsOfImporter(opts, rootProject) + return { purged: true } + } + throw new PnpmError( + 'PUBLIC_HOIST_PATTERN_DIFF', + 'This modules directory was created using a different public-hoist-pattern value.' + + ' Run "pnpm install" to recreate the modules directory.' + ) + } + + const importersToPurge: ImporterToPurge[] = [] + + if (opts.forceHoistPattern && (rootProject != null)) { + try { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (!equals(opts.currentHoistPattern, opts.hoistPattern || undefined)) { + throw new PnpmError( + 'HOIST_PATTERN_DIFF', + 'This modules directory was created using a different hoist-pattern value.' + + ' Run "pnpm install" to recreate the modules directory.' + ) + } + } catch (err: any) { // eslint-disable-line + if (!opts.forceNewModules) throw err + importersToPurge.push(rootProject) + } + } + for (const project of projects) { + try { + checkCompatibility(modules, { + modulesDir: project.modulesDir, + storeDir: opts.storeDir, + virtualStoreDir: opts.virtualStoreDir, + }) + if (opts.lockfileDir !== project.rootDir && (opts.include != null) && modules.included) { + for (const depsField of DEPENDENCIES_FIELDS) { + if (opts.include[depsField] !== modules.included[depsField]) { + throw new PnpmError('INCLUDED_DEPS_CONFLICT', + `modules directory (at "${opts.lockfileDir}") was installed with ${stringifyIncludedDeps(modules.included)}. ` + + `Current install wants ${stringifyIncludedDeps(opts.include)}.` + ) + } + } + } + } catch (err: any) { // eslint-disable-line + if (!opts.forceNewModules) throw err + importersToPurge.push(project) + } + } + if (importersToPurge.length > 0 && (rootProject == null)) { + importersToPurge.push({ + modulesDir: path.join(opts.lockfileDir, opts.modulesDir), + rootDir: opts.lockfileDir as ProjectRootDir, + }) + } + + const purged = importersToPurge.length > 0 + if (purged) { + await purgeModulesDirsOfImporters(opts, importersToPurge) + } + + return { purged } +} + +async function purgeModulesDirsOfImporter ( + opts: { + confirmModulesPurge?: boolean + virtualStoreDir: string + }, + importer: ImporterToPurge +): Promise { + return purgeModulesDirsOfImporters(opts, [importer]) +} + +async function purgeModulesDirsOfImporters ( + opts: { + confirmModulesPurge?: boolean + virtualStoreDir: string + }, + importers: ImporterToPurge[] +): Promise { + if (opts.confirmModulesPurge ?? true) { + const confirmed = await enquirer.prompt<{ question: boolean }>({ + type: 'confirm', + name: 'question', + message: importers.length === 1 + ? `The modules directory at "${importers[0].modulesDir}" will be removed and reinstalled from scratch. Proceed?` + : 'The modules directories will be removed and reinstalled from scratch. Proceed?', + initial: true, + }) + if (!confirmed.question) { + throw new PnpmError('ABORTED_REMOVE_MODULES_DIR', 'Aborted removal of modules directory') + } + } + await Promise.all(importers.map(async (importer) => { + logger.info({ + message: `Recreating ${importer.modulesDir}`, + prefix: importer.rootDir, + }) + try { + // We don't remove the actual modules directory, just the contents of it. + // 1. we will need the directory anyway. + // 2. in some setups, pnpm won't even have permission to remove the modules directory. + await removeContentsOfDir(importer.modulesDir, opts.virtualStoreDir) + } catch (err: any) { // eslint-disable-line + if (err.code !== 'ENOENT') throw err + } + })) +} + +async function removeContentsOfDir (dir: string, virtualStoreDir: string): Promise { + const items = await fs.readdir(dir) + await Promise.all(items.map(async (item) => { + // The non-pnpm related hidden files are kept + if ( + item.startsWith('.') && + item !== '.bin' && + item !== '.modules.yaml' && + !dirsAreEqual(path.join(dir, item), virtualStoreDir) + ) { + return + } + await rimraf(path.join(dir, item)) + })) +} + +function dirsAreEqual (dir1: string, dir2: string): boolean { + return path.relative(dir1, dir2) === '' +} + +function stringifyIncludedDeps (included: IncludedDependencies): string { + return DEPENDENCIES_FIELDS.filter((depsField) => included[depsField]).join(', ') +} diff --git a/pkg-manager/get-context/package.json b/pkg-manager/get-context/package.json index ae6621ff82..f4c67375c8 100644 --- a/pkg-manager/get-context/package.json +++ b/pkg-manager/get-context/package.json @@ -40,15 +40,12 @@ "dependencies": { "@pnpm/constants": "workspace:*", "@pnpm/core-loggers": "workspace:*", - "@pnpm/error": "workspace:*", "@pnpm/lockfile.fs": "workspace:*", "@pnpm/modules-yaml": "workspace:*", "@pnpm/read-projects-context": "workspace:*", "@pnpm/resolver-base": "workspace:*", "@pnpm/types": "workspace:*", - "@zkochan/rimraf": "catalog:", "ci-info": "catalog:", - "enquirer": "catalog:", "path-absolute": "catalog:", "ramda": "catalog:" }, diff --git a/pkg-manager/get-context/src/index.ts b/pkg-manager/get-context/src/index.ts index d29f73042e..9dcad4dac8 100644 --- a/pkg-manager/get-context/src/index.ts +++ b/pkg-manager/get-context/src/index.ts @@ -1,9 +1,7 @@ import { promises as fs } from 'fs' import path from 'path' import { contextLogger, packageManifestLogger } from '@pnpm/core-loggers' -import { PnpmError } from '@pnpm/error' import { type Lockfile } from '@pnpm/lockfile.fs' -import { logger } from '@pnpm/logger' import { type IncludedDependencies, type Modules, @@ -12,7 +10,6 @@ import { readProjectsContext } from '@pnpm/read-projects-context' import { type WorkspacePackages } from '@pnpm/resolver-base' import { type DepPath, - DEPENDENCIES_FIELDS, type HoistedDependencies, type ProjectId, type ProjectManifest, @@ -22,19 +19,14 @@ import { type ProjectRootDir, type ProjectRootDirRealPath, } from '@pnpm/types' -import rimraf from '@zkochan/rimraf' -import { isCI } from 'ci-info' -import enquirer from 'enquirer' import pathAbsolute from 'path-absolute' import clone from 'ramda/src/clone' -import equals from 'ramda/src/equals' -import { checkCompatibility } from './checkCompatibility' -import { UnexpectedStoreError } from './checkCompatibility/UnexpectedStoreError' -import { UnexpectedVirtualStoreDirError } from './checkCompatibility/UnexpectedVirtualStoreDirError' import { readLockfiles } from './readLockfiles' -export { UnexpectedStoreError, UnexpectedVirtualStoreDirError } - +/** + * Note that some fields are affected by modules directory state. Such fields should be used for + * mutating the modules directory only or in a manner that does not influence dependency resolution. + */ export interface PnpmContext { currentLockfile: Lockfile currentLockfileIsUpToDate: boolean @@ -42,9 +34,11 @@ export interface PnpmContext { existsWantedLockfile: boolean existsNonEmptyWantedLockfile: boolean extraBinPaths: string[] + /** Affected by existing modules directory, if it exists. */ extraNodePaths: string[] lockfileHadConflicts: boolean hoistedDependencies: HoistedDependencies + /** Required included dependencies or dependencies currently included by the modules directory. */ include: IncludedDependencies modulesFile: Modules | null pendingBuilds: string[] @@ -54,11 +48,17 @@ export interface PnpmContext { } & HookOptions & Required> rootModulesDir: string hoistPattern: string[] | undefined + /** As applied to existing modules directory, if it exists. */ + currentHoistPattern: string[] | undefined hoistedModulesDir: string publicHoistPattern: string[] | undefined + /** As applied to existing modules directory, if it exists. */ + currentPublicHoistPattern: string[] | undefined lockfileDir: string virtualStoreDir: string + /** As applied to existing modules directory, otherwise options. */ virtualStoreDirMaxLength: number + /** As applied to existing modules directory, if it exists. */ skipped: Set storeDir: string wantedLockfile: Lockfile @@ -87,7 +87,6 @@ export interface GetContextOptions { allProjects: Array confirmModulesPurge?: boolean force: boolean - forceNewModules?: boolean frozenLockfile?: boolean extraBinPaths: string[] extendNodePath?: boolean @@ -112,47 +111,14 @@ export interface GetContextOptions { forcePublicHoistPattern?: boolean global?: boolean } -interface ImporterToPurge { - modulesDir: string - rootDir: ProjectRootDir -} export async function getContext ( opts: GetContextOptions ): Promise { const modulesDir = opts.modulesDir ?? 'node_modules' - let importersContext = await readProjectsContext(opts.allProjects, { lockfileDir: opts.lockfileDir, modulesDir }) + const importersContext = await readProjectsContext(opts.allProjects, { lockfileDir: opts.lockfileDir, modulesDir }) const virtualStoreDir = pathAbsolute(opts.virtualStoreDir ?? path.join(modulesDir, '.pnpm'), opts.lockfileDir) - if (importersContext.modules != null) { - const { purged } = await validateModules(importersContext.modules, importersContext.projects, { - currentHoistPattern: importersContext.currentHoistPattern, - currentPublicHoistPattern: importersContext.currentPublicHoistPattern, - forceNewModules: opts.forceNewModules === true, - include: opts.include, - lockfileDir: opts.lockfileDir, - modulesDir, - registries: opts.registries, - storeDir: opts.storeDir, - virtualStoreDir, - virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength, - confirmModulesPurge: opts.confirmModulesPurge && !isCI, - - forceHoistPattern: opts.forceHoistPattern, - hoistPattern: opts.hoistPattern, - - forcePublicHoistPattern: opts.forcePublicHoistPattern, - publicHoistPattern: opts.publicHoistPattern, - global: opts.global, - }) - if (purged) { - importersContext = await readProjectsContext(opts.allProjects, { - lockfileDir: opts.lockfileDir, - modulesDir, - }) - } - } - await fs.mkdir(opts.storeDir, { recursive: true }) for (const project of opts.allProjects) { @@ -175,19 +141,20 @@ export async function getContext ( if (opts.hoistPattern?.length) { extraBinPaths.unshift(path.join(hoistedModulesDir, '.bin')) } - const hoistPattern = importersContext.currentHoistPattern ?? opts.hoistPattern const ctx: PnpmContext = { extraBinPaths, - extraNodePaths: getExtraNodePaths({ extendNodePath: opts.extendNodePath, nodeLinker: opts.nodeLinker, hoistPattern, virtualStoreDir }), + extraNodePaths: getExtraNodePaths({ extendNodePath: opts.extendNodePath, nodeLinker: opts.nodeLinker, hoistPattern: importersContext.currentHoistPattern ?? opts.hoistPattern, virtualStoreDir }), hoistedDependencies: importersContext.hoistedDependencies, hoistedModulesDir, - hoistPattern, + hoistPattern: opts.hoistPattern, + currentHoistPattern: importersContext.currentHoistPattern, include: opts.include ?? importersContext.include, lockfileDir: opts.lockfileDir, modulesFile: importersContext.modules, pendingBuilds: importersContext.pendingBuilds, projects: Object.fromEntries(importersContext.projects.map((project) => [project.rootDir, project])), - publicHoistPattern: importersContext.currentPublicHoistPattern ?? opts.publicHoistPattern, + publicHoistPattern: opts.publicHoistPattern, + currentPublicHoistPattern: importersContext.currentPublicHoistPattern, registries: opts.registries, rootModulesDir: importersContext.rootModulesDir, skipped: importersContext.skipped, @@ -218,192 +185,13 @@ export async function getContext ( return ctx } -async function validateModules ( - modules: Modules, - projects: Array<{ - modulesDir: string - id: string - rootDir: ProjectRootDir - }>, - opts: { - currentHoistPattern?: string[] - currentPublicHoistPattern?: string[] - forceNewModules: boolean - include?: IncludedDependencies - lockfileDir: string - modulesDir: string - registries: Registries - storeDir: string - virtualStoreDir: string - virtualStoreDirMaxLength: number - confirmModulesPurge?: boolean - - hoistPattern?: string[] | undefined - forceHoistPattern?: boolean - - publicHoistPattern?: string[] | undefined - forcePublicHoistPattern?: boolean - global?: boolean - } -): Promise<{ purged: boolean }> { - const rootProject = projects.find(({ id }) => id === '.') - if (opts.virtualStoreDirMaxLength !== modules.virtualStoreDirMaxLength) { - if (opts.forceNewModules && (rootProject != null)) { - await purgeModulesDirsOfImporter(opts, rootProject) - return { purged: true } - } - throw new PnpmError( - 'VIRTUAL_STORE_DIR_MAX_LENGTH_DIFF', - 'This modules directory was created using a different virtual-store-dir-max-length value.' + - ' Run "pnpm install" to recreate the modules directory.' - ) - } - if ( - opts.forcePublicHoistPattern && - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - !equals(modules.publicHoistPattern, opts.publicHoistPattern || undefined) - ) { - if (opts.forceNewModules && (rootProject != null)) { - await purgeModulesDirsOfImporter(opts, rootProject) - return { purged: true } - } - throw new PnpmError( - 'PUBLIC_HOIST_PATTERN_DIFF', - 'This modules directory was created using a different public-hoist-pattern value.' + - ' Run "pnpm install" to recreate the modules directory.' - ) - } - - const importersToPurge: ImporterToPurge[] = [] - - if (opts.forceHoistPattern && (rootProject != null)) { - try { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (!equals(opts.currentHoistPattern, opts.hoistPattern || undefined)) { - throw new PnpmError( - 'HOIST_PATTERN_DIFF', - 'This modules directory was created using a different hoist-pattern value.' + - ' Run "pnpm install" to recreate the modules directory.' - ) - } - } catch (err: any) { // eslint-disable-line - if (!opts.forceNewModules) throw err - importersToPurge.push(rootProject) - } - } - for (const project of projects) { - try { - checkCompatibility(modules, { - modulesDir: project.modulesDir, - storeDir: opts.storeDir, - virtualStoreDir: opts.virtualStoreDir, - }) - if (opts.lockfileDir !== project.rootDir && (opts.include != null) && modules.included) { - for (const depsField of DEPENDENCIES_FIELDS) { - if (opts.include[depsField] !== modules.included[depsField]) { - throw new PnpmError('INCLUDED_DEPS_CONFLICT', - `modules directory (at "${opts.lockfileDir}") was installed with ${stringifyIncludedDeps(modules.included)}. ` + - `Current install wants ${stringifyIncludedDeps(opts.include)}.` - ) - } - } - } - } catch (err: any) { // eslint-disable-line - if (!opts.forceNewModules) throw err - importersToPurge.push(project) - } - } - if (importersToPurge.length > 0 && (rootProject == null)) { - importersToPurge.push({ - modulesDir: path.join(opts.lockfileDir, opts.modulesDir), - rootDir: opts.lockfileDir as ProjectRootDir, - }) - } - - const purged = importersToPurge.length > 0 - if (purged) { - await purgeModulesDirsOfImporters(opts, importersToPurge) - } - - return { purged } -} - -async function purgeModulesDirsOfImporter ( - opts: { - confirmModulesPurge?: boolean - virtualStoreDir: string - }, - importer: ImporterToPurge -): Promise { - return purgeModulesDirsOfImporters(opts, [importer]) -} - -async function purgeModulesDirsOfImporters ( - opts: { - confirmModulesPurge?: boolean - virtualStoreDir: string - }, - importers: ImporterToPurge[] -): Promise { - if (opts.confirmModulesPurge ?? true) { - const confirmed = await enquirer.prompt<{ question: boolean }>({ - type: 'confirm', - name: 'question', - message: importers.length === 1 - ? `The modules directory at "${importers[0].modulesDir}" will be removed and reinstalled from scratch. Proceed?` - : 'The modules directories will be removed and reinstalled from scratch. Proceed?', - initial: true, - }) - if (!confirmed.question) { - throw new PnpmError('ABORTED_REMOVE_MODULES_DIR', 'Aborted removal of modules directory') - } - } - await Promise.all(importers.map(async (importer) => { - logger.info({ - message: `Recreating ${importer.modulesDir}`, - prefix: importer.rootDir, - }) - try { - // We don't remove the actual modules directory, just the contents of it. - // 1. we will need the directory anyway. - // 2. in some setups, pnpm won't even have permission to remove the modules directory. - await removeContentsOfDir(importer.modulesDir, opts.virtualStoreDir) - } catch (err: any) { // eslint-disable-line - if (err.code !== 'ENOENT') throw err - } - })) -} - -async function removeContentsOfDir (dir: string, virtualStoreDir: string): Promise { - const items = await fs.readdir(dir) - await Promise.all(items.map(async (item) => { - // The non-pnpm related hidden files are kept - if ( - item.startsWith('.') && - item !== '.bin' && - item !== '.modules.yaml' && - !dirsAreEqual(path.join(dir, item), virtualStoreDir) - ) { - return - } - await rimraf(path.join(dir, item)) - })) -} - -function dirsAreEqual (dir1: string, dir2: string): boolean { - return path.relative(dir1, dir2) === '' -} - -function stringifyIncludedDeps (included: IncludedDependencies): string { - return DEPENDENCIES_FIELDS.filter((depsField) => included[depsField]).join(', ') -} - export interface PnpmSingleContext { currentLockfile: Lockfile currentLockfileIsUpToDate: boolean existsCurrentLockfile: boolean existsWantedLockfile: boolean existsNonEmptyWantedLockfile: boolean + /** Affected by existing modules directory, if it exists. */ extraBinPaths: string[] extraNodePaths: string[] lockfileHadConflicts: boolean @@ -414,6 +202,7 @@ export interface PnpmSingleContext { modulesDir: string importerId: string prefix: string + /** Required included dependencies or dependencies currently included by the modules directory. */ include: IncludedDependencies modulesFile: Modules | null pendingBuilds: string[] @@ -422,6 +211,7 @@ export interface PnpmSingleContext { rootModulesDir: string lockfileDir: string virtualStoreDir: string + /** As applied to existing modules directory, if it exists. */ skipped: Set storeDir: string wantedLockfile: Lockfile @@ -435,7 +225,6 @@ export async function getContextForSingleImporter ( excludeLinksFromLockfile: boolean peersSuffixMaxLength: number force: boolean - forceNewModules?: boolean confirmModulesPurge?: boolean extraBinPaths: string[] extendNodePath?: boolean @@ -458,12 +247,10 @@ export async function getContextForSingleImporter ( publicHoistPattern?: string[] | undefined forcePublicHoistPattern?: boolean - }, - alreadyPurged: boolean = false + } ): Promise { const { currentHoistPattern, - currentPublicHoistPattern, hoistedDependencies, projects, include, @@ -491,31 +278,6 @@ export async function getContextForSingleImporter ( const importerId = importer.id const virtualStoreDir = pathAbsolute(opts.virtualStoreDir ?? 'node_modules/.pnpm', opts.lockfileDir) - if ((modules != null) && !alreadyPurged) { - const { purged } = await validateModules(modules, projects, { - currentHoistPattern, - currentPublicHoistPattern, - forceNewModules: opts.forceNewModules === true, - include: opts.include, - lockfileDir: opts.lockfileDir, - modulesDir: opts.modulesDir ?? 'node_modules', - registries: opts.registries, - storeDir: opts.storeDir, - virtualStoreDir, - virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength, - confirmModulesPurge: opts.confirmModulesPurge && !isCI, - - forceHoistPattern: opts.forceHoistPattern, - hoistPattern: opts.hoistPattern, - - forcePublicHoistPattern: opts.forcePublicHoistPattern, - publicHoistPattern: opts.publicHoistPattern, - }) - if (purged) { - return getContextForSingleImporter(manifest, opts, true) - } - } - await fs.mkdir(storeDir, { recursive: true }) const extraBinPaths = [ ...opts.extraBinPaths || [], @@ -524,13 +286,12 @@ export async function getContextForSingleImporter ( if (opts.hoistPattern?.length) { extraBinPaths.unshift(path.join(hoistedModulesDir, '.bin')) } - const hoistPattern = currentHoistPattern ?? opts.hoistPattern const ctx: PnpmSingleContext = { extraBinPaths, - extraNodePaths: getExtraNodePaths({ extendNodePath: opts.extendNodePath, nodeLinker: opts.nodeLinker, hoistPattern, virtualStoreDir }), + extraNodePaths: getExtraNodePaths({ extendNodePath: opts.extendNodePath, nodeLinker: opts.nodeLinker, hoistPattern: currentHoistPattern ?? opts.hoistPattern, virtualStoreDir }), hoistedDependencies, hoistedModulesDir, - hoistPattern, + hoistPattern: opts.hoistPattern, importerId, include: opts.include ?? include, lockfileDir: opts.lockfileDir, @@ -539,7 +300,7 @@ export async function getContextForSingleImporter ( modulesFile: modules, pendingBuilds, prefix: opts.dir, - publicHoistPattern: currentPublicHoistPattern ?? opts.publicHoistPattern, + publicHoistPattern: opts.publicHoistPattern, registries: { ...opts.registries, ...registries, diff --git a/pkg-manager/get-context/tsconfig.json b/pkg-manager/get-context/tsconfig.json index fd19ba4562..7566b8f34e 100644 --- a/pkg-manager/get-context/tsconfig.json +++ b/pkg-manager/get-context/tsconfig.json @@ -18,9 +18,6 @@ { "path": "../../packages/core-loggers" }, - { - "path": "../../packages/error" - }, { "path": "../../packages/logger" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd2d7d356e..5780f8fc2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4219,6 +4219,12 @@ importers: '@zkochan/rimraf': specifier: 'catalog:' version: 3.0.2 + ci-info: + specifier: 'catalog:' + version: 3.9.0 + enquirer: + specifier: 'catalog:' + version: 2.4.1 is-inner-link: specifier: 'catalog:' version: 4.0.0 @@ -4316,9 +4322,6 @@ importers: '@yarnpkg/core': specifier: 'catalog:' version: 4.0.5(typanion@3.14.0) - ci-info: - specifier: 'catalog:' - version: 3.9.0 deep-require-cwd: specifier: 'catalog:' version: 1.0.0 @@ -4395,9 +4398,6 @@ importers: '@pnpm/core-loggers': specifier: workspace:* version: link:../../packages/core-loggers - '@pnpm/error': - specifier: workspace:* - version: link:../../packages/error '@pnpm/lockfile.fs': specifier: workspace:* version: link:../../lockfile/fs @@ -4413,15 +4413,9 @@ importers: '@pnpm/types': specifier: workspace:* version: link:../../packages/types - '@zkochan/rimraf': - specifier: 'catalog:' - version: 3.0.2 ci-info: specifier: 'catalog:' version: 3.9.0 - enquirer: - specifier: 'catalog:' - version: 2.4.1 path-absolute: specifier: 'catalog:' version: 1.0.1 @@ -13879,6 +13873,7 @@ packages: uid-number@0.0.6: resolution: {integrity: sha512-c461FXIljswCuscZn67xq9PpszkPT6RjheWFQTgCyabJrTUozElanb0YEqv2UGgk247YpcJkFBuSGNvBlpXM9w==} + deprecated: This package is no longer supported. umask@1.1.0: resolution: {integrity: sha512-lE/rxOhmiScJu9L6RTNVgB/zZbF+vGC0/p6D3xnkAePI2o0sMyFG966iR5Ki50OI/0mNi2yaRnxfLsPmEZF/JA==}