diff --git a/packages/core/src/constants/package-manager.ts b/packages/core/src/constants/package-manager.ts index 94fb2c1d..0e4b7efe 100644 --- a/packages/core/src/constants/package-manager.ts +++ b/packages/core/src/constants/package-manager.ts @@ -5,12 +5,15 @@ export const SUPPORTED_PMS: PackageManager[] = ['pnpm', 'bun', 'npm'] /** Order of preference when nothing is specified. */ export const DEFAULT_PM_PRIORITY: PackageManager[] = ['pnpm', 'bun', 'npm'] -/** The pnpm version this repo develops and tests against. Not injected into generated projects. */ +/** The pnpm version this repo develops and tests against. Only its major reaches generated projects, via `PNPM_PIN_MAJOR`. */ export const PNPM_PIN = 'pnpm@12.5.1' /** `PNPM_PIN` without the `pnpm@` prefix. */ export const PNPM_PIN_VERSION = PNPM_PIN.slice(PNPM_PIN.indexOf('@') + 1) +/** The pnpm major a generated project's Dockerfile installs: patches flow in, a new major cannot break the image build. */ +export const PNPM_PIN_MAJOR = PNPM_PIN_VERSION.split('.')[0] + /** * Oldest pnpm battlestack supports. Preflight fails below it. * A supported-version floor, not a capability floor; docs/troubleshooting.md states the basis. diff --git a/packages/core/src/utils/package-manager.ts b/packages/core/src/utils/package-manager.ts index 52b3c917..8b1524ae 100644 --- a/packages/core/src/utils/package-manager.ts +++ b/packages/core/src/utils/package-manager.ts @@ -2,11 +2,12 @@ import path from 'node:path' import { run } from './run.js' import { exists, readJson } from './fs.js' import type { PackageManager } from '../types/package-manager.js' -import { BOILERPLATE_ALLOWED_BUILDS, DEFAULT_PM_PRIORITY, SUPPORTED_PMS } from '../constants/package-manager.js' +import { BOILERPLATE_ALLOWED_BUILDS, DEFAULT_PM_PRIORITY, PNPM_PIN_MAJOR, SUPPORTED_PMS } from '../constants/package-manager.js' -/** Shell commands installing the PM globally on a clean Node image. Unpinned. */ +/** Shell commands installing the PM globally on a clean Node image. pnpm is pinned to a major, bun is not. */ export function pmInstallGlobalCommands(pm: PackageManager): string[] { if (pm === 'npm') return [] + if (pm === 'pnpm') return [`npm install -g pnpm@${PNPM_PIN_MAJOR}`] return [`npm install -g ${pm}`] } diff --git a/packages/core/test/package-manager.project.test.ts b/packages/core/test/package-manager.project.test.ts index 05b30e9d..4dbf32d2 100644 --- a/packages/core/test/package-manager.project.test.ts +++ b/packages/core/test/package-manager.project.test.ts @@ -2,6 +2,7 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import os from 'node:os' import path from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { PNPM_PIN_MAJOR } from '../src/constants/package-manager.js' import { ensureWorkspaceMarker, parseIgnoredBuilds, pmInstallGlobalCommands, resolveProjectPM, writeWorkspaceReleaseAge } from '../src/utils/package-manager.js' let projectDir: string @@ -117,7 +118,12 @@ describe('parseIgnoredBuilds', () => { describe('pmInstallGlobalCommands', () => { it('npm needs no bootstrap; pnpm/bun install globally via npm', () => { expect(pmInstallGlobalCommands('npm')).toEqual([]) - expect(pmInstallGlobalCommands('pnpm')).toEqual(['npm install -g pnpm']) + expect(pmInstallGlobalCommands('pnpm')).toEqual([`npm install -g pnpm@${PNPM_PIN_MAJOR}`]) expect(pmInstallGlobalCommands('bun')).toEqual(['npm install -g bun']) }) + + // A new pnpm major can reject, under --frozen-lockfile, a lockfile the previous one accepted. + it('pins pnpm to a major, never to latest', () => { + expect(pmInstallGlobalCommands('pnpm')[0]).toMatch(/^npm install -g pnpm@\d+$/) + }) }) diff --git a/packages/preset-nuxt4/src/features/docker.ts b/packages/preset-nuxt4/src/features/docker.ts index 55cca83d..bdd3b8ad 100644 --- a/packages/preset-nuxt4/src/features/docker.ts +++ b/packages/preset-nuxt4/src/features/docker.ts @@ -26,7 +26,7 @@ const PROFILE_FLAGS = ['--profile', 'prod'] /** Production Dockerfile and `battlestack prod` commands, reusing the existing docker-compose.yml. */ export const dockerFeature: Feature = { id: 'shared:docker', - version: '1.2.0', + version: '1.2.1', label: 'Production Dockerfile + prod commands', stage: STAGE.GITIGNORE, failureIsNonFatal: true,