diff --git a/.agents/skills/webjs-doc-sync/SKILL.md b/.agents/skills/webjs-doc-sync/SKILL.md index ba3b28b63..5eaaf0f51 100644 --- a/.agents/skills/webjs-doc-sync/SKILL.md +++ b/.agents/skills/webjs-doc-sync/SKILL.md @@ -16,7 +16,7 @@ when_to_use: | # Keep every webjs doc surface in sync with shipped behaviour -webjs ships its documentation across SEVERAL independent surfaces. The recurring +WebJs ships its documentation across SEVERAL independent surfaces. The recurring failure mode is updating ONE (usually `AGENTS.md`) and silently missing the rest, so the docs site, the marketing website, and the scaffold's per-agent rule files drift behind the framework. HTTP-verb server actions (#488) shipped with diff --git a/.claude/hooks/block-prose-punctuation.sh b/.claude/hooks/block-prose-punctuation.sh index c32e8afd7..46277cea7 100755 --- a/.claude/hooks/block-prose-punctuation.sh +++ b/.claude/hooks/block-prose-punctuation.sh @@ -233,4 +233,54 @@ EOF exit 2 fi +# --- 5. Lowercase "webjs" at a prose sentence start --------------------- +# The brand renders "WebJs" when it BEGINS a sentence and lowercase +# "webjs" everywhere else. This flags the lowercase form only at a +# sentence-initial PROSE position: after . ! ?, or at a prose line start +# (optionally behind markdown / comment markers and emphasis), or right +# after a prose HTML tag. A following space is required, so "webjs.dev", +# "webjs-suspense", "@webjsdev", and "webjsdev" never match. A "webjs" +# immediately followed by a CLI subcommand (webjs dev, webjs check, ...) +# is a command reference and is kept lowercase, so it is filtered out. +# Inline `code`, fenced blocks, and mid-sentence brand uses are not a +# sentence start and do not match. The rule biases toward false negatives +# (a missed case) over false positives (a wrongly blocked write), the +# same tradeoff as the pause rules above. +webjs_cli='create|dev|start|test|check|db|ui|doctor|types|typecheck|mcp|vendor|add|init|generate|migrate|push|studio|seed|pin|unpin|list|audit|outdated|update|view' + +webjs_hits=$(printf '%s\n' "$new_content" | grep -nE \ + -e '[.!?]["'"'"')]?[[:space:]]+(\*\*|__|\*|_|")?webjs(\*\*|__|\*|_|")?[[:space:]]' \ + -e '^[[:space:]]*((//|#{1,6}|>|[-*])[[:space:]]+)*(\*\*|__|\*|_|")?webjs(\*\*|__|\*|_|")?[[:space:]]' \ + -e '<(p|li|td|h[1-6]|strong|em|blockquote)[^>]*>[[:space:]]*(\*\*|__|\*|_|")?webjs(\*\*|__|\*|_|")?[[:space:]]' \ + 2>/dev/null || true) + +if [ -n "$webjs_hits" ]; then + # Drop hits whose "webjs" is a CLI subcommand reference (kept lowercase). + offending=$(printf '%s\n' "$webjs_hits" \ + | grep -vE "webjs[[:space:]]+(${webjs_cli})([[:space:]]|[.,:;)]|\$)" 2>/dev/null || true) + if [ -n "$offending" ]; then + cat >&2 <<'EOF' +BLOCKED: lowercase "webjs" at a prose sentence start. + +The brand is "WebJs" when it BEGINS a sentence and lowercase "webjs" +everywhere else. Capitalize this occurrence. + + Bad: webjs ships a cache() helper. + Good: WebJs ships a cache() helper. + Bad: ...round-trips. webjs rewrites the import. + Good: ...round-trips. WebJs rewrites the import. + +Still lowercase (NOT a sentence start, do NOT capitalize these): + - a CLI command: `webjs dev`, `webjs check`, `webjs create my-app` + - a domain / package / config / env: webjs.dev, @webjsdev, + "webjs": { ... }, WEBJS_PUBLIC_* + - mid-sentence: "Most webjs apps ship without a build step." + +Rule: AGENTS.md, Invariants section, item 11. +Hook: .claude/hooks/block-prose-punctuation.sh. +EOF + exit 2 + fi +fi + exit 0 diff --git a/.claude/skills/webjs-doc-sync/SKILL.md b/.claude/skills/webjs-doc-sync/SKILL.md index dccc8a0c5..381c16772 100644 --- a/.claude/skills/webjs-doc-sync/SKILL.md +++ b/.claude/skills/webjs-doc-sync/SKILL.md @@ -16,7 +16,7 @@ when_to_use: | # Keep every webjs doc surface in sync with shipped behaviour -webjs ships its documentation across SEVERAL independent surfaces. The recurring +WebJs ships its documentation across SEVERAL independent surfaces. The recurring failure mode is updating ONE (usually `AGENTS.md`) and silently missing the rest, so the docs site, the marketing website, and the scaffold's per-agent rule files drift behind the framework. HTTP-verb server actions (#488) shipped with diff --git a/AGENTS.md b/AGENTS.md index 8f9f413c5..684d0bf4a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,7 +32,7 @@ reference there. ## AI-driven development: guardrails for all agents -**webjs is AI-first. These rules apply to ALL agents (Claude, Cursor, Copilot, Antigravity, Aider), enforced via per-agent config the scaffold ships** (`AGENTS.md` + `CONVENTIONS.md` + `CLAUDE.md`, `.claude/settings.json` hooks, `.cursorrules`, `.agents/rules/workflow.md`, `.github/copilot-instructions.md`, a PR template, `.editorconfig`), all carrying the same rules in each agent's format. +**WebJs is AI-first. These rules apply to ALL agents (Claude, Cursor, Copilot, Antigravity, Aider), enforced via per-agent config the scaffold ships** (`AGENTS.md` + `CONVENTIONS.md` + `CLAUDE.md`, `.claude/settings.json` hooks, `.cursorrules`, `.agents/rules/workflow.md`, `.github/copilot-instructions.md`, a PR template, `.editorconfig`), all carrying the same rules in each agent's format. ### Before starting ANY work: verify and sync the branch @@ -44,7 +44,7 @@ Claude Code enforces step 1 via `.claude/hooks/guard-branch-context.sh`. Other a ### One task per git worktree when agents run concurrently -webjs is worked by MULTIPLE agents at once. If more than one agent (or more than one in-flight task) shares ONE working checkout, they collide: a `git checkout` in one moves `HEAD` under the other, so the next commit lands on the WRONG branch. This has happened (a `chore: release` commit landed on an unrelated `feat/` branch, with a contaminated changelog). So give each task its own worktree: +WebJs is worked by MULTIPLE agents at once. If more than one agent (or more than one in-flight task) shares ONE working checkout, they collide: a `git checkout` in one moves `HEAD` under the other, so the next commit lands on the WRONG branch. This has happened (a `chore: release` commit landed on an unrelated `feat/` branch, with a contaminated changelog). So give each task its own worktree: ```sh git worktree add -b / ../- origin/main @@ -109,7 +109,7 @@ An **AI-first, web-components-first** framework inspired by NextJs, Lit, and Rai ## Execution model (read this to avoid the RSC mental model) -webjs has **no server/client component split.** There is no RSC render tree, no Flight protocol, no "use client" / "use server" component boundary. +WebJs has **no server/client component split.** There is no RSC render tree, no Flight protocol, no "use client" / "use server" component boundary. **Pages, layouts, and components are isomorphic modules** (same source both sides), but hydrate differently: @@ -440,7 +440,7 @@ const result = await optimistic(liked, true, () => likePost(postId)); 9. **No backtick characters inside `html\`...\`` template bodies**, even inside CSS / HTML comments. A nested backtick closes the literal at JS-parse time and 500s in prod. 10. **TypeScript must be erasable.** Set `compilerOptions.erasableSyntaxOnly: true`. No `enum`, no value `namespace`, no constructor parameter properties, no legacy decorators with `emitDecoratorMetadata`, no `import = require`. Types are stripped via Node 24+'s `module.stripTypeScriptTypes` (buildless, no bundler fallback); non-erasable syntax 500s at strip time. Enforced by `erasable-typescript-only` (tsconfig flag) and `no-non-erasable-typescript` (source scan). See `agent-docs/typescript.md`. -11. **No em-dashes (U+2014), no hyphen or semicolon used as pause-punctuation in prose, and no colon attached to a code-shaped LHS.** Banned as a pause: U+2014, a space-surrounded hyphen between words, a space-surrounded semicolon between words. Banned colon attachments: a colon-then-prose after `xyz()`, a ``, an `[expr]` subscript, or a `foo()` definition list (rephrase verb-led). Prefer a period, comma, a colon on a plain-noun LHS, parentheses, or a restructure. Plain hyphens stay fine in compound words, flags, filenames, ranges; semicolons and colons stay fine inside code / TS / JSON / CSS. Enforced via `.claude/hooks/block-prose-punctuation.sh`, which scans only NEW content (you can still edit an existing line to remove a glyph). +11. **No em-dashes (U+2014), no hyphen or semicolon used as pause-punctuation in prose, and no colon attached to a code-shaped LHS.** Banned as a pause: U+2014, a space-surrounded hyphen between words, a space-surrounded semicolon between words. Banned colon attachments: a colon-then-prose after `xyz()`, a ``, an `[expr]` subscript, or a `foo()` definition list (rephrase verb-led). Prefer a period, comma, a colon on a plain-noun LHS, parentheses, or a restructure. Plain hyphens stay fine in compound words, flags, filenames, ranges; semicolons and colons stay fine inside code / TS / JSON / CSS. The same hook also enforces brand casing: write `WebJs` when the brand BEGINS a sentence and lowercase `webjs` everywhere else (a CLI command like `webjs dev`, a `webjs.dev` domain, an `@webjsdev` package, a `webjs.*` config key, a `WEBJS_*` env var, and any mid-sentence mention all stay lowercase). Enforced via `.claude/hooks/block-prose-punctuation.sh`, which scans only NEW content (you can still edit an existing line to remove a glyph or fix casing). --- diff --git a/README.md b/README.md index 6e1d7dd7d..acee9bb83 100644 --- a/README.md +++ b/README.md @@ -271,7 +271,7 @@ Pre-1.0. Current packages: `@webjsdev/core` 0.7.1, `@webjsdev/server` 0.7.2, `@w - **Core:** Signals (`signal`, `computed`, `effect`, `batch`, TC39 Stage 1 shape) as the default state primitive, with WebComponent's built-in SignalWatcher auto-tracking `.get()` reads inside `render()`. Reactive properties via the declare-free base-class factory `extends WebComponent({ count: Number })` (the `prop()` helper carries options like `reflect` / `state` / `attribute` / `default`), reserved for HTML attribute round-trip (a direct `static properties` block throws at runtime, flagged by the `no-static-properties` rule, and a class-field initializer on a factory prop is caught by `reactive-props-no-class-field`). Full lit-API parity: ReactiveController hooks (`hostConnected`, `hostDisconnected`, `hostUpdate`, `hostUpdated`) and lifecycle (`shouldUpdate`, `willUpdate`, `update`, `updated`, `firstUpdated`, `updateComplete`), 12 directives (`repeat`, `unsafeHTML`, `live`, `keyed`, `guard`, `templateContent`, `ref` + `createRef`, `cache`, `until`, `asyncAppend`, `asyncReplace`, `watch`). SSR with DSD (opt-in) + light-DOM hydration (default), light-DOM `` projection (framework-driven, same API as shadow DOM), fine-grained client renderer, `Suspense()`, client router with `composedPath()` for shadow DOM, mixed-attribute interpolation, MutationObserver upgrade safety net. - **Data:** Server actions with webjs's built-in serializer (`Date`, `Map`, `Set`, `BigInt`, `TypedArray`, `Blob`, `File`, `FormData`, reference cycles all survive the wire). Two-marker server-file convention: `.server.{js,ts}` for path-level source-protection (browser imports get a throw-at-load stub), `'use server'` for RPC registration (file is also browser-callable). REST over HTTP via a `route.ts` (or the `route()` adapter) with an optional `validate` config export. `json()` + `richFetch()` for content-negotiated APIs. `cache()` for server-side query caching with TTL + `invalidate()`. `WEBJS_PUBLIC_*` env vars injected into `window.process.env` at SSR (no build step, no transform). - **Server:** File router with `page.ts`, `layout.ts`, `route.ts`, `error.ts`, `loading.ts`, `not-found.ts`, `middleware.ts`, metadata routes (`sitemap`, `robots`, `manifest`, `icon`, `opengraph-image`), per-segment middleware, `rateLimit()`, WebSockets (`WS` export + `connectWS()` + `broadcast()`), CSRF, gzip / brotli compression, HTTP/2, 103 Early Hints, modulepreload hints, health probes, graceful shutdown on `SIGTERM`, `Session` class with `SessionStorage` (cookie or store-backed), NextAuth-style `createAuth()` (Credentials, Google, GitHub), single pluggable cache store (in-memory by default, swap to Redis with one `setStore()` call shared by auth, sessions, caching, and rate limiting). -- **DX:** Node 24+ or Bun runtime (run a Bun app with `bun --bun run dev` / `start`, and the CLI hot-reloads via `node --watch` on Node and `bun --hot` on Bun), with the dev server stripping TypeScript via Node's built-in `module.stripTypeScriptTypes` (or `amaro` on Bun, byte-identical), zero build, position-preserving, no sourcemap. Non-erasable TS (enums, value-carrying namespaces, constructor parameter properties, legacy decorators) fails with a 500 pointing at the `no-non-erasable-typescript` lint rule. webjs is buildless end-to-end and has no bundler fallback. Vendor (`node_modules`) packages resolve through importmap to jspm.io URLs at runtime; the webjs server doesn't bundle them. `webjs vendor pin` writes resolved URLs to `.webjs/vendor/importmap.json` for deterministic deploys; `webjs vendor pin --download` additionally vendors bundle bytes for offline-capable production. `webjs check` lint covers `use-server-needs-extension`, `no-server-env-in-components`, `no-static-properties`, `reactive-props-no-class-field`, `erasable-typescript-only`, `no-non-erasable-typescript`, `shell-in-non-root-layout`, and more (run `webjs check --rules` to enumerate). `AGENTS.md` contract + `CLAUDE.md` + per-tool agent configs (`.cursorrules`, `.agents/rules/workflow.md` for Antigravity, `.github/copilot-instructions.md`, `.claude/settings.json` PreToolUse hook guarding edits on `main`). Live reload in dev (`fs.watch` + SSE). `@webjsdev/intellisense` is the standalone editor-only piece (no Lit dependency): its own `` html`…` `` template parser drives go-to-definition on tags / attributes / CSS classes, binding-aware completions, value/binding diagnostics, and hover, all gated by the file's import graph. The `webjs` VS Code / Cursor / Windsurf extension bundles it. Not required for the framework to run. +- **DX:** Node 24+ or Bun runtime (run a Bun app with `bun --bun run dev` / `start`, and the CLI hot-reloads via `node --watch` on Node and `bun --hot` on Bun), with the dev server stripping TypeScript via Node's built-in `module.stripTypeScriptTypes` (or `amaro` on Bun, byte-identical), zero build, position-preserving, no sourcemap. Non-erasable TS (enums, value-carrying namespaces, constructor parameter properties, legacy decorators) fails with a 500 pointing at the `no-non-erasable-typescript` lint rule. WebJs is buildless end-to-end and has no bundler fallback. Vendor (`node_modules`) packages resolve through importmap to jspm.io URLs at runtime; the webjs server doesn't bundle them. `webjs vendor pin` writes resolved URLs to `.webjs/vendor/importmap.json` for deterministic deploys; `webjs vendor pin --download` additionally vendors bundle bytes for offline-capable production. `webjs check` lint covers `use-server-needs-extension`, `no-server-env-in-components`, `no-static-properties`, `reactive-props-no-class-field`, `erasable-typescript-only`, `no-non-erasable-typescript`, `shell-in-non-root-layout`, and more (run `webjs check --rules` to enumerate). `AGENTS.md` contract + `CLAUDE.md` + per-tool agent configs (`.cursorrules`, `.agents/rules/workflow.md` for Antigravity, `.github/copilot-instructions.md`, `.claude/settings.json` PreToolUse hook guarding edits on `main`). Live reload in dev (`fs.watch` + SSE). `@webjsdev/intellisense` is the standalone editor-only piece (no Lit dependency): its own `` html`…` `` template parser drives go-to-definition on tags / attributes / CSS classes, binding-aware completions, value/binding diagnostics, and hover, all gated by the file's import graph. The `webjs` VS Code / Cursor / Windsurf extension bundles it. Not required for the framework to run. - **Release:** Per-package per-version changelog under `changelog//.md`, auto-generated on the same commit that bumps a `package.json` `version` field (universal pre-commit hook). The `.github/workflows/release.yml` workflow watches for new changelog files on `main` and dual-publishes to npm (`npm publish --workspace=@webjsdev/`) and GitHub Releases (`gh release create @`), both idempotent so re-runs pick up where they left off. Free for public repos via `NPM_TOKEN` + the auto-provisioned `GITHUB_TOKEN`. ## License diff --git a/agent-docs/advanced.md b/agent-docs/advanced.md index 291957d78..2a29af210 100644 --- a/agent-docs/advanced.md +++ b/agent-docs/advanced.md @@ -108,7 +108,7 @@ Five stacked zero-build optimizations: ## No-build production model -webjs has no bundler and no `webjs build` step. The same `.js` / `.ts` +WebJs has no bundler and no `webjs build` step. The same `.js` / `.ts` source files that run in dev are served as-is to the browser in production. The Rails 7+ / Hotwire pattern: @@ -139,7 +139,7 @@ production. The Rails 7+ / Hotwire pattern: npm **vendor** URLs those shipped modules reach (#754). The browser parallelizes fetches instead of waterfalling through nested imports. This is what closes most of the perceived gap vs a bundle. **The honest caveat:** - a bundle still wins on a DEEP vendor tree. webjs flattens the FIRST level + a bundle still wins on a DEEP vendor tree. WebJs flattens the FIRST level (the vendor entries your code imports are hinted up front, with their SRI `integrity`, byte-identical to the importmap target so there is no double fetch), but a vendor's OWN transitive deps are still discovered by parsing diff --git a/agent-docs/built-ins.md b/agent-docs/built-ins.md index d419e9547..ae9d3d964 100644 --- a/agent-docs/built-ins.md +++ b/agent-docs/built-ins.md @@ -6,7 +6,7 @@ Opinionated defaults: **set `REDIS_URL` and everything scales.** ## Caching (HTTP standards, Remix-style) -webjs uses standard HTTP caching via `Cache-Control` on responses. Let +WebJs uses standard HTTP caching via `Cache-Control` on responses. Let browsers, CDNs, and reverse proxies handle caching. No framework cache layer to debug. @@ -301,7 +301,7 @@ Both are opt-in and types-only. See `agent-docs/typescript.md` and the auth reci ## File storage (`FileStore` + `diskStore`) -webjs round-trips a native `File` / `Blob` / `FormData` over the wire, and the file-storage primitive decides WHERE the bytes land. The model mirrors the cache / session adapters: a documented `FileStore` interface, a default local-disk adapter (`diskStore`), and a module singleton (`setFileStore` / `getFileStore`) so an app swaps the backend in one call without touching any call site. +WebJs round-trips a native `File` / `Blob` / `FormData` over the wire, and the file-storage primitive decides WHERE the bytes land. The model mirrors the cache / session adapters: a documented `FileStore` interface, a default local-disk adapter (`diskStore`), and a module singleton (`setFileStore` / `getFileStore`) so an app swaps the backend in one call without touching any call site. ```js import { getFileStore, generateKey, signedUrl, verifySignedUrl } from '@webjsdev/server'; @@ -360,7 +360,7 @@ The content-type a store records is the one the BROWSER sent at upload time, so ### S3-pluggability (call-site stability) -The interface operates on web-standard objects only, so an S3 / R2 / GCS / MinIO adapter is a drop-in: it implements the same `put` (PutObject, streaming the body), `get` (GetObject, returning the SDK's response stream as `body`), `delete` (DeleteObject), and `url` (the object / CDN URL). Because the shape is identical, `setFileStore(s3Store({ ... }))` switches the whole app with no call-site change. webjs ships no S3 SDK (no new dependency); the adapter is a thin wrapper an app provides. +The interface operates on web-standard objects only, so an S3 / R2 / GCS / MinIO adapter is a drop-in: it implements the same `put` (PutObject, streaming the body), `get` (GetObject, returning the SDK's response stream as `body`), `delete` (DeleteObject), and `url` (the object / CDN URL). Because the shape is identical, `setFileStore(s3Store({ ... }))` switches the whole app with no call-site change. WebJs ships no S3 SDK (no new dependency); the adapter is a thin wrapper an app provides. See the "Receive and persist an uploaded file" recipe in `agent-docs/recipes.md` for the no-JS `
` upload + serving route end to end. diff --git a/agent-docs/components.md b/agent-docs/components.md index 01ce6b95c..6605cdef7 100644 --- a/agent-docs/components.md +++ b/agent-docs/components.md @@ -189,7 +189,7 @@ Decision: a bare `async render()` for request-time data that should be in the fi ## Display-only components are elided from the browser A component that does no client-side work renders the same SSR'd HTML -whether or not its JavaScript ever reaches the browser. webjs detects +whether or not its JavaScript ever reaches the browser. WebJs detects these statically and strips their import from the served source, so the browser never downloads them (and their unique vendor dependencies drop from the importmap). This is automatic, with no opt-in keyword and no @@ -517,7 +517,7 @@ resolution). See the "Slots" section below. ## Slots: full shadow-DOM parity in both DOM modes -webjs supports the entire shadow-DOM `` surface in light DOM. The +WebJs supports the entire shadow-DOM `` surface in light DOM. The same `render()` template projects children identically whether your component declares `static shadow = true` or leaves it at the default `false`. Migrating between modes never requires a template rewrite. diff --git a/agent-docs/configuration.md b/agent-docs/configuration.md index 9da3e49ef..d9af60be7 100644 --- a/agent-docs/configuration.md +++ b/agent-docs/configuration.md @@ -59,7 +59,7 @@ CSP is OFF by default and opt-in via a `webjs.csp` key in `package.json`. When e ## Declarative redirects: `webjs.redirects` in package.json (#254) -webjs already has `redirect(url)` (an imperative, request-time throw sentinel). For a MOVED URL (old-path -> new-path), SEO wants a DECLARATIVE permanent redirect so link equity transfers and search engines update their index. Declare those under `package.json` `"webjs": { "redirects": [...] }`, an array of `{ source, destination, permanent?, statusCode? }`, cohesive with the `webjs.headers` config: +WebJs already has `redirect(url)` (an imperative, request-time throw sentinel). For a MOVED URL (old-path -> new-path), SEO wants a DECLARATIVE permanent redirect so link equity transfers and search engines update their index. Declare those under `package.json` `"webjs": { "redirects": [...] }`, an array of `{ source, destination, permanent?, statusCode? }`, cohesive with the `webjs.headers` config: ```jsonc { diff --git a/agent-docs/framework-dev.md b/agent-docs/framework-dev.md index 3f9ee672f..5ef708bc2 100644 --- a/agent-docs/framework-dev.md +++ b/agent-docs/framework-dev.md @@ -40,7 +40,7 @@ The fix only repairs the LOCAL checkout. Commits and branches are always safe on ### Changelog: per-package, per-version, auto-generated -webjs ships per-package per-version changelogs under `changelog//.md`. The model: **a version bump is the trigger**. When any commit on `main` changes the `version` field in `packages//package.json`, the scripts/backfill-changelog.js generator emits a new `changelog//.md` summarising every conventional-commit (`feat:` / `fix:` / `breaking:` / `perf:`) that landed in that package since the prior bump. The website renders the union of all packages' files at `/changelog`. +WebJs ships per-package per-version changelogs under `changelog//.md`. The model: **a version bump is the trigger**. When any commit on `main` changes the `version` field in `packages//package.json`, the scripts/backfill-changelog.js generator emits a new `changelog//.md` summarising every conventional-commit (`feat:` / `fix:` / `breaking:` / `perf:`) that landed in that package since the prior bump. The website renders the union of all packages' files at `/changelog`. **How it works for AI agents and humans:** diff --git a/agent-docs/lit-muscle-memory-gotchas.md b/agent-docs/lit-muscle-memory-gotchas.md index b19b52b16..dcb8bf2c6 100644 --- a/agent-docs/lit-muscle-memory-gotchas.md +++ b/agent-docs/lit-muscle-memory-gotchas.md @@ -66,7 +66,7 @@ Practical consequences for agents writing webjs code. ## Use lit idioms, not vanilla DOM (the whole point of lit-style components) -webjs components are lit-shaped on purpose: the value is the declarative +WebJs components are lit-shaped on purpose: the value is the declarative DX (typed reactive props, signals, `html` templates, declarative bindings), not raw DOM scripting. Reaching for vanilla web-component muscle memory (`this.getAttribute`, `this.setAttribute`, `this.classList`, @@ -187,7 +187,7 @@ in the page function OR in the component itself with an `async render()` ### 2b. Lit has no async render; webjs does (#469) Lit keeps `render()` synchronous (its async-SSR work signals a promise the -renderer awaits BEFORE a still-sync render). webjs lets `render()` itself be +renderer awaits BEFORE a still-sync render). WebJs lets `render()` itself be `async`, so you write `const u = await getUser(this.id)` directly in the component and SSR bakes the resolved data into the first paint. Three things trip up Lit muscle memory: @@ -478,7 +478,7 @@ the `no-interpolation-in-raw-text-element` check scopes its flag. ### 13. A GET server action's first client call returns the SSR seed, not a fresh read -webjs seeds each GET-action result rendered during SSR into the page, and the +WebJs seeds each GET-action result rendered during SSR into the page, and the generated RPC stub reads that seed on its FIRST client call instead of hitting the network (#472). This kills the redundant on-hydration refetch. The muscle memory that bites: reaching for a re-call of the same query to REFRESH after a diff --git a/agent-docs/metadata.md b/agent-docs/metadata.md index 8881ddc7b..c22f7ece3 100644 --- a/agent-docs/metadata.md +++ b/agent-docs/metadata.md @@ -6,7 +6,7 @@ nested layouts (deeper wins). Surface is Next.js-compatible. ## Type it with `Metadata` -webjs exports a `Metadata` type covering every field below, so a typo +WebJs exports a `Metadata` type covering every field below, so a typo (`titel`, `descripton`), wrong nesting, or a wrong-typed value (`themeColor: 123`) is a tsserver / checkJs error instead of a silently dropped tag. Import it from `@webjsdev/core` (the same isomorphic surface diff --git a/agent-docs/nextjs-muscle-memory-gotchas.md b/agent-docs/nextjs-muscle-memory-gotchas.md index 18f5e865a..9deff10a5 100644 --- a/agent-docs/nextjs-muscle-memory-gotchas.md +++ b/agent-docs/nextjs-muscle-memory-gotchas.md @@ -156,7 +156,7 @@ check deeper in), each middleware calling `next()` to continue. ## 10. Server-only code: the `.server.ts` boundary, not a `server-only` package Next uses the `server-only` package to poison a module imported client-side. -webjs uses the file extension: `*.server.ts` is the path-level boundary (the file +WebJs uses the file extension: `*.server.ts` is the path-level boundary (the file router refuses to serve the source). A `'use server'` file's exports are RPC-callable; a `.server.ts` file WITHOUT `'use server'` is a server-only utility whose browser import throws at load. Never import a no-`'use server'` `.server.ts` diff --git a/agent-docs/runtime.md b/agent-docs/runtime.md index 8a1023608..9645596cf 100644 --- a/agent-docs/runtime.md +++ b/agent-docs/runtime.md @@ -1,6 +1,6 @@ # Runtime: Node and Bun (and future Deno) -webjs runs on **Node 24+** or **Bun**. The app source is identical on either; only +WebJs runs on **Node 24+** or **Bun**. The app source is identical on either; only the listener shell, the type stripper, and a few built-ins differ. The selection is a runtime-neutral seam in `startServer` (it picks the `node:http` shell on Node and the `Bun.serve` shell on Bun), designed to also host a future diff --git a/agent-docs/service-worker.md b/agent-docs/service-worker.md index e192c2ef5..106e0fb30 100644 --- a/agent-docs/service-worker.md +++ b/agent-docs/service-worker.md @@ -1,6 +1,6 @@ # Service worker / offline primitive (opt-in, #271) -webjs ships a hand-authored service worker and an offline fallback into the UI +WebJs ships a hand-authored service worker and an offline fallback into the UI scaffolds (`public/sw.js`, `public/offline.html`; the full-stack and saas templates, since the api template has no UI). It adds an offline experience and an asset cache **without changing the JavaScript-disabled baseline**: the diff --git a/agent-docs/testing.md b/agent-docs/testing.md index 8a18237d6..5412834bf 100644 --- a/agent-docs/testing.md +++ b/agent-docs/testing.md @@ -149,7 +149,7 @@ each app's OWN `webjs test` script, not the root runner. ### The Bun test matrix (#509) -webjs runs on Node 24+ or Bun (#508). The **Node suite (`npm test`) is the +WebJs runs on Node 24+ or Bun (#508). The **Node suite (`npm test`) is the source of truth**; a separate, additive **Bun matrix** re-runs the runtime-sensitive suite under Bun to catch the long tail of cross-runtime incompatibilities (a `node:*` API Bun implements differently, a crypto/stream diff --git a/agent-docs/types-and-mutations.md b/agent-docs/types-and-mutations.md index 2bfd85636..2a28c5a7c 100644 --- a/agent-docs/types-and-mutations.md +++ b/agent-docs/types-and-mutations.md @@ -19,7 +19,7 @@ the server module and trips the `no-server-import-in-browser-module` check. ## Why a value import breaks and a type import does not -webjs strips TypeScript at runtime with no bundler. The stripper erases every +WebJs strips TypeScript at runtime with no bundler. The stripper erases every `import type` and every type-only specifier, so those imports never exist in the served JavaScript. A value import survives stripping and becomes a real browser `import`. When the target is a `.server.ts` module, its browser stub throws at diff --git a/agent-docs/typescript.md b/agent-docs/typescript.md index d301f672b..9bf5fbbad 100644 --- a/agent-docs/typescript.md +++ b/agent-docs/typescript.md @@ -52,7 +52,7 @@ set. Run `webjs check` to confirm. If a third-party package ships `.ts` source using non-erasable syntax (rare; most npm packages publish compiled `.js`), the dev server fails at strip time and returns a 500 naming the file and -pointing at the `no-non-erasable-typescript` lint rule. webjs is +pointing at the `no-non-erasable-typescript` lint rule. WebJs is buildless end-to-end and has no bundler fallback. Your own code never hits this as long as `erasableSyntaxOnly` is set. @@ -134,7 +134,7 @@ gone (matches React Server Actions). The class-instance / function caveat above is a silent runtime surprise: a method-valued field or a function argument typechecks fine but vanishes on the wire. The opt-in `SerializableActionFn` annotation turns it into a compile-time -error. webjs actions stay plain `export async function`s (the framework rewrites +error. WebJs actions stay plain `export async function`s (the framework rewrites the client import to an RPC stub at runtime, it does not wrap the authored function), so the guard is an OPTIONAL type annotation the author applies when they want it: diff --git a/blog/server-side-caching-explained.md b/blog/server-side-caching-explained.md index 1d97d860b..e48a93977 100644 --- a/blog/server-side-caching-explained.md +++ b/blog/server-side-caching-explained.md @@ -11,7 +11,7 @@ Here is a pattern I see in almost every new app. A page loads a list of posts. E Caching is the fix. A cache stores a computed or fetched result so you can skip the work next time. The hard part was never storing the value. The hard part is knowing when the stored value is stale and throwing it away at the right moment. That second half is called invalidation, and it is where most caching bugs live. -webjs gives you a few caching layers, each aimed at a different kind of "same work repeated." This post walks through all of them from scratch, in plain language, and is careful about the one safety rule that matters most (never cache one user's data where another user can see it). +WebJs gives you a few caching layers, each aimed at a different kind of "same work repeated." This post walks through all of them from scratch, in plain language, and is careful about the one safety rule that matters most (never cache one user's data where another user can see it). # Layer 1: HTTP Cache-Control on responses @@ -34,7 +34,7 @@ export async function GET() { # Layer 2: cache() for query results, with tags -The HTTP header caches a whole response. Often you want to cache one expensive thing inside the request instead, like a database query. webjs ships a `cache()` helper for that. You wrap an async function, give it a key, and give it a time-to-live in seconds. +The HTTP header caches a whole response. Often you want to cache one expensive thing inside the request instead, like a database query. WebJs ships a `cache()` helper for that. You wrap an async function, give it a key, and give it a time-to-live in seconds. ```ts // modules/posts/queries/list-posts.server.ts @@ -95,13 +95,13 @@ Evict it early on a write with `revalidatePath('/blog')` from a server action. T Read this part twice. `export const revalidate` is you asserting "this page is the same for every visitor for N seconds." The HTML cache is keyed by the full URL only, with no per-user keying. So a page that reads `cookies()`, a session, or any per-user data MUST NOT set `revalidate`. If it does, the first visitor's rendered HTML can be served to the next visitor. A wrongly-cached per-user page is a data leak, plain and simple. -webjs backs this with a framework defense. When your render reads per-user state through a framework helper (`cookies()`, `headers()`, `getSession()`, or `auth()`), the framework auto-marks the request dynamic and refuses to cache it even if you set `revalidate`, warning you once. So a dashboard page that does `const session = await auth()` fails safe. The loud caveat: this only catches reads THROUGH those helpers. If you read an auth cookie raw instead of via `cookies()`, the page can still be cached wrongly. The rule holds regardless. Read per-user state through the helpers, or never set `revalidate` on a per-user page. +WebJs backs this with a framework defense. When your render reads per-user state through a framework helper (`cookies()`, `headers()`, `getSession()`, or `auth()`), the framework auto-marks the request dynamic and refuses to cache it even if you set `revalidate`, warning you once. So a dashboard page that does `const session = await auth()` fails safe. The loud caveat: this only catches reads THROUGH those helpers. If you read an auth cookie raw instead of via `cookies()`, the page can still be cached wrongly. The rule holds regardless. Read per-user state through the helpers, or never set `revalidate` on a per-user page. # Layer 4: conditional GET (ETags and 304s) The layers above avoid recomputing. This last one avoids re-sending bytes the client already has. -An ETag is a short fingerprint of a response, like a version stamp. webjs puts one on every cacheable response. The browser stores it, and on the next request it sends it back in an `If-None-Match` header, effectively asking "do you still have exactly this?" If the fingerprint still matches, the server replies `304 Not Modified` with an empty body, meaning "unchanged, reuse what you have." A tiny 304 instead of the full payload. +An ETag is a short fingerprint of a response, like a version stamp. WebJs puts one on every cacheable response. The browser stores it, and on the next request it sends it back in an `If-None-Match` header, effectively asking "do you still have exactly this?" If the fingerprint still matches, the server replies `304 Not Modified` with an empty body, meaning "unchanged, reuse what you have." A tiny 304 instead of the full payload. This is on by default and needs no code. It covers cacheable SSR pages, static assets in `public/`, your app's source modules, and the core and vendor runtime uniformly. The ETag is the hash of the response body, so an identical body always produces an identical ETag. diff --git a/docs/app/docs/ai-first/page.ts b/docs/app/docs/ai-first/page.ts index f405c261f..c9919226f 100644 --- a/docs/app/docs/ai-first/page.ts +++ b/docs/app/docs/ai-first/page.ts @@ -5,7 +5,7 @@ export const metadata = { title: 'AI-First Development | webjs' }; export default function AIFirst() { return html`

AI-First Development

-

webjs is designed from the ground up to be the framework AI agents can read, write, and ship. Every architectural decision (from the file layout to the naming conventions to the one-function-per-file rule) was made with one question in mind: can an LLM understand this without loading the entire codebase into context?

+

WebJs is designed from the ground up to be the framework AI agents can read, write, and ship. Every architectural decision (from the file layout to the naming conventions to the one-function-per-file rule) was made with one question in mind: can an LLM understand this without loading the entire codebase into context?

Why AI-First Matters

Modern AI coding assistants (Claude Code, GitHub Copilot, Cursor, Antigravity, etc.) are increasingly writing production code. But most frameworks were designed for humans who hold the whole project in their head. They rely on:

@@ -81,7 +81,7 @@ modules/posts/queries/get-post.server.ts → exports getPost()

5. 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 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.

+

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.

6. 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.

@@ -102,7 +102,7 @@ export async function createPost(

Zero indirection. Zero codegen. Zero schema drift.

8. JSDoc or TypeScript: Agent's Choice

-

Some AI agents work better with TypeScript, others prefer JSDoc. webjs supports both equally. The type-checking story is identical either way, since the TS language server reads both. An agent can generate whichever format it's more fluent in.

+

Some AI agents work better with TypeScript, others prefer JSDoc. WebJs supports both equally. The type-checking story is identical either way, since the TS language server reads both. An agent can generate whichever format it's more fluent in.

9. Cross-Agent Config Files

webjs create scaffolds guardrail config files for every major AI coding agent:

@@ -122,7 +122,7 @@ export async function createPost(

In a webjs project, the user never has to say "also write tests" or "also update the docs." Agents do this automatically with every code change. The convention is enforced via CONVENTIONS.md, webjs test, and webjs check. For an agent lint-and-fix loop, run webjs check --json: the structured output lets the agent parse violations, fix them, and re-run until the report is clean (the same correctness data the MCP check tool returns).

12. Scaffold + Persistence Defaults

-

When a layman user says "create a todo app with webjs", the agent should produce a real full-stack app with a real database, not a JSON-file simulation. webjs enforces this with three guardrails:

+

When a layman user says "create a todo app with webjs", the agent should produce a real full-stack app with a real database, not a JSON-file simulation. WebJs enforces this with three guardrails:

  • Exactly three scaffolds. webjs create <name> (full-stack default), --template api, --template saas. The CLI rejects any other --template value, so an agent can't hallucinate --template todo or --template blog.
  • Drizzle + SQLite wired up by default. Every scaffold ships db/schema.server.ts, db/columns.server.ts, db/connection.server.ts (exports db), the webjs.dev.before + webjs.start.before steps running webjs db migrate (idempotent, so a db:generate'd migration applies on the next boot), and npm run db:generate / db:migrate / db:studio / db:seed. The agent edits the schema, runs db:generate, and npm run dev applies it, and won't accidentally fall back to JSON files for persistence.
  • diff --git a/docs/app/docs/api-routes/page.ts b/docs/app/docs/api-routes/page.ts index a235bff5d..5782c1823 100644 --- a/docs/app/docs/api-routes/page.ts +++ b/docs/app/docs/api-routes/page.ts @@ -97,7 +97,7 @@ export async function POST(req: Request) {

    json() Helper: Content Negotiation

    The json() helper from @webjsdev/server adds smart content negotiation. It inspects the incoming request's Accept header and responds accordingly:

      -
    • If the client sent Accept: application/vnd.webjs+json (e.g. via richFetch()), the response is encoded with the webjs serializer so that Date, Map, Set, BigInt, TypedArray, Blob, File, FormData, and reference cycles all survive the round trip.
    • +
    • If the client sent Accept: application/vnd.webjs+json (e.g. via richFetch()), the response is encoded with the WebJs serializer so that Date, Map, Set, BigInt, TypedArray, Blob, File, FormData, and reference cycles all survive the round trip.
    • Otherwise, the response is plain application/json, the standard for curl, mobile apps, and third-party consumers.
    // app/api/posts/route.ts
    @@ -198,7 +198,7 @@ export default async function authGuard(
         

    Middleware files nest. If you have app/middleware.ts, app/api/middleware.ts, and app/api/admin/middleware.ts, a request to /api/admin/users runs all three in outermost-to-innermost order.

    Rate Limiting

    -

    webjs ships a built-in in-memory fixed-window rate limiter, shaped as a middleware:

    +

    WebJs ships a built-in in-memory fixed-window rate limiter, shaped as a middleware:

    import { rateLimit } from '@webjsdev/server';
     
     // In a middleware.ts file:
    @@ -248,7 +248,7 @@ export default async function cors(
     }

    Backend-Only Usage

    -

    webjs works as a pure API framework with no pages or components. If your app/ directory contains only route.ts and middleware.ts files (no page.ts, no layout.ts), webjs serves only API routes. No SSR, no import maps, no client JS. This is ideal for microservices, backends for mobile apps, or REST APIs. See Backend-Only Mode for a full guide.

    +

    WebJs works as a pure API framework with no pages or components. If your app/ directory contains only route.ts and middleware.ts files (no page.ts, no layout.ts), webjs serves only API routes. No SSR, no import maps, no client JS. This is ideal for microservices, backends for mobile apps, or REST APIs. See Backend-Only Mode for a full guide.

    Complete CRUD Example

    Here is a full route.ts implementing GET, POST, and DELETE for a resource:

    @@ -361,7 +361,7 @@ export async function DELETE(_req: Request, { params }: Ctx) {
  • Per-segment middleware.ts applies to all routes underneath
  • rateLimit() from @webjsdev/server for built-in rate limiting
  • CORS via the cors() middleware on route.ts files or in middleware.ts
  • -
  • webjs works as a backend-only API framework when no page files are present
  • +
  • WebJs works as a backend-only API framework when no page files are present
`; } diff --git a/docs/app/docs/architecture/page.ts b/docs/app/docs/architecture/page.ts index 268baf3ce..b6209ab96 100644 --- a/docs/app/docs/architecture/page.ts +++ b/docs/app/docs/architecture/page.ts @@ -5,7 +5,7 @@ export const metadata = { title: 'Architecture | webjs' }; export default function Architecture() { return html`

Architecture

-

webjs is a monorepo with three packages that together form the framework. Understanding the split helps when you need to import something specific or embed webjs into another runtime.

+

WebJs is a monorepo with three packages that together form the framework. Understanding the split helps when you need to import something specific or embed webjs into another runtime.

Mental model, especially if you come from React Server Components: webjs has no server/client component split. Pages, layouts, and components are isomorphic modules, the same source runs on the server to produce SSR'd HTML and then loads in the browser to add interactivity. There is no Flight protocol and no "server component" identity. The one server boundary is the .server.{js,ts} file, which is an RPC + source-protection mechanism (server actions and server-only utilities), not a server-rendered component. route.{js,ts} is a server-only HTTP handler. And elision (skipping the JS download of a module that does no client work) is a build-free optimization on those isomorphic modules, not a boundary, it never changes behaviour because progressive enhancement is the no-JS baseline.

@@ -19,7 +19,7 @@ export default function Architecture() { │ └── blog/ # reference app exercising every feature └── docs/ # this documentation site (built on webjs) -

webjs (core)

+

WebJs (core)

Isomorphic: safe to import on both server and client. Contains:

  • html / css: tagged template literals for templates and styles
  • @@ -97,7 +97,7 @@ import { Button } from '#components/ui/button.ts'; import { listPosts } from '#modules/posts/queries/list-posts.server.ts';

    This is Node's native package.json "imports" field. The scaffold ships a single catch-all key, so every top-level folder is aliased and a new one needs no config change:

    "imports": { "#*": "./*" }
    -

    It resolves at runtime on Node 24+ AND Bun with no build step and no tsconfig paths. The sigil is # (not @) and there is no slash after it (#lib/..., not #/lib/...): a #/-prefixed key does not resolve on Bun. webjs expands the same map for its import graph, so a #-aliased .server.ts still trips the server-only boundary, and the browser importmap gets a matching scope per top-level folder automatically. A same-directory import stays a plain relative (./sibling.ts); opt out anywhere by writing a relative path.

    +

    It resolves at runtime on Node 24+ AND Bun with no build step and no tsconfig paths. The sigil is # (not @) and there is no slash after it (#lib/..., not #/lib/...): a #/-prefixed key does not resolve on Bun. WebJs expands the same map for its import graph, so a #-aliased .server.ts still trips the server-only boundary, and the browser importmap gets a matching scope per top-level folder automatically. A same-directory import stays a plain relative (./sibling.ts); opt out anywhere by writing a relative path.

    Request Lifecycle

      @@ -124,7 +124,7 @@ import { listPosts } from '#modules/posts/queries/list-posts.server.ts';

      See Progressive Enhancement for the full design rationale and patterns.

      Embedding

      -

      webjs can be embedded in any Node-compatible runtime via createRequestHandler:

      +

      WebJs can be embedded in any Node-compatible runtime via createRequestHandler:

      import { createRequestHandler } from '@webjsdev/server';
       
       const app = await createRequestHandler({
      diff --git a/docs/app/docs/auth/page.ts b/docs/app/docs/auth/page.ts
      index 02045ae7b..4ef8c2cbc 100644
      --- a/docs/app/docs/auth/page.ts
      +++ b/docs/app/docs/auth/page.ts
      @@ -5,7 +5,7 @@ export const metadata = { title: 'Authentication | webjs' };
       export default function Auth() {
         return html`
           

      Authentication

      -

      webjs provides NextAuth-style authentication with OAuth providers, credentials login, and JWT sessions. No external auth library needed.

      +

      WebJs provides NextAuth-style authentication with OAuth providers, credentials login, and JWT sessions. No external auth library needed.

      Setup

      // lib/auth.server.ts: create once
      diff --git a/docs/app/docs/authentication/page.ts b/docs/app/docs/authentication/page.ts
      index b88b2a5c4..c56c1c513 100644
      --- a/docs/app/docs/authentication/page.ts
      +++ b/docs/app/docs/authentication/page.ts
      @@ -5,7 +5,7 @@ export const metadata = { title: 'Authentication | webjs' };
       export default function Authentication() {
         return html`
           

      Authentication

      -

      webjs doesn't ship an auth library. It provides the primitives you need to build session-based authentication cleanly. The blog example demonstrates a complete implementation using scrypt password hashing, session tokens in cookies, and middleware-based route protection.

      +

      WebJs doesn't ship an auth library. It provides the primitives you need to build session-based authentication cleanly. The blog example demonstrates a complete implementation using scrypt password hashing, session tokens in cookies, and middleware-based route protection.

      Architecture

      lib/
      diff --git a/docs/app/docs/backend-only/page.ts b/docs/app/docs/backend-only/page.ts
      index 7cada2197..2721711ef 100644
      --- a/docs/app/docs/backend-only/page.ts
      +++ b/docs/app/docs/backend-only/page.ts
      @@ -5,7 +5,7 @@ export const metadata = { title: 'Backend-Only Mode | webjs' };
       export default function BackendOnly() {
         return html`
           

      Backend-Only Mode

      -

      webjs works as a pure API framework with no pages, no SSR, and no web components. If you only need file-based routing, middleware, TypeScript, and a fast HTTP server, you can use webjs without writing a single page or component. Everything in the framework is designed around standard Request/Response objects, so the server-side features work independently of the rendering layer.

      +

      WebJs works as a pure API framework with no pages, no SSR, and no web components. If you only need file-based routing, middleware, TypeScript, and a fast HTTP server, you can use webjs without writing a single page or component. Everything in the framework is designed around standard Request/Response objects, so the server-side features work independently of the rendering layer.

      When to Choose Backend-Only

      Use backend-only mode when:

      @@ -45,7 +45,7 @@ export default function BackendOnly() { middleware.ts # root middleware (logging, timing) package.json tsconfig.json
      -

      There is no page.ts, no layout.ts, no components/ directory. webjs detects what files exist and only activates the features you use.

      +

      There is no page.ts, no layout.ts, no components/ directory. WebJs detects what files exist and only activates the features you use.

      File-Based API Routing

      A route.ts file anywhere under app/ becomes an API endpoint. Export functions named after HTTP methods:

      @@ -274,8 +274,8 @@ fastify.listen({ port: 8080 });

What webjs does not give you:

    -
  • Massive middleware ecosystem: Express has thousands of middleware packages (passport, multer, helmet, etc.). webjs has a handful of built-in utilities. You can still use any standard library that works with Request/Response.
  • -
  • Years of battle-testing: Express and Fastify have been production-proven at enormous scale. webjs is new.
  • +
  • Massive middleware ecosystem: Express has thousands of middleware packages (passport, multer, helmet, etc.). WebJs has a handful of built-in utilities. You can still use any standard library that works with Request/Response.
  • +
  • Years of battle-testing: Express and Fastify have been production-proven at enormous scale. WebJs is new.
  • Plugin system: Fastify's plugin architecture for encapsulated contexts does not have a webjs equivalent. The middleware chain and file conventions are the extension points.
  • Advanced schema validation: Fastify has built-in JSON Schema validation with Ajv. In webjs, use the validate config export (or the route() adapter's validate option) with zod, valibot, or any library.
diff --git a/docs/app/docs/cache/page.ts b/docs/app/docs/cache/page.ts index 393680e3b..582b7e9af 100644 --- a/docs/app/docs/cache/page.ts +++ b/docs/app/docs/cache/page.ts @@ -5,7 +5,7 @@ export const metadata = { title: 'Caching | webjs' }; export default function Cache() { return html`

Caching

-

webjs provides two complementary caching layers: cache() for server-side query result caching, and HTTP Cache-Control headers for page-level browser/CDN caching. Zero config in development (in-memory store). For horizontal scaling in production, call setStore(redisStore({ url: process.env.REDIS_URL })) once at app startup to share the cache across instances.

+

WebJs provides two complementary caching layers: cache() for server-side query result caching, and HTTP Cache-Control headers for page-level browser/CDN caching. Zero config in development (in-memory store). For horizontal scaling in production, call setStore(redisStore({ url: process.env.REDIS_URL })) once at app startup to share the cache across instances.

cache(): Server-Side Query Caching

Wrap any async function with cache() to cache its return value on the server. Same function + same arguments = cached result until TTL expires or you call invalidate().

diff --git a/docs/app/docs/client-router/page.ts b/docs/app/docs/client-router/page.ts index 2ef135b59..f9ab4b6bb 100644 --- a/docs/app/docs/client-router/page.ts +++ b/docs/app/docs/client-router/page.ts @@ -5,7 +5,7 @@ export const metadata = { title: 'Client Router | webjs' }; export default function ClientRouter() { return html`

Client Router

-

webjs ships a nested-layout-aware client router that intercepts same-origin <a> clicks and <form> submissions, fetches the target HTML, and swaps only the deepest layout boundary the two pages don't share. Outer layout DOM is preserved: sidenav scroll, input values, <details> open state, mounted custom elements all survive navigation without authors writing anything.

+

WebJs ships a nested-layout-aware client router that intercepts same-origin <a> clicks and <form> submissions, fetches the target HTML, and swaps only the deepest layout boundary the two pages don't share. Outer layout DOM is preserved: sidenav scroll, input values, <details> open state, mounted custom elements all survive navigation without authors writing anything.

The router is automatic and needs no import: it auto-enables whenever @webjsdev/core loads in the browser, which happens on any page that ships a component. For 99% of apps the contract is "write standard HTML, navigation gets faster." The advanced primitives below (frames, revalidation, programmatic navigation) exist for the cases where you need to take over.

The one edge: a fully-static page with zero components ships no JavaScript at all, so it has no router and its links do a normal full-page navigation (correct progressive enhancement, and cheaper). This is invisible during a session, since a router started on any earlier interactive page stays active across soft navigations. It only shows on a cold direct load of such a page (a bare error or 404 screen). If you want soft navigation there too, render any component in the page or its layout, or add import '@webjsdev/core/client-router' to force the router on.

@@ -26,7 +26,7 @@ export default function ClientRouter() {

When the destination streams (it has a Suspense or <webjs-suspense> boundary), the router applies the response PROGRESSIVELY: it swaps the shell (with the fallbacks) in immediately and advances the URL, then streams each resolved boundary into the live DOM as it arrives, fast-before-slow. So a soft navigation to a streamed page matches the initial-load experience (fallback first, content streams in) instead of buffering the whole response before the swap. A non-streaming page is unaffected (the response is read to completion and applied once). A navigation superseded mid-stream stops applying, and a mid-stream transport failure leaves the applied boundaries in place with the rest showing their fallback (non-destructive).

Form submissions

-

<form action="/x" method="post"> works exactly per the HTML spec. webjs intercepts the submit event in the bubble phase (after a component's own @submit handler) and routes the same fetch the browser would have sent through the partial-swap pipeline. Because it runs after, a component that calls e.preventDefault() in @submit keeps the form to itself and the router leaves it alone; the same applies to @click on links. Submitter attributes (formmethod, formaction, formenctype on a clicked <button>) take precedence over the form's own per HTML5.

+

<form action="/x" method="post"> works exactly per the HTML spec. WebJs intercepts the submit event in the bubble phase (after a component's own @submit handler) and routes the same fetch the browser would have sent through the partial-swap pipeline. Because it runs after, a component that calls e.preventDefault() in @submit keeps the form to itself and the router leaves it alone; the same applies to @click on links. Submitter attributes (formmethod, formaction, formenctype on a clicked <button>) take precedence over the form's own per HTML5.

  • GET forms: FormData is promoted to the URL query string (replacing any existing query on action). The URL is then fetched and applied like a link click.
  • POST / PUT / PATCH / DELETE forms: FormData is sent as the request body. After a successful response the snapshot cache is cleared (other cached URLs may reflect stale server state).
  • diff --git a/docs/app/docs/components/page.ts b/docs/app/docs/components/page.ts index c14a25f28..3b88ba3ae 100644 --- a/docs/app/docs/components/page.ts +++ b/docs/app/docs/components/page.ts @@ -5,7 +5,7 @@ export const metadata = { title: 'Components | webjs' }; export default function Components() { return html`

    Components

    -

    webjs components are standard HTML custom elements built on a thin base class called WebComponent. If you are coming from React, think of WebComponent as a class component whose render method returns a tagged template instead of JSX. The browser owns the component lifecycle. There is no virtual DOM, no reconciler, and no framework-specific component model to learn.

    +

    WebJs components are standard HTML custom elements built on a thin base class called WebComponent. If you are coming from React, think of WebComponent as a class component whose render method returns a tagged template instead of JSX. The browser owns the component lifecycle. There is no virtual DOM, no reconciler, and no framework-specific component model to learn.

    The WebComponent Base Class

    Every interactive component extends WebComponent, declares its property map by passing a shape into the base-class factory (extends WebComponent({ ... }), and optionally static styles for shadow-DOM components), implements render(), and registers itself by passing a hyphenated tag name to ClassName.register('tag-name'). The tag name is an argument to .register(), not a static field.

    @@ -261,7 +261,7 @@ static styles = css\` .accent { color: var(--accent); } \`; -
    This is fundamentally different from React CSS-in-JS solutions that require runtime injection or build tooling. webjs uses the platform: shadow DOM gives you scoping, CSS custom properties give you theming, and there is nothing to configure.
    +
    This is fundamentally different from React CSS-in-JS solutions that require runtime injection or build tooling. WebJs uses the platform: shadow DOM gives you scoping, CSS custom properties give you theming, and there is nothing to configure.

    Light DOM (default)

    Light DOM is the default because global CSS and Tailwind utility classes apply directly: no :host, no ::part, no CSS-variable plumbing. The browser renders a plain custom element with normal children. This is the mode the blog example uses everywhere except when shadow DOM buys something specific.

    @@ -415,7 +415,7 @@ class Cart extends WebComponent {

    This is the design rule that makes progressive enhancement work in webjs: the component's HTML lands in the response, with the right content, before any script runs.

    Compound components: reading the parent with closest() at SSR

    -

    A compound component (a tabs trigger, a toggle-group item) derives its active or pressed state by walking up to the parent and reading the parent's value. webjs supports this.closest(...) at SSR for tag-name selectors only, so the active or pressed state is marked in the first server paint, not only after hydration.

    +

    A compound component (a tabs trigger, a toggle-group item) derives its active or pressed state by walking up to the parent and reading the parent's value. WebJs supports this.closest(...) at SSR for tag-name selectors only, so the active or pressed state is marked in the first server paint, not only after hydration.

    class UiTabsTrigger extends WebComponent({ value: String }) {
       get _tabs() { return this.closest('ui-tabs'); }
    @@ -461,7 +461,7 @@ UserProfile.register('user-profile');

Slots: Content Projection

-

Slots are how a parent passes content into a component. If you are coming from React, think of the default slot as children. webjs supports the full shadow-DOM <slot> surface in light DOM as well as shadow DOM, so every example below works identically whether the component sets static shadow = true or leaves it at the default (light DOM). The light-DOM runtime mirrors HTMLSlotElement.assignedNodes(), assignedElements(), assignedSlot, and the slotchange event, plus named slots, fallback content, and first-wins resolution. To our knowledge no other web-components framework offers this complete parity in light DOM. Lit's slot APIs only work inside shadow roots, and Stencil's light-DOM slot polyfill has known gaps around fallback content and mixed shadow / non-shadow trees.

+

Slots are how a parent passes content into a component. If you are coming from React, think of the default slot as children. WebJs supports the full shadow-DOM <slot> surface in light DOM as well as shadow DOM, so every example below works identically whether the component sets static shadow = true or leaves it at the default (light DOM). The light-DOM runtime mirrors HTMLSlotElement.assignedNodes(), assignedElements(), assignedSlot, and the slotchange event, plus named slots, fallback content, and first-wins resolution. To our knowledge no other web-components framework offers this complete parity in light DOM. Lit's slot APIs only work inside shadow roots, and Stencil's light-DOM slot polyfill has known gaps around fallback content and mixed shadow / non-shadow trees.

Default Slot

The <slot></slot> element in a component's render() is where the parent's child content appears:

@@ -531,7 +531,7 @@ html\`

Content without a slot attribute goes to the default (unnamed) slot. Content with slot="name" is routed to the matching <slot name="name">. Text inside the <slot> tag itself is fallback content shown when no matching content is provided.

Lifecycle

-

webjs components use the standard custom element lifecycle callbacks. If you override them, always call super.

+

WebJs components use the standard custom element lifecycle callbacks. If you override them, always call super.

connectedCallback()

Called when the element is inserted into the document. This is where webjs attaches the shadow root, adopts styles, and performs the first render. Use it for setup work like fetching data, opening WebSocket connections, or reading from localStorage:

@@ -557,7 +557,7 @@ html\`

You do not need to call super.disconnectedCallback() (the base class is a no-op), but it does not hurt to include it for safety.

attributeChangedCallback(name, oldValue, newValue)

-

Called when one of the observedAttributes changes. webjs handles this for you. It coerces the attribute value based on the type declared in the factory shape, sets the corresponding instance property, and schedules a re-render. You rarely need to override this, but you can if you need side effects when a specific attribute changes:

+

Called when one of the observedAttributes changes. WebJs handles this for you. It coerces the attribute value based on the type declared in the factory shape, sets the corresponding instance property, and schedules a re-render. You rarely need to override this, but you can if you need side effects when a specific attribute changes:

attributeChangedCallback(name, oldVal, newVal) {
   super.attributeChangedCallback(name, oldVal, newVal);
@@ -641,7 +641,7 @@ render() {
 
     
MyCounter.register('my-counter');
-

webjs wraps the native API (and installs a compatible shim on the server) so the same line works in both environments:

+

WebJs wraps the native API (and installs a compatible shim on the server) so the same line works in both environments:

  • Browser: tells the browser to upgrade all <my-counter> elements with the MyCounter class, and mirrors the mapping into webjs's internal registry.
  • Server: stores the class in the internal registry so renderToString can look it up for Declarative Shadow DOM injection.
  • @@ -654,7 +654,7 @@ render() {

    The tag argument accepts any short-string quote style. register('my-counter'), register("my-counter"), and register(`my-counter`) are all equivalent. The framework's tag-name-has-hyphen lint rule reads the tag through any of them.

    Server Rendering

    -

    webjs components are server-rendered using Declarative Shadow DOM. When the server renders a page containing <my-counter count="5"></my-counter>, the output looks like:

    +

    WebJs components are server-rendered using Declarative Shadow DOM. When the server renders a page containing <my-counter count="5"></my-counter>, the output looks like:

    <my-counter count="5">
       <template shadowrootmode="open">
    diff --git a/docs/app/docs/configuration/page.ts b/docs/app/docs/configuration/page.ts
    index e75d0282a..40ebce843 100644
    --- a/docs/app/docs/configuration/page.ts
    +++ b/docs/app/docs/configuration/page.ts
    @@ -5,7 +5,7 @@ export const metadata = { title: 'Configuration | webjs' };
     export default function Configuration() {
       return html`
         

    Configuration

    -

    webjs is designed to work with zero configuration. File conventions handle routing, TypeScript works out of the box, and the server is pre-configured with sensible defaults. This page documents what you can configure when you need to.

    +

    WebJs is designed to work with zero configuration. File conventions handle routing, TypeScript works out of the box, and the server is pre-configured with sensible defaults. This page documents what you can configure when you need to.

    CLI Options

    webjs dev

    @@ -65,7 +65,7 @@ webjs db seed # run db/seed.server.ts

    webjs check runs a fixed set of correctness checks (a crash, a security leak, a build or type-strip failure). They always run; there is no project-level config to disable them, and the checks read no package.json config block of their own. Project conventions (layout, naming, testing) are guidance in CONVENTIONS.md, not a tool. See Conventions & AI Workflow for the split and run webjs check --rules to list the checks.

    Security response headers

    -

    webjs sets standard security headers on every response by default (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, plus Strict-Transport-Security in production over HTTPS). Override or extend them per path with a webjs.headers block in package.json, an array of { source, headers: [{ key, value }] } rules where source is a URLPattern path pattern and a null value removes a default. App middleware wins over the path config, which wins over the defaults. A webjs.csp key (off by default) additionally mints a per-request CSP nonce and emits a matching Content-Security-Policy header. See Deployment → Secure response headers for the full reference.

    +

    WebJs sets standard security headers on every response by default (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, plus Strict-Transport-Security in production over HTTPS). Override or extend them per path with a webjs.headers block in package.json, an array of { source, headers: [{ key, value }] } rules where source is a URLPattern path pattern and a null value removes a default. App middleware wins over the path config, which wins over the defaults. A webjs.csp key (off by default) additionally mints a per-request CSP nonce and emits a matching Content-Security-Policy header. See Deployment → Secure response headers for the full reference.

    Redirects

    For a moved URL, declare a redirect under webjs.redirects in package.json, an array of { source, destination, permanent?, statusCode? } rules. source is a URLPattern path pattern (so :param / :rest* works) and destination is the target: a path, a path referencing named groups from the source (/posts/:slug filled from /blog/:slug), or an absolute URL for an external redirect. permanent defaults to true (a 308 Permanent Redirect, what SEO wants so link equity transfers); permanent: false is a 307 Temporary Redirect. 308 / 307 preserve the request method (a redirected POST stays a POST); for a legacy 301 / 302 set statusCode explicitly. The incoming query string is preserved by default. Redirects apply at the very start of request handling, before routing, and a malformed entry is dropped with a warning rather than crashing the app.

    @@ -99,7 +99,7 @@ webjs db seed # run db/seed.server.ts
{ "webjs": { "maxBodyBytes": 262144, "maxMultipartBytes": 5242880, "requestTimeoutMs": 30000 } }

Environment Variables

-

Use process.env in server-side code (pages, actions, route handlers, middleware). webjs auto-loads <appDir>/.env into process.env once at boot using Node 24+'s built-in process.loadEnvFile, so a scaffolded app with a committed .env.example and a developer-copied .env just works without installing dotenv or wiring up the file path. The auto-load fires before any server-only module is imported, which matters for code that reads process.env at module-init time (e.g. createAuth({ secret: process.env.AUTH_SECRET })).

+

Use process.env in server-side code (pages, actions, route handlers, middleware). WebJs auto-loads <appDir>/.env into process.env once at boot using Node 24+'s built-in process.loadEnvFile, so a scaffolded app with a committed .env.example and a developer-copied .env just works without installing dotenv or wiring up the file path. The auto-load fires before any server-only module is imported, which matters for code that reads process.env at module-init time (e.g. createAuth({ secret: process.env.AUTH_SECRET })).

Precedence: shell wins over file. process.loadEnvFile does not override values that are already present in process.env, so values exported by the host shell or a process manager (Docker, systemd, Railway, Fly) take precedence over the same key in .env. This matches the Rails / Next / Astro convention: .env is for developer-local defaults; production secrets come from the platform.

@@ -130,7 +130,7 @@ export default (env) => { schema.parse(env); };

Any environment variable that does not start with WEBJS_PUBLIC_ is server-only. It is never sent to the browser. DATABASE_URL, AUTH_SECRET, OAuth client secrets, third-party API keys: read them in server actions, route handlers, middleware, or page functions, and pass derived values (not the raw secret) to components.

Public env vars (WEBJS_PUBLIC_*)

-

Any env var whose name starts with WEBJS_PUBLIC_ is exposed to the browser as process.env.WEBJS_PUBLIC_X. webjs injects an inline script in the SSR'd HTML head that sets window.process.env before any user code or vendor bundle runs. Components can read these directly:

+

Any env var whose name starts with WEBJS_PUBLIC_ is exposed to the browser as process.env.WEBJS_PUBLIC_X. WebJs injects an inline script in the SSR'd HTML head that sets window.process.env before any user code or vendor bundle runs. Components can read these directly:

// .env at the app root (auto-loaded at boot)
 WEBJS_PUBLIC_API_URL=https://api.example.com
 WEBJS_PUBLIC_STRIPE_KEY=pk_live_abc
diff --git a/docs/app/docs/controllers/page.ts b/docs/app/docs/controllers/page.ts
index d39ae3a67..96dcca30a 100644
--- a/docs/app/docs/controllers/page.ts
+++ b/docs/app/docs/controllers/page.ts
@@ -162,7 +162,7 @@ UserList.register('user-list');
DashboardWidget.register('dashboard-widget');

Built-in Controllers

-

webjs ships three controllers out of the box:

+

WebJs ships three controllers out of the box:

Task

Manages async operations with automatic loading/error states, abort support, and reactive args. Imported from webjs/task. See the Task Controller page for full documentation.

diff --git a/docs/app/docs/conventions/page.ts b/docs/app/docs/conventions/page.ts index 9028bf7ca..1b8d663b4 100644 --- a/docs/app/docs/conventions/page.ts +++ b/docs/app/docs/conventions/page.ts @@ -5,7 +5,7 @@ export const metadata = { title: 'Conventions & AI Workflow | webjs' }; export default function Conventions() { return html`

Conventions & AI Workflow

-

webjs is an AI-first framework. It ships an opinionated conventions system that both humans and AI agents follow. The conventions are enforced via config files, CLI commands, and guardrails that ensure consistent, high-quality code across the entire project, whether written by a person or an agent.

+

WebJs is an AI-first framework. It ships an opinionated conventions system that both humans and AI agents follow. The conventions are enforced via config files, CLI commands, and guardrails that ensure consistent, high-quality code across the entire project, whether written by a person or an agent.

CONVENTIONS.md

Every webjs app has a CONVENTIONS.md file at its root. This is the project-specific conventions document that all AI agents read before writing code. It defines:

@@ -51,7 +51,7 @@ webjs check --rules

webjs test

-

webjs ships a testing setup based on node:test and WTR + Playwright.

+

WebJs ships a testing setup based on node:test and WTR + Playwright.

Unit Tests

# Run all unit tests
@@ -94,7 +94,7 @@ webjs test --e2e
     

Tests-per-feature is a project convention (guidance), not a webjs check rule.

AI Agent Guardrails

-

webjs enforces disciplined AI workflows through config files and hooks. These guardrails apply to all agents: Claude, Cursor, Copilot, Antigravity, and others.

+

WebJs enforces disciplined AI workflows through config files and hooks. These guardrails apply to all agents: Claude, Cursor, Copilot, Antigravity, and others.

Branch Checking

AI agents must never commit directly to main or master. Before any edit, the agent checks what branch it is on:

diff --git a/docs/app/docs/data-fetching/page.ts b/docs/app/docs/data-fetching/page.ts index 6a0ad85a4..d190f091b 100644 --- a/docs/app/docs/data-fetching/page.ts +++ b/docs/app/docs/data-fetching/page.ts @@ -8,7 +8,7 @@ export const metadata = { export default function DataFetching() { return html`

Data Fetching

-

webjs gives a component four ways to get data into the page. This is the canonical guide to which one to reach for, and the anti-patterns to avoid. The headline is bare-await async render: a component fetches its own server data into the first paint, co-located, with no page orchestration.

+

WebJs gives a component four ways to get data into the page. This is the canonical guide to which one to reach for, and the anti-patterns to avoid. The headline is bare-await async render: a component fetches its own server data into the first paint, co-located, with no page orchestration.

The default: async render()

Make a component's render() async and call a 'use server' action directly. Writing await makes the function async; webjs awaits a promise-returning render() automatically on both the server and the client. There is no flag.

diff --git a/docs/app/docs/database/page.ts b/docs/app/docs/database/page.ts index 94beb44fd..6e735f94d 100644 --- a/docs/app/docs/database/page.ts +++ b/docs/app/docs/database/page.ts @@ -5,7 +5,7 @@ export const metadata = { title: 'Database (Drizzle) | webjs' }; export default function Database() { return html`

Database (Drizzle)

-

webjs uses Drizzle as the default ORM. It fits the buildless thesis: there is no codegen and no engine binary (what you write is what runs), it runs on Node and Bun, and the types are inferred straight from your schema. SQLite is the default; Postgres is a flag away. The scaffold wires it all up under a db/ folder.

+

WebJs uses Drizzle as the default ORM. It fits the buildless thesis: there is no codegen and no engine binary (what you write is what runs), it runs on Node and Bun, and the types are inferred straight from your schema. SQLite is the default; Postgres is a flag away. The scaffold wires it all up under a db/ folder.

What the scaffold gives you

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

Deployment

-

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.

+

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):

+

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

# Development: live reload, no compression, no caching, verbose errors
 npm run dev -- --port 8080
 
@@ -52,7 +52,7 @@ npm run start -- --port 8080

Pick the mode that matches your security posture. The choice is per-deploy, not per-package: either everything goes through jspm.io or everything is locally vendored. Mixing modes per-package is not supported.

Secure response headers

-

webjs sets a baseline of standard security headers on every response, so a deployed app is not clickjackable or MIME-sniffable without any reverse-proxy configuration. The defaults are literal HTTP headers:

+

WebJs sets a baseline of standard security headers on every response, so a deployed app is not clickjackable or MIME-sniffable without any reverse-proxy configuration. The defaults are literal HTTP headers:

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: SAMEORIGIN
  • @@ -60,7 +60,7 @@ npm run start -- --port 8080
  • Permissions-Policy: camera=(), microphone=(), geolocation=()
  • Strict-Transport-Security: max-age=63072000; includeSubDomains in production over HTTPS only
  • -

    HSTS is gated to production AND HTTPS. webjs detects the original scheme from X-Forwarded-Proto (the header the trusted edge proxy forwards after terminating TLS), honoring the same proxy-trust posture as the rest of the framework, so HSTS is never set on a plain-HTTP hop or in dev. Set WEBJS_NO_TRUST_PROXY=1 to stop trusting forwarded headers when the container is directly exposed.

    +

    HSTS is gated to production AND HTTPS. WebJs detects the original scheme from X-Forwarded-Proto (the header the trusted edge proxy forwards after terminating TLS), honoring the same proxy-trust posture as the rest of the framework, so HSTS is never set on a plain-HTTP hop or in dev. Set WEBJS_NO_TRUST_PROXY=1 to stop trusting forwarded headers when the container is directly exposed.

    A default is set only when the response does not already carry that header, so anything your middleware, a route.{js,ts} handler, or expose sets always wins.

    Per-path overrides

    Declare per-path header rules in package.json under "webjs": { "headers": [...] }, shaped like Next's. The source is a path pattern matched with the native URLPattern API, so :param and :rest* tokens work:

    @@ -75,7 +75,7 @@ npm run start -- --port 8080

    A rule can ADD a header, OVERRIDE a default by giving a new value, or DISABLE a default on a path with a null value (the first example drops X-Frame-Options so a public-embed route can be framed). Precedence, lowest to highest, runs secure defaults, then the webjs.headers path config, then app middleware (which always wins, since its headers are already on the response when webjs merges).

    Content-Security-Policy (nonce, opt-in)

    -

    webjs can mint a fresh per-request CSP nonce and emit a matching Content-Security-Policy response header. It is OFF by default (a strict policy would break an app with third-party inline scripts/styles, so you opt in). Enable it with a webjs.csp key in package.json:

    +

    WebJs can mint a fresh per-request CSP nonce and emit a matching Content-Security-Policy response header. It is OFF by default (a strict policy would break an app with third-party inline scripts/styles, so you opt in). Enable it with a webjs.csp key in package.json:

    {
       "webjs": { "csp": true }
     }
    @@ -106,7 +106,7 @@ npm run start -- --port 8080

    Unhandled promise rejections are logged but do not crash the process. Uncaught exceptions trigger an orderly shutdown (state may be corrupted, so continuing is unsafe).

    Health and readiness probes

    -

    webjs answers two built-in probe endpoints, and the distinction matters under runtime-first boot:

    +

    WebJs answers two built-in probe endpoints, and the distinction matters under runtime-first boot:

    GET /__webjs/health    # liveness:  always 200 once the process is listening
     GET /__webjs/ready     # readiness: 503 until the instance is fully warm, then 200

    /__webjs/health is liveness. It returns 200 { "status": "ok" } as soon as the process is accepting connections, so an orchestrator can tell the process is alive. It never waits on the analysis.

    @@ -136,7 +136,7 @@ export default async function ready() { }

    HTTP/2: at the edge, not in webjs

    -

    webjs delegates TLS termination and HTTP/2 negotiation to whatever sits in front of npm run start. The framework's HTTP server speaks plain HTTP/1.1. ALPN, certificates, and h2 framing are entirely the proxy's concern. Two reasons:

    +

    WebJs delegates TLS termination and HTTP/2 negotiation to whatever sits in front of npm run start. The framework's HTTP server speaks plain HTTP/1.1. ALPN, certificates, and h2 framing are entirely the proxy's concern. Two reasons:

    • PaaS already gives you HTTP/2. Railway, Fly, Render, Vercel, Cloudflare Pages, and Heroku all terminate TLS + HTTP/2 at their edge and proxy plain HTTP/1.1 to your container. Zero framework configuration: you get HTTP/2 to the browser the moment you deploy.
    • For bare-VM, reverse proxies do it better. nginx, Caddy, and Traefik are battle-tested for TLS termination. They handle cert renewal (ACME), OCSP, ALPN, HTTP/3, and h2-to-h1 downgrade more capably than Node's http2 module.
    • @@ -145,7 +145,7 @@ export default async function ready() {

      Forwarding 103 Early Hints. webjs sends a 103 Early Hints response carrying Link: rel=modulepreload headers before SSR begins, so the browser can start fetching JS while the server renders. Most major edges (Cloudflare, fly-proxy, Fastly) forward 103 responses to the client transparently. If yours doesn't, the page still works (the headers are just lost) but you skip the head-start. Early Hints are disabled in dev because file churn could send stale URLs.

      Pluggable Logger

      -

      webjs includes a minimal logger that writes structured JSON in production and human-readable lines in development:

      +

      WebJs includes a minimal logger that writes structured JSON in production and human-readable lines in development:

      # Dev output:
       [webjs] webjs dev server ready on http://localhost:8080
       
      @@ -168,7 +168,7 @@ const app = await createRequestHandler({
       });

      Observability: access log, request id, error hook, build-info

      -

      Day-2 ops needs more than liveness probes. webjs ships four standards-native observability surfaces, all wired at the single response funnel so they apply uniformly across pages, route handlers, server actions, and assets.

      +

      Day-2 ops needs more than liveness probes. WebJs ships four standards-native observability surfaces, all wired at the single response funnel so they apply uniformly across pages, route handlers, server actions, and assets.

      Per-request access log

      Every handled request emits ONE structured info line through the pluggable logger after the response is produced, carrying method, path, status, durationMs, and requestId. It never logs request bodies or secrets. In prod the default logger writes it as one JSON object per line; in dev it is a readable line.

      @@ -267,7 +267,7 @@ const app = await createRequestHandler({ appDir: Deno.cwd(), dev: false }); Deno.serve({ port: 8080 }, (req) => app.handle(req));

      Environment Variables

      -

      webjs reads the following environment variables:

      +

      WebJs reads the following environment variables: