Skip to content

Commit 13fa51e

Browse files
Elon Muskclaude
andauthored
fix(cli): scaffold both pnpm build-approval keys so a new project installs on pnpm 11 (#10405) (#10522)
`renderPnpmWorkspaceYaml()` wrote only `onlyBuiltDependencies`. pnpm 11 does not read that key, and it turned an unapproved dependency build script from a warning into a hard error — so `objectstack init my-app && cd my-app && pnpm install` exited 1 with ERR_PNPM_IGNORED_BUILDS, on the first command a newcomer runs after scaffolding. The rendered file now also carries `allowBuilds`, built from the same `SCAFFOLD_BUILT_DEPENDENCIES` list, which is the only key pnpm 11 reads. This is the shape the blank template already ships for the other scaffold path. Measured, one clean install per pnpm version, each with its own store, on a project scaffolded by `objectstack init -t app`: 10.0.0 – 10.25.0 read onlyBuiltDependencies only 10.26.0 – 10.34.x read either key 11.x read allowBuilds only so both keys are load-bearing and neither is redundant. Build permission is still granted to exactly two named packages, no wildcard: esbuild (postinstall installs its platform binary, used to compile objectstack.config.ts) and better-sqlite3 (ships a binding.gyp, which pnpm treats as a native build). Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r Co-authored-by: Claude <noreply@anthropic.com>
1 parent 02d56b4 commit 13fa51e

3 files changed

Lines changed: 141 additions & 4 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`objectstack init` now writes both build-approval keys into the scaffolded
6+
`pnpm-workspace.yaml`, so a brand-new project's first `pnpm install` succeeds
7+
on pnpm 11 (#10405).
8+
9+
The renderer emitted only `onlyBuiltDependencies`. pnpm 11 does not read that
10+
key at all, and it turned an unapproved dependency build script from a warning
11+
into a hard error — so `objectstack init my-app && cd my-app && pnpm install`
12+
exited 1 with `ERR_PNPM_IGNORED_BUILDS`, on the very first command after
13+
scaffolding. The rendered file now also carries `allowBuilds`, built from the
14+
same source list, which is the only key pnpm 11 reads. Measured one clean
15+
install per pnpm version, each with its own store: pnpm 10.0.0-10.25.0 read
16+
`onlyBuiltDependencies`, 10.26.0-10.34.x read either key, and 11.x reads
17+
`allowBuilds` only — so both keys are load-bearing and neither is redundant.
18+
19+
Build permission is still granted to exactly the two packages that need it and
20+
nothing else: `esbuild` (a `postinstall` that installs its platform binary,
21+
used to compile `objectstack.config.ts`) and `better-sqlite3` (ships a
22+
`binding.gyp`, which pnpm treats as a native build; without it `objectstack
23+
serve` can fail with "Could not locate the bindings file"). No wildcard.
24+
25+
Existing scaffolds are unaffected — `init` never overwrites a
26+
`pnpm-workspace.yaml` that is already there. To fix a project scaffolded by an
27+
earlier CLI, add to its `pnpm-workspace.yaml`:
28+
29+
```yaml
30+
allowBuilds:
31+
better-sqlite3: true
32+
esbuild: true
33+
```

‎packages/cli/src/commands/init.ts‎

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,27 @@ export const SCAFFOLD_ALLOWED_PEER_VERSIONS: Record<string, string> = {
117117
* declares the two known-benign peer skews.
118118
* Kept minimal (no `packages:` key) so it acts purely as a settings file for
119119
* the single-package scaffold rather than declaring a workspace.
120+
*
121+
* The allowlist is emitted TWICE, under two keys that no single pnpm version
122+
* range reads both of. Measured against a scaffold of this exact shape, one
123+
* clean install per pnpm version, each with its own store:
124+
*
125+
* pnpm 10.0.0 – 10.25.0 honour `onlyBuiltDependencies`; `allowBuilds`
126+
* alone leaves the builds unrun (a warning, exit 0).
127+
* pnpm 10.26.0 – 10.34.x honour either key.
128+
* pnpm 11.x honour `allowBuilds` ONLY. With
129+
* `onlyBuiltDependencies` alone, `pnpm install`
130+
* exits 1 with ERR_PNPM_IGNORED_BUILDS — byte for
131+
* byte the same failure as approving nothing at
132+
* all, because pnpm 11 turned an unapproved build
133+
* script from a warning into a hard error.
134+
*
135+
* Emitting only the older key is what made a freshly scaffolded project fail
136+
* its very first `pnpm install` for every user on pnpm 11. Both lists come
137+
* from the same `builtDeps` argument, so the two populations can never be
138+
* granted different build permission. This is the shape
139+
* `packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml`
140+
* already ships for the other scaffold path.
120141
*/
121142
export function renderPnpmWorkspaceYaml(
122143
builtDeps: string[] = SCAFFOLD_BUILT_DEPENDENCIES,
@@ -125,12 +146,26 @@ export function renderPnpmWorkspaceYaml(
125146
const peerEntries = Object.entries(allowedPeerVersions);
126147

127148
return [
128-
'# Allowlist native dependency build scripts so `pnpm install` compiles',
129-
'# them (pnpm 10+ blocks build scripts by default). Without this,',
130-
'# better-sqlite3 ships uncompiled and `objectstack serve` fails with',
131-
'# "Could not locate the bindings file".',
149+
'# pnpm does not run dependency build scripts unless they are approved',
150+
'# here. Without this file a fresh `pnpm install` exits 1 on pnpm 11 with',
151+
'# ERR_PNPM_IGNORED_BUILDS — pnpm 10 only warned, pnpm 11 made it a hard',
152+
'# error. Without the build, better-sqlite3 can ship without a usable',
153+
'# binding and `objectstack serve` fails with "Could not locate the',
154+
'# bindings file".',
155+
'#',
156+
'# Both keys are needed; no single pnpm version range reads both:',
157+
'# allowBuilds pnpm >= 10.26, and the ONLY key pnpm 11 reads',
158+
'# — with onlyBuiltDependencies alone pnpm 11',
159+
'# exits 1 exactly as if nothing were approved.',
160+
'# onlyBuiltDependencies pnpm 10.0–10.25, which ignore allowBuilds.',
161+
'# pnpm 11 ignores this key.',
162+
'#',
163+
'# npm, yarn and bun ignore this file and build native modules anyway.',
132164
'onlyBuiltDependencies:',
133165
...builtDeps.map((d) => ` - ${d}`),
166+
'',
167+
'allowBuilds:',
168+
...builtDeps.map((d) => ` ${d}: true`),
134169
// No rules, no header: a bare `peerDependencyRules:` would advertise a
135170
// declaration that is not there.
136171
...(peerEntries.length === 0 ? [] : [

‎packages/cli/test/init.test.ts‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,75 @@ describe('native build allowlist (pnpm-workspace.yaml)', () => {
118118
});
119119
});
120120

121+
// pnpm 11 honours ONLY `allowBuilds`. Rendering `onlyBuiltDependencies` alone
122+
// — which is what this scaffolder shipped — makes a brand-new project's very
123+
// first `pnpm install` exit 1 with ERR_PNPM_IGNORED_BUILDS, byte for byte the
124+
// same failure as approving nothing at all. Measured one clean install per
125+
// pnpm version, each with its own store, on a project scaffolded by
126+
// `objectstack init -t app`:
127+
//
128+
// 10.0.0–10.25.0 read onlyBuiltDependencies; allowBuilds alone leaves the
129+
// builds unrun (warning, exit 0)
130+
// 10.26.0–10.34.x read either key
131+
// 11.x read allowBuilds only; onlyBuiltDependencies alone exits 1
132+
//
133+
// So both keys are load-bearing and neither is redundant. The blank template
134+
// (`packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml`)
135+
// already ships this shape and ratchets it in `template-consistency.test.ts`;
136+
// these are the mirrored ratchets for the second scaffold path.
137+
describe('pnpm 11 build approvals in the rendered workspace file', () => {
138+
// Strip comments first: the prose above these keys names them, and must not
139+
// be what satisfies an assertion about the settings themselves.
140+
const settings = renderPnpmWorkspaceYaml().replace(/^\s*#.*$/gm, '');
141+
142+
const blockOf = (key: string, re: RegExp) =>
143+
re.exec(settings)?.[1] ?? `__no ${key} block__`;
144+
145+
it('sets allowBuilds.<pkg> = true, the only key pnpm 11 reads', () => {
146+
const block = blockOf('allowBuilds', /^allowBuilds:\n((?:[ \t]+.*\n?)*)/m);
147+
for (const pkg of SCAFFOLD_BUILT_DEPENDENCIES) {
148+
expect(
149+
new RegExp(`^\\s+${pkg}:\\s*true\\s*$`, 'm').test(block),
150+
`allowBuilds must set "${pkg}: true" — pnpm 11 ignores onlyBuiltDependencies ` +
151+
'and exits 1 on an unapproved build script',
152+
).toBe(true);
153+
}
154+
});
155+
156+
it('keeps listing the same packages under onlyBuiltDependencies for pnpm < 10.26', () => {
157+
const block = blockOf('onlyBuiltDependencies', /^onlyBuiltDependencies:\n((?:[ \t]*-.*\n?)*)/m);
158+
for (const pkg of SCAFFOLD_BUILT_DEPENDENCIES) {
159+
expect(
160+
new RegExp(`^\\s*-\\s*${pkg}\\s*$`, 'm').test(block),
161+
`onlyBuiltDependencies must list "${pkg}" — pnpm 10.0–10.25 ignore allowBuilds`,
162+
).toBe(true);
163+
}
164+
});
165+
166+
it('grants exactly the same set under both keys', () => {
167+
// Two hand-maintained lists would drift, and the drift is invisible: each
168+
// key is read by a different pnpm population, so a package dropped from one
169+
// of them still builds fine on whichever pnpm the author happened to run.
170+
const only = [...blockOf('onlyBuiltDependencies', /^onlyBuiltDependencies:\n((?:[ \t]*-.*\n?)*)/m)
171+
.matchAll(/^\s*-\s*(\S+)\s*$/gm)].map((m) => m[1]);
172+
const allow = [...blockOf('allowBuilds', /^allowBuilds:\n((?:[ \t]+.*\n?)*)/m)
173+
.matchAll(/^\s+(\S+):\s*true\s*$/gm)].map((m) => m[1]);
174+
expect(allow.sort()).toEqual([...SCAFFOLD_BUILT_DEPENDENCIES].sort());
175+
expect(only.sort()).toEqual(allow.sort());
176+
});
177+
178+
it('approves named packages only — never a wildcard', () => {
179+
// `allowBuilds` is not a blank cheque: whatever this scaffolder writes
180+
// becomes the standing build permission of every project created from it.
181+
// A glob would hand arbitrary install-time code execution to any future
182+
// transitive dependency, in every new project, silently.
183+
for (const pkg of SCAFFOLD_BUILT_DEPENDENCIES) {
184+
expect(pkg, 'build approvals must name one package each').not.toMatch(/[*?]/);
185+
}
186+
expect(settings).not.toMatch(/^\s*['"]?\*/m);
187+
});
188+
});
189+
121190
// A brand-new scaffold's first `pnpm install` reported two unmet peers, on the
122191
// one screen where a newcomer decides whether this project is solid, with
123192
// nothing they did to cause it (#10326). Both ranges belong to third-party

0 commit comments

Comments
 (0)