mirror of
https://github.com/pnpm/pnpm.git
synced 2026-03-30 04:52:04 -04:00
fix: only validate modules directory if required (e.g. for install) (#8657)
This commit is contained in:
5
.changeset/itchy-horses-wink.md
Normal file
5
.changeset/itchy-horses-wink.md
Normal file
@@ -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).
|
||||
5
.changeset/long-peaches-applaud.md
Normal file
5
.changeset/long-peaches-applaud.md
Normal file
@@ -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).
|
||||
5
.changeset/many-cooks-develop.md
Normal file
5
.changeset/many-cooks-develop.md
Normal file
@@ -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`).
|
||||
5
.changeset/perfect-spoons-call.md
Normal file
5
.changeset/perfect-spoons-call.md
Normal file
@@ -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).
|
||||
5
.changeset/pretty-houses-refuse.md
Normal file
5
.changeset/pretty-houses-refuse.md
Normal file
@@ -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).
|
||||
5
.changeset/spicy-apricots-beam.md
Normal file
5
.changeset/spicy-apricots-beam.md
Normal file
@@ -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.
|
||||
5
.changeset/tiny-terms-help.md
Normal file
5
.changeset/tiny-terms-help.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/get-context": major
|
||||
---
|
||||
|
||||
Argument `alreadyPurged` removed from `getContextForSingleImporter` [#8657](https://github.com/pnpm/pnpm/pull/8657).
|
||||
@@ -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:",
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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({
|
||||
|
||||
202
pkg-manager/core/src/install/validateModules.ts
Normal file
202
pkg-manager/core/src/install/validateModules.ts
Normal file
@@ -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<void> {
|
||||
return purgeModulesDirsOfImporters(opts, [importer])
|
||||
}
|
||||
|
||||
async function purgeModulesDirsOfImporters (
|
||||
opts: {
|
||||
confirmModulesPurge?: boolean
|
||||
virtualStoreDir: string
|
||||
},
|
||||
importers: ImporterToPurge[]
|
||||
): Promise<void> {
|
||||
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<void> {
|
||||
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(', ')
|
||||
}
|
||||
@@ -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:"
|
||||
},
|
||||
|
||||
@@ -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<ProjectOptions>>
|
||||
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<DepPath>
|
||||
storeDir: string
|
||||
wantedLockfile: Lockfile
|
||||
@@ -87,7 +87,6 @@ export interface GetContextOptions {
|
||||
allProjects: Array<ProjectOptions & HookOptions>
|
||||
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<PnpmContext> {
|
||||
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<void> {
|
||||
return purgeModulesDirsOfImporters(opts, [importer])
|
||||
}
|
||||
|
||||
async function purgeModulesDirsOfImporters (
|
||||
opts: {
|
||||
confirmModulesPurge?: boolean
|
||||
virtualStoreDir: string
|
||||
},
|
||||
importers: ImporterToPurge[]
|
||||
): Promise<void> {
|
||||
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<void> {
|
||||
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<string>
|
||||
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<PnpmSingleContext> {
|
||||
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,
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
{
|
||||
"path": "../../packages/core-loggers"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/error"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/logger"
|
||||
},
|
||||
|
||||
19
pnpm-lock.yaml
generated
19
pnpm-lock.yaml
generated
@@ -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==}
|
||||
|
||||
Reference in New Issue
Block a user