fix(stack): emit wasm-inline types from the main config so entries share one class - #964
Conversation
🦋 Changeset detectedLatest commit: c034508 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
…are one class
`packages/stack/tsup.config.ts` ran a second, independent declaration pass for
the `wasm-inline` entry. Two rollup runs meant two separately-emitted copies of
`EncryptedV3Column`, which carries `private readonly columnName` — and TypeScript
compares classes with private members by DECLARATION ORIGIN, not structurally.
So a table authored with `encryptedTable`/`types` from
`@cipherstash/stack/wasm-inline` was a COMPILE error against every first-party
adapter's `schemas` and against the native `Encryption`, and native-authored
tables were rejected by the WASM `Encryption` — symmetrically:
error TS2322: Types have separate declarations of a private property
'columnName'.
The runtime always accepted these tables (`hasBuildColumnKeyMap` /
`isV3ColumnLike` probe structurally), so only the compiler refused, which made
`as any` the tempting workaround. `wasm-inline` is the entry the edge examples
use, so this was the published shape for Workers, Deno, Bun and Supabase Edge.
The main config's `dts` now lists `src/wasm-inline.ts` and the wasm-inline config
sets `dts: false`, so all entries share one `types-public-*.d.ts` chunk. The dts
list is derived from the entry list rather than repeated, so a new subpath cannot
lose its types. JS emission is untouched: `./wasm-inline` keeps its ESM-only
shape and no `require` branch.
Gates, one per resolution mode, both over the built `.d.ts`:
- `dist-types/wasm-inline-type-identity.ts` — `moduleResolution: bundler`
- `dist-types/node16/wasm-inline.mts` — Node16, by package name via `exports`
Both pin all four cross-entry directions. Source-only type tests structurally
cannot catch this: they map the subpaths to `src` and compare source to source.
`test:types:dist` is already wired into CI and cannot skip.
Comment corrections — the previous explanation was too narrow. CJS does not
code-split at all, so `dist/adapter-kit.cjs`, `dist/eql/v3/index.cjs` and
`dist/encryption/v3.cjs` each define the class independently and a `require()`
consumer hit the RUNTIME bug on any subpath, not just via wasm-inline. Verified
against the build output. Updated `stack-supabase/src/column-map.ts`,
`stack/src/adapter-kit.ts`, the supabase tests, and the pending changeset.
The logger comment overclaimed: the `!process.env` guard keeps only that one read
safe. `initStackLogger()` runs at module scope and evlog's `detectEnvironment()`
and `isDev()` guard `typeof process` but then read `process.env.NODE_ENV`, which
throws under `globalThis.process = {}`. Verified in evlog 1.11.0; comment now
scoped honestly.
`logger-edge-safety.test.ts` turns a skip into a hard failure under CI — a skip
is local convenience and a green gate that never ran.
Skills: `stash-edge` documented "Schema Modules Do Not Cross Entries" and
`stash-encryption` "the schema is not shareable between entries" as permanent
behaviour. This fix makes both false, so they now describe the shared-schema
story and keep the diagnostic as an upgrade signal. `stash-supabase` documents
both authoring entries and the column-level fail-closed error.
92f9e50 to
c1bf387
Compare
freshtonic
left a comment
There was a problem hiding this comment.
Requesting changes on one coherence item this PR's own change creates; everything else verified clean and this is careful, well-gated work.
The blocker: scripts/__tests__/skills-supabase-edge-schema-entry.test.mjs now enforces a rule this PR abolishes
That guard (from #951) fails any shipped document whose snippet pairs encryptedSupabase from @cipherstash/stack-supabase/wasm-inline with encryptedTable/types from @cipherstash/stack/wasm-inline, and its error message states the pairing "does not compile: … TS2322, separate declarations of a private property 'columnName'". After this PR that statement is false — making the pairing compile is the PR's entire point, dist-types/wasm-inline-type-identity.ts now asserts it compiles, and skills/stash-supabase now actively recommends it ("Use the wasm-inline entry when the schema module is shared with an Edge Function"). The guard passes today only because no code fence literally shows the pairing (stash-edge smuggles it in as a comment — // …or from '@cipherstash/stack/wasm-inline' — which reads like it's dodging its own guard). The first person to write the example the skill now recommends gets blocked by a guard citing a compiler error that no longer happens — and this repo's own doctrine says a guard asserting a false claim is worse than no guard.
Fix in this PR, either way is fine: delete the guard (the property it protected is now held, in the correct direction, by the two dist-types gates), or repurpose it to assert the docs carry the version caveat. And once it's gone, consider showing the wasm-inline pairing as a real fence in stash-edge rather than a comment.
Verified clean
- The mechanism is right and minimal.
dts: { entry: [...mainEntry, 'src/wasm-inline.ts'] }on the main config withdts: falseon the ESM-only wasm-inline config — one DTS pass, sharedtypes-public-*.d.tschunk, JS emission untouched. ThemainEntryderivation avoids the two-hand-maintained-lists trap, and the unreferencedwasm-inline.d.ctsballast is documented with its reason (tsup can't scopedtsper format). ESM-onlyexportsshape preserved. - The gates are real and they run.
test:types:distexecutes in CI (tests.yml:255 → turbotest:types:dist), and the two gate files cover both resolution modes (bundler over relativedist/paths; node16 by package name through theexportsmap), both directions of the symmetric failure, and a bare column so a loosenedAnyV3Tablecan't quietly pass. The body's claim that they were watched failing against main's config before going green is the right discipline. - The four skills are corrected consistently, with version-aware caveats ("on older versions… upgrade rather than assert") instead of silently rewriting history — good pattern for docs that land in customer repos pinned to arbitrary versions. The newly documented per-column fail-closed refusal is real code (
column-map.ts:189), not aspiration. - The rebase of a 5-week-old branch was done honestly: the runtime-claims and read-backstop prose guards from the #951/#952 era all pass on the reworked skill text (CI green), the
create.tserror-message extension is consistent with the new story, and the widenedisV3ColumnLikecomment (CJS-no-code-splitting makes everyrequire()consumer hit the runtime half, not just wasm-inline authors) is a genuinely better account of the hazard than the one it replaces. logger-edge-safety.test.ts's skip-becomes-CI-failure is exactly the anti-vacuous-gate pattern this repo keeps paying for the absence of; the honest correction of the partial-polyfill claim inlogger/index.tsis welcome.- Changeset: right three packages, right level (patch), accurate text. CI fully green.
Fix or retire the stale guard and this is an approve.
`scripts/__tests__/skills-supabase-edge-schema-entry.test.mjs` (#951) failed any shipped document whose snippet paired `encryptedSupabase` from `@cipherstash/stack-supabase/wasm-inline` with `encryptedTable`/`types` from `@cipherstash/stack/wasm-inline`, on the stated grounds that the pairing "does not compile: ... TS2322, separate declarations of a private property 'columnName'". That sentence is false as of the previous commit. Making the pairing compile is the whole point of it, `dist-types/wasm-inline-type-identity.ts` now asserts it compiles, and `stash-supabase` actively recommends it. The guard passed only because no fence literally showed the pairing — `stash-edge` smuggled it in as a trailing comment, which is a document dodging its own lint. The first person to write the example the skill recommends would have been blocked by a guard citing a compiler error that no longer happens. Deleted rather than reworded. The obvious repurposing — assert the docs carry the version caveat — guards prose phrasing, which drifts by legitimate rewording rather than by defect, and it could only ever fire in a world where the compile property is already broken. `wasm-inline-type-identity.ts` catches that first, in the codebase, by type-checking rather than text-matching. The property the guard protected is now held there, in the correct direction: the adapter types `schemas` as `Record<string, AnyV3Table>` with `AnyV3Table` from `@cipherstash/stack/eql/v3` (`schema-builder.ts:7`), which is the same declaration the gate asserts assignability to. Recorded in that file's comment so the reason the guard went lives where the property is kept, rather than only in this message. Asserting through `encryptedSupabase` itself was considered and rejected: it would give `packages/stack`'s declaration gate a build-graph dependency on a package that depends on it. `lib/package-readmes.mjs` keeps three other consumers, so nothing is orphaned. `stash-edge` now shows the wasm-inline pairing as a real fence beside the `eql/v3` one, which is what the smuggled comment was avoiding. Refs #963 Claude-Session: https://claude.ai/code/session_01HAonmKdSJMxYZcuRCiZPXE
freshtonic
left a comment
There was a problem hiding this comment.
Approving at c034508. The blocker is resolved exactly right, and then some: the stale guard is deleted rather than reworded (with the reasoning recorded where the replacement lives), the assignability line in dist-types/wasm-inline-type-identity.ts now explicitly documents that it is the adapter-pairing pin — via V3Schemas = Record<string, AnyV3Table> resolving the same declaration — so the property is enforced by the compiler in the true direction instead of by a grep in the false one, and stash-edge's smuggled trailing comment became a real second fence showing the pairing the skill recommends. That last touch matters: the doc now demonstrates what it claims instead of gesturing at it. CI green. Everything else stands as verified in my previous review — nice fix.
tsupran a separate declaration pass forwasm-inline, so the package shipped two copies of every column class.EncryptedV3Columnhas aprivatefield, and TypeScript compares those classes by declaration origin — so a schema authored on@cipherstash/stack/wasm-inlinewas a compile error againstAnyV3Table, every adapter'sschemas, and the nativeEncryption, and vice versa. It ran fine; only the compiler refused.Emitting those declarations from the main config gives every entry one declaration of each class.
./wasm-inlinestays ESM-only — only types moved.scripts/__tests__/skills-supabase-edge-schema-entry.test.mjsis deleted: it failed any shipped snippet pairingencryptedSupabasefrom the adapter's edge entry with a schema from@cipherstash/stack/wasm-inline, because that did not compile. It does now. It passed only because no fence literally showed the pairing —stash-edgesmuggled it into a comment — so the first person to write the example the skill recommends would have been blocked by a guard citing an error that no longer happens. The property it protected is held bywasm-inline-type-identity.ts, which asserts assignability to the sameAnyV3Tablethe adapter typesschemaswith.stash-edgenow shows the pairing as a real fence.The two
dist-types/gates were watched failing against a build frommain's config (exit 2) before going green; the Node16 one was run alone so it isn't riding on the bundler one. Four skills documented this as permanent behaviour and are corrected.Closes #963.
Review notes. Written 2026-07-30, never pushed, rebased onto current
main. Four conflicts resolved by hand — a released changeset dropped;stack-supabase/src/index.tstookmain's side with the wording reapplied atcreate.tswheremainmoved it; the two skills keptmain's newer content with only the schema-sharing claims changed. Thediagnosticsentry also runs its own DTS pass and is deliberately untouched: it declares no column classes, and itssplitting: falseis the point of that entry.https://claude.ai/code/session_01HAonmKdSJMxYZcuRCiZPXE