Skip to content

Commit 36fc09e

Browse files
os-zhuangclaude
andcommitted
fix(create-objectstack): declare pnpm build approvals so a fresh install works on pnpm 11
`npx create-objectstack myapp && cd myapp && pnpm install` has been exiting 1 on pnpm 11 — ERR_PNPM_IGNORED_BUILDS for better-sqlite3 and esbuild. pnpm 11 turned an unapproved dependency build script from a warning into a hard error, and the blank template declared no approvals at all. Verified against published 15.1.1: the first command a new user runs fails. The scaffold now ships a pnpm-workspace.yaml approving the two packages it depends on building. Both keys are required — neither alone covers the supported range: pnpm 11 honors only `allowBuilds` (understood back to 10.31), while pnpm 10.0–10.30 understand only `onlyBuiltDependencies`. npm and yarn ignore the file. This is why Publish Smoke has been red on main since #3100 introduced it. The gate was never broken: it was correctly reporting a real, shipping product bug on its first run. Pinning the smoke's pnpm to the repo's `packageManager` would have turned it green by testing a pnpm no user runs — the same "in-repo settings hide the user's real resolution" mistake as #3091, which is the exact failure class this gate exists to catch. So the pnpm version stays unpinned, and the reasoning is recorded in the script header. publish-smoke.sh now appends its tarball overrides to the file the template ships instead of hand-writing the whole thing. It previously declared the approvals itself, so it proved nothing about what users get — that is how this bug stayed invisible to a gate designed to find it. It now fails loudly if the scaffold declares no approvals. Verified end-to-end under CI-identical resolution (corepack shim: pnpm 10.31.0 in-repo, 11.13.1 in the app dir): full smoke green — clean install, auth sign-up/sign-in/get-session 200, REST CRUD, no error logs. Negative control: stripping `allowBuilds` reproduces the exact CI failure; restoring it passes. The new template-consistency ratchet fails on both regression shapes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 021ba4c commit 36fc09e

4 files changed

Lines changed: 154 additions & 19 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
"create-objectstack": patch
3+
---
4+
5+
fix(create-objectstack): the blank scaffold declares pnpm build approvals, so a fresh `pnpm install` no longer exits 1 on pnpm 11
6+
7+
pnpm 11 turned an unapproved dependency build script from a warning into a hard
8+
error. The blank template declared no build approvals, so the very first command
9+
a new user runs failed on any current pnpm:
10+
11+
```
12+
npx create-objectstack myapp && cd myapp && pnpm install
13+
# [ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: better-sqlite3@12.11.1, esbuild@0.28.1
14+
# exit 1
15+
```
16+
17+
The scaffold now ships a `pnpm-workspace.yaml` approving the two packages it
18+
actually depends on building — `better-sqlite3` (the native sqlite driver behind
19+
`@objectstack/driver-sql`) and `esbuild` (compiles `objectstack.config.ts`).
20+
21+
Both approval keys are present because pnpm reads them by version, and neither
22+
alone covers the supported range:
23+
24+
- `allowBuilds` (a package → boolean map) — the only key pnpm 11 honors, and
25+
understood back to pnpm 10.31. `onlyBuiltDependencies` alone still errors.
26+
- `onlyBuiltDependencies` (a list) — pnpm 10.0–10.30, which ignore `allowBuilds`.
27+
28+
npm and yarn ignore the file, so the npm install path is unaffected. Both
29+
packages ship prebuilt binaries, so this was an install-time hard stop rather
30+
than a runtime defect — the project ran fine once installed.
31+
32+
This is the #3091 failure class (in-repo settings masking what users resolve)
33+
and was caught by the publish smoke gate added in #3100, which installs the
34+
release candidate the way a user does — on whatever pnpm corepack hands a fresh
35+
machine.

packages/create-objectstack/src/template-consistency.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,49 @@ describe('blank template manifest engines.protocol (ADR-0087 D1)', () => {
7070
});
7171
});
7272

