Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .changeset/clean-first-install-peer-warnings.md
Original file line number Diff line number Diff line change
@@ -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 `<declaring package>><peer>` 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.
70 changes: 68 additions & 2 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,84 @@ 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 `<declaring package>><peer>` — 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<string, string> = {
'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<string, string> = 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,',
'# better-sqlite3 ships uncompiled and `objectstack serve` fails with',
'# "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');
}
Expand Down
56 changes: 55 additions & 1 deletion packages/cli/test/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 <declaring package>><peer>`).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}$/;

Expand Down
49 changes: 49 additions & 0 deletions packages/create-objectstack/src/template-consistency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <declaring package>><peer>`).toContain('>');
}
});
});

describe('README template table', () => {
it('lists exactly the templates in the TEMPLATES registry', () => {
const readme = fs.readFileSync(path.join(pkgRoot, 'README.md'), 'utf8');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
23 changes: 20 additions & 3 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading