You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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).
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.
Problem
webjs is Node 24+ only today. The single hard Node binding is the TypeScript stripper:
dev.jsserves.ts/.mtsby calling Node 24+'s built-inmodule.stripTypeScriptTypes(assertNodeVersionenforces 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 viatest:bunscripts; 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) shipspackages/node-tsx, a--import remix/node-tsxloader whose only deps areoxc-transform(the WASM TS/JSX transform, position-preserving) andget-tsconfig. It runs the same source unbundled on Node and Bun (every package has atest:bunscript). webjs should adopt the same shape: a WASM transform (oxc-transform, oramarowhich is what Node wraps internally) behind the stripper seam, so the no-build dev loop is identical on both runtimes. TanStack'srouter-corepackage.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):
node:cryptosynchronous digests (asset-hash?v=hashing): move the sync sha-256 to a portable path (Web Cryptocrypto.subtleis async-only, so this may need an async seam or a tiny portable sync hash). Audit everydigestHexcall 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 supportsfs.watch).module.registerHooksseed 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).createRequestHandleralready returns a web-standardhandle(req): Response, so the HTTP boundary is portable as-is;startServer(which ownsnode:http) stays the Node convenience, with a Bun-native serve path forwebjs 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
webjs create,webjs dev, andwebjs startall work end-to-end on Bun (a real app boots, serves SSR HTML, hydrates, and a server action round-trips).node:cryptosync digests,fs.watch(dev-only), themodule.registerHooksseed facade (no-op off-Node), and the Node-version gate (relaxed to a capability check).test:bun), plus the existing Node suite stays green.agent-docs/deployment.md, and the scaffold so a new app documents running on Bun or Node.