SvelteKit Node server (via Node.js adapter)
This example builds a small TODO application with the official SvelteKit Node
adapter, then runs the same generated request handler directly in Node.js and as
a WebAssembly component using jco-std's Node API support. Both launchers accept
an explicit PORT, serve SvelteKit's SSR HTML and client assets, and execute the
same form actions. TODO state lives in memory and starts fresh with each server
process.
Warning
Jco's Node.js built-in compatibility for components is experimental and subject to change. APIs, behavior, and generated component interfaces may change incompatibly without a semver-major release.
The standard SvelteKit Node deployment flow starts when Vite builds the
application with @sveltejs/adapter-node. The adapter produces
build/handler.js, containing SvelteKit's SSR server, form-action routing, and
static asset middleware. Normally, an application imports that handler, passes
it to a node:http server, and starts listening.
When Jco builds the same server into a WebAssembly component, the application code stays the same, but the build output and Node API boundary change:
src/component.tsexposesstart(port)andstop(); the native launcher insrc/serve-node.tscalls them directly.- Jco componentizes that same server module and exports the lifecycle functions through the example's WIT world. Its bundler replaces Node imports with jco-std interfaces.
- Jco transpiles the StarlingMonkey component back to an instantiable ES
module.
src/run-transpiled.tssupplies the Node API and WASI hosts, starts the component, and listens on the requested port.
JavaScript compiled into a WebAssembly component does not normally have access to Node.js APIs. This example opts into jco-std's Node API built-in support so the component can use these ordinary Node APIs:
| API | Purpose |
|---|---|
node:http |
Listen for requests and run the adapter's handler. |
node:fs and node:path |
Index and serve the generated SvelteKit client assets. |
node:process |
Read the adapter's environment and current working directory. |
node:stream, node:buffer, and related portable APIs |
Bridge Node request bodies and Web responses. |
The runner in src/run-transpiled.ts supplies direct
Node HTTP, filesystem, and process providers plus preview2-shim's WASI imports.
The adapter dependency graph also imports node:tty through Node's stream
surface; the example provides a non-terminal host because the application
performs no terminal I/O but Node still initializes its standard streams.
The generated WIT world and dependencies are committed under wit.
pnpm run generate:types uses Jco to regenerate the strict TypeScript component
shape under generated/types. svelte-check verifies the
Svelte templates, route actions, server lifecycle, launcher, and e2e tests.
Use Node.js 24 or newer and pnpm. This example uses workspace packages because it exercises Node compatibility support from this checkout. From this example directory, install and build the tooling once:
pnpm install
pnpm --filter @bytecodealliance/jco run build
pnpm --filter @bytecodealliance/jco-std run build:ts
pnpm --filter @bytecodealliance/preview2-shim run build
pnpm run setup:browserThen build and test the complete example:
pnpm run allThe all script generates types, builds the SvelteKit application, performs
strict type checking, creates and transpiles component.wasm, and runs the same
HTTP assertions against native Node.js and the component. It then runs a
separate headless Puppeteer test against the component. Together the tests cover
SSR, generated CSS and JavaScript assets, add/toggle/remove actions, validation,
404s, process-local state, automatically allocated ports, and real browser
interaction.
Run the browser test with a visible Chrome window when debugging:
pnpm run test:browser:headfulAfter pnpm run all, start the component on the default port:
pnpm run serveOpen http://127.0.0.1:3000. Select another port with PORT:
PORT=4173 pnpm run serveTo compare with native Node.js, stop the component server and run the same adapter output directly:
PORT=4173 pnpm run serve:nodeStarlingMonkey does not yet support component-model async guest exports, so the
WIT world keeps start and stop synchronous. Jco's generated JSPI-aware
TypeScript bindings still let the Node runner await those calls and the async
HTTP callbacks.
jco-std's AsyncLocalStorage support is intentionally synchronous, while
SvelteKit uses async request-local storage. The component-only bundle config
selects SvelteKit's existing serialized-request fallback. It also bypasses
String.prototype.normalize for generated ASCII asset names because that
method is not currently available in StarlingMonkey.
The Node stream implementation does not yet model fs.createReadStream across
the component boundary. src/runtime.ts therefore serves
SvelteKit's generated /_app assets with readFileSync before handing all other
requests to the adapter. The component has access to the host filesystem so it
can read build/client; production-grade filesystem confinement,
authentication, and persistent TODO storage are outside this example's scope.
The component launcher uses Node's --experimental-wasm-jspi flag for async
component callbacks.
Wizer is the part of the componentization pipeline that runs the JavaScript module once at build time and snapshots its initialized engine state into the resulting Wasm. The future runtime's host capabilities are not attached during that build-time execution.
Loading node:http initializes host-backed Node state, and the Node adapter
reads process.env and indexes its generated client directory. The wrapper
therefore dynamically imports src/runtime.ts, which loads
node:http and the adapter entry point, inside start after the component
embedder has supplied HTTP, process, filesystem, and TTY capabilities.