Skip to content

refactor(ts): resolve extensions through the core's plan contract - #1126

Merged
thekevinscott merged 4 commits into
mainfrom
claude/1121-ts-ext
Sep 18, 2026
Merged

thekevinscott merged 4 commits into
mainfrom
claude/1121-ts-ext

Conversation

@thekevinbot

@thekevinbot thekevinbot commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

Slice C of #1111. The TypeScript SDK stops carrying its own port of extension
resolution and reaches the Rust core's plan contract
(dirsql::extension_resolution, landed in #1124) through the napi addon —
mirroring what Python did in #1125.

resolveConfigsExtensionSpecs reads each config off disk and hands
{ path, contents } pairs to planConfigExtensions; resolvePackage hands the
located package directory and its files to selectLoadable;
resolveExtensionPath classifies through isBareName. TypeScript keeps only
the two halves it alone can do: reading the files, and require.resolve.

Six modules go away (is-bare-name, has-bare-name, load-extension-entries,
resolve-entries, platform-suffixes, resolve-config-extension-specs)
against two added (read-config, plan-entry-path — split apart only to
satisfy one-function-per-file).

smol-toml is gone. The core parses the config now, so the dependency and
the #720 lazy-load guard that deferred importing it both go with it; the CLI
helper imports the resolver statically again.

Behavior change

The missing-loadable error is now rendered by the core, which writes its suffix
list as globs: no loadable extension file (*.so / *.node) found in package 'x' (searched /dir), where TypeScript previously wrote (.so / .node). Python
already emitted the glob form, so this restores parity. Red integration + e2e
tests for the new wording were pushed as their own commit (9da1e01) and CI was
confirmed red on that assertion alone before the implementation landed. A
migration fragment documents the change.

Marshaling note: napi omits a None object field rather than emitting
null, so ExtensionPlanEntry is typed as a union of variants whose absent
side is undefined. A top-level Option<Vec<T>> does come through as null,
so the "no config entries" signal is unchanged.

Parity

PARITY.md updated: the "temporary drift" note flagging TypeScript as the
lagging SDK is removed — both SDKs now plan through the core. This restores
parity
; no new drift. Slice D (#1122) does the final sweep.

Fixes #1121

E2E Verification

  • Ran e2e suites locally for every affected SDK
  • Python SDK e2e: N/A (no Python change)
  • TypeScript SDK e2e: pass
  • Rust core e2e (if applicable): N/A (packages/rust untouched; only the ts napi crate changed)
  • packages/python/e2e-attestations/<branch>.json receipt written if packages/python changed (just e2e-attest-python) — N/A
  • packages/ts/e2e-attestations/<branch>.json receipt written if packages/ts changed (just e2e-attest-ts)
  • Command(s) run: pnpm test:e2e (packages/ts), just e2e-attest-ts, pnpm test:binding, pnpm test:integration, pnpm coverage, cargo test -p dirsql-napi, just preflight
  • Result summary: e2e 17 passed (5 files); binding 114 passed (19 files); hermetic 17 passed; unit coverage 100% across all files; napi crate 22 passed; preflight 0 failing pairs.

Manual exercise

Built packages/rust/tests/fixtures/testext and staged it as
node_modules/dirsql-manual-ext/testext.so with a config naming the bare
package:

$ node dist/cli/dirsql.js query "SELECT dirsql_testext_answer() AS answer" --config /tmp/manual-1121/dirsql.toml
[{"answer":42}]

Same result from the SDK (new DirSQL({ root, config }) → db.query(...) →
[{"answer":42}]). And the new wording, against a package with no loadable:

$ node dist/cli/dirsql.js query "SELECT 1" --config /tmp/manual-1121/empty.toml
dirsql: no loadable extension file (*.so / *.node) found in package 'dirsql-manual-empty' (searched .../node_modules/dirsql-manual-empty)

Changelog / Migrations

  • Changelog fragment added under <root>/<pkg>/changelog.d/ for each changed package (or: skip-changelog trailer on a commit with reason)
  • Migration fragment added under <root>/<pkg>/migrations.d/ (or: not required -- additive/bugfix only)

Follow-up

withResolvedExtensions no longer awaits anything but stays async; making it
sync ripples into cli/main and is slice D's (#1122) to take.

The TypeScript resolver renders its own "no loadable extension file
(.so / .node)" message. Once resolution moves into the core's
SelectError the suffixes render as globs (*.so), so pin that wording
from the binding tier and from the CLI.

skip-changelog: red test only; the behavior change ships with the
implementation commit
`resolveConfigsExtensionSpecs` now reads each config off disk and hands
`{ path, contents }` pairs to the addon's `planConfigExtensions`;
`resolvePackage` hands the located package directory and its files to
`selectLoadable`; `resolveExtensionPath` classifies through `isBareName`.
TypeScript keeps only the two halves it alone can do: reading the files,
and `require.resolve`.

Six modules go away against two added, and with them `smol-toml` and the
lazy-load guard that dependency needed. The core renders the
missing-loadable error, so its suffix list is now glob-shaped
(`*.so / *.node`), matching Python.
@thekevinscott
thekevinscott merged commit fc8860f into main Sep 18, 2026
50 checks passed
@thekevinscott
thekevinscott deleted the claude/1121-ts-ext branch September 18, 2026 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

#1111 Slice C: migrate TypeScript extension resolution to Rust plan

2 participants