fix(create-objectstack,cli): a clean first install no longer reports two unmet peers - #10407
Conversation
…n first install reports A brand-new scaffold's very first `pnpm install` printed two unmet-peer lines — `better-auth 1.7.1` wanting `better-sqlite3@^12.0.0` against the resolved 13.0.3, and `@better-auth/scim@1.7.0-rc.1` wanting an exact `better-call@1.3.7` against the resolved 1.4.0. Reproduced on published `create-objectstack@17.1.0` and again on `objectstack init`, both with no lockfile and no node_modules. better-sqlite3: the peer is OPTIONAL and governs only a raw better-sqlite3 `Database` handed to better-auth's `database` option — a path ObjectStack never takes (AuthManager passes an ObjectQL adapter factory). Measured on the path it does govern, better-auth 1.7.1 behaves identically on 13.0.3 and 12.11.1, so the upstream range is stale: widen it, do not pin ours back to ^12. @better-auth/scim: `npm view` shows a stable latest, but stable 1.7.x ships the rc.2 whole-model rewrite, so the exact rc pin stays and its adoption remains a separate migration. A better-auth plugin must share the HOST's better-call instance, so the single 1.4.0 copy every install resolves is the correct tree. Both are pnpm `peerDependencyRules.allowedVersions` entries, scoped `<declaring package>><peer>`, shipped inside the scaffold because such a block in this repo's own workspace file does not travel with published packages. `allowedVersions` changes what pnpm REPORTS, never what it resolves: measured on both scaffold paths the lockfile is byte-identical, 0 lines of diff. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
📓 Docs Drift CheckThis PR changes 2 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e6deea5a4f21b9574c1461c60caf0cd56b08e8f5 && git checkout e6deea5a4f21b9574c1461c60caf0cd56b08e8f5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fda3d67f99a14e1e3ccb8283ee9e4665ac3f1c84 83e1c328c527ad796a4bf1844908afa69e6fa53f && git checkout -B drift-repro fda3d67f99a14e1e3ccb8283ee9e4665ac3f1c84 && git merge --no-ff 83e1c328c527ad796a4bf1844908afa69e6fa53f
node scripts/docs-audit/affected-docs.mjs --json fda3d67f99a14e1e3ccb8283ee9e4665ac3f1c84
|
Fixes #10326
A brand-new scaffold's very first
pnpm installreported two unmet peer dependencies. Nothing was broken — but it is the one screen where a newcomer is deciding whether this project is solid, and there is nothing they did to cause it or can do about it.Premise: reproduced before anything changed
Clean install means: scaffolded fresh from published
create-objectstack@17.1.0, thenrm -rf node_modules pnpm-lock.yamlandpnpm installagain — no lockfile, nonode_modules, nothing configured by the user. The pnpm in this container is 11.22.0, which moved the per-package lines out of the install summary and behindpnpm peers check; the filing's inline✕ unmet peer …shape is pnpm 10's rendering of the same two facts.A second scaffold path reproduces both, identically, and the filing did not name it:
objectstack init(packages/cli) renders its ownpnpm-workspace.yaml. Same two lines, same versions.① better-sqlite3 — the pin is right; the upstream range is stale, so it is widened
The question is behavioural, not textual, so it was measured rather than read off the range string.
What the range actually governs. better-auth 1.7.1 declares
better-sqlite3as an optional peer (peerDependenciesMeta.better-sqlite3.optional === true) at^12.0.0. It governs exactly one configuration: a raw better-sqlite3Databasehanded to better-auth'sdatabaseoption, which its Kysely dialect then drives. ObjectStack never takes that path —AuthManager.createDatabaseConfig()returnscreateObjectQLAdapterFactory(this.config.dataEngine), and all 14better-sqlite3mentions underplugin-authare knex'sclient: 'better-sqlite3'underneath ObjectQL (the grep was controlled against a known-present neighbour term in the same files).Measured on the configuration it does govern. A probe built better-auth 1.7.1 with
database: new Database(':memory:')and drove it end to end:Two controls make that green mean something:
Database.prototype.prepareneutered fails, in kysely'sSqliteIntrospector— so the green is better-sqlite3 actually executing, not an unexercised path.Therefore: widen, do not correct the pin. Pinning our own
^13.0.3declarations (driver-sql, driver-turso, plugin-sharing, cli) back to^12would downgrade a native module across the whole platform to satisfy a range that measurement shows is simply behind. The remedy is pnpm'speerDependencyRules.allowedVersions, which is the only way to widen a third-party peer range we do not own.② @better-auth/scim — the rc pin stays; this card stops at the reading
The
npm viewreading, verbatim, taken today:latestis a stable, non-rc version. No migration is performed here. Stable 1.7.x ships the rc.2 whole-model rewrite, so adopting it is an architecture card of its own, not a version bump — and the pin at1.7.0-rc.1is deliberate, not a decayed version stamp.On the warning itself: the rc peers an exact
better-call@1.3.7while better-auth 1.7.1 depends on1.4.0. A better-auth plugin has to share the host's better-call instance, so the single 1.4.0 copy every install already resolves is the correct tree rather than a skew to repair. Measured: exactly onebetter-callversion in the lockfile (better-call@1.4.0), with scim'snode_modules/better-callsymlinked to it — in the scaffolded downstream tree and in this repo's own lockfile alike. The declaration retires together with the rc pin.A correction to the workspace prose, in scope because it is this half's own evidence.
pnpm-workspace.yaml's scim note claimed "so the tree carries two copies". It does not, and never did in the tree as resolved. That sentence is corrected in place, with the measurement, so the next author doing the SCIM migration is not working from a false picture. The root file's diff is comment-only — verified by filtering the diff to non-#lines, which comes back empty; the pin line itself does not appear in the diff at all.What changed
Both remedies are
peerDependencyRules.allowedVersionsentries, keyed with pnpm's scoped spelling (the declaring package's name, then a greater-than sign, then the peer's name) so each widens exactly one declaration — a barebetter-sqlite3: '13'would silence that peer for every package declaring it, including one whose complaint would be real. A test pins that scoping in both packages.They ship inside the scaffold, not in this repo's workspace file, and that placement is load-bearing: a
peerDependencyRulesblock here would not travel with published packages — the same limitation theoverridesblock already documents, and the reasonplugin-authrestates its ranges. So the entries land in the two files a newcomer actually gets:packages/create-objectstack/src/templates/blank/pnpm-workspace.yamlrenderPnpmWorkspaceYaml()inpackages/cli/src/commands/init.ts(data-driven from an exportedSCAFFOLD_ALLOWED_PEER_VERSIONS; an empty map emits no block at all, rather than a dangling header advertising a declaration that is not there)Verified gone on the same clean-install path that produced them. Both scaffolds, both re-installed from zero:
and the install summary no longer carries
[WARN] Issues with peer dependencies found.Lockfile blast radius: zero, measured both ways
allowedVersionschanges what pnpm reports, never what it resolves. Rather than assert that, both scaffolds were installed from scratch with and without the block and the lockfiles diffed:difflines between baseline and fixed lockfilecreate-objectstack@17.1.0objectstack initNothing moved beyond the two named packages because nothing moved at all — no version, no range, no resolution, in the scaffolds or in this repo (whose lockfile this PR does not touch).
One thing deliberately not done
This repo's own
pnpm-workspace.yamlgets nopeerDependencyRulesblock. It does not show these warnings, becauseauto-install-peers=truein the root.npmrcquietly installs a second, entirely unusedbetter-sqlite3@12.11.1to satisfy the stale peer — which is exactly why CI never saw what a scaffolded project shows on its first screen. Adding the rule here was tried and measured: it moves 0 lockfile lines today, so it would sit inert until some unrelated future re-resolve dropped the duplicate as unreviewed drift in this repo's hottest merge-conflict file. That masking is recorded in the workspace comment instead, where the next reader will find it.Verification
Reverse verification, run from the committed state. Both suites read source, not
dist—init.test.tsimports../src/commands/initby relative path and the template test readssrc/templates/…off disk, neither resolving through a package'sexports— so no rebuild is involved and none was needed. Predicted direction before running: the declarations' removal reds the new assertions, except the "emits nothing when given no rules" test, which should stay green because an empty map genuinely should emit nothing. Observed, exactly:packages/clicreate-objectstackTargeted suites and typechecks, script names echoed in the output (so no zero-match silent pass):
pnpm --filter create-objectstack test—Test Files 7 passed (7),Tests 84 passed (84)pnpm --filter @objectstack/cli exec vitest run test/init.test.ts—Tests 37 passed (37)pnpm --filter @objectstack/cli typecheckandpnpm --filter create-objectstack typecheck— both cleanGate union —
node scripts/pm/dispatch-gates.mjs, no paths passedDerived after the final commit on a clean worktree at
83e1c328c, exit codes captured before any pipe. 20 families run, 19 exit 0. The one non-zero is pre-existing and provably not mine:Its own verdict line reads
⛔ A STABLE release is available for 1 prerelease pin(s) — the trigger condition of #3653 has ARRIVED.That is the standing SCIM watch firing on thenpm viewreading quoted above, and it is red onmainfor the same reason: this PR's only change topnpm-workspace.yamlis comment text, and the scim pin line is not in the diff.Green, each quoted from its own exit code:
check:nul-bytes,check:changeset-gate-self-tests,check:cross-package-test-inputs,check:objectui-changeset,check:override-consistency,check:slot-lookup,check:template-version-sync,check-adr-0087-registration.mjs,check-changeset-fixed.mjs,check-changeset-no-major.mjs,check-cross-package-test-inputs.mjs,check-empty-changeset.mjs,check-osv-exemptions.mjs,docs-audit/check-affected-docs.mjs, plus the convention-triggered set the two edited test files move:check:query-options-erasure,check:type-check-coverage,check:engine-double-contract,check:where-matcher, andcheck:type-check-debt— the last run against a fully built workspace closure (turbo run build,70 successful, 70 total), reporting33 ledger entr(ies) re-measured in 236.8s, 1924 raw tsc error(s) total, none above its recorded numberandsurplus: none. No ledger entry was raised or touched.A note on where the CLI test edits are typechecked:
packages/cli/tsconfig.jsonincludes onlysrc, sotest/**sits outside that program — pre-existing, documented debt already carried in the type-check-coverage ledger (packages/cliis named in the script's own header). The additions were checked directly against the file anyway and introduce no error naming a line this PR wrote.Changeset
Shipped, and it is the gate's call rather than habit:
check-empty-changeset.mjs/check-changeset-fixed.mjsare in this card's derived gate union, and the change is user-visible — it alters what publishedcreate-objectstackand@objectstack/cliwrite into a new project.patchon both.Out of scope, filed not fixed
objectstack init's scaffold hard-failspnpm installon pnpm 11 withERR_PNPM_IGNORED_BUILDS(exit 1), becauserenderPnpmWorkspaceYamlemits onlyonlyBuiltDependenciesand not theallowBuildskey that pnpm 11 reads — the same defect class the blank template already fixed for itself. It is a different defect from this card's (an install that fails outright, not a warning on an install that succeeds), so it is filed rather than ridden. See #10405; that issue is not addressed here.Generated by Claude Code