diff --git a/.changeset/scaffold-pnpm11-build-approvals.md b/.changeset/scaffold-pnpm11-build-approvals.md new file mode 100644 index 0000000000..5472aa39a8 --- /dev/null +++ b/.changeset/scaffold-pnpm11-build-approvals.md @@ -0,0 +1,35 @@ +--- +"create-objectstack": patch +--- + +fix(create-objectstack): the blank scaffold declares pnpm build approvals, so a fresh `pnpm install` no longer exits 1 on pnpm 11 + +pnpm 11 turned an unapproved dependency build script from a warning into a hard +error. The blank template declared no build approvals, so the very first command +a new user runs failed on any current pnpm: + +``` +npx create-objectstack myapp && cd myapp && pnpm install +# [ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: better-sqlite3@12.11.1, esbuild@0.28.1 +# exit 1 +``` + +The scaffold now ships a `pnpm-workspace.yaml` approving the two packages it +actually depends on building — `better-sqlite3` (the native sqlite driver behind +`@objectstack/driver-sql`) and `esbuild` (compiles `objectstack.config.ts`). + +Both approval keys are present because pnpm reads them by version, and neither +alone covers the supported range: + +- `allowBuilds` (a package → boolean map) — the only key pnpm 11 honors, and + understood back to pnpm 10.31. `onlyBuiltDependencies` alone still errors. +- `onlyBuiltDependencies` (a list) — pnpm 10.0–10.30, which ignore `allowBuilds`. + +npm and yarn ignore the file, so the npm install path is unaffected. Both +packages ship prebuilt binaries, so this was an install-time hard stop rather +than a runtime defect — the project ran fine once installed. + +This is the #3091 failure class (in-repo settings masking what users resolve) +and was caught by the publish smoke gate added in #3100, which installs the +release candidate the way a user does — on whatever pnpm corepack hands a fresh +machine. diff --git a/packages/create-objectstack/src/template-consistency.test.ts b/packages/create-objectstack/src/template-consistency.test.ts index 22db434966..c2ad977768 100644 --- a/packages/create-objectstack/src/template-consistency.test.ts +++ b/packages/create-objectstack/src/template-consistency.test.ts @@ -70,6 +70,49 @@ describe('blank template manifest engines.protocol (ADR-0087 D1)', () => { }); }); +// pnpm 11 turned an unapproved dependency build script from a warning into a +// hard error, so the template declaring nothing meant `npx create-objectstack` +// + `pnpm install` exited 1 for every user on a current pnpm (#3110). Both keys +// are load-bearing and read by different pnpm versions: pnpm 11 honours only +// `allowBuilds`, while pnpm 10.0–10.30 understand only `onlyBuiltDependencies`. +describe('blank template pnpm build approvals (#3110)', () => { + const wsPath = path.join(pkgRoot, 'src', 'templates', 'blank', 'pnpm-workspace.yaml'); + const APPROVED = ['better-sqlite3', 'esbuild']; + // Strip comments: the prose below explains these keys and must not satisfy + // an assertion that the settings themselves are supposed to satisfy. + const settings = fs.existsSync(wsPath) + ? fs.readFileSync(wsPath, 'utf8').replace(/^\s*#.*$/gm, '') + : ''; + + it('ships a pnpm-workspace.yaml', () => { + expect( + fs.existsSync(wsPath), + 'without it a fresh `pnpm install` exits 1 on pnpm 11 (ERR_PNPM_IGNORED_BUILDS)', + ).toBe(true); + }); + + it('sets allowBuilds. = true, the only key pnpm 11 reads', () => { + const block = /^allowBuilds:\n((?:[ \t]+.*\n?)*)/m.exec(settings)?.[1] ?? ''; + for (const pkg of APPROVED) { + expect( + new RegExp(`^\\s+${pkg}:\\s*true\\s*$`, 'm').test(block), + `allowBuilds must set "${pkg}: true" — pnpm 11 errors on an unapproved build ` + + 'and ignores onlyBuiltDependencies', + ).toBe(true); + } + }); + + it('lists the same packages under onlyBuiltDependencies for pnpm 10.0–10.30', () => { + const block = /^onlyBuiltDependencies:\n((?:[ \t]*-.*\n?)*)/m.exec(settings)?.[1] ?? ''; + for (const pkg of APPROVED) { + expect( + new RegExp(`^\\s*-\\s*${pkg}\\s*$`, 'm').test(block), + `onlyBuiltDependencies must list "${pkg}" — pnpm < 10.31 does not understand allowBuilds`, + ).toBe(true); + } + }); +}); + describe('README template table', () => { it('lists exactly the templates in the TEMPLATES registry', () => { const readme = fs.readFileSync(path.join(pkgRoot, 'README.md'), 'utf8'); diff --git a/packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml b/packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml new file mode 100644 index 0000000000..04a1dda154 --- /dev/null +++ b/packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml @@ -0,0 +1,23 @@ +# pnpm does not run dependency install scripts unless they are approved here. +# Without this file a fresh `pnpm install` on pnpm 11 exits 1 with +# ERR_PNPM_IGNORED_BUILDS — pnpm 10 only warned, pnpm 11 made it a hard error. +# +# Both keys are needed; they are read by different pnpm versions: +# allowBuilds pnpm >= 10.31 and pnpm 11+. pnpm 11 reads ONLY this +# one — onlyBuiltDependencies alone still errors. +# onlyBuiltDependencies pnpm 10.0–10.30, which do not understand allowBuilds. +# +# better-sqlite3 is the native sqlite driver (@objectstack/driver-sql's optional +# dependency); esbuild compiles objectstack.config.ts. Both ship prebuilt +# binaries, so an unapproved build degrades rather than breaks — but pnpm 11 +# fails the install before it gets that far. +# +# npm and yarn ignore this file; it costs them nothing. + +onlyBuiltDependencies: + - better-sqlite3 + - esbuild + +allowBuilds: + better-sqlite3: true + esbuild: true diff --git a/scripts/publish-smoke.sh b/scripts/publish-smoke.sh index 68192703be..ca61542b42 100644 --- a/scripts/publish-smoke.sh +++ b/scripts/publish-smoke.sh @@ -36,6 +36,20 @@ # runtime falls back to the WASM sqlite driver (#2229) — the smoke must never # be blocked on node-gyp. # +# pnpm version: deliberately NOT pinned in the scaffolded project. This repo's +# `packageManager` pin applies to this checkout, not to a project outside it, +# so corepack resolves the LATEST pnpm there — which is what a user on a fresh +# machine gets, and therefore what this gate must test. Stamping packageManager +# into the generated app would buy reproducibility by testing a pnpm nobody +# downstream runs: the same "in-repo settings hide the user's real resolution" +# mistake that #3091 was, in a new costume. The tradeoff is real — a new pnpm +# major can turn this red with no code change here — but that is signal, not +# noise: it means a fresh install is broken for new users and the +# create-objectstack template needs updating. It has already happened once: +# pnpm 11 turned unapproved build scripts from a warning into a hard error, so +# every `npx create-objectstack` + `pnpm install` exited 1 until the template +# started declaring `allowBuilds` (#3110). +# # Usage: # bash scripts/publish-smoke.sh # Env: @@ -108,33 +122,53 @@ if [ "$SMOKE_MODE" = "pack" ]; then (cd "$SMOKE_ROOT" && node "$REPO_ROOT/packages/create-objectstack/bin/create-objectstack.js" \ "$APP_NAME" --skip-install --skip-skills) - # The project gets its OWN pnpm-workspace.yaml: it is its own workspace - # root (never resolves settings from this repo), and — because pnpm v10 - # reads overrides only from this file — it pins every @objectstack/* to - # the packed tarballs. Anything NOT in the map (transitive deps, - # better-auth, hono, …) resolves from the registry exactly as it would - # for a real user; that unpinned resolution is the thing under test. + # The project is its own workspace root: having a pnpm-workspace.yaml at all + # stops pnpm walking up, so it never inherits this repo's settings. We APPEND + # the tarball pins to the file the TEMPLATE ships rather than writing our own. + # + # Appending, not overwriting, is load-bearing: the template's build-approval + # block (allowBuilds / onlyBuiltDependencies) is part of what a user gets, so + # it has to be part of what this gate tests. This script used to hand-write + # the whole file, declaring the approvals itself — which is exactly how #3110 + # stayed invisible here: a locally-authored declaration proves nothing about + # what the template ships. + # + # Anything NOT in the override map (transitive deps, better-auth, hono, …) + # resolves from the registry exactly as it would for a real user; that + # unpinned resolution is the thing under test. log "Pinning @objectstack/* to local tarballs via project-local overrides" node - "$SMOKE_ROOT/tarballs/overrides.json" "$APP_DIR/pnpm-workspace.yaml" <<'EOF' -const { readFileSync, writeFileSync } = require('node:fs'); -const [overridesPath, outPath] = process.argv.slice(2); +const { existsSync, readFileSync, writeFileSync } = require('node:fs'); +const [overridesPath, wsPath] = process.argv.slice(2); const overrides = JSON.parse(readFileSync(overridesPath, 'utf8')); + +// Whatever the template shipped stays verbatim; we only add `overrides:`. +const base = existsSync(wsPath) ? readFileSync(wsPath, 'utf8').replace(/\s*$/, '') : ''; + +// Fail here rather than let pnpm fail cryptically 200 lines later: no build +// approvals means a fresh `pnpm install` exits 1 on pnpm 11 for every user. +if (!/^\s*(allowBuilds|onlyBuiltDependencies)\s*:/m.test(base)) { + console.error( + '::error::the scaffolded project declares no pnpm build approvals ' + + '(allowBuilds / onlyBuiltDependencies). A fresh `pnpm install` will exit 1 ' + + 'on pnpm 11. Fix the create-objectstack template — not this script.', + ); + process.exit(1); +} + const lines = [ - '# Generated by scripts/publish-smoke.sh — standalone workspace root, no', - '# settings inherited from the framework repo. @objectstack/* pinned to the', - '# about-to-publish tarballs; everything else resolves from the registry.', - 'packages:', - " - '.'", - '', - 'onlyBuiltDependencies:', - ' - better-sqlite3', - ' - esbuild', + base, '', + '# ── appended by scripts/publish-smoke.sh ─────────────────────────────────', + '# @objectstack/* pinned to the about-to-publish tarballs; everything above', + '# is what the template ships, everything else resolves from the registry.', 'overrides:', ...Object.entries(overrides).map(([name, spec]) => ` '${name}': '${spec}'`), ]; -writeFileSync(outPath, lines.join('\n') + '\n'); -console.log(` wrote ${outPath} (${Object.keys(overrides).length} overrides)`); +writeFileSync(wsPath, lines.join('\n') + '\n'); +console.log( + ` wrote ${wsPath} (${Object.keys(overrides).length} overrides, template settings preserved)`, +); EOF log "Installing (pnpm, tarball-pinned)"