Skip to content

Support both Bun and Node runtimes (first-class create + run) #508

@vivek7405

Description

@vivek7405

Problem

webjs is Node 24+ only today. The single hard Node binding is the TypeScript stripper: dev.js serves .ts/.mts by calling Node 24+'s built-in module.stripTypeScriptTypes (assertNodeVersion enforces the floor). Everything else is already close to runtime-agnostic, so "Node-only" is an adapter-and-stripper problem, not an architectural one. Bun is the second runtime that actually matters in practice (it is the one even Remix 3, a portability-branded peer, concretely tests via test:bun scripts; Deno is a smaller niche and Cloudflare/edge is a separate deployment category, out of scope here). Making webjs run first-class on both Node and Bun is high value for low, bounded work.

Design / approach

Make the TS-stripping a pluggable seam with a default Node backend (module.stripTypeScriptTypes) and a runtime-agnostic backend usable on Bun.

Prior art to reference: Remix 3 (remix-the-web, cloned locally) ships packages/node-tsx, a --import remix/node-tsx loader whose only deps are oxc-transform (the WASM TS/JSX transform, position-preserving) and get-tsconfig. It runs the same source unbundled on Node and Bun (every package has a test:bun script). webjs should adopt the same shape: a WASM transform (oxc-transform, or amaro which is what Node wraps internally) behind the stripper seam, so the no-build dev loop is identical on both runtimes. TanStack's router-core package.json also models per-runtime conditional exports (node/bun/deno/workerd) if a conditional-entry approach is wanted later.

Touchpoints to audit/swap (mostly small):

  • TS stripper (the main one): introduce the seam, keep Node's built-in as default, add the WASM backend (used on Bun, or always for parity). Preserve byte-exact position mapping so stack traces stay correct (no shipped sourcemap).
  • node:crypto synchronous digests (asset-hash ?v= hashing): move the sync sha-256 to a portable path (Web Crypto crypto.subtle is async-only, so this may need an async seam or a tiny portable sync hash). Audit every digestHex call site.
  • fs.watch (recursive, dev live-reload): dev-only, so not a deploy blocker; gate it behind a capability check and use the runtime's watcher (Bun supports fs.watch).
  • module.registerHooks seed facade (feat: seed SSR action results into hydration so async render does not re-fetch (follow-up to #469) #472): Node-specific module customization; make it a no-op off-Node (it is a fail-open optimization, so disabling it degrades to a normal RPC, never wrong data).
  • createRequestHandler already returns a web-standard handle(req): Response, so the HTTP boundary is portable as-is; startServer (which owns node:http) stays the Node convenience, with a Bun-native serve path for webjs start / webjs dev.
  • assertNodeVersion: relax to a capability check (does a working stripper exist?) rather than a hard Node-major gate, so Bun is admitted.

Out of scope (separate later issue): Cloudflare Workers / edge runtimes, which have no filesystem and would need a deploy-time snapshot of the servable file set (in mild tension with the read-source-from-disk model). This issue is Node + Bun only, both of which have real filesystems.

Acceptance criteria

  • The TS stripper is a pluggable seam; Node uses the built-in, a WASM backend (oxc-transform/amaro) runs the same source on Bun, with byte-exact position preservation verified.
  • webjs create, webjs dev, and webjs start all work end-to-end on Bun (a real app boots, serves SSR HTML, hydrates, and a server action round-trips).
  • The Node path is unchanged (no regression; default backend stays the built-in stripper).
  • node-specific touchpoints audited and made portable or gated: the node:crypto sync digests, fs.watch (dev-only), the module.registerHooks seed facade (no-op off-Node), and the Node-version gate (relaxed to a capability check).
  • A Bun smoke test in CI (mirroring Remix 3's test:bun), plus the existing Node suite stays green.
  • Docs updated: AGENTS.md (drop/qualify the Node-24-only claim), agent-docs/deployment.md, and the scaffold so a new app documents running on Bun or Node.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions