From 83e1c328c527ad796a4bf1844908afa69e6fa53f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 17:55:48 +0000 Subject: [PATCH] fix(create-objectstack,cli): declare the two benign peer skews a clean first install reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `>`, 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 Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx --- .../clean-first-install-peer-warnings.md | 30 ++++++++ packages/cli/src/commands/init.ts | 70 ++++++++++++++++++- packages/cli/test/init.test.ts | 56 ++++++++++++++- .../src/template-consistency.test.ts | 49 +++++++++++++ .../src/templates/blank/pnpm-workspace.yaml | 26 +++++++ pnpm-workspace.yaml | 23 +++++- 6 files changed, 248 insertions(+), 6 deletions(-) create mode 100644 .changeset/clean-first-install-peer-warnings.md diff --git a/.changeset/clean-first-install-peer-warnings.md b/.changeset/clean-first-install-peer-warnings.md new file mode 100644 index 0000000000..c3ebe6f919 --- /dev/null +++ b/.changeset/clean-first-install-peer-warnings.md @@ -0,0 +1,30 @@ +--- +"create-objectstack": patch +"@objectstack/cli": patch +--- + +**First-run polish:** a brand-new scaffold's very first `pnpm install` no longer reports two unmet peer dependencies (#10326). + +Reproduced on a clean scaffold from published `create-objectstack@17.1.0` — no lockfile, `node_modules` removed, nothing configured by the user — and again on the second scaffold path, `objectstack init`. Both printed the same two: + +``` +✕ unmet peer better-call + Installed: 1.4.0 + Wanted: + 1.3.7: + @better-auth/scim@1.7.0-rc.1 + +✕ unmet peer better-sqlite3 + Installed: 13.0.3 + Wanted: + ^12.0.0: + better-auth@1.7.1 +``` + +Nothing was broken — but it is the first screen a newcomer sees, and there is nothing they did to cause it or can do about it. + +**`better-sqlite3`: the pin is right and the upstream range is stale — so it is widened, not corrected.** better-auth 1.7.1 declares `better-sqlite3` as an **optional** peer at `^12.0.0`, and it governs exactly one configuration: a raw better-sqlite3 `Database` handed to better-auth's `database` option, which its Kysely dialect then drives. ObjectStack never takes that path — `AuthManager.createDatabaseConfig()` returns `createObjectQLAdapterFactory(dataEngine)`, and every `better-sqlite3` use under `plugin-auth` is knex's `client: 'better-sqlite3'` beneath ObjectQL. Measured anyway on the configuration the range *does* govern: better-auth 1.7.1 with `database: new Database(':memory:')`, running `getMigrations().runMigrations()`, `signUpEmail`, `signInEmail` and adapter `findOne`/`update`/`delete`, is green on **better-sqlite3 13.0.3** and byte-for-byte equivalent on **12.11.1**. The same probe with `Database.prototype.prepare` neutered fails, so that green is the driver's and not an unexercised path. Pinning our own `^13.0.3` declarations back to `^12` would downgrade a native module across the platform to satisfy a range measurement shows is simply behind. + +**`@better-auth/scim`: the rc pin stays, and one `better-call` copy is the correct tree.** `npm view @better-auth/scim dist-tags` reads `latest: '1.7.1'`, but stable 1.7.x ships the rc.2 whole-model rewrite, so adopting it is a separate migration rather than a version bump; the exact `1.7.0-rc.1` pin is deliberate. The rc peers an exact `better-call@1.3.7` while better-auth 1.7.1 depends on `1.4.0` — and 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 right, not a skew to repair. This declaration retires together with the rc pin. + +**What changed, and what deliberately did not.** Both remedies are pnpm `peerDependencyRules.allowedVersions` entries, scoped `>` so each widens exactly one declaration. They ship *inside* the scaffold — the bundled `pnpm-workspace.yaml` template and the one `objectstack init` renders — because 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 with and without it (0 lines of diff), and no dependency version, range or resolution moved anywhere. This repo's own resolutions are untouched. diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 61d738a93a..72ebf9783c 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -77,11 +77,53 @@ export function sanitizeNamespace(name: string): string { export const SCAFFOLD_BUILT_DEPENDENCIES = ['better-sqlite3', 'esbuild']; /** - * Render the `pnpm-workspace.yaml` that allowlists native build scripts. + * Third-party peer ranges that resolve outside what their declaring package + * states, keyed `>` — pnpm's scoped `allowedVersions` + * spelling, so each entry widens exactly one declaration and nothing else. + * + * Both are reported by `pnpm install` on a brand-new scaffold, and neither is a + * real incompatibility. They are declared here because that report is the first + * thing a newcomer sees, on the one screen where they are deciding whether this + * project is solid, and there is nothing they did to cause it. + * + * - `better-auth>better-sqlite3` — better-auth 1.7.1 peers `^12.0.0` while the + * tree resolves 13.x (`@objectstack/driver-sql`'s optional dependency). The + * peer is OPTIONAL and governs one configuration only: a raw better-sqlite3 + * `Database` handed to better-auth's `database` option. ObjectStack never + * does that — `AuthManager.createDatabaseConfig()` passes an ObjectQL + * adapter factory. Measured on the configuration the range *does* govern + * (better-auth's own Kysely dialect: migrations, sign-up, sign-in, adapter + * find/update/delete), 1.7.1 behaves identically on better-sqlite3 13.0.3 + * and on 12.11.1. So the upstream range is stale and 13 is right — widening + * is the correct remedy, not pinning our own declaration back to 12. + * + * - `@better-auth/scim>better-call` — scim is held at `1.7.0-rc.1` + * deliberately (stable 1.7.x ships a whole-model rewrite that is its own + * migration), and the rc peers an exact `better-call@1.3.7` while + * better-auth itself depends on 1.4.0. A better-auth plugin must share the + * HOST's better-call instance, so the single 1.4.0 copy every install + * already resolves is the correct tree, not a skew to repair. + * ⚠️ This entry retires together with the SCIM rc pin — delete both at once. + * + * `allowedVersions` suppresses the report ONLY; it moves no resolution. + */ +export const SCAFFOLD_ALLOWED_PEER_VERSIONS: Record = { + 'better-auth>better-sqlite3': '13', + '@better-auth/scim>better-call': '1.4.0', +}; + +/** + * Render the `pnpm-workspace.yaml` that allowlists native build scripts and + * declares the two known-benign peer skews. * Kept minimal (no `packages:` key) so it acts purely as a settings file for * the single-package scaffold rather than declaring a workspace. */ -export function renderPnpmWorkspaceYaml(builtDeps: string[] = SCAFFOLD_BUILT_DEPENDENCIES): string { +export function renderPnpmWorkspaceYaml( + builtDeps: string[] = SCAFFOLD_BUILT_DEPENDENCIES, + allowedPeerVersions: Record = SCAFFOLD_ALLOWED_PEER_VERSIONS, +): string { + const peerEntries = Object.entries(allowedPeerVersions); + return [ '# Allowlist native dependency build scripts so `pnpm install` compiles', '# them (pnpm 10+ blocks build scripts by default). Without this,', @@ -89,6 +131,30 @@ export function renderPnpmWorkspaceYaml(builtDeps: string[] = SCAFFOLD_BUILT_DEP '# "Could not locate the bindings file".', 'onlyBuiltDependencies:', ...builtDeps.map((d) => ` - ${d}`), + // No rules, no header: a bare `peerDependencyRules:` would advertise a + // declaration that is not there. + ...(peerEntries.length === 0 ? [] : [ + '', + '# Two third-party peer ranges resolve outside what their declaring package', + '# states, and pnpm reports both on a first install. Neither is a real', + '# incompatibility:', + '#', + '# better-auth peers better-sqlite3 ^12.0.0 while the tree resolves 13.x.', + '# That peer is optional and covers handing better-auth a raw', + '# better-sqlite3 `Database`; ObjectStack hands it an ObjectQL adapter', + '# instead. Measured on the configuration the range does cover,', + '# better-auth 1.7.1 behaves identically on 13.0.3 and on 12.11.1.', + '#', + '# @better-auth/scim (held at a release candidate deliberately) peers an', + '# exact better-call 1.3.7, while better-auth itself depends on 1.4.0. A', + '# better-auth plugin has to share the host\'s better-call instance, so', + '# the single 1.4.0 copy is the correct resolution.', + '#', + '# These suppress the report only — no resolution moves.', + 'peerDependencyRules:', + ' allowedVersions:', + ...peerEntries.map(([k, v]) => ` '${k}': '${v}'`), + ]), '', ].join('\n'); } diff --git a/packages/cli/test/init.test.ts b/packages/cli/test/init.test.ts index db94bd22e1..535732fe5b 100644 --- a/packages/cli/test/init.test.ts +++ b/packages/cli/test/init.test.ts @@ -6,7 +6,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import { fileURLToPath } from 'url'; -import { TEMPLATES, getCliVersion, detectPackageManager, sanitizeNamespace, SCAFFOLD_BUILT_DEPENDENCIES, renderPnpmWorkspaceYaml } from '../src/commands/init'; +import { TEMPLATES, getCliVersion, detectPackageManager, sanitizeNamespace, SCAFFOLD_BUILT_DEPENDENCIES, SCAFFOLD_ALLOWED_PEER_VERSIONS, renderPnpmWorkspaceYaml } from '../src/commands/init'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const pkg = JSON.parse( @@ -118,6 +118,60 @@ describe('native build allowlist (pnpm-workspace.yaml)', () => { }); }); +// A brand-new scaffold's first `pnpm install` reported two unmet peers, on the +// one screen where a newcomer decides whether this project is solid, with +// nothing they did to cause it (#10326). Both ranges belong to third-party +// packages we cannot edit, so the remedy is pnpm's scoped `allowedVersions` — +// and it must travel INSIDE the scaffold, because a `peerDependencyRules` block +// in this repo's own pnpm-workspace.yaml does not ship with published packages. +describe('benign peer-skew declarations (#10326)', () => { + // Comments in the rendered YAML explain these keys; they must not be what + // satisfies an assertion about the keys themselves. + const settings = renderPnpmWorkspaceYaml().replace(/^\s*#.*$/gm, ''); + + it('widens better-auth\'s stale better-sqlite3 peer rather than pinning ours back', () => { + // better-auth 1.7.1 peers `^12.0.0` while the tree resolves 13.x. The peer + // is OPTIONAL and governs one configuration only — a raw better-sqlite3 + // `Database` passed to better-auth's `database` option — which ObjectStack + // never does (AuthManager passes an ObjectQL adapter factory). Measured on + // the configuration it does govern, 1.7.1 behaves identically on 13.0.3 and + // 12.11.1, so 13 is right and the upstream range is stale. + expect(SCAFFOLD_ALLOWED_PEER_VERSIONS['better-auth>better-sqlite3']).toBe('13'); + expect(settings).toMatch(/^ {4}'better-auth>better-sqlite3': '13'$/m); + }); + + it('accepts the single better-call copy @better-auth/scim resolves to', () => { + // scim is held at 1.7.0-rc.1 on purpose; the rc peers an EXACT + // `better-call@1.3.7` while better-auth depends on 1.4.0. A better-auth + // plugin must share the HOST's better-call instance, so one 1.4.0 copy is + // the correct tree. Retires with the scim rc pin. + expect(SCAFFOLD_ALLOWED_PEER_VERSIONS['@better-auth/scim>better-call']).toBe('1.4.0'); + expect(settings).toMatch(/^ {4}'@better-auth\/scim>better-call': '1\.4\.0'$/m); + }); + + it('renders the rules under peerDependencyRules.allowedVersions', () => { + expect(settings).toMatch(/^peerDependencyRules:$/m); + expect(settings).toMatch(/^ {2}allowedVersions:$/m); + }); + + it('scopes every rule to one declaring package, never a bare peer name', () => { + // A bare `better-sqlite3: '13'` would silence that peer for every package + // declaring it — including one whose complaint would be real. + const keys = Object.keys(SCAFFOLD_ALLOWED_PEER_VERSIONS); + expect(keys.length).toBeGreaterThan(0); + for (const key of keys) { + expect(key, `"${key}" must be spelled >`).toContain('>'); + } + }); + + it('emits nothing at all when the caller supplies no rules', () => { + // The block is data-driven, so an empty map must not leave a dangling + // `allowedVersions:` header claiming a declaration that is not there. + const bare = renderPnpmWorkspaceYaml(SCAFFOLD_BUILT_DEPENDENCIES, {}); + expect(bare).not.toMatch(/^peerDependencyRules:$/m); + }); +}); + describe('sanitizeNamespace', () => { const NS_RE = /^[a-z][a-z0-9_]{1,19}$/; diff --git a/packages/create-objectstack/src/template-consistency.test.ts b/packages/create-objectstack/src/template-consistency.test.ts index dacb7a0c7c..ef190a866e 100644 --- a/packages/create-objectstack/src/template-consistency.test.ts +++ b/packages/create-objectstack/src/template-consistency.test.ts @@ -361,6 +361,55 @@ describe('blank template pnpm build approvals (#3119)', () => { }); }); +// A brand-new scaffold's very first `pnpm install` reported two unmet peers — +// on the one screen where a newcomer is deciding whether this project is solid, +// with nothing they did to cause it and nothing they can do about it (#10326). +// Both are third-party ranges we cannot edit, so the declaration is pnpm's +// scoped `allowedVersions`, and it has to travel INSIDE the scaffold: a +// `peerDependencyRules` block in this repo's own pnpm-workspace.yaml would not +// ship with the published packages, exactly as the overrides note there says. +describe('blank template peer-skew declarations (#10326)', () => { + const wsPath = path.join(pkgRoot, 'src', 'templates', 'blank', 'pnpm-workspace.yaml'); + // Same comment-stripping as the block above: the prose explaining these keys + // must not be what satisfies an assertion about the keys. + const settings = fs.existsSync(wsPath) + ? fs.readFileSync(wsPath, 'utf8').replace(/^\s*#.*$/gm, '') + : ''; + const allowed = /^ {2}allowedVersions:\n((?:[ \t]+.*\n?)*)/m.exec(settings)?.[1] ?? ''; + + it('declares the stale better-auth > better-sqlite3 peer', () => { + // better-auth 1.7.1 peers `^12.0.0`; @objectstack/driver-sql resolves 13.x. + // 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, 1.7.1 behaves identically on 13.0.3 and 12.11.1. + expect( + /^\s*'better-auth>better-sqlite3':\s*'13'\s*$/m.test(allowed), + "allowedVersions must widen better-auth's stale better-sqlite3 peer to 13", + ).toBe(true); + }); + + it('declares the frozen @better-auth/scim > better-call peer', () => { + // scim is held at 1.7.0-rc.1 deliberately; the rc peers an EXACT 1.3.7 + // while better-auth depends on 1.4.0. A better-auth plugin must share the + // host's better-call instance, so the single 1.4.0 copy is correct. + expect( + /^\s*'@better-auth\/scim>better-call':\s*'1\.4\.0'\s*$/m.test(allowed), + 'allowedVersions must accept the single better-call 1.4.0 copy scim resolves to', + ).toBe(true); + }); + + it('scopes every rule to one declaring package, never a bare peer name', () => { + // A bare `better-sqlite3: '13'` would silence that peer for EVERY package + // that declares it, including ones whose complaint would be real. + const entries = [...allowed.matchAll(/^\s*'([^']+)':/gm)].map((m) => m[1]); + expect(entries.length).toBeGreaterThan(0); + for (const key of entries) { + expect(key, `"${key}" must be spelled >`).toContain('>'); + } + }); +}); + describe('README template table', () => { it('lists exactly the templates in the TEMPLATES registry', () => { const readme = fs.readFileSync(path.join(pkgRoot, 'README.md'), 'utf8'); diff --git a/packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml b/packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml index 04a1dda154..9657121786 100644 --- a/packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml +++ b/packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml @@ -21,3 +21,29 @@ onlyBuiltDependencies: allowBuilds: better-sqlite3: true esbuild: true + +# Two third-party peer ranges resolve outside what their declaring package +# states. `pnpm install` reports both as unmet peers — which would be the first +# thing a brand-new project shows you — and neither is a real incompatibility: +# +# better-auth peers better-sqlite3 ^12.0.0 while the tree resolves 13.x. That +# peer is optional and covers handing better-auth a raw better-sqlite3 +# `Database`; ObjectStack hands it an ObjectQL adapter instead, so nothing +# here goes down that path. Measured on the configuration the range does +# cover — better-auth's own Kysely dialect, running its migrations, sign-up, +# sign-in and adapter find/update/delete — 1.7.1 behaves identically on +# better-sqlite3 13.0.3 and on 12.11.1. The upstream range is stale. +# +# @better-auth/scim (held at a release candidate deliberately, not by +# neglect) peers better-call at an exact 1.3.7, while better-auth itself +# depends on 1.4.0. A better-auth plugin has to share the host's better-call +# instance, so the single 1.4.0 copy is the correct resolution rather than a +# skew to repair. This entry retires when SCIM moves off the rc. +# +# These suppress the report only: no resolution moves, and the lockfile is +# byte-identical with and without this block. + +peerDependencyRules: + allowedVersions: + 'better-auth>better-sqlite3': '13' + '@better-auth/scim>better-call': '1.4.0' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fe34a04edd..a248064d91 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -96,9 +96,26 @@ onlyBuiltDependencies: # `@better-auth/core` at `^1.7.0-rc.1`) are satisfied by the stable 1.7.1 # the family now resolves to. # KNOWN SKEW while this hold lasts: scim rc.1 peers `better-call@1.3.7` -# while better-auth 1.7.1 depends on `better-call@1.4.0`, so the tree -# carries two copies. Measured green on the plugin-auth suite; it retires -# with #3653. +# while better-auth 1.7.1 depends on `better-call@1.4.0`. CORRECTION +# (#10326, measured): the tree does NOT carry two copies — this lockfile +# holds exactly ONE better-call version, 1.4.0, and scim rc.1 resolves to +# it, in a scaffolded downstream install too. That is the correct tree +# rather than a skew to repair: a better-auth plugin has to share the +# HOST's better-call instance, and the exact `1.3.7` is just the rc's +# frozen stamp. What the skew does produce is an unmet-peer line on a +# newcomer's very first `pnpm install`, which the scaffold templates now +# declare away (see `peerDependencyRules` in +# `packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml` +# and `renderPnpmWorkspaceYaml` in `packages/cli`). Measured green on the +# plugin-auth suite; the skew, and both template entries, retire with +# #3653. +# Its sibling line is better-auth's own stale `better-sqlite3@^12.0.0` +# peer against our `^13.0.3`. That one is NOT reported here, because +# `auto-install-peers=true` (.npmrc) quietly installs a second, unused +# better-sqlite3 12.11.1 to satisfy it — which is why CI never saw what a +# scaffolded project shows on its first screen. #10326 measured 1.7.1 as +# behaviourally identical on 13.0.3 and 12.11.1 and left this workspace's +# resolution untouched; only the scaffolds declare it. # `scripts/check-prerelease-pin-watch.mjs` keeps watching this pin (it is # now the only entry on its watch list) so #3653 has a producer. # - @better-auth/oauth-provider: GHSA-p2fr-6hmx-4528 — same better-auth