Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .changeset/scaffold-pnpm11-build-approvals.md
Original file line number Diff line number Diff line change
@@ -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.
43 changes: 43 additions & 0 deletions packages/create-objectstack/src/template-consistency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.<pkg> = 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');
Expand Down
Original file line number Diff line number Diff line change
@@ -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
72 changes: 53 additions & 19 deletions scripts/publish-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)"
Expand Down
Loading