diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a42c16781..07ac28ec1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,26 @@ jobs: npx prisma db seed - run: npm test + bun: + name: Bun runtime smoke (#508) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: '24' + cache: npm + # npm ci installs the workspace + the `amaro` optionalDependency that the + # Bun stripper backend needs. + - run: npm ci + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + # Boot a webjs app under Bun: SSR + TypeScript strip (via amaro, since Bun + # has no built-in stripper) + a server-action RPC round-trip. + - name: webjs runtime smoke on Bun + run: bun test/bun/smoke.mjs + browser: name: Browser (web-test-runner / Playwright) runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index dff746d8a..a48717cb0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,7 +78,7 @@ See `agent-docs/framework-dev.md` for monorepo commands, workspace layout, per-f An **AI-first, web-components-first** framework inspired by NextJs, Lit, and Rails. The component runtime API matches lit (reactive `static properties`, the lit lifecycle hooks, ReactiveControllers, the `lit-html` directive set, `html` / `css` templates) so lit training data transfers directly, but webjs ships its own no-build implementation under `packages/core/src/`. Decorators are the one lit exception (invariant 10); use `declare` + `static properties`. -- **No build step.** Source files are served as native ES modules. JSDoc `.js` is default; `.ts` / `.mts` is stripped via Node 24+'s `module.stripTypeScriptTypes` (invariant 10 + `agent-docs/typescript.md`). Node 24+ is required, enforced by an early `assertNodeVersion()` preflight. +- **No build step.** Source files are served as native ES modules. JSDoc `.js` is default; `.ts` / `.mts` is stripped through a pluggable stripper (#508): Node 24+'s built-in `module.stripTypeScriptTypes`, or `amaro` on Bun (byte-identical, position-preserving) (invariant 10 + `agent-docs/typescript.md`). **Runs on Node 24+ or Bun** (run a Bun app with `bun --bun run dev` / `start`); the early `assertNodeVersion()` preflight enforces the Node floor and admits Bun. Edge runtimes (no filesystem) are a separate, later target. - **SSR + CSR by default.** Pages are server-rendered HTML; components render light DOM by default, shadow DOM opt-in via `static shadow = true` with DSD SSR. - **Progressive enhancement is the default architecture.** Pages and components are SSR'd; with JS off, content reads, `` navigates, `
` server actions submit, display-only elements render. JS is opt-in *per interactive behaviour* (`@click`, a reactive property assignment, a signal mutation). Never write a first paint that depends on hydration; never use `fetch` + JS where a `` + server action would do. - **Display-only components are elided from the browser.** A component with no interactivity signal renders identical HTML with or without its JS, so the framework strips its import (and any vendor reachable only through it, importmap entry included) from the served source. Automatic, conservative, verified differentially. Disable with `"webjs": { "elide": false }` or `WEBJS_ELIDE=0`. See `agent-docs/components.md`. diff --git a/agent-docs/typescript.md b/agent-docs/typescript.md index 19ee2b8bd..1b196008d 100644 --- a/agent-docs/typescript.md +++ b/agent-docs/typescript.md @@ -6,12 +6,13 @@ run is part of the user-visible workflow, no separate build step: - **Editor** (VS Code) runs the TypeScript language server continuously. Red-squiggle on wrong types, including non-erasable syntax (see below). - **CI** (optional) runs `tsc --noEmit` against `tsconfig.json`. Type-check only. Also catches non-erasable syntax via `erasableSyntaxOnly`. -- **Dev + prod server** (runtime, both directions): Node 24+'s built-in TypeScript type-stripping handles server-side `.ts` imports automatically (`process.features.typescript === 'strip'`). Browser-bound `.ts` requests go through `module.stripTypeScriptTypes` on the dev server, which performs whitespace replacement: every (line, column) in the source maps to the same position in the stripped output, so no sourcemap needs to be shipped and stack traces are byte-exact. The transform is cached by mtime (~microseconds per cache hit). Implementation backing: Node ships the [`amaro`](https://github.com/nodejs/amaro) package internally, which wraps SWC's WASM TypeScript transform in a position-preserving strip-only mode. If the framework ever needs to run on a non-Node runtime (Bun, Deno) we will install `amaro` directly or an equivalent position-preserving stripper (Sucrase preserves lines but not columns; SWC's strip mode also works). +- **Dev + prod server** (runtime, both directions): the runtime's TypeScript type-stripping handles server-side `.ts` imports automatically (on Node, `process.features.typescript === 'strip'`; Bun runs `.ts` natively). Browser-bound `.ts` requests go through the pluggable stripper in `packages/server/src/ts-strip.js` (#508) on the dev server, which performs whitespace replacement: every (line, column) in the source maps to the same position in the stripped output, so no sourcemap needs to be shipped and stack traces are byte-exact. The transform is cached by mtime (~microseconds per cache hit). Implementation backing: on **Node 24+** it is the built-in `module.stripTypeScriptTypes` (which itself wraps the [`amaro`](https://github.com/nodejs/amaro) package, SWC's WASM TypeScript transform in position-preserving strip-only mode); on **Bun** (no such built-in) it is `amaro` loaded directly, an `optionalDependency` of `@webjsdev/server`, producing byte-identical output. `WEBJS_TS_STRIPPER=builtin|amaro` forces a backend. Edge runtimes without a filesystem are a separate, later target. ## TypeScript feature support: erasable only -The framework uses Node 24+'s built-in `module.stripTypeScriptTypes`, -which only supports **erasable TypeScript**: type annotations, +The framework strips with Node 24+'s built-in `module.stripTypeScriptTypes` +(or `amaro` on Bun, the same engine), which only supports **erasable +TypeScript**: type annotations, `interface`, `type`, `declare`, generics, `import type`, `as` casts, and `satisfies`. Non-erasable syntax is rejected. @@ -62,8 +63,8 @@ off so you catch the configuration drift before runtime. ## Import convention -Use explicit `.ts` extensions in imports. Node 24+'s built-in -type-stripping and the dev server's HTTP handler both key on the +Use explicit `.ts` extensions in imports. The runtime's type-stripping +and the dev server's HTTP handler both key on the file URL ending in `.ts` / `.mts`. For mixed codebases, `.js` imports that point at a `.ts` sibling also resolve in the dev server. Still prefer explicit `.ts`. diff --git a/docs/app/docs/ai-first/page.ts b/docs/app/docs/ai-first/page.ts index 3817adc9a..a9533bfdf 100644 --- a/docs/app/docs/ai-first/page.ts +++ b/docs/app/docs/ai-first/page.ts @@ -58,7 +58,7 @@ modules/posts/queries/get-post.server.ts → exports getPost()

4. No Build Step = What You See Is What Runs

Frameworks with build pipelines transform source code before it executes. The JSX you write becomes React.createElement calls. Your imports become webpack chunks. Your CSS modules get hashed classnames. An AI agent reading the source sees one thing, while the runtime does another.

-

webjs has no build step you run. The .ts file you see is the file that runs. .ts imports are stripped of types by Node 24+'s built-in module.stripTypeScriptTypes, which is whitespace replacement: every (line, column) in the source maps to the same position in the output, so stack traces stay byte-exact. The same transform runs server-side and on browser-bound requests. No intermediate representation, no generated code, no output directory. An AI agent can reason about what the code does by reading the file, because the file IS what runs. See No-Build Model for the full pipeline.

+

webjs has no build step you run. The .ts file you see is the file that runs. .ts imports are stripped of types by the runtime's stripper (Node 24+'s built-in module.stripTypeScriptTypes, or amaro on Bun), which is whitespace replacement: every (line, column) in the source maps to the same position in the output, so stack traces stay byte-exact. The same transform runs server-side and on browser-bound requests. No intermediate representation, no generated code, no output directory. An AI agent can reason about what the code does by reading the file, because the file IS what runs. See No-Build Model for the full pipeline.

5. Explicit Server Boundary

The .server.ts extension is a visible, greppable marker that says "this code runs only on the server." An AI agent never accidentally puts a database call in a component, because the naming convention prevents it. And the framework enforces it: .server.ts files are rewritten to RPC stubs for the browser.

diff --git a/docs/app/docs/deployment/page.ts b/docs/app/docs/deployment/page.ts index fd3197195..10380c537 100644 --- a/docs/app/docs/deployment/page.ts +++ b/docs/app/docs/deployment/page.ts @@ -5,7 +5,7 @@ export const metadata = { title: 'Deployment | webjs' }; export default function Deployment() { return html`

Deployment

-

webjs runs as a standard Node.js server. There is no static export, no serverless adapter, no edge runtime. Deploy it anywhere you can run Node 24+ (the minimum is set by Node's built-in TypeScript type-stripping): a VPS, a container, a PaaS like Fly.io or Railway, or behind a reverse proxy on bare metal.

+

webjs runs as a standard server on Node 24+ or Bun. There is no static export, no serverless adapter, and no edge runtime yet. Deploy it anywhere you can run Node or Bun: a VPS, a container, a PaaS like Fly.io or Railway, or behind a reverse proxy on bare metal. On Node the minimum is set by the built-in TypeScript type-stripping; on Bun the stripping comes from amaro automatically, so the same source runs on either.

Dev vs Prod

webjs has two modes, controlled by the npm script (which wraps the underlying webjs dev / webjs start CLI):

@@ -334,7 +334,7 @@ HEALTHCHECK CMD curl -f http://localhost:8080/__webjs/health || exit 1 CMD ["npx", "webjs", "start"]

Tips: