Skip to content

fix(stack): emit wasm-inline types from the main config so entries share one class - #964

Merged
tobyhede merged 2 commits into
mainfrom
fix/963-wasm-inline-type-identity
Sep 2, 2026
Merged

fix(stack): emit wasm-inline types from the main config so entries share one class#964
tobyhede merged 2 commits into
mainfrom
fix/963-wasm-inline-type-identity

Conversation

@tobyhede

@tobyhede tobyhede commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

tsup ran a separate declaration pass for wasm-inline, so the package shipped two copies of every column class. EncryptedV3Column has a private field, and TypeScript compares those classes by declaration origin — so a schema authored on @cipherstash/stack/wasm-inline was a compile error against AnyV3Table, every adapter's schemas, and the native Encryption, 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-inline stays ESM-only — only types moved.

scripts/__tests__/skills-supabase-edge-schema-entry.test.mjs is deleted: it failed any shipped snippet pairing encryptedSupabase from 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-edge smuggled 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 by wasm-inline-type-identity.ts, which asserts assignability to the same AnyV3Table the adapter types schemas with. stash-edge now shows the pairing as a real fence.

The two dist-types/ gates were watched failing against a build from main'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.ts took main's side with the wording reapplied at create.ts where main moved it; the two skills kept main's newer content with only the schema-sharing claims changed. The diagnostics entry also runs its own DTS pass and is deliberately untouched: it declares no column classes, and its splitting: false is the point of that entry.

https://claude.ai/code/session_01HAonmKdSJMxYZcuRCiZPXE

@changeset-bot

changeset-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c034508

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@cipherstash/stack Patch
@cipherstash/stack-supabase Patch
stash Patch
@cipherstash/bench Patch
@cipherstash/stack-drizzle Patch
@cipherstash/stack-prisma Patch
@cipherstash/test-kit Patch
@cipherstash/basic-example Patch
@cipherstash/prisma-example Patch
@cipherstash/e2e Patch
@cipherstash/wizard Patch

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.
@tobyhede
tobyhede force-pushed the fix/963-wasm-inline-type-identity branch from 92f9e50 to c1bf387 Compare September 2, 2026 05:21
@tobyhede
tobyhede marked this pull request as ready for review September 2, 2026 05:31
@tobyhede
tobyhede requested a review from a team as a code owner September 2, 2026 05:31

@freshtonic freshtonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 with dts: false on the ESM-only wasm-inline config — one DTS pass, shared types-public-*.d.ts chunk, JS emission untouched. The mainEntry derivation avoids the two-hand-maintained-lists trap, and the unreferenced wasm-inline.d.cts ballast is documented with its reason (tsup can't scope dts per format). ESM-only exports shape preserved.
  • The gates are real and they run. test:types:dist executes in CI (tests.yml:255 → turbo test:types:dist), and the two gate files cover both resolution modes (bundler over relative dist/ paths; node16 by package name through the exports map), both directions of the symmetric failure, and a bare column so a loosened AnyV3Table can'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.ts error-message extension is consistent with the new story, and the widened isV3ColumnLike comment (CJS-no-code-splitting makes every require() 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 in logger/index.ts is 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
@tobyhede
tobyhede requested a review from freshtonic September 2, 2026 05:52

@freshtonic freshtonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@tobyhede
tobyhede merged commit 013e3ff into main Sep 2, 2026
27 checks passed
@tobyhede
tobyhede deleted the fix/963-wasm-inline-type-identity branch September 2, 2026 06:06
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.

A schema authored on @cipherstash/stack/wasm-inline is a compile error against every other entry — tsup emits two copies of the column classes

2 participants