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
44 changes: 44 additions & 0 deletions .changeset/better-sqlite3-peer-record-remeasured.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
"@objectstack/cli": patch
"create-objectstack": patch
---

fix(cli): re-measure the `better-auth` > `better-sqlite3` peer record, correct what it credits, and pin the declaration it justifies (#16813)

A tree containing `@objectstack/cli` reports an unmet peer on every fresh
resolve — `better-auth` peers `better-sqlite3@^12.0.0`, the CLI declares
`^13.0.3` — and the reading that decides what to do about it lived only inside
the scaffold generator's prose. No range moves here and no resolution moves:
what changes is the recorded reason, which had two measured errors in it, plus
a gate that now holds the declaration to that reason.

**The declaration is correct and stays at `^13`.** Three readings, taken rather
than inherited:

- The peer is `optional`, and it governs exactly one configuration — a raw
better-sqlite3 `Database` passed to better-auth's `database` option.
`AuthManager.createDatabaseConfig()` returns an ObjectQL adapter factory, or
`undefined` for better-auth's in-memory adapter. Never a `Database`.
- better-auth cannot be incompatible with better-sqlite3 13, because it never
touches it: of the 464 files in the published `better-auth@1.7.2` tarball,
exactly one names better-sqlite3 — `package.json`, the peer declaration
itself — and no code file references it (positive control: `kysely` names 9).
It accepts a `Database` the caller constructs; its own sqlite test path uses
node's built-in `node:sqlite`.
- Pinning back to `^12` is not a neutral alternative. Measured on a bare
project depending on `@objectstack/cli@17.3.0`, it clears the report only by
resolving a **second** native better-sqlite3 (12.11.1 beside 13.0.3) that
nothing loads. The scaffold's existing `allowedVersions` entry clears the
same report with the lockfile byte-identical.

**Two corrections to the record.** It credited `@objectstack/driver-sql` for
the 13.x copy; on the chain that actually reports
(`cli` → `runtime` → `plugin-auth` → `better-auth`) the binding copy is the
CLI's own `optionalDependencies` entry, which pnpm names in the warning itself.
And it was measured on better-auth 1.7.1 while the family has been pinned at
1.7.2 since — re-measured, with the empirical reading replaced by a structural
one.

The scaffold's rendered `pnpm-workspace.yaml` comment changes wording in both
producers (`objectstack init` and the `create-objectstack` blank template); the
declarations, the widening entry and the resolution are untouched.
63 changes: 48 additions & 15 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,16 +98,44 @@ export const SCAFFOLD_BUILT_DEPENDENCIES = ['better-sqlite3', 'esbuild'];
* 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>better-sqlite3` — better-auth peers `^12.0.0` while the tree
* resolves 13.x. 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 (or `undefined`, better-auth's own in-memory
* adapter). So the upstream range is stale and 13 is right — widening is
* the correct remedy, not pinning our own declaration back to 12.
*
* RE-MEASURED on the pinned 1.7.2 (#16813). The original reading was taken
* on 1.7.1 (#10326) and was behavioural: better-auth's own Kysely dialect —
* migrations, sign-up, sign-in, adapter find/update/delete — behaves
* identically on better-sqlite3 13.0.3 and on 12.11.1. 1.7.2 makes that
* structural instead of empirical: of the 464 files in the published
* `better-auth@1.7.2` tarball, exactly ONE names better-sqlite3 —
* `package.json`, i.e. the peer declaration itself. Zero code files
* reference it (positive control: `kysely` names 9). better-auth never
* imports the package; it accepts a `Database` the CALLER constructs and
* hands it to Kysely, and its own sqlite test path uses node's built-in
* `node:sqlite` `DatabaseSync`. There is therefore no better-auth call site
* that could touch an API moved between better-sqlite3 12 and 13 — the
* range is a statement about an instance we never supply.
*
* ⚠️ TWO CORRECTIONS to what this entry used to say, both measured:
* • the 13.x copy better-auth binds to is `@objectstack/cli`'s OWN
* `optionalDependencies` entry, NOT `@objectstack/driver-sql`'s. On the
* chain that actually reports (`cli` → `runtime` → `plugin-auth` →
* `better-auth`) the CLI is the ancestor, so its copy is the one pnpm
* resolves the peer against — pnpm names it in the warning itself
* ("found 13.0.3 in @objectstack/cli"). Editing driver-sql alone would
* not move this line.
* • pinning the CLI back to `^12` is not a neutral alternative. Measured
* on a bare project depending on `@objectstack/cli@17.3.0`, it clears
* the report only by installing a SECOND native better-sqlite3
* (12.11.1 alongside 13.0.3), and the 12 copy is dead weight — the CLI
* loads better-sqlite3 itself (`src/utils/sqlite-occupancy.ts`) and
* knex resolves 13.x through driver-sql regardless. This
* `allowedVersions` entry clears the same report with the resolution
* byte-identical (0 lines of lockfile diff).
*
* - RETIRED (#3653): `@better-auth/scim>better-call` — the rc.1-era scim pin
* peered an exact `better-call@1.3.7` against the host's 1.4.0, and this
Expand Down Expand Up @@ -474,11 +502,16 @@ export function renderPnpmWorkspaceYaml(
'# package states, and that pnpm reports on a first install. None 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 peers better-sqlite3 ^12.0.0 while the tree resolves 13.x',
'# (the copy @objectstack/cli declares for its own sqlite tooling). 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. Re-measured on better-auth 1.7.2:',
'# no file in the published package references better-sqlite3 at all —',
'# it only accepts a Database you construct — so there is no call site',
'# that could depend on what changed between 12 and 13. The upstream',
'# range is stale; pinning back to 12 would just install a second,',
'# unused native copy.',
'#',
'# @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',
Expand Down
132 changes: 132 additions & 0 deletions packages/cli/test/better-sqlite3-peer-declaration.pin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* **The pin for `@objectstack/cli`'s own `better-sqlite3` declaration**, and
* for its agreement with the scaffold widening that exists because of it
* (#16813).
*
* ## The report this is about
*
* A consumer that installs `@objectstack/cli` sees, whenever pnpm actually
* runs the resolution step:
*
* └─┬ @objectstack/cli 17.3.0
* └─┬ @objectstack/runtime 17.4.0
* └─┬ @objectstack/plugin-auth 17.4.0
* └─┬ better-auth 1.7.2
* └── ✕ unmet peer better-sqlite3@^12.0.0: found 13.0.3
*
* ⚠️ It is easy to conclude that this file's job is to stop, and that the fix
* is to move the declaration below back inside `^12`. It is not, and the
* measurement that says so is recorded here so the next reader does not have
* to re-take it:
*
* 1. **The peer is optional and governs a configuration we never use.**
* better-auth's `better-sqlite3` peer is
* `peerDependenciesMeta.better-sqlite3.optional === true`, and it covers
* one thing: a raw better-sqlite3 `Database` handed to better-auth's
* `database` option. `AuthManager.createDatabaseConfig()` returns an
* ObjectQL adapter factory, or `undefined` for better-auth's in-memory
* adapter — never a `Database`.
* 2. **better-auth cannot be incompatible with better-sqlite3 13, because it
* never touches it.** Of the 464 files in the published
* `better-auth@1.7.2` tarball, exactly one names better-sqlite3 —
* `package.json`, i.e. the peer declaration itself. Zero code files
* reference it (positive control: `kysely` names 9). It accepts a
* `Database` the caller constructs; its own sqlite test path uses node's
* built-in `node:sqlite`.
* 3. **Pinning back to `^12` costs a second native module and buys nothing.**
* Measured on a bare project depending on `@objectstack/cli@17.3.0`:
* rewriting only this declaration to `^12.11.1` does clear the report, but
* the resolved tree then carries better-sqlite3 **12.11.1 and 13.0.3**,
* and the 12 copy is never loaded — this package loads better-sqlite3
* itself (`src/utils/sqlite-occupancy.ts`) and knex resolves 13.x through
* `@objectstack/driver-sql` either way. The scaffold's
* `allowedVersions` entry clears the same report with the resolution
* byte-identical (0 lines of lockfile diff).
*
* ⇒ 13 is the correct declaration and the upstream range is stale. What was
* missing was not a different range but a gate holding the range to the
* reasoning that justifies it, which is what this file is.
*
* ## Why the two assertions are one pin and not two
*
* `SCAFFOLD_ALLOWED_PEER_VERSIONS['better-auth>better-sqlite3']` widens
* better-auth's peer to a **major**, and the only reason that major is right
* is the declaration below. Held apart, either can move alone and stay green:
* a declaration lifted to `^14` leaves the scaffold suppressing a report it no
* longer matches (so the NEXT real skew arrives pre-silenced), and a
* declaration dropped to `^12` leaves a suppression with no skew behind it —
* the exact shape `init.test.ts` refuses for the retired
* `@better-auth/scim>better-call` entry. So the major is read out of the
* manifest and compared, rather than written down twice.
*
* `init.test.ts` pins the widening's literal value and its rendering; this
* file pins what the widening is ABOUT. Neither restates the other.
*
* The manifest read stays inside this package (`test/` → package root), so it
* is not a `check:cross-package-test-inputs` escape and needs no declaration.
*/

import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
// `.js`, not extensionless: this package is `moduleResolution: NodeNext`.
import { SCAFFOLD_ALLOWED_PEER_VERSIONS } from '../src/commands/init.js';

const HERE = resolve(fileURLToPath(import.meta.url), '..');

// One line on purpose — `check:cross-package-test-inputs` reconstructs reads by
// source scan, and a split `resolve(HERE, …)` is a spelling it does not know.
const MANIFEST = resolve(HERE, '../package.json');

const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8')) as {
optionalDependencies?: Record<string, string>;
dependencies?: Record<string, string>;
};

const WIDENING_KEY = 'better-auth>better-sqlite3';

/** The single major a caret/tilde range admits, or null if it admits more. */
function soleMajor(range: string): string | null {
const m = /^[\^~]?(\d+)\.\d+\.\d+$/.exec(range.trim());
return m ? m[1] : null;
}

describe('@objectstack/cli better-sqlite3 declaration (#16813)', () => {
it('declares better-sqlite3 as an OPTIONAL dependency, not a hard one', () => {
// Optional is load-bearing: `objectstack serve` must start on a host where
// the native build failed, falling back to the wasm driver. A hard
// dependency turns that degraded start into a failed install.
expect(manifest.optionalDependencies?.['better-sqlite3']).toBeTypeOf('string');
expect(manifest.dependencies?.['better-sqlite3']).toBeUndefined();
});

it('keeps the declaration on one major, so the scaffold can widen to it', () => {
const declared = manifest.optionalDependencies?.['better-sqlite3'] ?? '';
expect(
soleMajor(declared),
`better-sqlite3 is declared "${declared}"; this pin needs a range admitting exactly one major`,
).not.toBeNull();
});

it('widens better-auth\'s peer to the major this package actually declares', () => {
// ⛔ Not "to 13" — that is `init.test.ts`'s assertion. This one says the
// widening and the declaration are the SAME major, so neither can move
// without the other.
const declared = manifest.optionalDependencies?.['better-sqlite3'] ?? '';
expect(SCAFFOLD_ALLOWED_PEER_VERSIONS[WIDENING_KEY]).toBe(soleMajor(declared));
});

it('refuses a retreat to better-sqlite3 12 (the report is not the defect)', () => {
// The failure this guards is a well-meant one: reading the unmet-peer
// report as a defect and "fixing" it here. Measured, that clears the line
// only by installing a second native better-sqlite3 that nothing loads —
// see this file's header for the three readings. If better-auth ever
// WIDENS its peer upstream, the right change is to retire the scaffold
// entry, not to move this declaration.
const declared = manifest.optionalDependencies?.['better-sqlite3'] ?? '';
expect(soleMajor(declared)).not.toBe('12');
});
});
10 changes: 7 additions & 3 deletions packages/cli/test/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,12 +323,16 @@ describe('benign peer-skew declarations (#10326)', () => {
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
// better-auth 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.
// 12.11.1; re-measured on the pinned 1.7.2 (#16813), the published package
// does not reference better-sqlite3 in any file, so no call site of ours
// can reach an API that moved 12 -> 13. 13 is right, the range is stale.
// The declaration this widening is ABOUT is pinned in
// `test/better-sqlite3-peer-declaration.pin.test.ts`.
expect(SCAFFOLD_ALLOWED_PEER_VERSIONS['better-auth>better-sqlite3']).toBe('13');
expect(settings).toMatch(/^ {4}'better-auth>better-sqlite3': '13'$/m);
});
Expand Down
9 changes: 7 additions & 2 deletions packages/create-objectstack/src/template-consistency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,11 +503,16 @@ describe('blank template peer-skew declarations (#10326)', () => {
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.
// better-auth peers `^12.0.0`; the tree resolves 13.x — @objectstack/cli's
// own optionalDependency, which is the copy pnpm names in the report on the
// cli -> runtime -> plugin-auth -> better-auth chain (#16813; this comment
// used to credit @objectstack/driver-sql, which is not the binding copy).
// 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.
// path it does govern, 1.7.1 behaves identically on 13.0.3 and 12.11.1;
// re-measured on 1.7.2, the published package references better-sqlite3 in
// no file at all, so there is no call site to be incompatible.
expect(
/^\s*'better-auth>better-sqlite3':\s*'13'\s*$/m.test(allowed),
"allowedVersions must widen better-auth's stale better-sqlite3 peer to 13",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,18 @@ allowBuilds:
# first thing a brand-new project shows you — and none 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 peers better-sqlite3 ^12.0.0 while the tree resolves 13.x — the
# copy @objectstack/cli declares for its own sqlite tooling, and the one pnpm
# names in the report. 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. First measured behaviourally
# on 1.7.1 — better-auth's own Kysely dialect, running its migrations,
# sign-up, sign-in and adapter find/update/delete, behaves identically on
# better-sqlite3 13.0.3 and on 12.11.1. Re-measured on the pinned 1.7.2 and
# now structural: no file in the published package references better-sqlite3
# at all, so no better-auth call site can depend on what moved between 12 and
# 13. The upstream range is stale. Pinning our own declaration back to 12
# would clear the report only by installing a second, unused native copy.
#
# (The '@better-auth/scim>better-call' entry that used to sit here retired
# with the scim release-candidate pin — stable @better-auth/scim 1.7.1 peers
Expand Down
Loading