Skip to content

Commit e95920e

Browse files
committed
fix(create-objectstack,cli): the scaffolded pnpm-workspace.yaml is one comment line per block, and both scaffold paths write the same bytes
The blank template carried 66 comment lines and the os init / os create render 57, each explaining measurements and version history the new project's reader does not decide. Both now emit the same 21-line file: the four configuration blocks unchanged, each under one comment line naming its reason. The facts the two files carried that the init.ts docblocks did not (esbuild's role, better-sqlite3 as driver-sql's optional dependency, the 2026-09-11 better-auth 1.7.3 re-read) move into those docblocks. scaffold-workspace-consistency.test.ts gains two limbs: the two producers render the same bytes, and each stays within one comment line per block, directly above its key, inside 80 columns. Its per-key version-boundary limb reads the new one-line shape. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
1 parent 7d7943d commit e95920e

5 files changed

Lines changed: 158 additions & 154 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"create-objectstack": patch
3+
"@objectstack/cli": patch
4+
---
5+
6+
A new project's `pnpm-workspace.yaml` is now a short settings file: one comment line above each block instead of 57 to 66 lines of version history and measurements. `npm create objectstack`, `objectstack init` and a standalone `objectstack create` now write the same 21-line file.
7+
8+
Clause-②: no
9+
10+
- The settings are unchanged: `packages: []`, the same `onlyBuiltDependencies` and `allowBuilds` lists, and the same `peerDependencyRules.allowedVersions` entries, in the same order. A project installs exactly as before.
11+
- Each block keeps one line that says why it is there: which pnpm versions read each build-approval key, and that the peer rules only silence a warning that was measured harmless.
12+
- The measurements behind each setting now live in the `@objectstack/cli` source next to the values they explain, so they are kept up to date there instead of in every project.
13+
- Already-scaffolded projects are not touched. Their longer comments can be deleted or kept; the settings are identical.

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

