perf: faster repeat install (#8838)

This commit is contained in:
Zoltan Kochan
2024-12-08 23:42:33 +01:00
committed by GitHub
parent 6483b646fe
commit d47c4266db
45 changed files with 587 additions and 277 deletions

View File

@@ -0,0 +1,9 @@
---
"@pnpm/plugin-commands-installation": minor
"pnpm": minor
"@pnpm/pnpmfile": minor
"@pnpm/workspace.state": major
"@pnpm/deps.status": major
---
On repeat install perform a fast check if `node_modules` is up to date [#8838](https://github.com/pnpm/pnpm/pull/8838).

View File

@@ -40,6 +40,7 @@
"@pnpm/lockfile.settings-checker": "workspace:*",
"@pnpm/lockfile.verification": "workspace:*",
"@pnpm/parse-overrides": "workspace:*",
"@pnpm/pnpmfile": "workspace:*",
"@pnpm/resolver-base": "workspace:*",
"@pnpm/types": "workspace:*",
"@pnpm/workspace.find-packages": "workspace:*",

View File

@@ -28,6 +28,7 @@ import {
} from '@pnpm/lockfile.verification'
import { globalWarn, logger } from '@pnpm/logger'
import { parseOverrides } from '@pnpm/parse-overrides'
import { getPnpmfilePath } from '@pnpm/pnpmfile'
import { type WorkspacePackages } from '@pnpm/resolver-base'
import {
type DependencyManifest,
@@ -39,7 +40,8 @@ import { findWorkspacePackages } from '@pnpm/workspace.find-packages'
import { readWorkspaceManifest } from '@pnpm/workspace.read-manifest'
import { loadWorkspaceState, updateWorkspaceState } from '@pnpm/workspace.state'
import { assertLockfilesEqual } from './assertLockfilesEqual'
import { statManifestFile } from './statManifestFile'
import { safeStat, safeStatSync, statManifestFile } from './statManifestFile'
import { type WorkspaceStateSettings } from '@pnpm/workspace.state/src/types'
export type CheckDepsStatusOptions = Pick<Config,
| 'allProjects'
@@ -55,9 +57,13 @@ export type CheckDepsStatusOptions = Pick<Config,
| 'sharedWorkspaceLockfile'
| 'virtualStoreDir'
| 'workspaceDir'
>
| 'patchesDir'
| 'pnpmfile'
> & {
ignoreFilteredInstallCache?: boolean
} & WorkspaceStateSettings
export async function checkDepsStatus (opts: CheckDepsStatusOptions): Promise<{ upToDate: boolean, issue?: string }> {
export async function checkDepsStatus (opts: CheckDepsStatusOptions): Promise<{ upToDate: boolean | undefined, issue?: string }> {
try {
return await _checkDepsStatus(opts)
} catch (error) {
@@ -71,7 +77,7 @@ export async function checkDepsStatus (opts: CheckDepsStatusOptions): Promise<{
}
}
async function _checkDepsStatus (opts: CheckDepsStatusOptions): Promise<{ upToDate: boolean, issue?: string }> {
async function _checkDepsStatus (opts: CheckDepsStatusOptions): Promise<{ upToDate: boolean | undefined, issue?: string }> {
const {
allProjects,
autoInstallPeers,
@@ -89,17 +95,31 @@ async function _checkDepsStatus (opts: CheckDepsStatusOptions): Promise<{ upToDa
? getOptionsFromRootManifest(rootProjectManifestDir, rootProjectManifest)
: undefined
if (allProjects && workspaceDir) {
const workspaceState = loadWorkspaceState(workspaceDir)
if (!workspaceState) {
const workspaceState = loadWorkspaceState(workspaceDir ?? rootProjectManifestDir)
if (!workspaceState) {
return {
upToDate: false,
issue: 'Cannot check whether dependencies are outdated',
}
}
if (opts.ignoreFilteredInstallCache && workspaceState.filteredInstall) {
return { upToDate: undefined }
}
for (const [settingName, settingValue] of Object.entries(workspaceState.settings)) {
if (settingName === 'catalogs') continue
// @ts-expect-error
if (!equals(settingValue, opts[settingName])) {
return {
upToDate: false,
issue: 'Cannot check whether dependencies are outdated',
issue: `The value of the ${settingName} setting has changed`,
}
}
}
if (allProjects && workspaceDir) {
if (!equals(
filter(value => value != null, workspaceState.catalogs ?? {}),
filter(value => value != null, workspaceState.settings.catalogs ?? {}),
filter(value => value != null, catalogs ?? {})
)) {
return {
@@ -108,8 +128,13 @@ async function _checkDepsStatus (opts: CheckDepsStatusOptions): Promise<{ upToDa
}
}
const currentProjectRootDirs = allProjects.map(project => project.rootDir).sort()
if (!equals(workspaceState.projectRootDirs, currentProjectRootDirs)) {
if (allProjects.length !== Object.keys(workspaceState.projects).length ||
!allProjects.every((currentProject) => {
const prevProject = workspaceState.projects[currentProject.rootDir]
if (!prevProject) return false
return prevProject.name === currentProject.manifest.name && (prevProject.version ?? '0.0.0') === (currentProject.manifest.version ?? '0.0.0')
})
) {
return {
upToDate: false,
issue: 'The workspace structure has changed since last install',
@@ -135,6 +160,17 @@ async function _checkDepsStatus (opts: CheckDepsStatusOptions): Promise<{ upToDa
return { upToDate: true }
}
const issue = await patchesAreModified({
rootManifestOptions,
rootDir: rootProjectManifestDir,
lastValidatedTimestamp: workspaceState.lastValidatedTimestamp,
pnpmfile: opts.pnpmfile,
hadPnpmfile: workspaceState.pnpmfileExists,
})
if (issue) {
return { upToDate: false, issue }
}
logger.debug({ msg: 'Some manifest files were modified since the last validation. Continuing check.' })
let readWantedLockfileAndDir: (projectDir: string) => Promise<{
@@ -204,23 +240,28 @@ async function _checkDepsStatus (opts: CheckDepsStatusOptions): Promise<{ upToDa
rootManifestOptions,
}
await Promise.all(modifiedProjects.map(async ({ project }) => {
const { wantedLockfile, wantedLockfileDir } = await readWantedLockfileAndDir(project.rootDir)
await assertWantedLockfileUpToDate(assertCtx, {
projectDir: project.rootDir,
projectId: getProjectId(project),
projectManifest: project.manifest,
wantedLockfile,
wantedLockfileDir,
})
}))
try {
await Promise.all(modifiedProjects.map(async ({ project }) => {
const { wantedLockfile, wantedLockfileDir } = await readWantedLockfileAndDir(project.rootDir)
await assertWantedLockfileUpToDate(assertCtx, {
projectDir: project.rootDir,
projectId: getProjectId(project),
projectManifest: project.manifest,
wantedLockfile,
wantedLockfileDir,
})
}))
} catch (err) {
return { upToDate: false, issue: (util.types.isNativeError(err) && 'message' in err) ? err.message : undefined }
}
// update lastValidatedTimestamp to prevent pointless repeat
await updateWorkspaceState({
allProjects,
catalogs,
lastValidatedTimestamp: Date.now(),
workspaceDir,
pnpmfileExists: workspaceState.pnpmfileExists,
settings: opts,
filteredInstall: workspaceState.filteredInstall,
})
return { upToDate: true }
@@ -260,6 +301,17 @@ async function _checkDepsStatus (opts: CheckDepsStatusOptions): Promise<{ upToDa
if (!wantedLockfileStats) return throwLockfileNotFound(rootProjectManifestDir)
const issue = await patchesAreModified({
rootManifestOptions,
rootDir: rootProjectManifestDir,
lastValidatedTimestamp: wantedLockfileStats.mtime.valueOf(),
pnpmfile: opts.pnpmfile,
hadPnpmfile: workspaceState.pnpmfileExists,
})
if (issue) {
return { upToDate: false, issue }
}
if (currentLockfileStats && wantedLockfileStats.mtime.valueOf() > currentLockfileStats.mtime.valueOf()) {
const currentLockfile = await currentLockfilePromise
const wantedLockfile = (await wantedLockfilePromise) ?? throwLockfileNotFound(rootProjectManifestDir)
@@ -273,23 +325,27 @@ async function _checkDepsStatus (opts: CheckDepsStatusOptions): Promise<{ upToDa
if (manifestStats.mtime.valueOf() > wantedLockfileStats.mtime.valueOf()) {
logger.debug({ msg: 'The manifest is newer than the lockfile. Continuing check.' })
await assertWantedLockfileUpToDate({
autoInstallPeers,
injectWorkspacePackages,
config: opts,
excludeLinksFromLockfile,
linkWorkspacePackages,
getManifestsByDir: () => ({}),
getWorkspacePackages: () => undefined,
rootDir: rootProjectManifestDir,
rootManifestOptions,
}, {
projectDir: rootProjectManifestDir,
projectId: '.' as ProjectId,
projectManifest: rootProjectManifest,
wantedLockfile: (await wantedLockfilePromise) ?? throwLockfileNotFound(rootProjectManifestDir),
wantedLockfileDir: rootProjectManifestDir,
})
try {
await assertWantedLockfileUpToDate({
autoInstallPeers,
injectWorkspacePackages,
config: opts,
excludeLinksFromLockfile,
linkWorkspacePackages,
getManifestsByDir: () => ({}),
getWorkspacePackages: () => undefined,
rootDir: rootProjectManifestDir,
rootManifestOptions,
}, {
projectDir: rootProjectManifestDir,
projectId: '.' as ProjectId,
projectManifest: rootProjectManifest,
wantedLockfile: (await wantedLockfilePromise) ?? throwLockfileNotFound(rootProjectManifestDir),
wantedLockfileDir: rootProjectManifestDir,
})
} catch (err) {
return { upToDate: false, issue: (util.types.isNativeError(err) && 'message' in err) ? err.message : undefined }
}
} else if (currentLockfileStats) {
logger.debug({ msg: 'The manifest file is not newer than the lockfile. Exiting check.' })
} else {
@@ -308,7 +364,7 @@ async function _checkDepsStatus (opts: CheckDepsStatusOptions): Promise<{ upToDa
// `rootProjectManifest` being `undefined` means that there's no root manifest.
// Both means that `pnpm run` would fail, so checking lockfiles here is pointless.
globalWarn('Skipping check.')
return { upToDate: true }
return { upToDate: undefined }
}
interface AssertWantedLockfileUpToDateContext {
@@ -428,3 +484,35 @@ function throwLockfileNotFound (wantedLockfileDir: string): never {
hint: 'Run `pnpm install` to create the lockfile',
})
}
async function patchesAreModified (opts: {
rootManifestOptions: OptionsFromRootManifest | undefined
rootDir: string
lastValidatedTimestamp: number
pnpmfile: string
hadPnpmfile: boolean
}): Promise<string | undefined> {
if (opts.rootManifestOptions?.patchedDependencies) {
const allPatchStats = await Promise.all(Object.values(opts.rootManifestOptions.patchedDependencies).map((patchFile) => {
return safeStat(path.relative(opts.rootDir, patchFile))
}))
if (allPatchStats.some(
(patch) =>
patch && patch.mtime.valueOf() > opts.lastValidatedTimestamp
)) {
return 'Patches were modified'
}
}
const pnpmfilePath = getPnpmfilePath(opts.rootDir, opts.pnpmfile)
const pnpmfileStats = safeStatSync(pnpmfilePath)
if (pnpmfileStats != null && pnpmfileStats.mtime.valueOf() > opts.lastValidatedTimestamp) {
return `pnpmfile at "${pnpmfilePath}" was modified`
}
if (opts.hadPnpmfile && pnpmfileStats == null) {
return `pnpmfile at "${pnpmfilePath}" was removed`
}
if (!opts.hadPnpmfile && pnpmfileStats != null) {
return `pnpmfile at "${pnpmfilePath}" was added`
}
return undefined
}

View File

@@ -4,18 +4,31 @@ import util from 'util'
import { MANIFEST_BASE_NAMES } from '@pnpm/constants'
export async function statManifestFile (projectRootDir: string): Promise<fs.Stats | undefined> {
const attempts = await Promise.all(MANIFEST_BASE_NAMES.map(async baseName => {
const attempts = await Promise.all(MANIFEST_BASE_NAMES.map((baseName) => {
const manifestPath = path.join(projectRootDir, baseName)
let stats: fs.Stats
try {
stats = await fs.promises.stat(manifestPath)
} catch (error) {
if (util.types.isNativeError(error) && 'code' in error && error.code === 'ENOENT') {
return undefined
}
throw error
}
return stats
return safeStat(manifestPath)
}))
return attempts.find(stats => stats != null)
}
export async function safeStat (filePath: string): Promise<fs.Stats | undefined> {
try {
return await fs.promises.stat(filePath)
} catch (error) {
if (util.types.isNativeError(error) && 'code' in error && error.code === 'ENOENT') {
return undefined
}
throw error
}
}
export function safeStatSync (filePath: string): fs.Stats | undefined {
try {
return fs.statSync(filePath)
} catch (error) {
if (util.types.isNativeError(error) && 'code' in error && error.code === 'ENOENT') {
return undefined
}
throw error
}
}

View File

@@ -21,6 +21,9 @@
{
"path": "../../crypto/object-hasher"
},
{
"path": "../../hooks/pnpmfile"
},
{
"path": "../../lockfile/fs"
},

View File

@@ -39,6 +39,7 @@ export const DEFAULT_OPTS = {
pending: false,
pnpmfile: './.pnpmfile.cjs',
pnpmHomeDir: '',
preferWorkspacePackages: true,
proxy: undefined,
rawConfig: { registry: REGISTRY_URL },
rawLocalConfig: {},
@@ -67,6 +68,7 @@ export const DLX_DEFAULT_OPTS = {
bail: false,
bin: 'node_modules/.bin',
cacheDir: path.join(tmp, 'cache'),
excludeLinksFromLockfile: false,
extraEnv: {},
extraBinPaths: [],
cliOptions: {},
@@ -80,6 +82,7 @@ export const DLX_DEFAULT_OPTS = {
lock: true,
pnpmfile: '.pnpmfile.cjs',
pnpmHomeDir: '',
preferWorkspacePackages: true,
rawConfig: { registry: REGISTRY_URL },
rawLocalConfig: { registry: REGISTRY_URL },
registries: {

View File

@@ -56,7 +56,7 @@ test('throw an error if verifyDepsBeforeRun is set to error', async () => {
} catch (_err) {
err = _err as Error
}
expect(err.message).toContain('Cannot find a lockfile in')
expect(err.message).toContain('Cannot check whether dependencies are outdated')
})
test('install the dependencies if verifyDepsBeforeRun is set to install', async () => {

View File

@@ -1,5 +1,6 @@
import type { CookedHooks } from './requireHooks'
export { getPnpmfilePath } from './getPnpmfilePath'
export { requireHooks } from './requireHooks'
export { requirePnpmfile, BadReadPackageHookError } from './requirePnpmfile'
export type { HookContext } from './Hooks'

View File

@@ -23,6 +23,7 @@ export const DEFAULT_OPTS = {
ca: undefined,
cacheDir: '../cache',
cert: undefined,
excludeLinksFromLockfile: false,
extraEnv: {},
cliOptions: {},
fetchRetries: 2,
@@ -46,6 +47,7 @@ export const DEFAULT_OPTS = {
pending: false,
pnpmfile: './.pnpmfile.cjs',
pnpmHomeDir: '',
preferWorkspacePackages: true,
proxy: undefined,
rawConfig,
rawLocalConfig: {},

View File

@@ -24,7 +24,7 @@
"meta-updater": "pnpm --filter=@pnpm-private/updater compile && pnpm exec meta-updater",
"lint:meta": "pnpm run meta-updater --test",
"copy-artifacts": "ts-node __utils__/scripts/src/copy-artifacts.ts",
"make-release-description": "pnpm --filter=@pnpm/get-release-text run write-release-text",
"make-release-description": "pnpm --filter=@pnpm/get-release-text run write-release-ext",
"release": "pnpm --filter=@pnpm/exe publish --tag=next-10 --access=public && pnpm publish --filter=!pnpm --filter=!@pnpm/exe --access=public && pnpm publish --filter=pnpm --tag=next-10 --access=public",
"dev-setup": "pnpm -C=./pnpm/dev link -g"
},

View File

@@ -11,6 +11,7 @@ export const DEFAULT_OPTS = {
ca: undefined,
cacheDir: '../cache',
cert: undefined,
excludeLinksFromLockfile: false,
extraEnv: {},
cliOptions: {},
fetchRetries: 2,
@@ -34,6 +35,7 @@ export const DEFAULT_OPTS = {
pending: false,
pnpmfile: './.pnpmfile.cjs',
pnpmHomeDir: '',
preferWorkspacePackages: true,
proxy: undefined,
rawConfig: { registry: REGISTRY },
rawLocalConfig: {},

View File

@@ -66,6 +66,7 @@
"@pnpm/constants": "workspace:*",
"@pnpm/core": "workspace:*",
"@pnpm/dedupe.check": "workspace:*",
"@pnpm/deps.status": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/filter-workspace-packages": "workspace:*",
"@pnpm/find-workspace-dir": "workspace:*",

View File

@@ -260,6 +260,7 @@ export type InstallCommandOptions = Pick<Config,
| 'depth'
| 'dev'
| 'engineStrict'
| 'excludeLinksFromLockfile'
| 'frozenLockfile'
| 'global'
| 'globalPnpmfile'
@@ -274,6 +275,7 @@ export type InstallCommandOptions = Pick<Config,
| 'nodeLinker'
| 'pnpmfile'
| 'preferFrozenLockfile'
| 'preferWorkspacePackages'
| 'production'
| 'registries'
| 'rootProjectManifest'

View File

@@ -4,6 +4,7 @@ import {
tryReadProjectManifest,
} from '@pnpm/cli-utils'
import { type Config, getOptionsFromRootManifest } from '@pnpm/config'
import { checkDepsStatus } from '@pnpm/deps.status'
import { PnpmError } from '@pnpm/error'
import { arrayOfWorkspacePackagesToMap } from '@pnpm/get-context'
import { filterPkgsBySelectorObjects } from '@pnpm/filter-workspace-packages'
@@ -19,10 +20,10 @@ import {
type MutateModulesOptions,
type WorkspacePackages,
} from '@pnpm/core'
import { logger } from '@pnpm/logger'
import { globalInfo, logger } from '@pnpm/logger'
import { sequenceGraph } from '@pnpm/sort-packages'
import { createPkgGraph } from '@pnpm/workspace.pkgs-graph'
import { updateWorkspaceState } from '@pnpm/workspace.state'
import { updateWorkspaceState, type WorkspaceStateSettings } from '@pnpm/workspace.state'
import isSubdir from 'is-subdir'
import { getPinnedVersion } from './getPinnedVersion'
import { getSaveType } from './getSaveType'
@@ -55,6 +56,7 @@ export type InstallDepsOptions = Pick<Config,
| 'depth'
| 'dev'
| 'engineStrict'
| 'excludeLinksFromLockfile'
| 'global'
| 'globalPnpmfile'
| 'hooks'
@@ -66,6 +68,7 @@ export type InstallDepsOptions = Pick<Config,
| 'lockfileOnly'
| 'pnpmfile'
| 'production'
| 'preferWorkspacePackages'
| 'rawLocalConfig'
| 'registries'
| 'rootProjectManifestDir'
@@ -133,6 +136,16 @@ export async function installDeps (
opts: InstallDepsOptions,
params: string[]
): Promise<void> {
if (!opts.update && !opts.dedupe && params.length === 0) {
const { upToDate } = await checkDepsStatus({
...opts,
ignoreFilteredInstallCache: true,
})
if (upToDate) {
globalInfo('Already up to date')
return
}
}
if (opts.workspace) {
if (opts.latest) {
throw new PnpmError('BAD_OPTIONS', 'Cannot use --latest with --workspace simultaneously')
@@ -308,6 +321,15 @@ when running add/update with the --workspace option')
if (opts.save !== false) {
await writeProjectManifest(updatedImporter.manifest)
}
if (!opts.lockfileOnly) {
await updateWorkspaceState({
allProjects,
settings: opts,
workspaceDir: opts.workspaceDir ?? opts.lockfileDir ?? opts.dir,
pnpmfileExists: opts.hooks?.calculatePnpmfileChecksum != null,
filteredInstall: allProjects.length !== Object.keys(opts.selectedProjectsGraph ?? {}).length,
})
}
return
}
@@ -351,6 +373,16 @@ when running add/update with the --workspace option')
skipIfHasSideEffectsCache: true,
}
)
} else {
if (!opts.lockfileOnly) {
await updateWorkspaceState({
allProjects,
settings: opts,
workspaceDir: opts.workspaceDir ?? opts.lockfileDir ?? opts.dir,
pnpmfileExists: opts.hooks?.calculatePnpmfileChecksum != null,
filteredInstall: allProjects.length !== Object.keys(opts.selectedProjectsGraph ?? {}).length,
})
}
}
}
@@ -363,15 +395,18 @@ function selectProjectByDir (projects: Project[], searchedDir: string): Projects
async function recursiveInstallThenUpdateWorkspaceState (
allProjects: Project[],
params: string[],
opts: RecursiveOptions & Pick<InstallDepsOptions, 'catalogs' | 'workspaceDir'>,
opts: RecursiveOptions & WorkspaceStateSettings,
cmdFullName: CommandFullName
): Promise<boolean | string> {
const recursiveResult = await recursive(allProjects, params, opts, cmdFullName)
await updateWorkspaceState({
allProjects,
catalogs: opts.catalogs,
lastValidatedTimestamp: Date.now(),
workspaceDir: opts.workspaceDir,
})
if (!opts.lockfileOnly) {
await updateWorkspaceState({
allProjects,
settings: opts,
workspaceDir: opts.workspaceDir,
pnpmfileExists: opts.hooks?.calculatePnpmfileChecksum != null,
filteredInstall: allProjects.length !== Object.keys(opts.selectedProjectsGraph ?? {}).length,
})
}
return recursiveResult
}

View File

@@ -16,6 +16,7 @@ const DEFAULT_OPTIONS = {
bail: false,
bin: 'node_modules/.bin',
cacheDir: path.join(tmp, 'cache'),
excludeLinksFromLockfile: false,
extraEnv: {},
cliOptions: {},
deployAllFiles: false,
@@ -25,6 +26,7 @@ const DEFAULT_OPTIONS = {
optionalDependencies: true,
},
lock: true,
preferWorkspacePackages: true,
pnpmfile: '.pnpmfile.cjs',
pnpmHomeDir: '',
rawConfig: { registry: REGISTRY_URL },

View File

@@ -14,6 +14,7 @@ const DEFAULT_OPTIONS = {
bin: 'node_modules/.bin',
cliOptions: {},
deployAllFiles: false,
excludeLinksFromLockfile: false,
extraEnv: {},
include: {
dependencies: true,
@@ -21,6 +22,7 @@ const DEFAULT_OPTIONS = {
optionalDependencies: true,
},
lock: true,
preferWorkspacePackages: true,
pnpmfile: '.pnpmfile.cjs',
pnpmHomeDir: '',
rawConfig: { registry: REGISTRY_URL },

View File

@@ -16,6 +16,7 @@ const DEFAULT_OPTIONS = {
bail: false,
bin: 'node_modules/.bin',
cacheDir: path.join(tmp, 'cache'),
excludeLinksFromLockfile: false,
extraEnv: {},
cliOptions: {},
deployAllFiles: false,
@@ -26,6 +27,7 @@ const DEFAULT_OPTIONS = {
},
lock: true,
pnpmfile: '.pnpmfile.cjs',
preferWorkspacePackages: true,
rawConfig: { registry: REGISTRY_URL },
rawLocalConfig: { registry: REGISTRY_URL },
registries: {

View File

@@ -28,6 +28,7 @@ const DEFAULT_OPTS = {
lockStaleDuration: 90,
networkConcurrency: 16,
offline: false,
preferWorkspacePackages: true,
proxy: undefined,
pnpmHomeDir: '',
rawConfig: { registry: REGISTRY },

View File

@@ -26,6 +26,7 @@ const DEFAULT_OPTS = {
lockStaleDuration: 90,
networkConcurrency: 16,
offline: false,
preferWorkspacePackages: true,
proxy: undefined,
pnpmHomeDir: '',
rawConfig: { registry: REGISTRY },

View File

@@ -14,6 +14,7 @@ const DEFAULT_OPTIONS = {
bail: false,
bin: 'node_modules/.bin',
cacheDir: path.join(TMP, 'cache'),
excludeLinksFromLockfile: false,
extraEnv: {},
cliOptions: {},
deployAllFiles: false,
@@ -25,6 +26,7 @@ const DEFAULT_OPTIONS = {
lock: true,
pnpmfile: '.pnpmfile.cjs',
pnpmHomeDir: '',
preferWorkspacePackages: true,
rawConfig: { registry: REGISTRY_URL },
rawLocalConfig: { registry: REGISTRY_URL },
registries: {

View File

@@ -16,6 +16,7 @@ const DEFAULT_OPTIONS = {
},
bail: false,
bin: 'node_modules/.bin',
excludeLinksFromLockfile: false,
extraEnv: {},
cliOptions: {},
deployAllFiles: false,
@@ -28,6 +29,7 @@ const DEFAULT_OPTIONS = {
linkWorkspacePackages: true,
pnpmfile: '.pnpmfile.cjs',
pnpmHomeDir: '',
preferWorkspacePackages: true,
rawConfig: { registry: REGISTRY_URL },
rawLocalConfig: { registry: REGISTRY_URL },
registries: {

View File

@@ -21,6 +21,7 @@ const DEFAULT_OPTIONS = {
},
bail: false,
bin: 'node_modules/.bin',
excludeLinksFromLockfile: false,
extraEnv: {},
cliOptions: {},
deployAllFiles: false,
@@ -32,6 +33,7 @@ const DEFAULT_OPTIONS = {
lock: true,
pnpmfile: '.pnpmfile.cjs',
pnpmHomeDir: '',
preferWorkspacePackages: true,
rawConfig: { registry: REGISTRY_URL },
rawLocalConfig: { registry: REGISTRY_URL },
registries: {

View File

@@ -18,6 +18,7 @@ const DEFAULT_OPTIONS = {
},
bail: false,
bin: 'node_modules/.bin',
excludeLinksFromLockfile: false,
extraEnv: {},
cliOptions: {},
deployAllFiles: false,
@@ -29,6 +30,7 @@ const DEFAULT_OPTIONS = {
lock: true,
pnpmfile: '.pnpmfile.cjs',
pnpmHomeDir: '',
preferWorkspacePackages: true,
rawConfig: { registry: REGISTRY_URL },
rawLocalConfig: { registry: REGISTRY_URL },
registries: {

View File

@@ -11,6 +11,7 @@ export const DEFAULT_OPTS = {
ca: undefined,
cacheDir: '../cache',
cert: undefined,
excludeLinksFromLockfile: false,
extraEnv: {},
cliOptions: {},
deployAllFiles: false,
@@ -35,6 +36,7 @@ export const DEFAULT_OPTS = {
pending: false,
pnpmfile: './.pnpmfile.cjs',
pnpmHomeDir: '',
preferWorkspacePackages: true,
proxy: undefined,
rawConfig: { registry: REGISTRY },
rawLocalConfig: {},

View File

@@ -39,6 +39,9 @@
{
"path": "../../dedupe/check"
},
{
"path": "../../deps/status"
},
{
"path": "../../env/plugin-commands-env"
},

12
pnpm-lock.yaml generated
View File

@@ -1824,6 +1824,9 @@ importers:
'@pnpm/parse-overrides':
specifier: workspace:*
version: link:../../config/parse-overrides
'@pnpm/pnpmfile':
specifier: workspace:*
version: link:../../hooks/pnpmfile
'@pnpm/resolver-base':
specifier: workspace:*
version: link:../../resolving/resolver-base
@@ -5088,6 +5091,9 @@ importers:
'@pnpm/dedupe.check':
specifier: workspace:*
version: link:../../dedupe/check
'@pnpm/deps.status':
specifier: workspace:*
version: link:../../deps/status
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
@@ -7901,6 +7907,9 @@ importers:
'@pnpm/catalogs.types':
specifier: workspace:*
version: link:../../catalogs/types
'@pnpm/config':
specifier: workspace:*
version: link:../../config/config
'@pnpm/logger':
specifier: workspace:*
version: link:../../packages/logger
@@ -7917,6 +7926,9 @@ importers:
'@pnpm/workspace.state':
specifier: workspace:*
version: 'link:'
'@types/ramda':
specifier: 'catalog:'
version: 0.29.12
packages:

View File

@@ -179,14 +179,14 @@ test('use node versions specified by pnpm.executionEnv.nodeVersion in workspace
name: 'node-version-unset',
version: '1.0.0',
scripts: {
install: 'node -v > node-version.txt',
test: 'node -v > node-version.txt',
},
},
{
name: 'node-version-18',
version: '1.0.0',
scripts: {
install: 'node -v > node-version.txt',
test: 'node -v > node-version.txt',
},
pnpm: {
executionEnv: {
@@ -198,7 +198,7 @@ test('use node versions specified by pnpm.executionEnv.nodeVersion in workspace
name: 'node-version-20',
version: '1.0.0',
scripts: {
install: 'node -v > node-version.txt',
test: 'node -v > node-version.txt',
},
pnpm: {
executionEnv: {
@@ -212,7 +212,7 @@ test('use node versions specified by pnpm.executionEnv.nodeVersion in workspace
packages: ['*'],
})
execPnpmSync(['install'])
execPnpmSync(['-r', 'test'])
expect(
['node-version-unset', 'node-version-18', 'node-version-20'].map(name => {
const filePath = path.join(projects[name].dir(), 'node-version.txt')
@@ -220,7 +220,7 @@ test('use node versions specified by pnpm.executionEnv.nodeVersion in workspace
})
).toStrictEqual([process.version, 'v18.0.0', 'v20.0.0'])
execPnpmSync(['--config.use-node-version=19.0.0', 'install'])
execPnpmSync(['--config.use-node-version=19.0.0', '-r', 'test'])
expect(
['node-version-unset', 'node-version-18', 'node-version-20'].map(name => {
const filePath = path.join(projects[name].dir(), 'node-version.txt')

View File

@@ -549,7 +549,7 @@ test('installation with --link-workspace-packages links packages even if they we
},
])
await execPnpm(['recursive', 'install', '--no-link-workspace-packages'])
await execPnpm(['-r', 'install', '--no-link-workspace-packages'])
{
const lockfile = projects.project.readLockfile()
@@ -557,7 +557,7 @@ test('installation with --link-workspace-packages links packages even if they we
expect(lockfile.importers['.'].dependencies?.negative.version).toBe('is-negative@1.0.0')
}
await execPnpm(['recursive', 'install', '--link-workspace-packages'])
await execPnpm(['-r', 'install', '--link-workspace-packages'])
{
const lockfile = projects.project.readLockfile()
@@ -590,7 +590,7 @@ test('shared-workspace-lockfile: installation with --link-workspace-packages lin
writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })
fs.writeFileSync('.npmrc', 'shared-workspace-lockfile = true\nlink-workspace-packages = true', 'utf8')
await execPnpm(['recursive', 'install'])
await execPnpm(['install'])
{
const lockfile = readYamlFile<LockfileFile>(WANTED_LOCKFILE)
@@ -608,7 +608,7 @@ test('shared-workspace-lockfile: installation with --link-workspace-packages lin
version: '1.0.0',
})
await execPnpm(['recursive', 'install'])
await execPnpm(['install'])
{
const lockfile = readYamlFile<LockfileFile>(WANTED_LOCKFILE)
@@ -1243,8 +1243,8 @@ test('dependencies of workspace projects are built during headless installation'
fs.writeFileSync('.npmrc', 'shared-workspace-lockfile=false', 'utf8')
writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })
await execPnpm(['recursive', 'install', '--lockfile-only'])
await execPnpm(['recursive', 'install', '--frozen-lockfile'])
await execPnpm(['install', '--lockfile-only'])
await execPnpm(['install', '--frozen-lockfile'])
{
const generatedByPreinstall = projects['project-1'].requireModule('@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-preinstall')
@@ -1277,14 +1277,15 @@ test("linking the package's bin to another workspace package in a monorepo", asy
writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })
await execPnpm(['recursive', 'install'])
await execPnpm(['install'])
projects.main.isExecutable('.bin/hello')
expect(fs.existsSync('main/node_modules')).toBeTruthy()
rimraf('main/node_modules')
rimraf('node_modules')
await execPnpm(['recursive', 'install', '--frozen-lockfile'])
await execPnpm(['install', '--frozen-lockfile'])
projects.main.isExecutable('.bin/hello')
})
@@ -1433,6 +1434,7 @@ test('custom virtual store directory in a workspace with not shared lockfile', a
rimraf('project-1/virtual-store')
rimraf('project-1/node_modules')
rimraf('node_modules')
await execPnpm(['install', '--frozen-lockfile'])

View File

@@ -25,16 +25,14 @@ test('single package workspace', async () => {
{
const { status, stdout } = execPnpmSync([...CONFIG, ...EXEC])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('Cannot find a lockfile')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
}
await execPnpm([...CONFIG, 'install'])
// installing dependencies on a single package workspace should not create a packages list cache
{
const workspaceState = loadWorkspaceState(process.cwd())
expect(workspaceState).toBeUndefined()
}
// installing dependencies on a single package workspace should create a packages list cache
const workspaceState = loadWorkspaceState(process.cwd())
expect(workspaceState).toBeDefined()
// should be able to execute a command after dependencies have been installed
{
@@ -62,7 +60,7 @@ test('single package workspace', async () => {
{
const { status, stdout } = execPnpmSync([...CONFIG, ...EXEC])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
}
await execPnpm([...CONFIG, 'install'])
@@ -82,7 +80,7 @@ test('single package workspace', async () => {
{
const { status, stdout } = execPnpmSync([...CONFIG, ...EXEC])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
}
await execPnpm([...CONFIG, 'install'])
@@ -167,15 +165,16 @@ test('multi-project workspace', async () => {
// pnpm install should create a packages list cache
{
const workspaceState = loadWorkspaceState(process.cwd())
expect(workspaceState).toStrictEqual({
catalogs: {},
expect(workspaceState).toStrictEqual(expect.objectContaining({
lastValidatedTimestamp: expect.any(Number),
projectRootDirs: [
path.resolve('.'),
path.resolve('foo'),
path.resolve('bar'),
].sort(),
})
pnpmfileExists: false,
filteredInstall: false,
projects: {
[path.resolve('.')]: { name: 'root', version: '0.0.0' },
[path.resolve('foo')]: { name: 'foo', version: '0.0.0' },
[path.resolve('bar')]: { name: 'bar', version: '0.0.0' },
},
}))
}
// should be able to execute a command in root after dependencies have been installed
@@ -229,7 +228,7 @@ test('multi-project workspace', async () => {
{
const { status, stdout } = execPnpmSync([...CONFIG, ...EXEC])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
expect(stdout.toString()).toContain('project of id foo')
}
// attempting to execute a command in any workspace package without updating dependencies should fail
@@ -238,7 +237,7 @@ test('multi-project workspace', async () => {
cwd: projects.foo.dir(),
})
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
expect(stdout.toString()).toContain('project of id foo')
}
{
@@ -246,21 +245,21 @@ test('multi-project workspace', async () => {
cwd: projects.bar.dir(),
})
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
expect(stdout.toString()).toContain('project of id foo')
}
// attempting to execute a command recursively without updating dependencies should fail
{
const { status, stdout } = execPnpmSync([...CONFIG, '--recursive', ...EXEC])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
expect(stdout.toString()).toContain('project of id foo')
}
// attempting to execute a command with filter without updating dependencies should fail
{
const { status, stdout } = execPnpmSync([...CONFIG, '--filter=foo', ...EXEC])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
expect(stdout.toString()).toContain('project of id foo')
}
@@ -300,7 +299,7 @@ test('multi-project workspace', async () => {
}
manifests.baz = {
name: 'bar',
name: 'baz',
private: true,
dependencies: {
'@pnpm.e2e/foo': '=100.0.0',
@@ -321,16 +320,17 @@ test('multi-project workspace', async () => {
// pnpm install should update the packages list cache
{
const workspaceState = loadWorkspaceState(process.cwd())
expect(workspaceState).toStrictEqual({
catalogs: {},
expect(workspaceState).toStrictEqual(expect.objectContaining({
lastValidatedTimestamp: expect.any(Number),
projectRootDirs: [
path.resolve('.'),
path.resolve('foo'),
path.resolve('bar'),
path.resolve('baz'),
].sort(),
})
pnpmfileExists: false,
filteredInstall: false,
projects: {
[path.resolve('.')]: { name: 'root', version: '0.0.0' },
[path.resolve('foo')]: { name: 'foo' },
[path.resolve('bar')]: { name: 'bar', version: '0.0.0' },
[path.resolve('baz')]: { name: 'baz' },
},
}))
}
// should be able to execute a command after projects list have been updated

View File

@@ -90,15 +90,16 @@ test('single dependency', async () => {
// pnpm install should create a packages list cache
{
const workspaceState = loadWorkspaceState(process.cwd())
expect(workspaceState).toStrictEqual({
catalogs: {},
expect(workspaceState).toStrictEqual(expect.objectContaining({
lastValidatedTimestamp: expect.any(Number),
projectRootDirs: [
path.resolve('.'),
path.resolve('foo'),
path.resolve('bar'),
].sort(),
})
pnpmfileExists: false,
filteredInstall: false,
projects: {
[path.resolve('.')]: { name: 'root', version: '0.0.0' },
[path.resolve('foo')]: { name: 'foo', version: '0.0.0' },
[path.resolve('bar')]: { name: 'bar', version: '0.0.0' },
},
}))
}
// should be able to execute a script in root after dependencies have been installed
@@ -159,7 +160,7 @@ test('single dependency', async () => {
{
const { status, stdout } = execPnpmSync([...CONFIG, '--reporter=ndjson', 'start'])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
expect(stdout.toString()).toContain('project of id foo')
expect(stdout.toString()).not.toContain('No manifest files were modified since the last validation. Exiting check.')
expect(stdout.toString()).toContain('Some manifest files were modified since the last validation. Continuing check.')
@@ -170,7 +171,7 @@ test('single dependency', async () => {
cwd: projects.foo.dir(),
})
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
expect(stdout.toString()).toContain('project of id foo')
}
{
@@ -178,21 +179,21 @@ test('single dependency', async () => {
cwd: projects.bar.dir(),
})
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
expect(stdout.toString()).toContain('project of id foo')
}
// attempting to execute a script recursively without updating dependencies should fail
{
const { status, stdout } = execPnpmSync([...CONFIG, '--recursive', 'start'])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
expect(stdout.toString()).toContain('project of id foo')
}
// attempting to execute a script with filter without updating dependencies should fail
{
const { status, stdout } = execPnpmSync([...CONFIG, '--filter=foo', 'start'])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
expect(stdout.toString()).toContain('project of id foo')
}
@@ -233,7 +234,7 @@ test('single dependency', async () => {
}
manifests.baz = {
name: 'bar',
name: 'baz',
private: true,
dependencies: {
'@pnpm.e2e/foo': '=100.0.0',
@@ -258,16 +259,17 @@ test('single dependency', async () => {
// pnpm install should update the packages list cache
{
const workspaceState = loadWorkspaceState(process.cwd())
expect(workspaceState).toStrictEqual({
catalogs: {},
expect(workspaceState).toStrictEqual(expect.objectContaining({
lastValidatedTimestamp: expect.any(Number),
projectRootDirs: [
path.resolve('.'),
path.resolve('foo'),
path.resolve('bar'),
path.resolve('baz'),
].sort(),
})
pnpmfileExists: false,
filteredInstall: false,
projects: {
[path.resolve('.')]: { name: 'root', version: '0.0.0' },
[path.resolve('foo')]: { name: 'foo' },
[path.resolve('bar')]: { name: 'bar', version: '0.0.0' },
[path.resolve('baz')]: { name: 'baz' },
},
}))
}
// should be able to execute a script after projects list have been updated
@@ -362,15 +364,16 @@ test('multiple lockfiles', async () => {
// pnpm install should create a packages list cache
{
const workspaceState = loadWorkspaceState(process.cwd())
expect(workspaceState).toStrictEqual({
catalogs: {},
expect(workspaceState).toStrictEqual(expect.objectContaining({
lastValidatedTimestamp: expect.any(Number),
projectRootDirs: [
path.resolve('.'),
path.resolve('foo'),
path.resolve('bar'),
].sort(),
})
pnpmfileExists: false,
filteredInstall: false,
projects: {
[path.resolve('.')]: { name: 'root', version: '0.0.0' },
[path.resolve('foo')]: { name: 'foo', version: '0.0.0' },
[path.resolve('bar')]: { name: 'bar', version: '0.0.0' },
},
}))
}
// should be able to execute a script in root after dependencies have been installed
@@ -431,7 +434,7 @@ test('multiple lockfiles', async () => {
{
const { status, stdout } = execPnpmSync([...config, 'start'])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
expect(stdout.toString()).toContain(`The lockfile in ${path.resolve('foo')} does not satisfy project of id .`)
}
// attempting to execute a script in any workspace package without updating dependencies should fail
@@ -440,7 +443,7 @@ test('multiple lockfiles', async () => {
cwd: projects.foo.dir(),
})
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
expect(stdout.toString()).toContain(`The lockfile in ${path.resolve('foo')} does not satisfy project of id .`)
}
{
@@ -448,21 +451,21 @@ test('multiple lockfiles', async () => {
cwd: projects.bar.dir(),
})
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
expect(stdout.toString()).toContain(`The lockfile in ${path.resolve('foo')} does not satisfy project of id .`)
}
// attempting to execute a script recursively without updating dependencies should fail
{
const { status, stdout } = execPnpmSync([...config, '--recursive', 'start'])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
expect(stdout.toString()).toContain(`The lockfile in ${path.resolve('foo')} does not satisfy project of id .`)
}
// attempting to execute a script with filter without updating dependencies should fail
{
const { status, stdout } = execPnpmSync([...config, '--filter=foo', 'start'])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
expect(stdout.toString()).toContain(`The lockfile in ${path.resolve('foo')} does not satisfy project of id .`)
}
@@ -501,7 +504,7 @@ test('multiple lockfiles', async () => {
}
manifests.baz = {
name: 'bar',
name: 'baz',
private: true,
dependencies: {
'@pnpm.e2e/foo': '=100.0.0',
@@ -525,16 +528,17 @@ test('multiple lockfiles', async () => {
// pnpm install should update the packages list cache
{
const workspaceState = loadWorkspaceState(process.cwd())
expect(workspaceState).toStrictEqual({
catalogs: {},
expect(workspaceState).toStrictEqual(expect.objectContaining({
lastValidatedTimestamp: expect.any(Number),
projectRootDirs: [
path.resolve('.'),
path.resolve('foo'),
path.resolve('bar'),
path.resolve('baz'),
].sort(),
})
pnpmfileExists: false,
filteredInstall: false,
projects: {
[path.resolve('.')]: { name: 'root', version: '0.0.0' },
[path.resolve('foo')]: { name: 'foo' },
[path.resolve('bar')]: { name: 'bar', version: '0.0.0' },
[path.resolve('baz')]: { name: 'baz' },
},
}))
}
// should be able to execute a script after projects list have been updated
@@ -611,7 +615,7 @@ test('filtered install', async () => {
{
const { status, stdout } = execPnpmSync([...CONFIG, '--filter=foo', 'start'])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
}
await execPnpm([...CONFIG, '--filter=foo', 'install'])
@@ -671,15 +675,16 @@ test('no dependencies', async () => {
// pnpm install should create a packages list cache
{
const workspaceState = loadWorkspaceState(process.cwd())
expect(workspaceState).toStrictEqual({
catalogs: {},
expect(workspaceState).toStrictEqual(expect.objectContaining({
lastValidatedTimestamp: expect.any(Number),
projectRootDirs: [
path.resolve('.'),
path.resolve('foo'),
path.resolve('bar'),
].sort(),
})
pnpmfileExists: false,
filteredInstall: false,
projects: {
[path.resolve('.')]: { name: 'root', version: '0.0.0' },
[path.resolve('foo')]: { name: 'foo', version: '0.0.0' },
[path.resolve('bar')]: { name: 'bar', version: '0.0.0' },
},
}))
}
// should be able to execute a script after `pnpm install`
@@ -761,7 +766,7 @@ test('nested `pnpm run` should not check for mutated manifest', async () => {
{
const { status, stdout } = execPnpmSync([...CONFIG, '--recursive', 'start'])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
}
await execPnpm([...CONFIG, 'install'])
@@ -849,15 +854,19 @@ test('should check for outdated catalogs', async () => {
{
const workspaceState = loadWorkspaceState(process.cwd())
expect(workspaceState).toStrictEqual({
catalogs: {
default: workspaceManifest.catalog,
},
settings: expect.objectContaining({
catalogs: {
default: workspaceManifest.catalog,
},
}),
pnpmfileExists: false,
filteredInstall: false,
lastValidatedTimestamp: expect.any(Number),
projectRootDirs: [
path.resolve('.'),
path.resolve('foo'),
path.resolve('bar'),
].sort(),
projects: {
[path.resolve('.')]: { name: 'root', version: '0.0.0' },
[path.resolve('foo')]: { name: 'foo', version: '0.0.0' },
[path.resolve('bar')]: { name: 'bar', version: '0.0.0' },
},
})
}
@@ -954,6 +963,6 @@ test('failed to install dependencies', async () => {
{
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
}
})

View File

@@ -26,16 +26,14 @@ test('single dependency', async () => {
{
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('Cannot find a lockfile in')
expect(stdout.toString()).toContain('Cannot check whether dependencies are outdated')
}
await execPnpm([...CONFIG, 'install'])
// installing dependencies on a single package workspace should not create a packages list cache
{
const workspaceState = loadWorkspaceState(process.cwd())
expect(workspaceState).toBeUndefined()
}
// installing dependencies on a single package workspace should create a packages list cache
const workspaceState = loadWorkspaceState(process.cwd())
expect(workspaceState).toBeDefined()
// should be able to execute a script after dependencies have been installed
{
@@ -65,7 +63,7 @@ test('single dependency', async () => {
{
const { status, stdout } = execPnpmSync([...CONFIG, '--reporter=ndjson', 'start'])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
expect(stdout.toString()).not.toContain('The manifest file is not newer than the lockfile. Exiting check.')
expect(stdout.toString()).toContain('The manifest is newer than the lockfile. Continuing check.')
}
@@ -89,7 +87,7 @@ test('single dependency', async () => {
{
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
}
await execPnpm([...CONFIG, 'install'])
@@ -122,16 +120,14 @@ test('deleting node_modules after install', async () => {
{
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('Cannot find a lockfile in')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
}
await execPnpm([...CONFIG, 'install'])
// installing dependencies on a single package workspace should not create a packages list cache
{
const workspaceState = loadWorkspaceState(process.cwd())
expect(workspaceState).toBeUndefined()
}
// installing dependencies on a single package workspace should create a packages list cache
const workspaceState = loadWorkspaceState(process.cwd())
expect(workspaceState).toBeDefined()
// should be able to execute a script after dependencies have been installed
{
@@ -145,7 +141,7 @@ test('deleting node_modules after install', async () => {
{
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_DEPS')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
}
})
@@ -164,16 +160,14 @@ test('no dependencies', async () => {
{
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('Cannot find a lockfile in')
expect(stdout.toString()).toContain('Cannot check whether dependencies are outdated')
}
await execPnpm([...CONFIG, 'install'])
// installing dependencies on a single package workspace should not create a packages list cache
{
const workspaceState = loadWorkspaceState(process.cwd())
expect(workspaceState).toBeUndefined()
}
// installing dependencies on a single package workspace should create a packages list cache
const workspaceState = loadWorkspaceState(process.cwd())
expect(workspaceState).toBeDefined()
// should be able to execute a script after the lockfile has been created
{
@@ -220,7 +214,7 @@ test('nested `pnpm run` should not check for mutated manifest', async () => {
{
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('Cannot find a lockfile in')
expect(stdout.toString()).toContain('Cannot check whether dependencies are outdated')
}
await execPnpm([...CONFIG, 'install'])
@@ -236,7 +230,7 @@ test('nested `pnpm run` should not check for mutated manifest', async () => {
{
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
expect(status).not.toBe(0)
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
expect(stdout.toString()).toContain('ERR_PNPM_VERIFY_DEPS_BEFORE_RUN')
}
await execPnpm([...CONFIG, 'install'])

View File

@@ -12,6 +12,7 @@ export const DEFAULT_OPTS = {
cacheDir: '../cache',
cert: undefined,
extraEnv: {},
excludeLinksFromLockfile: false,
cliOptions: {},
deployAllFiles: false,
fetchRetries: 2,
@@ -35,6 +36,7 @@ export const DEFAULT_OPTS = {
pending: false,
pnpmfile: './.pnpmfile.cjs',
pnpmHomeDir: '',
preferWorkspacePackages: true,
proxy: undefined,
rawConfig: { registry: REGISTRY },
rawLocalConfig: {},

View File

@@ -9,6 +9,7 @@ export const DEFAULT_OPTS = {
ca: undefined,
cacheDir: '../cache',
cert: undefined,
excludeLinksFromLockfile: false,
extraEnv: {},
cliOptions: {},
fetchRetries: 2,
@@ -32,6 +33,7 @@ export const DEFAULT_OPTS = {
pending: false,
pnpmfile: './.pnpmfile.cjs',
pnpmHomeDir: '',
preferWorkspacePackages: true,
proxy: undefined,
rawConfig: { registry: REGISTRY },
rawLocalConfig: {},

View File

@@ -10,6 +10,7 @@ export const DEFAULT_OPTS = {
bin: 'node_modules/.bin',
ca: undefined,
cert: undefined,
excludeLinksFromLockfile: false,
extraEnv: {},
cliOptions: {},
fetchRetries: 2,
@@ -34,6 +35,7 @@ export const DEFAULT_OPTS = {
pnpmfile: './.pnpmfile.cjs',
pnpmHomeDir: '',
proxy: undefined,
preferWorkspacePackages: true,
rawConfig: { registry: REGISTRY },
rawLocalConfig: {},
registries: { default: REGISTRY },

View File

@@ -11,6 +11,7 @@ export const DEFAULT_OPTS = {
ca: undefined,
cacheDir: '../cache',
cert: undefined,
excludeLinksFromLockfile: false,
extraEnv: {},
cliOptions: {},
deployAllFiles: false,
@@ -36,6 +37,7 @@ export const DEFAULT_OPTS = {
pending: false,
pnpmfile: './.pnpmfile.cjs',
pnpmHomeDir: '',
preferWorkspacePackages: true,
proxy: undefined,
rawConfig: { registry: REGISTRY },
rawLocalConfig: {},

View File

@@ -38,9 +38,11 @@ function prepareOptions (dir: string) {
original: [],
},
cliOptions: {},
excludeLinksFromLockfile: false,
linkWorkspacePackages: true,
bail: true,
pnpmHomeDir: dir,
preferWorkspacePackages: true,
registries: {
default: 'https://registry.npmjs.org/',
},

View File

@@ -32,13 +32,15 @@
"funding": "https://opencollective.com/pnpm",
"dependencies": {
"@pnpm/catalogs.types": "workspace:*",
"@pnpm/config": "workspace:*",
"@pnpm/logger": "workspace:*",
"@pnpm/types": "workspace:*",
"ramda": "catalog:"
},
"devDependencies": {
"@pnpm/prepare": "workspace:*",
"@pnpm/workspace.state": "workspace:*"
"@pnpm/workspace.state": "workspace:*",
"@types/ramda": "catalog:"
},
"exports": {
".": "./lib/index.js"

View File

@@ -1,14 +1,41 @@
import { type Catalogs } from '@pnpm/catalogs.types'
import { type WorkspaceState, type ProjectsList } from './types'
import pick from 'ramda/src/pick'
import { type WorkspaceState, type WorkspaceStateSettings, type ProjectsList } from './types'
export interface CreateWorkspaceStateOptions {
allProjects: ProjectsList
catalogs: Catalogs | undefined
lastValidatedTimestamp: number
pnpmfileExists: boolean
filteredInstall: boolean
settings: WorkspaceStateSettings
}
export const createWorkspaceState = (opts: CreateWorkspaceStateOptions): WorkspaceState => ({
catalogs: opts.catalogs,
lastValidatedTimestamp: opts.lastValidatedTimestamp,
projectRootDirs: opts.allProjects.map(project => project.rootDir).sort(),
lastValidatedTimestamp: Date.now(),
projects: Object.fromEntries(opts.allProjects.map(project => [
project.rootDir,
{
name: project.manifest.name,
version: project.manifest.version,
},
])),
pnpmfileExists: opts.pnpmfileExists,
settings: pick([
'autoInstallPeers',
'catalogs',
'dedupeDirectDeps',
'dedupeInjectedDeps',
'dedupePeerDependents',
'dev',
'excludeLinksFromLockfile',
'hoistPattern',
'hoistWorkspacePackages',
'injectWorkspacePackages',
'linkWorkspacePackages',
'nodeLinker',
'optional',
'preferWorkspacePackages',
'production',
'publicHoistPattern',
'workspacePackagePatterns',
], opts.settings),
filteredInstall: opts.filteredInstall,
})

View File

@@ -1,3 +1,3 @@
export { loadWorkspaceState } from './loadWorkspaceState'
export { type UpdateWorkspaceStateOptions, updateWorkspaceState } from './updateWorkspaceState'
export { type WorkspaceState, type ProjectsList } from './types'
export { type WorkspaceState, type WorkspaceStateSettings, type ProjectsList } from './types'

View File

@@ -1,10 +1,35 @@
import { type Catalogs } from '@pnpm/catalogs.types'
import { type Config } from '@pnpm/config'
import { type Project, type ProjectRootDir } from '@pnpm/types'
export type ProjectsList = Array<Pick<Project, 'rootDir'>>
export type ProjectsList = Array<Pick<Project, 'rootDir' | 'manifest'>>
export interface WorkspaceState {
catalogs: Catalogs | undefined
lastValidatedTimestamp: number
projectRootDirs: ProjectRootDir[]
projects: Record<ProjectRootDir, {
name?: string
version?: string
}>
pnpmfileExists: boolean
filteredInstall: boolean
settings: WorkspaceStateSettings
}
export type WorkspaceStateSettings = Pick<Config,
| 'autoInstallPeers'
| 'catalogs'
| 'dedupeDirectDeps'
| 'dedupeInjectedDeps'
| 'dedupePeerDependents'
| 'dev'
| 'excludeLinksFromLockfile'
| 'hoistPattern'
| 'hoistWorkspacePackages'
| 'injectWorkspacePackages'
| 'linkWorkspacePackages'
| 'nodeLinker'
| 'optional'
| 'preferWorkspacePackages'
| 'production'
| 'publicHoistPattern'
| 'workspacePackagePatterns'
>

View File

@@ -1,16 +1,16 @@
import fs from 'fs'
import path from 'path'
import { type Catalogs } from '@pnpm/catalogs.types'
import { logger } from '@pnpm/logger'
import { getFilePath } from './filePath'
import { createWorkspaceState } from './createWorkspaceState'
import { type ProjectsList } from './types'
import { type WorkspaceStateSettings, type ProjectsList } from './types'
export interface UpdateWorkspaceStateOptions {
allProjects: ProjectsList
catalogs: Catalogs | undefined
lastValidatedTimestamp: number
settings: WorkspaceStateSettings
workspaceDir: string
pnpmfileExists: boolean
filteredInstall: boolean
}
export async function updateWorkspaceState (opts: UpdateWorkspaceStateOptions): Promise<void> {

View File

@@ -3,22 +3,28 @@ import { prepareEmpty, preparePackages } from '@pnpm/prepare'
import { type ProjectRootDir } from '@pnpm/types'
import { createWorkspaceState } from '../src/createWorkspaceState'
const lastValidatedTimestamp = Date.now()
test('createWorkspaceState() on empty list', () => {
prepareEmpty()
expect(
createWorkspaceState({
allProjects: [],
catalogs: undefined,
lastValidatedTimestamp,
pnpmfileExists: true,
filteredInstall: false,
settings: {
autoInstallPeers: true,
dedupeDirectDeps: true,
excludeLinksFromLockfile: false,
preferWorkspacePackages: false,
linkWorkspacePackages: false,
injectWorkspacePackages: false,
},
})
).toStrictEqual({
catalogs: undefined,
lastValidatedTimestamp,
projectRootDirs: [],
})
).toStrictEqual(expect.objectContaining({
projects: {},
pnpmfileExists: true,
lastValidatedTimestamp: expect.any(Number),
}))
})
test('createWorkspaceState() on non-empty list', () => {
@@ -30,30 +36,42 @@ test('createWorkspaceState() on non-empty list', () => {
expect(
createWorkspaceState({
allProjects: [
{ rootDir: path.resolve('packages/c') as ProjectRootDir },
{ rootDir: path.resolve('packages/b') as ProjectRootDir },
{ rootDir: path.resolve('packages/a') as ProjectRootDir },
{ rootDir: path.resolve('packages/d') as ProjectRootDir },
{ rootDir: path.resolve('packages/c') as ProjectRootDir, manifest: {} },
{ rootDir: path.resolve('packages/b') as ProjectRootDir, manifest: {} },
{ rootDir: path.resolve('packages/a') as ProjectRootDir, manifest: {} },
{ rootDir: path.resolve('packages/d') as ProjectRootDir, manifest: {} },
],
lastValidatedTimestamp,
settings: {
autoInstallPeers: true,
dedupeDirectDeps: true,
excludeLinksFromLockfile: false,
preferWorkspacePackages: false,
linkWorkspacePackages: false,
injectWorkspacePackages: false,
catalogs: {
default: {
foo: '0.1.2',
},
},
},
pnpmfileExists: false,
filteredInstall: false,
})
).toStrictEqual(expect.objectContaining({
settings: expect.objectContaining({
catalogs: {
default: {
foo: '0.1.2',
},
},
})
).toStrictEqual({
catalogs: {
default: {
foo: '0.1.2',
},
}),
lastValidatedTimestamp: expect.any(Number),
projects: {
[path.resolve('packages/a')]: {},
[path.resolve('packages/b')]: {},
[path.resolve('packages/c')]: {},
[path.resolve('packages/d')]: {},
},
lastValidatedTimestamp,
projectRootDirs: [
path.resolve('packages/a'),
path.resolve('packages/b'),
path.resolve('packages/c'),
path.resolve('packages/d'),
],
})
pnpmfileExists: false,
}))
})

View File

@@ -41,18 +41,28 @@ test('loadWorkspaceState() when cache file exists and is correct', async () => {
const cacheFile = getFilePath(workspaceDir)
fs.mkdirSync(path.dirname(cacheFile), { recursive: true })
const workspaceState: WorkspaceState = {
catalogs: {
default: {
foo: '0.1.2',
settings: {
autoInstallPeers: true,
dedupeDirectDeps: true,
excludeLinksFromLockfile: false,
preferWorkspacePackages: false,
injectWorkspacePackages: false,
catalogs: {
default: {
foo: '0.1.2',
},
},
linkWorkspacePackages: true,
},
lastValidatedTimestamp,
projectRootDirs: [
path.resolve('packages/a') as ProjectRootDir,
path.resolve('packages/b') as ProjectRootDir,
path.resolve('packages/c') as ProjectRootDir,
path.resolve('packages/d') as ProjectRootDir,
],
projects: {
[path.resolve('packages/a') as ProjectRootDir]: {},
[path.resolve('packages/b') as ProjectRootDir]: {},
[path.resolve('packages/c') as ProjectRootDir]: {},
[path.resolve('packages/d') as ProjectRootDir]: {},
},
pnpmfileExists: false,
filteredInstall: false,
}
fs.writeFileSync(cacheFile, JSON.stringify(workspaceState))
expect(loadWorkspaceState(workspaceDir)).toStrictEqual(workspaceState)

View File

@@ -4,8 +4,6 @@ import { preparePackages } from '@pnpm/prepare'
import { type ProjectRootDir } from '@pnpm/types'
import { loadWorkspaceState, updateWorkspaceState } from '../src/index'
const lastValidatedTimestamp = Date.now()
const originalLoggerDebug = logger.debug
afterEach(() => {
logger.debug = originalLoggerDebug
@@ -23,46 +21,65 @@ test('updateWorkspaceState()', async () => {
logger.debug = jest.fn(originalLoggerDebug)
await updateWorkspaceState({
lastValidatedTimestamp,
pnpmfileExists: true,
workspaceDir,
catalogs: undefined,
allProjects: [],
filteredInstall: false,
settings: {
autoInstallPeers: true,
dedupeDirectDeps: true,
excludeLinksFromLockfile: false,
preferWorkspacePackages: false,
linkWorkspacePackages: false,
injectWorkspacePackages: false,
},
})
expect((logger.debug as jest.Mock).mock.calls).toStrictEqual([[{ msg: 'updating workspace state' }]])
expect(loadWorkspaceState(workspaceDir)).toStrictEqual({
lastValidatedTimestamp,
projectRootDirs: [],
})
expect(loadWorkspaceState(workspaceDir)).toStrictEqual(expect.objectContaining({
lastValidatedTimestamp: expect.any(Number),
projects: {},
}))
logger.debug = jest.fn(originalLoggerDebug)
await updateWorkspaceState({
lastValidatedTimestamp,
pnpmfileExists: false,
workspaceDir,
catalogs: {
default: {
foo: '0.1.2',
settings: {
autoInstallPeers: true,
dedupeDirectDeps: true,
excludeLinksFromLockfile: false,
preferWorkspacePackages: false,
injectWorkspacePackages: false,
catalogs: {
default: {
foo: '0.1.2',
},
},
linkWorkspacePackages: true,
},
allProjects: [
{ rootDir: path.resolve('packages/c') as ProjectRootDir },
{ rootDir: path.resolve('packages/a') as ProjectRootDir },
{ rootDir: path.resolve('packages/d') as ProjectRootDir },
{ rootDir: path.resolve('packages/b') as ProjectRootDir },
{ rootDir: path.resolve('packages/c') as ProjectRootDir, manifest: {} },
{ rootDir: path.resolve('packages/a') as ProjectRootDir, manifest: {} },
{ rootDir: path.resolve('packages/d') as ProjectRootDir, manifest: {} },
{ rootDir: path.resolve('packages/b') as ProjectRootDir, manifest: {} },
],
filteredInstall: false,
})
expect((logger.debug as jest.Mock).mock.calls).toStrictEqual([[{ msg: 'updating workspace state' }]])
expect(loadWorkspaceState(workspaceDir)).toStrictEqual({
catalogs: {
default: {
foo: '0.1.2',
expect(loadWorkspaceState(workspaceDir)).toStrictEqual(expect.objectContaining({
settings: expect.objectContaining({
catalogs: {
default: {
foo: '0.1.2',
},
},
}),
lastValidatedTimestamp: expect.any(Number),
projects: {
[path.resolve('packages/a')]: {},
[path.resolve('packages/b')]: {},
[path.resolve('packages/c')]: {},
[path.resolve('packages/d')]: {},
},
lastValidatedTimestamp,
projectRootDirs: [
path.resolve('packages/a'),
path.resolve('packages/b'),
path.resolve('packages/c'),
path.resolve('packages/d'),
],
})
}))
})

View File

@@ -15,6 +15,9 @@
{
"path": "../../catalogs/types"
},
{
"path": "../../config/config"
},
{
"path": "../../packages/logger"
},