73+
// pnpm 11 turned an unapproved dependency build script from a warning into a
74+
// hard error, so the template declaring nothing meant `npx create-objectstack`
75+
// + `pnpm install` exited 1 for every user on a current pnpm (#3110). Both keys
76+
// are load-bearing and read by different pnpm versions: pnpm 11 honours only
77+
// `allowBuilds`, while pnpm 10.0–10.30 understand only `onlyBuiltDependencies`.
78+
describe('blank template pnpm build approvals (#3110)', () => {
79+
const wsPath = path.join(pkgRoot, 'src', 'templates', 'blank', 'pnpm-workspace.yaml');
80+
const APPROVED = ['better-sqlite3', 'esbuild'];
81+
// Strip comments: the prose below explains these keys and must not satisfy
82+
// an assertion that the settings themselves are supposed to satisfy.
83+
const settings = fs.existsSync(wsPath)
84+
? fs.readFileSync(wsPath, 'utf8').replace(/^\s*#.*$/gm, '')
85+
: '';
86+
87+
it('ships a pnpm-workspace.yaml', () => {
88+
expect(
89+
fs.existsSync(wsPath),
90+
'without it a fresh `pnpm install` exits 1 on pnpm 11 (ERR_PNPM_IGNORED_BUILDS)',
91+
).toBe(true);
92+
});
93+
94+
it('sets allowBuilds.<pkg> = true, the only key pnpm 11 reads', () => {
95+
const block = /^allowBuilds:\n((?:[ \t]+.*\n?)*)/m.exec(settings)?.[1] ?? '';
96+
for (const pkg of APPROVED) {
97+
expect(
98+
new RegExp(`^\\s+${pkg}:\\s*true\\s*$`, 'm').test(block),
99+
`allowBuilds must set "${pkg}: true" — pnpm 11 errors on an unapproved build ` +
100+
'and ignores onlyBuiltDependencies',
101+
).toBe(true);
102+
}
103+
});
104+
105+
it('lists the same packages under onlyBuiltDependencies for pnpm 10.0–10.30', () => {
106+
const block = /^onlyBuiltDependencies:\n((?:[ \t]*-.*\n?)*)/m.exec(settings)?.[1] ?? '';
107+
for (const pkg of APPROVED) {
108+
expect(
109+
new RegExp(`^\\s*-\\s*${pkg}\\s*$`, 'm').test(block),
110+
`onlyBuiltDependencies must list "${pkg}" — pnpm < 10.31 does not understand allowBuilds`,
111+
).toBe(true);
112+
}
113+
});
114+
});
115+
73116
describe('README template table', () => {
74117
it('lists exactly the templates in the TEMPLATES registry', () => {
75118
const readme = fs.readFileSync(path.join(pkgRoot, 'README.md'), 'utf8');
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# pnpm does not run dependency install scripts unless they are approved here.
2+
# Without this file a fresh `pnpm install` on pnpm 11 exits 1 with
3+
# ERR_PNPM_IGNORED_BUILDS — pnpm 10 only warned, pnpm 11 made it a hard error.
4+
#
5+
# Both keys are needed; they are read by different pnpm versions:
6+
# allowBuilds pnpm >= 10.31 and pnpm 11+. pnpm 11 reads ONLY this
7+
# one — onlyBuiltDependencies alone still errors.
8+
# onlyBuiltDependencies pnpm 10.0–10.30, which do not understand allowBuilds.
9+
#
10+
# better-sqlite3 is the native sqlite driver (@objectstack/driver-sql's optional
11+
# dependency); esbuild compiles objectstack.config.ts. Both ship prebuilt
12+
# binaries, so an unapproved build degrades rather than breaks — but pnpm 11
13+
# fails the install before it gets that far.
14+
#
15+
# npm and yarn ignore this file; it costs them nothing.
16+
17+
onlyBuiltDependencies:
18+
- better-sqlite3
19+
- esbuild
20+
21+
allowBuilds:
22+
better-sqlite3: true
23+
esbuild: true

scripts/publish-smoke.sh

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@
3636
# runtime falls back to the WASM sqlite driver (#2229) — the smoke must never
3737
# be blocked on node-gyp.
3838
#
39+
# pnpm version: deliberately NOT pinned in the scaffolded project. This repo's
40+
# `packageManager` pin applies to this checkout, not to a project outside it,
41+
# so corepack resolves the LATEST pnpm there — which is what a user on a fresh
42+
# machine gets, and therefore what this gate must test. Stamping packageManager
43+
# into the generated app would buy reproducibility by testing a pnpm nobody
44+
# downstream runs: the same "in-repo settings hide the user's real resolution"
45+
# mistake that #3091 was, in a new costume. The tradeoff is real — a new pnpm
46+
# major can turn this red with no code change here — but that is signal, not
47+
# noise: it means a fresh install is broken for new users and the
48+
# create-objectstack template needs updating. It has already happened once:
49+
# pnpm 11 turned unapproved build scripts from a warning into a hard error, so
50+
# every `npx create-objectstack` + `pnpm install` exited 1 until the template
51+
# started declaring `allowBuilds` (#3110).
52+
#
3953
# Usage:
4054
# bash scripts/publish-smoke.sh
4155
# Env:
@@ -108,33 +122,53 @@ if [ "$SMOKE_MODE" = "pack" ]; then
108122
(cd "$SMOKE_ROOT" && node "$REPO_ROOT/packages/create-objectstack/bin/create-objectstack.js" \
109123
"$APP_NAME" --skip-install --skip-skills)
110124

111-
# The project gets its OWN pnpm-workspace.yaml: it is its own workspace
112-
# root (never resolves settings from this repo), and — because pnpm v10
113-
# reads overrides only from this file — it pins every @objectstack/* to
114-
# the packed tarballs. Anything NOT in the map (transitive deps,
115-
# better-auth, hono, …) resolves from the registry exactly as it would
116-
# for a real user; that unpinned resolution is the thing under test.
125+
# The project is its own workspace root: having a pnpm-workspace.yaml at all
126+
# stops pnpm walking up, so it never inherits this repo's settings. We APPEND
127+
# the tarball pins to the file the TEMPLATE ships rather than writing our own.
128+
#
129+
# Appending, not overwriting, is load-bearing: the template's build-approval
130+
# block (allowBuilds / onlyBuiltDependencies) is part of what a user gets, so
131+
# it has to be part of what this gate tests. This script used to hand-write
132+
# the whole file, declaring the approvals itself — which is exactly how #3110
133+
# stayed invisible here: a locally-authored declaration proves nothing about
134+
# what the template ships.
135+
#
136+
# Anything NOT in the override map (transitive deps, better-auth, hono, …)
137+
# resolves from the registry exactly as it would for a real user; that
138+
# unpinned resolution is the thing under test.
117139
log "Pinning @objectstack/* to local tarballs via project-local overrides"
118140
node - "$SMOKE_ROOT/tarballs/overrides.json" "$APP_DIR/pnpm-workspace.yaml" <<'EOF'
119-
const { readFileSync, writeFileSync } = require('node:fs');
120-
const [overridesPath, outPath] = process.argv.slice(2);
141+
const { existsSync, readFileSync, writeFileSync } = require('node:fs');
142+
const [overridesPath, wsPath] = process.argv.slice(2);
121143
const overrides = JSON.parse(readFileSync(overridesPath, 'utf8'));
144+
145+
// Whatever the template shipped stays verbatim; we only add `overrides:`.
146+
const base = existsSync(wsPath) ? readFileSync(wsPath, 'utf8').replace(/\s*$/, '') : '';
147+
148+
// Fail here rather than let pnpm fail cryptically 200 lines later: no build
149+
// approvals means a fresh `pnpm install` exits 1 on pnpm 11 for every user.
150+
if (!/^\s*(allowBuilds|onlyBuiltDependencies)\s*:/m.test(base)) {
151+
console.error(
152+
'::error::the scaffolded project declares no pnpm build approvals ' +
153+
'(allowBuilds / onlyBuiltDependencies). A fresh `pnpm install` will exit 1 ' +
154+
'on pnpm 11. Fix the create-objectstack template — not this script.',
155+
);
156+
process.exit(1);
157+
}
158+
122159
const lines = [
123-
'# Generated by scripts/publish-smoke.sh — standalone workspace root, no',
124-
'# settings inherited from the framework repo. @objectstack/* pinned to the',
125-
'# about-to-publish tarballs; everything else resolves from the registry.',
126-
'packages:',
127-
" - '.'",
128-
'',
129-
'onlyBuiltDependencies:',
130-
' - better-sqlite3',
131-
' - esbuild',
160+
base,
132161
'',
162+
'# ── appended by scripts/publish-smoke.sh ─────────────────────────────────',
163+
'# @objectstack/* pinned to the about-to-publish tarballs; everything above',
164+
'# is what the template ships, everything else resolves from the registry.',
133165
'overrides:',
134166
...Object.entries(overrides).map(([name, spec]) => ` '${name}': '${spec}'`),
135167
];
136-
writeFileSync(outPath, lines.join('\n') + '\n');
137-
console.log(` wrote ${outPath} (${Object.keys(overrides).length} overrides)`);
168+
writeFileSync(wsPath, lines.join('\n') + '\n');
169+
console.log(
170+
` wrote ${wsPath} (${Object.keys(overrides).length} overrides, template settings preserved)`,
171+
);
138172
EOF
139173

140174
log "Installing (pnpm, tarball-pinned)"

0 commit comments

Comments
 (0)