Lines changed: 43 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ export function manifestIdSlug(name: string): string {
111111
* default standalone SQLite store, via knex) ships uncompiled and `serve`
112112
* fails with "Could not locate the bindings file".
113113
*
114+
* Why each entry is here: `better-sqlite3` is the native SQLite driver, an
115+
* `optionalDependencies` entry of `@objectstack/driver-sql` (and of this
116+
* package, for its own sqlite tooling); `esbuild` is what compiles
117+
* `objectstack.config.ts` when the CLI loads it through `bundle-require`.
118+
*
114119
* Current pnpm reads this from `pnpm-workspace.yaml`, NOT the `pnpm` field in
115120
* package.json (that field is now ignored and emits a deprecation warning).
116121
* npm/yarn/bun build native modules by default and ignore this file.
@@ -147,7 +152,10 @@ export const SCAFFOLD_BUILT_DEPENDENCIES = ['better-sqlite3', 'esbuild'];
147152
* hands it to Kysely, and its own sqlite test path uses node's built-in
148153
* `node:sqlite` `DatabaseSync`. There is therefore no better-auth call site
149154
* that could touch an API moved between better-sqlite3 12 and 13 — the
150-
* range is a statement about an instance we never supply.
155+
* range is a statement about an instance we never supply. Re-read
156+
* 2026-09-11 on the 1.7.3 pin (the rendered file's prose carried this
157+
* reading until it shrank to one line per block): the published package
158+
* still names better-sqlite3 only in that peer declaration.
151159
*
152160
* ⚠️ TWO CORRECTIONS to what this entry used to say, both measured:
153161
* • the 13.x copy better-auth binds to is `@objectstack/cli`'s OWN
@@ -486,85 +494,59 @@ export function renderScaffoldPackageJson(
486494
* Emitting only the older key is what made a freshly scaffolded project fail
487495
* its very first `pnpm install` for every user on pnpm 11. Both lists come
488496
* from the same `builtDeps` argument, so the two populations can never be
489-
* granted different build permission. This is the shape
490-
* `packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml`
491-
* already ships for the other scaffold path.
497+
* granted different build permission.
498+
*
499+
* ## One comment line per block — the reasons live here, not in the file
500+
*
501+
* The rendered file is the first thing a newcomer opens beside a README that
502+
* calls the project a clean slate, and none of the measurements behind it is
503+
* that reader's decision. So each block carries ONE comment line naming its
504+
* reason, and everything else is kept beside the value it explains:
505+
*
506+
* packages: [] this docblock (above) and `SCAFFOLD_PNPM_RANGE`.
507+
* onlyBuiltDependencies, this docblock (the per-pnpm-version table above)
508+
* allowBuilds and `SCAFFOLD_BUILT_DEPENDENCIES` (why each
509+
* package needs its build).
510+
* peerDependencyRules `SCAFFOLD_ALLOWED_PEER_VERSIONS`: one measured
511+
* paragraph per entry, its retirement condition,
512+
* and the retired `@better-auth/scim>better-call`.
513+
*
514+
* The file carried 57 comment lines here, and 66 in the template, until the
515+
* maintainer asked for it to stay minimal with the rationale moved into the
516+
* scaffolder source (#22162). A later change to any value meets its reason in
517+
* this file, which is where the change is made.
518+
*
519+
* `packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml`, the
520+
* other scaffold path, ships these exact bytes: that package cannot import this
521+
* renderer (see the shared emission policy block above), so
522+
* `test/scaffold-workspace-consistency.test.ts` holds the two files equal and
523+
* holds the comment budget, and an edit to one is made to both.
492524
*/
493525
export function renderPnpmWorkspaceYaml(
494526
builtDeps: string[] = SCAFFOLD_BUILT_DEPENDENCIES,
495527
allowedPeerVersions: Record<string, string> = SCAFFOLD_ALLOWED_PEER_VERSIONS,
496528
): string {
497529
const peerEntries = Object.entries(allowedPeerVersions);
498530

531+
// One comment line per block, directly above its key and inside 80 columns
532+
// (held by test/scaffold-workspace-consistency.test.ts). The reasons behind
533+
// each line live in the docblocks named above, not in the user's file.
499534
return [
500-
'# An explicit EMPTY workspace: this project has no member packages, so',
501-
'# this file is settings-only. The key is not decoration — pnpm 9.x and',
502-
'# 10.0–10.4 parse this file BEFORE they read `engines`, and refuse a file',
503-
'# without a `packages:` key outright ("ERROR packages field missing or',
504-
'# empty") before resolving a single dependency.',
505-
'# Not `packages: [\'.\']`: that would declare this project a workspace',
506-
'# MEMBER — a monorepo root, which it is not.',
535+
'# Older pnpm needs this key; empty because this project is not a monorepo.',
507536
'packages: []',
508537
'',
509-
'# pnpm does not run dependency build scripts unless they are approved',
510-
'# here. Without this file a fresh `pnpm install` exits 1 on pnpm 11 with',
511-
'# ERR_PNPM_IGNORED_BUILDS — pnpm 10 only warned, pnpm 11 made it a hard',
512-
'# error. Without the build, better-sqlite3 can ship without a usable',
513-
'# binding and `objectstack serve` fails with "Could not locate the',
514-
'# bindings file".',
515-
'#',
516-
'# Both keys are needed; no single pnpm version range reads both:',
517-
'# allowBuilds pnpm >= 10.26, and the ONLY key pnpm 11 reads',
518-
'# — with onlyBuiltDependencies alone pnpm 11',
519-
'# exits 1 exactly as if nothing were approved.',
520-
'# onlyBuiltDependencies pnpm 10.0–10.25, which ignore allowBuilds.',
521-
'# pnpm 11 ignores this key.',
522-
'#',
523-
'# npm, yarn and bun ignore this file and build native modules anyway.',
538+
'# Build-script approvals for pnpm 10.0–10.25, which do not read allowBuilds.',
524539
'onlyBuiltDependencies:',
525540
...builtDeps.map((d) => ` - ${d}`),
526541
'',
542+
'# The same approvals for pnpm 10.26 and later; pnpm 11 reads only this key.',
527543
'allowBuilds:',
528544
...builtDeps.map((d) => ` ${d}: true`),
529545
// No rules, no header: a bare `peerDependencyRules:` would advertise a
530546
// declaration that is not there.
531547
...(peerEntries.length === 0 ? [] : [
532548
'',
533-
'# Third-party peer ranges that resolve outside what their declaring',
534-
'# package states, and that pnpm reports on a first install. None is a',
535-
'# real incompatibility:',
536-
'#',
537-
'# better-auth peers better-sqlite3 ^12.0.0 while the tree resolves 13.x',
538-
'# (the copy @objectstack/cli declares for its own sqlite tooling). That',
539-
'# peer is optional and covers handing better-auth a raw better-sqlite3',
540-
'# `Database`; ObjectStack hands it an ObjectQL adapter instead, so',
541-
'# nothing here goes down that path. Re-measured 2026-09-11 on',
542-
'# better-auth 1.7.3: nothing in the published package references',
543-
'# better-sqlite3 except that peer declaration itself — it only accepts',
544-
'# a Database you construct — so there is no call site that could',
545-
'# depend on what changed between 12 and 13. The upstream',
546-
'# range is stale; pinning back to 12 would just install a second,',
547-
'# unused native copy.',
548-
'#',
549-
'# (The \'@better-auth/scim>better-call\' entry that used to sit here is',
550-
'# retired. The rc-era scim pin peered an exact better-call 1.3.7 against',
551-
'# better-auth\'s own 1.4.0 and this map declared that skew away; the pin',
552-
'# is a stable release now, and @better-auth/scim 1.7.3 peers an exact',
553-
'# better-call 1.4.0 — the one copy better-auth depends on. Re-measured',
554-
'# 2026-09-12 off the installed manifest: the skew is gone.)',
555-
'#',
556-
'# @better-auth/core, /oauth-provider, /scim and /sso each peer an exact',
557-
'# @better-auth/utils 0.4.2, while better-call (better-auth\'s own HTTP',
558-
'# layer) depends on ^0.5.0 and is what the tree resolves them against.',
559-
'# 0.5.0 keeps every symbol those four import — base64/base64Url,',
560-
'# createHash, createRandomStringGenerator — with the same signatures',
561-
'# and the same values on the inputs they pass, so the report is the',
562-
'# only difference. Pinning utils back instead would drag better-call',
563-
'# off its own declared range, which is a real violation rather than a',
564-
'# reported one.',
565-
'#',
566-
'# These suppress the report only — no resolution moves, and the lockfile',
567-
'# is byte-identical with and without this block.',
549+
'# Upstream peer ranges measured harmless; this only silences the warning.',
568550
'peerDependencyRules:',
569551
' allowedVersions:',
570552
...peerEntries.map(([k, v]) => ` '${k}': '${v}'`),

‎packages/cli/test/scaffold-workspace-consistency.test.ts‎

Lines changed: 91 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,27 @@
6969
// What is compared is the rendered `allowedVersions` MAP — the keys each file
7070
// widens and the version each key is widened to — for the same reason the
7171
// build limb compares rendered grants: no expected value is restated here, so
72-
// each producer's expectation is the other producer. The prose above each
73-
// block is NOT compared: the two files explain these skews in their own words
74-
// on purpose, and the measurement they describe is the same either way.
72+
// each producer's expectation is the other producer.
73+
//
74+
// ── One file, one comment line per block (#22162) ───────────────────────────
75+
//
76+
// The two files used to explain these settings in their own words, 57 and 66
77+
// comment lines of measurements and version history, and the prose was not
78+
// compared. That left a retired entry's explanation shipping in one of them
79+
// with nothing to fail on it (#17093). The maintainer then asked for
80+
// the file to stay minimal, with the rationale in the scaffolder source, and
81+
// triage ruled that both scaffold paths render the same file. So two more
82+
// limbs now hold it:
83+
//
84+
// * the two producers render the SAME BYTES — comments included, which is
85+
// what closes the drift above: there is one text left to keep true;
86+
// * each file stays a settings file — at most one comment line per block,
87+
// directly above its key, inside 80 columns. The measurements live in the
88+
// docblocks of `renderPnpmWorkspaceYaml`, `SCAFFOLD_BUILT_DEPENDENCIES`
89+
// and `SCAFFOLD_ALLOWED_PEER_VERSIONS` in `src/commands/init.ts`.
90+
//
91+
// The limbs above stay as they were: when the bytes do differ, they say WHICH
92+
// declaration or version claim differs, which a byte comparison cannot.
7593

7694
import { describe, it, expect } from 'vitest';
7795
import { readFileSync } from 'node:fs';
@@ -141,31 +159,52 @@ function grantedBuilds(yaml: string): Record<(typeof APPROVAL_KEYS)[number], str
141159
/**
142160
* The prose each file attaches to each approval key, as one string per key.
143161
*
144-
* Both files write it as a definition list inside the header comment — the key
145-
* at the start of a comment line, its explanation column-aligned after it, and
146-
* continuation lines indented under that. The two wordings differ and are meant
147-
* to; only the VERSION CLAIMS inside them are compared.
162+
* Each file writes it as the one comment line directly above the key (the
163+
* budget the last limb below holds). Only the VERSION CLAIMS inside it are
164+
* compared, so the limb still says which boundary differs when the bytes do.
148165
*/
149166
function keyProse(yaml: string): Map<string, string> {
150167
const prose = new Map<string, string>();
151-
let current: string | null = null;
152-
for (const line of yaml.split('\n')) {
153-
const definition = /^#[ \t]{2,}(allowBuilds|onlyBuiltDependencies)[ \t]{2,}(\S.*)$/.exec(line);
154-
if (definition) {
155-
current = definition[1];
156-
prose.set(current, definition[2].trim());
157-
continue;
168+
const lines = yaml.split('\n');
169+
lines.forEach((line, i) => {
170+
const key = /^(allowBuilds|onlyBuiltDependencies):/.exec(line)?.[1];
171+
const above = i > 0 ? /^#[ \t]*(\S.*)$/.exec(lines[i - 1]) : null;
172+
if (key && above) prose.set(key, above[1].trim());
173+
});
174+
return prose;
175+
}
176+
177+
/** The widest comment line the rendered file may carry. */
178+
const MAX_COMMENT_COLUMNS = 80;
179+
180+
/**
181+
* Every way a file breaks its comment budget, one entry per offending line.
182+
*
183+
* The budget is at most one comment line per top-level block: a full-line
184+
* comment, directly above its key, inside {@link MAX_COMMENT_COLUMNS}. A line
185+
* count alone would let one block take every line the others save, and a line
186+
* rule without a width would let one line hold a paragraph; this is the
187+
* smallest rule that bounds both, and it adds up to a few hundred bytes of
188+
* comment however the file grows. No value in this file can contain `#` (they
189+
* are package names, versions and `true`), so any `#` is a comment.
190+
*/
191+
function commentBudgetBreaches(yaml: string): string[] {
192+
const lines = yaml.split('\n');
193+
const breaches: string[] = [];
194+
lines.forEach((line, i) => {
195+
if (!line.includes('#')) return;
196+
const where = `line ${i + 1} ${JSON.stringify(line)}`;
197+
if (!line.startsWith('#')) {
198+
breaches.push(`${where} puts a comment inside or after a setting`);
199+
} else if (!/^[A-Za-z][\w-]*:/.test(lines[i + 1] ?? '')) {
200+
breaches.push(`${where} is not the one comment line directly above a top-level key`);
158201
}
159-
// A continuation is an indented comment line that starts no new key; a bare
160-
// `#` (or anything that is not an indented comment) closes the entry.
161-
const continuation = current === null ? null : /^#[ \t]{2,}(\S.*)$/.exec(line);
162-
if (continuation) {
163-
prose.set(current!, `${prose.get(current!)} ${continuation[1].trim()}`);
164-
continue;
202+
const columns = [...line].length;
203+
if (columns > MAX_COMMENT_COLUMNS) {
204+
breaches.push(`${where} is ${columns} columns wide, over ${MAX_COMMENT_COLUMNS}`);
165205
}
166-
current = null;
167-
}
168-
return prose;
206+
});
207+
return breaches;
169208
}
170209

171210
/**
@@ -274,3 +313,32 @@ describe('the two scaffold paths render the same pnpm build approvals (#10499)',
274313
).toEqual(template);
275314
});
276315
});
316+
317+
describe('the two scaffold paths render one minimal file (#22162)', () => {
318+
it('renders the same bytes in both scaffold paths', () => {
319+
expect(
320+
RENDERED[CLI],
321+
`${CLI} and ${TEMPLATE} write different pnpm-workspace.yaml files. Both scaffold ` +
322+
'paths ship ONE file: make the same edit to renderPnpmWorkspaceYaml() and to the ' +
323+
"template, and put any new reasoning in init.ts's docblocks rather than the file.",
324+
).toBe(RENDERED[TEMPLATE]);
325+
});
326+
327+
it.each(Object.keys(RENDERED) as Producer[])(
328+
'%s carries at most one short comment line per block',
329+
(producer) => {
330+
const yaml = RENDERED[producer];
331+
// Non-vacuity: a file with no top-level key has no block to comment, and
332+
// an empty breach list over it would read as a file within budget.
333+
expect(yaml).toMatch(/^[A-Za-z][\w-]*:/m);
334+
expect(
335+
commentBudgetBreaches(yaml),
336+
`${producer} writes more comment than its settings need. A scaffolded project ` +
337+
'opens with this file, and the measurements behind each block belong in the ' +
338+
"docblocks of renderPnpmWorkspaceYaml, SCAFFOLD_BUILT_DEPENDENCIES and " +
339+
'SCAFFOLD_ALLOWED_PEER_VERSIONS in src/commands/init.ts: one comment line per ' +
340+
`block, directly above its key, at most ${MAX_COMMENT_COLUMNS} columns.`,
341+
).toEqual([]);
342+
},
343+
);
344+
});

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -410,9 +410,14 @@ describe('templates survive npm packing', () => {
410410
// of every AI-written app on the platform, so a line that reads as "add member
411411
// packages here" is the expensive half of that choice.
412412
//
413-
// `objectstack init` renders the second copy of this rule
413+
// `objectstack init` renders the second copy of this file
414414
// (`renderPnpmWorkspaceYaml` in packages/cli/src/commands/init.ts) and is
415-
// ratcheted there; both paths carry it until the two renderers are unified.
415+
// ratcheted there. The two are one file (#22162): the same bytes, one comment
416+
// line per block, held by packages/cli/test/scaffold-workspace-consistency.test.ts.
417+
// So the measurements behind every block of this file — the pnpm version table,
418+
// why each build is approved, each peer skew and when it retires — live in
419+
// init.ts's docblocks (`renderPnpmWorkspaceYaml`, `SCAFFOLD_BUILT_DEPENDENCIES`,
420+
// `SCAFFOLD_ALLOWED_PEER_VERSIONS`), not in the file a new project opens.
416421
describe('blank template explicit empty workspace', () => {
417422
const wsPath = path.join(pkgRoot, 'src', 'templates', 'blank', 'pnpm-workspace.yaml');
418423
// Same comment-stripping as the blocks below: the prose above the key names

0 commit comments

Comments
 (0)