ReviewOS

stacks/stx

bun-plugin ships a stale bundled signals runtime — x-else (#1734) never reaches `serve` consumers

#1735
Closed glennmichael123 opened this 11 days ago · {{ comments.length }} {{ comments.length === 1 ? 'comment' : 'comments' }}
11 days ago

Summary

@stacksjs/stx and bun-plugin each carry the client signals runtime (the browser-side x-if/x-for/x-else engine). bun-plugin bundles its own copy at build time (Bun inlines it from @stacksjs/stx). So when the runtime in @stacksjs/stx/src/signals.ts changes — e.g. #1734 adding reactive x-else / x-else-if — rebuilding @stacksjs/stx updates its dist, but bun-plugin/dist keeps the snapshot from whenever it was last built. Since apps render through bun-plugin-stx/serve, the injected runtime is the stale one, and the new directives silently don't work (x-else is treated as an orphan → branches mis-render).

Evidence (current repo state)

  • packages/stx/dist — x-else present (rebuilt with #1734): ~650 matches
  • packages/bun-plugin/dist — x-else: 0 (not rebuilt)
  • The injected client runtime lives in the bun-plugin bundle — packages/bun-plugin/dist / installed bun-plugin-stx/dist/chunk-*.js contains bindIf / x-else-if / stx-if-chain.
  • packages/bun-plugin/src/index.ts imports processing/runtime from @stacksjs/stx (processDirectives, renderToString, processSignals, …), so Bun inlines a copy into bun-plugin/dist at build time.
  • Both packages are 0.2.67, and the published @stacksjs/stx@0.2.67 / bun-plugin-stx@0.2.67 predate #1734 — so no version signals "has x-else", and the two packages can be mutually out of sync at the same version.

Impact

Consumers using bun-plugin-stx/serve (the documented dev/prod path) can't use reactive directives newly added to stx until bun-plugin is independently rebuilt and republished. The lockstep 0.2.67 version makes the staleness invisible — bun install won't fix it.

Repro

  1. @stacksjs/stx/src/signals.ts gains x-else (#1734); rebuild @stacksjs/stx only.
  2. Don't rebuild bun-plugin.
  3. In an app on bun-plugin-stx/serve, write <div x-if="a">…</div><div x-else>…</div>.
  4. The x-else branch renders as a plain element (orphan warning) — the fix has no effect.

Suggested fixes (any one)

  • Rebuild + republish bun-plugin whenever the @stacksjs/stx runtime changes; bump both versions together on release.
  • CI guard that fails if bun-plugin/dist's runtime diverges from @stacksjs/stx/dist (or rebuild bun-plugin inside the stx build pipeline).
  • Single-source the client runtime: have bun-plugin load/re-export it from @stacksjs/stx instead of bundling a private copy, so a stx version bump is sufficient.

Found while adopting #1734 (x-else) in a downstream app — the directive works once bun-plugin is rebuilt, but not via the published bun-plugin-stx.

11 days ago

Investigated — here's the full picture, and a guard so this can't regress.

The structural fix is already in place. packages/bun-plugin/build.ts has marked the runtime external since 2026-01-13:

external: ['@stacksjs/stx'], // Don't bundle stx to avoid duplicate exports

So a fresh bun-plugin build does not inline the signals runtime — it imports it from the installed @stacksjs/stx at runtime. Verified against the current build: bun-plugin/dist references import { … } from "@stacksjs/stx" and contains zero runtime markers (bindIf / bindIfChain / stx-if-chain / createAutoUnwrapProxy / generateSignalsRuntime).

That reframes the "0 x-else in bun-plugin/dist" evidence: 0 is expected when the runtime is external — bun-plugin/dist never contains the runtime, stale or fresh. It's not a staleness signal on its own. (The bindIf/stx-if-chain you saw in an installed bun-plugin-stx/dist came from a publish built before the external flag, or from a dist that hadn't been rebuilt.)

So what actually bit the downstream app: the published bun-plugin-stx@0.2.67 / @stacksjs/stx@0.2.67 predate #1734. There's no released version with x-else yet — that's a release-timing gap, not a structural bundling bug in current main. prepublishOnly: "bun run build" already guarantees the next publish rebuilds bun-plugin against the externalized config.

Guard added (commit 2f8ad1cf6c, test(bun-plugin): guard the stx-runtime-external contract): a two-layer regression test so the external contract can't silently regress and reintroduce a private bundled copy:

  • static — build.ts keeps @stacksjs/stx in the external array;
  • behavioral — a fresh build's dist imports @stacksjs/stx and contains none of the runtime markers.

Verified it bites: removing external from build.ts makes all 3 tests fail (dist starts inlining bindIfChain/stx-if-chain); restoring it makes them pass.

Remaining to relieve consumers: a coordinated release — republish @stacksjs/stx + bun-plugin-stx together so x-else (#1734) ships. Two things worth deciding at release time:

  1. Version-pin range. When bun-plugin's @stacksjs/stx: workspace:* is resolved on publish, prefer a caret (^x.y.z) over an exact pin so an app can bump @stacksjs/stx alone and pick up runtime changes without waiting for a bun-plugin-stx republish (which the external design already supports). An exact pin re-creates the lockstep-masking the issue describes.
  2. (Separate papercut) packages/bun-plugin/bunfig.toml's test preload path (../../test-utils/happy-dom.ts) resolves to a nonexistent repo-root dir — the real file is packages/stx/test-utils/happy-dom.ts. bun-plugin tests only run from the repo root today (CI's path). Not addressed here; flagging for a follow-up.

Closing since the structural protection is in place + guarded and the only remaining step is a routine release. Reopen if you'd rather track the release here.

Sign in to comment on this issue.