diff --git a/.changeset/curly-starfishes-end.md b/.changeset/curly-starfishes-end.md new file mode 100644 index 00000000000..d08589c785f --- /dev/null +++ b/.changeset/curly-starfishes-end.md @@ -0,0 +1,7 @@ +--- +"@pnpm/headless": major +"@pnpm/core": patch +"pnpm": patch +--- + +Don't print "Lockfile is up-to-date" message before finishing all the lockfile checks [#6544](https://github.com/pnpm/pnpm/issues/6544). diff --git a/.changeset/metal-turkeys-know.md b/.changeset/metal-turkeys-know.md new file mode 100644 index 00000000000..9c5d30957d8 --- /dev/null +++ b/.changeset/metal-turkeys-know.md @@ -0,0 +1,6 @@ +--- +"@pnpm/get-context": major +--- + +New property returned: `existsNonEmptyWantedLockfile`. +The `existsWantedLockfile` now means only that a file existed. diff --git a/hooks/types/src/index.ts b/hooks/types/src/index.ts index a97f4da2f70..a85f8b46291 100644 --- a/hooks/types/src/index.ts +++ b/hooks/types/src/index.ts @@ -5,7 +5,7 @@ export interface PreResolutionHookContext { wantedLockfile: Lockfile currentLockfile: Lockfile existsCurrentLockfile: boolean - existsWantedLockfile: boolean + existsNonEmptyWantedLockfile: boolean lockfileDir: string storeDir: string registries: Registries diff --git a/lockfile/lockfile-file/README.md b/lockfile/lockfile-file/README.md index 2331309f1d1..32d1d6c933e 100644 --- a/lockfile/lockfile-file/README.md +++ b/lockfile/lockfile-file/README.md @@ -28,7 +28,7 @@ if the lockfile file format is not compatible with the current library. Reads the lockfile file from `/lock.yaml`. -### `existsWantedLockfile(pkgPath) => Promise` +### `existsNonEmptyWantedLockfile(pkgPath) => Promise` Returns `true` if a `pnpm-lock.yaml` exists in the root of the package. diff --git a/lockfile/lockfile-file/src/existsWantedLockfile.ts b/lockfile/lockfile-file/src/existsWantedLockfile.ts index 82c8b7d358d..a7f937f5ffd 100644 --- a/lockfile/lockfile-file/src/existsWantedLockfile.ts +++ b/lockfile/lockfile-file/src/existsWantedLockfile.ts @@ -2,12 +2,12 @@ import fs from 'fs' import path from 'path' import { getWantedLockfileName } from './lockfileName' -interface ExistsWantedLockfileOptions { +interface existsNonEmptyWantedLockfileOptions { useGitBranchLockfile?: boolean mergeGitBranchLockfiles?: boolean } -export async function existsWantedLockfile (pkgPath: string, opts: ExistsWantedLockfileOptions = { +export async function existsNonEmptyWantedLockfile (pkgPath: string, opts: existsNonEmptyWantedLockfileOptions = { useGitBranchLockfile: false, mergeGitBranchLockfiles: false, }) { diff --git a/lockfile/lockfile-file/src/index.ts b/lockfile/lockfile-file/src/index.ts index d2aeab08f13..15c779b96b2 100644 --- a/lockfile/lockfile-file/src/index.ts +++ b/lockfile/lockfile-file/src/index.ts @@ -4,7 +4,7 @@ export { writeCurrentLockfile, writeWantedLockfile, } from './write' -export { existsWantedLockfile } from './existsWantedLockfile' +export { existsNonEmptyWantedLockfile } from './existsWantedLockfile' export { getLockfileImporterId } from './getLockfileImporterId' export * from '@pnpm/lockfile-types' export * from './read' diff --git a/lockfile/lockfile-file/test/read.test.ts b/lockfile/lockfile-file/test/read.test.ts index 109ae9d9d06..48b2d54da12 100644 --- a/lockfile/lockfile-file/test/read.test.ts +++ b/lockfile/lockfile-file/test/read.test.ts @@ -1,7 +1,7 @@ import path from 'path' import { getCurrentBranch } from '@pnpm/git-utils' import { - existsWantedLockfile, + existsNonEmptyWantedLockfile, readCurrentLockfile, readWantedLockfile, writeCurrentLockfile, @@ -154,9 +154,9 @@ test('writeCurrentLockfile()', async () => { expect(await readCurrentLockfile(projectPath, { ignoreIncompatible: false })).toEqual(wantedLockfile) }) -test('existsWantedLockfile()', async () => { +test('existsNonEmptyWantedLockfile()', async () => { const projectPath = tempy.directory() - expect(await existsWantedLockfile(projectPath)).toBe(false) + expect(await existsNonEmptyWantedLockfile(projectPath)).toBe(false) await writeWantedLockfile(projectPath, { importers: { '.': { @@ -192,7 +192,7 @@ test('existsWantedLockfile()', async () => { }, }, }) - expect(await existsWantedLockfile(projectPath)).toBe(true) + expect(await existsNonEmptyWantedLockfile(projectPath)).toBe(true) }) test('readWantedLockfile() when useGitBranchLockfile', async () => { diff --git a/pkg-manager/core/src/install/index.ts b/pkg-manager/core/src/install/index.ts index 2b51ada00ba..d5030d962da 100644 --- a/pkg-manager/core/src/install/index.ts +++ b/pkg-manager/core/src/install/index.ts @@ -32,7 +32,7 @@ import { type PatchFile, } from '@pnpm/lockfile-file' import { writePnpFile } from '@pnpm/lockfile-to-pnp' -import { extendProjectsWithTargetDirs } from '@pnpm/lockfile-utils' +import { extendProjectsWithTargetDirs, satisfiesPackageManifest } from '@pnpm/lockfile-utils' import { logger, globalInfo, streamParser } from '@pnpm/logger' import { getAllDependenciesFromManifest } from '@pnpm/manifest-utils' import { writeModulesManifest } from '@pnpm/modules-yaml' @@ -251,7 +251,7 @@ export async function mutateModules ( currentLockfile: ctx.currentLockfile, wantedLockfile: ctx.wantedLockfile, existsCurrentLockfile: ctx.existsCurrentLockfile, - existsWantedLockfile: ctx.existsWantedLockfile, + existsNonEmptyWantedLockfile: ctx.existsNonEmptyWantedLockfile, lockfileDir: ctx.lockfileDir, storeDir: ctx.storeDir, registries: ctx.registries, @@ -327,7 +327,7 @@ export async function mutateModules ( }), patchedDependencies) : undefined const frozenLockfile = opts.frozenLockfile || - opts.frozenLockfileIfExists && ctx.existsWantedLockfile + opts.frozenLockfileIfExists && ctx.existsNonEmptyWantedLockfile let outdatedLockfileSettings = false if (!opts.ignorePackageManifest) { const outdatedLockfileSettingName = getOutdatedLockfileSetting(ctx.wantedLockfile, { @@ -375,7 +375,7 @@ export async function mutateModules ( !needsFullResolution && opts.preferFrozenLockfile && (!opts.pruneLockfileImporters || Object.keys(ctx.wantedLockfile.importers).length === Object.keys(ctx.projects).length) && - ctx.existsWantedLockfile && + ctx.existsNonEmptyWantedLockfile && ( ctx.wantedLockfile.lockfileVersion === LOCKFILE_VERSION || ctx.wantedLockfile.lockfileVersion === LOCKFILE_VERSION_V6 @@ -401,6 +401,31 @@ Note that in CI environments, this setting is enabled by default.`, } ) } + if (!opts.ignorePackageManifest) { + const _satisfiesPackageManifest = satisfiesPackageManifest.bind(null, { + autoInstallPeers: opts.autoInstallPeers, + excludeLinksFromLockfile: opts.excludeLinksFromLockfile, + }) + for (const { id, manifest, rootDir } of Object.values(ctx.projects)) { + const { satisfies, detailedReason } = _satisfiesPackageManifest(ctx.wantedLockfile.importers[id], manifest) + if (!satisfies) { + if (!ctx.existsWantedLockfile) { + throw new PnpmError('NO_LOCKFILE', + `Cannot install with "frozen-lockfile" because ${WANTED_LOCKFILE} is present`, { + hint: 'Note that in CI environments this setting is true by default. If you still need to run install in such cases, use "pnpm install --no-frozen-lockfile"', + }) + } + throw new PnpmError('OUTDATED_LOCKFILE', + `Cannot install with "frozen-lockfile" because ${WANTED_LOCKFILE} is not up to date with ` + + path.relative(opts.lockfileDir, path.join(rootDir, 'package.json')), { + hint: `Note that in CI environments this setting is true by default. If you still need to run install in such cases, use "pnpm install --no-frozen-lockfile" + + Failure reason: + ${detailedReason ?? ''}`, + }) + } + } + } if (opts.lockfileOnly) { // The lockfile will only be changed if the workspace will have new projects with no dependencies. await writeWantedLockfile(ctx.lockfileDir, ctx.wantedLockfile) @@ -408,7 +433,7 @@ Note that in CI environments, this setting is enabled by default.`, updatedProjects: projects.map((mutatedProject) => ctx.projects[mutatedProject.rootDir]), } } - if (!ctx.existsWantedLockfile) { + if (!ctx.existsNonEmptyWantedLockfile) { if (Object.values(ctx.projects).some((project) => pkgHasDependencies(project.manifest))) { throw new Error(`Headless installation requires a ${WANTED_LOCKFILE} file`) } @@ -463,7 +488,7 @@ Note that in CI environments, this setting is enabled by default.`, error.code !== 'ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY' && !BROKEN_LOCKFILE_INTEGRITY_ERRORS.has(error.code) ) || - (!ctx.existsWantedLockfile && !ctx.existsCurrentLockfile) + (!ctx.existsNonEmptyWantedLockfile && !ctx.existsCurrentLockfile) ) throw error if (BROKEN_LOCKFILE_INTEGRITY_ERRORS.has(error.code)) { needsFullResolution = true @@ -641,12 +666,12 @@ Note that in CI environments, this setting is enabled by default.`, // Unfortunately, the private lockfile may differ from the public one. // A user might run named installations on a project that has a pnpm-lock.yaml file before running a noop install const makePartialCurrentLockfile = !installsOnly && ( - ctx.existsWantedLockfile && !ctx.existsCurrentLockfile || + ctx.existsNonEmptyWantedLockfile && !ctx.existsCurrentLockfile || !ctx.currentLockfileIsUpToDate ) const result = await installInContext(projectsToInstall, ctx, { ...opts, - currentLockfileIsUpToDate: !ctx.existsWantedLockfile || ctx.currentLockfileIsUpToDate, + currentLockfileIsUpToDate: !ctx.existsNonEmptyWantedLockfile || ctx.currentLockfileIsUpToDate, makePartialCurrentLockfile, needsFullResolution, pruneVirtualStore, @@ -1377,7 +1402,7 @@ const installInContext: InstallFunction = async (projects, ctx, opts) => { } catch (error: any) { // eslint-disable-line if ( !BROKEN_LOCKFILE_INTEGRITY_ERRORS.has(error.code) || - (!ctx.existsWantedLockfile && !ctx.existsCurrentLockfile) + (!ctx.existsNonEmptyWantedLockfile && !ctx.existsCurrentLockfile) ) throw error opts.needsFullResolution = true // Ideally, we would not update but currently there is no other way to redownload the integrity of the package diff --git a/pkg-manager/core/test/install/frozenLockfile.ts b/pkg-manager/core/test/install/frozenLockfile.ts index fccf04c834e..929967e4afc 100644 --- a/pkg-manager/core/test/install/frozenLockfile.ts +++ b/pkg-manager/core/test/install/frozenLockfile.ts @@ -124,7 +124,7 @@ test(`frozen-lockfile: should fail if no ${WANTED_LOCKFILE} is present`, async ( 'is-positive': '^3.0.0', }, }, await testDefaults({ frozenLockfile: true })) - ).rejects.toThrow(`Headless installation requires a ${WANTED_LOCKFILE} file`) + ).rejects.toThrow(`Cannot install with "frozen-lockfile" because ${WANTED_LOCKFILE} is present`) }) test(`prefer-frozen-lockfile: should prefer headless installation when ${WANTED_LOCKFILE} satisfies package.json`, async () => { diff --git a/pkg-manager/get-context/src/index.ts b/pkg-manager/get-context/src/index.ts index ccb75320c66..4ab7f8cb6c4 100644 --- a/pkg-manager/get-context/src/index.ts +++ b/pkg-manager/get-context/src/index.ts @@ -34,6 +34,7 @@ export interface PnpmContext { currentLockfileIsUpToDate: boolean existsCurrentLockfile: boolean existsWantedLockfile: boolean + existsNonEmptyWantedLockfile: boolean extraBinPaths: string[] extraNodePaths: string[] lockfileHadConflicts: boolean @@ -383,6 +384,7 @@ export interface PnpmSingleContext { currentLockfileIsUpToDate: boolean existsCurrentLockfile: boolean existsWantedLockfile: boolean + existsNonEmptyWantedLockfile: boolean extraBinPaths: string[] extraNodePaths: string[] lockfileHadConflicts: boolean diff --git a/pkg-manager/get-context/src/readLockfiles.ts b/pkg-manager/get-context/src/readLockfiles.ts index d79bfdc3b62..5d7f9d98087 100644 --- a/pkg-manager/get-context/src/readLockfiles.ts +++ b/pkg-manager/get-context/src/readLockfiles.ts @@ -5,7 +5,7 @@ import { } from '@pnpm/constants' import { createLockfileObject, - existsWantedLockfile, + existsNonEmptyWantedLockfile, isEmptyLockfile, type Lockfile, readCurrentLockfile, @@ -21,6 +21,7 @@ export interface PnpmContext { currentLockfile: Lockfile existsCurrentLockfile: boolean existsWantedLockfile: boolean + existsNonEmptyWantedLockfile: boolean wantedLockfile: Lockfile } @@ -47,6 +48,7 @@ export async function readLockfiles ( currentLockfileIsUpToDate: boolean existsCurrentLockfile: boolean existsWantedLockfile: boolean + existsNonEmptyWantedLockfile: boolean wantedLockfile: Lockfile wantedLockfileIsModified: boolean lockfileHadConflicts: boolean @@ -83,7 +85,7 @@ export async function readLockfiles ( fileReads.push(readWantedLockfile(opts.lockfileDir, lockfileOpts)) } } else { - if (await existsWantedLockfile(opts.lockfileDir, lockfileOpts)) { + if (await existsNonEmptyWantedLockfile(opts.lockfileDir, lockfileOpts)) { logger.warn({ message: `A ${WANTED_LOCKFILE} file exists. The current configuration prohibits to read or write a lockfile`, prefix: opts.lockfileDir, @@ -131,11 +133,13 @@ export async function readLockfiles ( } } } + const existsWantedLockfile = files[0] != null return { currentLockfile, currentLockfileIsUpToDate: equals(currentLockfile, wantedLockfile), existsCurrentLockfile: files[1] != null, - existsWantedLockfile: files[0] != null && !isEmptyLockfile(wantedLockfile), + existsWantedLockfile, + existsNonEmptyWantedLockfile: existsWantedLockfile && !isEmptyLockfile(wantedLockfile), wantedLockfile, wantedLockfileIsModified, lockfileHadConflicts, diff --git a/pkg-manager/headless/src/index.ts b/pkg-manager/headless/src/index.ts index 67117392497..75ceaa0e1af 100644 --- a/pkg-manager/headless/src/index.ts +++ b/pkg-manager/headless/src/index.ts @@ -13,7 +13,6 @@ import { statsLogger, summaryLogger, } from '@pnpm/core-loggers' -import { PnpmError } from '@pnpm/error' import { filterLockfileByImportersAndEngine, } from '@pnpm/filter-lockfile' @@ -36,7 +35,6 @@ import { writePnpFile } from '@pnpm/lockfile-to-pnp' import { extendProjectsWithTargetDirs, nameVerFromPkgSnapshot, - satisfiesPackageManifest, } from '@pnpm/lockfile-utils' import { type LogBase, @@ -194,26 +192,6 @@ export async function headlessInstall (opts: HeadlessOptions): Promise { })).toBeTruthy() }) -test(`fail when ${WANTED_LOCKFILE} is not up to date with package.json`, async () => { - const projectDir = tempDir() - - const simpleDir = f.find('simple') - await fs.copyFile(path.join(simpleDir, 'package.json'), path.join(projectDir, 'package.json')) - - const simpleWithMoreDepsDir = f.find('simple-with-more-deps') - await fs.copyFile(path.join(simpleWithMoreDepsDir, WANTED_LOCKFILE), path.join(projectDir, WANTED_LOCKFILE)) - - try { - await headlessInstall(await testDefaults({ lockfileDir: projectDir })) - throw new Error() - } catch (err: any) { // eslint-disable-line - expect(err.message).toBe(`Cannot install with "frozen-lockfile" because ${WANTED_LOCKFILE} is not up to date with package.json`) - } -}) - test('installing local dependency', async () => { let prefix = f.prepare('has-local-dep') prefix = path.join(prefix, 'pkg') diff --git a/pnpm/test/install/hooks.ts b/pnpm/test/install/hooks.ts index a7f1d4a26e3..0c1a0ea1a2e 100644 --- a/pnpm/test/install/hooks.ts +++ b/pnpm/test/install/hooks.ts @@ -646,7 +646,7 @@ test('preResolution hook', async () => { expect(ctx.lockfileDir).toBeDefined() expect(ctx.storeDir).toBeDefined() expect(ctx.existsCurrentLockfile).toBe(false) - expect(ctx.existsWantedLockfile).toBe(false) + expect(ctx.existsNonEmptyWantedLockfile).toBe(false) expect(ctx.registries).toEqual({ default: 'http://localhost:7776/',