diff --git a/.changeset/nasty-seals-run.md b/.changeset/nasty-seals-run.md new file mode 100644 index 0000000000..2ad5fe2e26 --- /dev/null +++ b/.changeset/nasty-seals-run.md @@ -0,0 +1,9 @@ +--- +"@pnpm/cli-utils": major +"pnpm": major +"@pnpm/exportable-manifest": major +--- + +pnpm will now check the `package.json` file for a `packageManager` field. If this field is present and specifies a different package manager or a different version of pnpm than the one you're currently using, pnpm will not proceed. This ensures that you're always using the correct package manager and version that the project requires. + +To disable this behaviour, set the `package-manager-strict` setting to `false` or the `COREPACK_ENABLE_STRICT` env variable to `0`. diff --git a/cli/cli-utils/src/packageIsInstallable.ts b/cli/cli-utils/src/packageIsInstallable.ts index cd86c79456..975759e834 100644 --- a/cli/cli-utils/src/packageIsInstallable.ts +++ b/cli/cli-utils/src/packageIsInstallable.ts @@ -1,17 +1,20 @@ +import { PnpmError } from '@pnpm/error' import { packageManager } from '@pnpm/cli-meta' -import { logger } from '@pnpm/logger' +import { logger, globalWarn } from '@pnpm/logger' import { checkPackage, UnsupportedEngineError, type WantedEngine } from '@pnpm/package-is-installable' import { type SupportedArchitectures } from '@pnpm/types' export function packageIsInstallable ( pkgPath: string, pkg: { + packageManager?: string engines?: WantedEngine cpu?: string[] os?: string[] libc?: string[] }, opts: { + packageManagerStrict?: boolean engineStrict?: boolean nodeVersion?: string supportedArchitectures?: SupportedArchitectures @@ -20,6 +23,24 @@ export function packageIsInstallable ( const pnpmVersion = packageManager.name === 'pnpm' ? packageManager.stableVersion : undefined + if (pkg.packageManager) { + const [pmName, pmVersion] = pkg.packageManager.split('@') + if (pmName && pmName !== 'pnpm') { + const msg = `This project is configured to use ${pmName}` + if (opts.packageManagerStrict) { + throw new PnpmError('OTHER_PM_EXPECTED', msg) + } else { + globalWarn(msg) + } + } else if (pmVersion && pnpmVersion && pmVersion !== pnpmVersion) { + const msg = `This project is configured to use v${pmVersion} of pnpm. Your current pnpm is v${pnpmVersion}` + if (opts.packageManagerStrict) { + throw new PnpmError('BAD_PM_VERSION', msg) + } else { + globalWarn(msg) + } + } + } const err = checkPackage(pkgPath, pkg, { nodeVersion: opts.nodeVersion, pnpmVersion, diff --git a/cli/cli-utils/src/readProjectManifest.ts b/cli/cli-utils/src/readProjectManifest.ts index da7bdff8d1..b2a62d92e9 100644 --- a/cli/cli-utils/src/readProjectManifest.ts +++ b/cli/cli-utils/src/readProjectManifest.ts @@ -4,6 +4,7 @@ import { packageIsInstallable } from './packageIsInstallable' export interface ReadProjectManifestOpts { engineStrict?: boolean + packageManagerStrict?: boolean nodeVersion?: string supportedArchitectures?: SupportedArchitectures } diff --git a/config/config/src/Config.ts b/config/config/src/Config.ts index 28de9692fc..21440af3db 100644 --- a/config/config/src/Config.ts +++ b/config/config/src/Config.ts @@ -188,6 +188,7 @@ export interface Config { lockfile?: boolean dedupeInjectedDeps?: boolean nodeOptions?: string + packageManagerStrict?: boolean } export interface ConfigWithDeprecatedSettings extends Config { diff --git a/config/config/src/index.ts b/config/config/src/index.ts index 850cd79cfb..623ca781eb 100644 --- a/config/config/src/index.ts +++ b/config/config/src/index.ts @@ -103,6 +103,7 @@ export const types = Object.assign({ 'package-import-method': ['auto', 'hardlink', 'clone', 'copy'], 'patches-dir': String, pnpmfile: String, + 'package-manager-strict': Boolean, 'prefer-frozen-lockfile': Boolean, 'prefer-offline': Boolean, 'prefer-symlinked-executables': Boolean, @@ -240,6 +241,7 @@ export async function getConfig ( 'node-linker': 'isolated', 'package-lock': npmDefaults['package-lock'], pending: false, + 'package-manager-strict': process.env.COREPACK_ENABLE_STRICT !== '0', 'prefer-workspace-packages': false, 'public-hoist-pattern': [ '*eslint*', diff --git a/cspell.json b/cspell.json index f8f21828d6..e597a770c9 100644 --- a/cspell.json +++ b/cspell.json @@ -16,6 +16,7 @@ "autozoom", "babek", "badheaders", + "behaviour", "blabla", "brasileiro", "bryntum", diff --git a/packages/types/src/package.ts b/packages/types/src/package.ts index 29f2a11671..5ba50be37a 100644 --- a/packages/types/src/package.ts +++ b/packages/types/src/package.ts @@ -122,6 +122,7 @@ export interface PeerDependencyRules { export type AllowedDeprecatedVersions = Record export type ProjectManifest = BaseManifest & { + packageManager?: string workspaces?: string[] pnpm?: { neverBuiltDependencies?: string[] diff --git a/pkg-manifest/exportable-manifest/src/index.ts b/pkg-manifest/exportable-manifest/src/index.ts index 8cdb58f77a..2a926ae7f6 100644 --- a/pkg-manifest/exportable-manifest/src/index.ts +++ b/pkg-manifest/exportable-manifest/src/index.ts @@ -25,7 +25,7 @@ export async function createExportableManifest ( originalManifest: ProjectManifest, opts?: MakePublishManifestOptions ) { - const publishManifest: ProjectManifest = omit(['pnpm', 'scripts'], originalManifest) + const publishManifest: ProjectManifest = omit(['pnpm', 'scripts', 'packageManager'], originalManifest) if (originalManifest.scripts != null) { publishManifest.scripts = omit(PREPUBLISH_SCRIPTS, originalManifest.scripts) } diff --git a/pkg-manifest/exportable-manifest/test/index.test.ts b/pkg-manifest/exportable-manifest/test/index.test.ts index 24f192ca1c..0f2f6f1a34 100644 --- a/pkg-manifest/exportable-manifest/test/index.test.ts +++ b/pkg-manifest/exportable-manifest/test/index.test.ts @@ -29,6 +29,23 @@ test('the pnpm options are removed', async () => { }) }) +test('the packageManager field is removed', async () => { + expect(await createExportableManifest(process.cwd(), { + name: 'foo', + version: '1.0.0', + dependencies: { + qar: '2', + }, + packageManager: 'pnpm@8.0.0', + })).toStrictEqual({ + name: 'foo', + version: '1.0.0', + dependencies: { + qar: '2', + }, + }) +}) + test('publish lifecycle scripts are removed', async () => { expect(await createExportableManifest(process.cwd(), { name: 'foo', diff --git a/pnpm/test/install/misc.ts b/pnpm/test/install/misc.ts index 5a8221a6f0..42b39a4f4b 100644 --- a/pnpm/test/install/misc.ts +++ b/pnpm/test/install/misc.ts @@ -260,6 +260,43 @@ test('install should fail if the used pnpm version does not satisfy the pnpm ver expect(stdout.toString()).toContain('Your pnpm version is incompatible with') }) +test('install should fail if the used pnpm version does not satisfy the pnpm version specified in packageManager', async () => { + prepare({ + name: 'project', + version: '1.0.0', + + packageManager: 'pnpm@0.0.0', + }) + + const { status, stdout } = execPnpmSync(['install']) + + expect(status).toBe(1) + expect(stdout.toString()).toContain('This project is configured to use v0.0.0 of pnpm. Your current pnpm is') + + expect(execPnpmSync(['install', '--config.package-manager-strict=false']).status).toBe(0) + expect(execPnpmSync(['install'], { + env: { + COREPACK_ENABLE_STRICT: '0', + }, + }).status).toBe(0) +}) + +test('install should fail if the project requires a different package manager', async () => { + prepare({ + name: 'project', + version: '1.0.0', + + packageManager: 'yarn@4.0.0', + }) + + const { status, stdout } = execPnpmSync(['install']) + + expect(status).toBe(1) + expect(stdout.toString()).toContain('This project is configured to use yarn') + + expect(execPnpmSync(['install', '--config.package-manager-strict=false']).status).toBe(0) +}) + test('engine-strict=false: install should not fail if the used Node version does not satisfy the Node version specified in engines', async () => { prepare({ name: 'project', diff --git a/workspace/find-packages/src/index.ts b/workspace/find-packages/src/index.ts index 0d8e8a125c..4ba0a158ee 100644 --- a/workspace/find-packages/src/index.ts +++ b/workspace/find-packages/src/index.ts @@ -11,6 +11,7 @@ export async function findWorkspacePackages ( workspaceRoot: string, opts?: { engineStrict?: boolean + packageManagerStrict?: boolean nodeVersion?: string patterns?: string[] sharedWorkspaceLockfile?: boolean