@webjsdev/mcp 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/mcp",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "description": "The webjs Model Context Protocol server: live app introspection (routes / actions / components / check) plus the framework knowledge layer (docs, recipes, source) for AI coding agents",
6
6
  "bin": {
@@ -22,6 +22,9 @@ reference there.
22
22
  | `agent-docs/testing.md` | Unit, browser, convention validation, the `handle()` test harness (`@webjsdev/server/testing`) |
23
23
  | `agent-docs/framework-dev.md` | Monorepo dev (only when editing webjs itself): commands, repo-health git config, changelog flow, dev error overlay |
24
24
  | `agent-docs/recipes.md` | Page / route / action / component recipes |
25
+ | `agent-docs/orm.md` | Drizzle rc.3 query surface: `db.query` reads, the removed `db.select({...})` projection overload, no-arg `.returning()` (#804) |
26
+ | `agent-docs/types-and-mutations.md` | Where server-derived types live so a shipping module does not pin `db/*.server.ts` (`import type` vs value import, the carrier rule) (#804) |
27
+ | `agent-docs/components-shadowing.md` | Inherited `WebComponent` members app code must not shadow (`title`, `remove`, ...) and the `TS2415`/`TS2416` each produces (#804) |
25
28
  | `agent-docs/lit-muscle-memory-gotchas.md` | **READ FIRST** when writing components. Lit patterns that break webjs SSR or reactivity, with the webjs-shaped fix for each |
26
29
 
27
30
  ---
@@ -50,6 +53,8 @@ cd ../<repo>-<slug> # do ALL work for the task here
50
53
 
51
54
  Git enforces one-branch-per-worktree, so separate worktrees make the collision impossible. Before any commit in a shared checkout, confirm `git branch --show-current` is still the branch you created; if it moved, you are colliding, switch to a worktree. A lone agent in a clean checkout may still use a plain branch. The repo's `.hooks/pre-commit` additionally BLOCKS a published-library (`core`/`server`/`cli`/`mcp`/`ui`/`intellisense`) version bump on any non-`chore/release-*` branch, the canonical wrong-branch-release symptom.
52
55
 
56
+ **Cleanup is automatic after a merge.** The `.claude/hooks/cleanup-merged-worktree.sh` PostToolUse hook fires after any `gh pr merge` and removes each linked worktree whose branch is merged AND whose tree is clean, so a merged branch's worktree never leaks (accumulated stale worktrees are exactly what it prevents). It is conservative: it KEEPS anything with uncommitted changes, an unmerged branch, or the worktree you ran the merge from (you cannot remove your current directory, so `cd` out and `git worktree remove` it yourself), and never touches the primary checkout. Disable with `WEBJS_NO_WORKTREE_CLEANUP=1`. Test: `test/hooks/cleanup-merged-worktree.test.mjs`.
57
+
53
58
  ### Skills are routed deterministically, never skipped
54
59
 
55
60
  A Skill is model-invoked, so it fires only when the model judges a match. The `.claude/hooks/route-skills.sh` `UserPromptSubmit` hook makes routing deterministic: it keyword-matches each prompt against every skill's documented triggers and injects a directive to invoke the matched skill before other work. Check the available skills and invoke a matching one before starting. The skills themselves are committed under `.claude/skills/` (alongside the hooks), so a fresh clone has both the router and the skills it routes to (no machine-local dependency). Tests in `test/hooks/route-skills.test.mjs`, which also asserts every skill the hook references is committed in-repo.
@@ -64,7 +69,8 @@ Every code change MUST include, automatically:
64
69
 
65
70
  1. **Tests, every applicable layer (not just unit).** Ship the tests that prove the change across EVERY layer it touches: **unit** (`packages/*/test/**`, `test/**`, including the counterfactual that fails when reverted), **browser** (`*/test/**/browser/*` via `npm run test:browser`, for hydration / DOM / slots / client router / custom-element upgrade), **e2e** (`test/e2e/*.test.mjs` via `WEBJS_E2E=1`, including network probes / navigation / streaming), and **smoke** (`test/examples/*/smoke/*`). A unit test is NECESSARY BUT NOT SUFFICIENT for any client-router / component / browser-facing change (the headline behaviour is a browser/e2e assertion). **Bun parity is part of the task, not an afterthought:** webjs runs on Node 24+ AND Bun (#508), so a change to a runtime-sensitive surface (the serializer, the node:http vs `Bun.serve` listener + request path, SSR / action / CSRF dispatch, streams, `node:crypto`, the TS stripper, auth / session / cors) MUST be proven on Bun (`node scripts/run-bun-tests.js` + the touched `test/bun/*.mjs` under `bun`) AND ship an added/updated `test/bun/<feature>.mjs` cross-runtime assertion. `npm test` does NOT run browser, e2e, or Bun; run them yourself and report the result. Never report work done with failing or missing tests. See `agent-docs/testing.md`. Enforced by `.claude/hooks/require-tests-with-src.sh` (the scaffold variant WARNS unless `WEBJS_TEST_GATE=block`) and `.claude/hooks/require-bun-parity-with-runtime-src.sh` (BLOCKS a commit that stages runtime-sensitive source with no `test/bun/**` test; escape hatch `WEBJS_BUN_VERIFIED=1`).
66
71
  2. **Documentation, part of the definition of done (not optional).** A task is NOT done until EVERY doc surface its change touches is in sync: `AGENTS.md` + `agent-docs/*.md` for new API surface, `CONVENTIONS.md` (and per-package `AGENTS.md`) for new conventions, the docs site (`docs/app/docs/<topic>`), the marketing `website/`, the scaffold templates (`packages/cli/templates/` per-agent rule files), and `README.md` for a headline capability. Updating `AGENTS.md` alone reproduces the #488 gap (docs site left stale). Invoke the `webjs-doc-sync` skill to sync every applicable surface. Enforced by `.claude/hooks/require-docs-with-src.sh`, which BLOCKS a commit that stages public `packages/*/src` source with no doc surface alongside it (a genuinely internal refactor / CI / release / perf change with no behaviour change bypasses with `WEBJS_NO_DOC_GATE=1`).
67
- 3. **Convention validation.** Run `webjs check` and fix violations.
72
+ 3. **Scaffold sync, part of the definition of done (not optional).** The scaffold is webjs's PRIMARY teaching surface for AI agents: they learn the framework by reading the generated gallery/showcase and its comments, then build the real app by adapting them. So when a webjs FEATURE is added or changed, the scaffold that teaches it must move too, the same way the docs must. Ask on EVERY feature change: does a UI feature-gallery demo (`packages/cli/templates/gallery/`, shipped in full-stack + saas), an api backend-showcase endpoint (`packages/cli/lib/api-gallery.js`), a generator surface (`packages/cli/lib/{create,saas-template}.js` for the home/theme/schema/wiring), a scaffold convention (the per-agent rule files in lockstep), or a scaffold test (`test/scaffolds/**`) need to change? Invoke the `webjs-scaffold-sync` skill to walk every surface AND run its mandatory generate + boot + `webjs check` verification (the generators emit strings, so an escaping bug only shows in a freshly generated app). Enforced by `.claude/hooks/require-scaffold-with-src.sh`, which BLOCKS a commit that stages `packages/(core|server|cli)/src` feature source with no scaffold surface (`packages/cli/templates` or `packages/cli/lib`) alongside it (a bug fix, an internal refactor, or a tweak to an already-demoed feature that needs no scaffold change bypasses with `WEBJS_NO_SCAFFOLD_GATE=1`).
73
+ 4. **Convention validation.** Run `webjs check` and fix violations.
68
74
 
69
75
  ### Git workflow (mandatory)
70
76
 
@@ -95,7 +101,7 @@ An **AI-first, web-components-first** framework inspired by NextJs, Lit, and Rai
95
101
  - **Progressive enhancement is the default architecture.** Pages and components are SSR'd; with JS off, content reads, `<a>` navigates, `<form>` 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 `<form>` + server action would do.
96
102
  - **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`.
97
103
  - **Server actions with rich types.** A `*.server.{js,ts}` file with `'use server'` exports functions importable from the client (the import is rewritten to a typed RPC stub); the wire round-trips `Date` / `Map` / `Set` / `BigInt` / `Error` / typed arrays / `Blob` / `File` / `FormData` / Symbols / cycles. The source is never served to the browser (invariant 1).
98
- - **Only files reachable from a browser-bound entry are servable.** The dev server walks the static import graph from every page / layout / error / loading / not-found / component file (lazily on first request, re-derived after each `fs.watch` rebuild); that Set is the authorisation gate, so anything no client code imports returns 404.
104
+ - **Only files reachable from a browser-bound entry are servable.** The dev server walks the import graph from every page / layout / error / loading / not-found / component file (lazily on first request, re-derived after each `fs.watch` rebuild); that Set is the authorisation gate, so anything no client code imports returns 404. The walk follows static `import` / `export … from` AND string-literal dynamic `import('./x.ts')` edges (#751), so a lazily-imported app module is servable (it is NOT eagerly preloaded, since a dynamic import is lazy by intent). A computed `import(expr)` cannot be resolved statically and 404s with a dev hint.
99
105
  - **Sensible defaults, overridable.** Memory store in dev, Redis when configured. Built-in auth, sessions, caching, rate limiting, file storage (all pluggable). Tailwind is the default styling. See `agent-docs/built-ins.md`.
100
106
 
101
107
  ---
@@ -111,9 +117,7 @@ webjs has **no server/client component split.** There is no RSC render tree, no
111
117
 
112
118
  A page/layout module still **loads** in the browser for its top-level side effects: registering imported components (so their tags upgrade). The client router is automatic and needs no import: it auto-enables when `@webjsdev/core` loads in the browser (the bundle every component pulls), so any page that ships a component gets client navigation for free (#620). That load is also how its imports reach the client (`import dayjs` at the top of a page fetches dayjs when the module loads, not via hydration). An inert page/layout is dead weight, which is exactly when elision drops it. A page/layout that is non-inert ONLY because it imports interactive components is **import-only** (#605): since it never hydrates, the boot emits its component modules directly and drops the page/layout module, so the browser fetches just the interactive leaves (it still ships whole when it has its own client side effect: a `client-router` import, a module-scope call, or a self-registering bare import).
113
119
 
114
- **Keep pages and layouts as pure carriers (#621), so their modules stay out of the network tab.** A page/layout drops from the boot (import-only / inert) only while its sole browser-relevant job is registering the components it imports. It starts SHIPPING its own module the moment its closure does any OTHER client work, which is invisible in `npm test` (an elision verdict, not a behaviour change) and easy to introduce by accident. Avoid, in a page/layout (and in any util it imports):
115
- - module-scope client work: a top-level call, a `new SomethingNonData()`, a browser-global (`window` / `document` / `customElements`) access, or a bare side-effect import of a non-component or `@webjsdev/core/client-router` (routing is automatic). A page-template `@event` / inline-`<script>` is fine (it is SSR output, never module client work, #623 / #634).
116
- - importing a client-effecting NON-component util. A helper that touches a client global or self-executes drags the whole page in. Put client-only behaviour inside a component; put server-only work in `.server.{js,ts}` (it never reaches the client closure); and if a util MIXES a pure helper with client-global code (the `cn.ts` shape, #619), split the client part into its own module so the pure helper does not pin every importer.
120
+ - importing a client-effecting NON-component util. A helper that touches a client global or self-executes drags the whole page in. Put client-only behaviour inside a component; put server-only work in `.server.{js,ts}` (it never reaches the client closure); and if a util MIXES a pure helper with client-global code (the `cn.ts` shape, #619), split the client part into its own module so the pure helper does not pin every importer. Note: if a page imports a client-effecting utility (like `cn.ts` which accesses client globals), the page module is marked as client-effecting and will be browser-shipped. When this happens, importing any bare server-only utility (like `*.server.ts` queries) inside that page will trigger a `no-server-import-in-browser-module` check violation. To fix this, convert the query to a `'use server'` action or fetch it through a route handler.
117
121
 
118
122
  Self-check: `page.ts` / `layout.ts` should NOT appear in the network tab or the boot `<script type="module">`; if one does, something in its closure does client work and is not a component.
119
123
 
@@ -152,7 +156,7 @@ sitemap.js robots.js manifest.js icon.js opengraph-image.js twitter-image.js app
152
156
  lib/ app-wide code (lib/*.server.js infra, lib/utils/ browser-safe helpers)
153
157
  modules/<feature>/ feature-scoped: actions/ (mutations), queries/ (reads), components/, utils/, types.js
154
158
  components/*.js SHARED presentational primitives
155
- public/* static assets, served at /<name>
159
+ public/* static assets, served at /public/<name> (favicon, sw.js, offline.html serve at root)
156
160
  db/*.server.{js,ts} data layer (Drizzle: schema, columns, connection)
157
161
  ```
158
162
 
@@ -195,12 +199,12 @@ The bare `@webjsdev/core` specifier resolves to a BROWSER bundle dropping server
195
199
  | `register(tag, C)` | Tag binding. Auto-called by `Class.register('tag')`. |
196
200
  | `render(v, el)` | Client-side render into a DOM element. |
197
201
  | `renderToString` | Server-side async render to HTML with DSD (from `/server`). |
198
- | `notFound()` / `redirect(url[, status])` | Throw to return 404, or a redirect. No-status default is convention-picked at the catching site: 302 for a GET page-render gate, 307 (method-preserving) for a server-action redirect. Override with `redirect(url, 308)` or `redirect(url, { status })`. |
202
+ | `notFound()` / `redirect(url[, status])` | Throw to return 404, or a redirect. No-status default is convention-picked at the catching site: 302 for a GET page-render gate, 307 (method-preserving) for a server-action redirect. Override with `redirect(url, 308)` or `redirect(url, { status })`. **NEVER** throw `redirect()` inside API route handlers (`route.ts`), as they must return a standard `Response.redirect(url, 303)` response object instead. |
199
203
  | `Suspense({fallback, children})` | Page/region-level streaming boundary (a value in a hole). `repeat` keyed-list directive is also re-exported. |
200
204
  | `<webjs-suspense .fallback=${html\`…\`}>` | Component-level streaming boundary element (#471): wraps one or more components, flushes `.fallback` on the first byte, streams the resolved content in (concurrently across boundaries, progressively on soft nav). The renderer-recognized opt-in for SLOW async-render data. |
201
205
  | `connectWS(url, handlers)` / `richFetch<T>` | Client WebSocket (auto-reconnect, queued sends); content-negotiated rich-type fetch. |
202
206
  | `navigate(url, opts?)` / `revalidate(url?)` | Programmatic client-router nav; evict the BROWSER snapshot cache. |
203
- | `optimistic(signal, value, action)` | Set `signal` immediately, run `action`, roll back on error or `{ success: false }`. |
207
+ | `optimistic(signal, value, action)` / `optimistic(host, { source, update })` | Imperative: set `signal` immediately, run `action`, roll back on error. Declarative (preferred): queue optimistic updates with auto-release via `.add(payload, promise?)`. See `agent-docs/advanced.md`. |
204
208
  | `renderStream(payload)` / `WebjsFrame` | `<webjs-stream>` element-level updates (#248); `<webjs-frame>` partial-swap regions (#253). See `agent-docs/advanced.md`. |
205
209
  | `Metadata` / `PageProps<R>` / `LayoutProps<R>` / `RouteHandlerContext<R>` / `WebjsConfig` (type-only) | Types for metadata, page/layout/route args (`R` narrows `params` against the `webjs types` route union), and the `webjs` config block. See `agent-docs/metadata.md` + `agent-docs/configuration.md`. |
206
210
 
@@ -237,7 +241,7 @@ MyThing.register('my-thing');
237
241
 
238
242
  **Signals are the default state primitive.** Import `signal` / `computed` from `@webjsdev/core`, read with `signal.get()` inside `render()`, and the built-in `SignalWatcher` re-renders on change. Module-scope signals share state across components and survive navigations; instance signals (constructor) are component-local.
239
243
 
240
- **Reactive properties ride HTML attributes, reflect to them, or arrive via SSR hydration.** They are declared ONLY by passing the property shape into the base-class factory `WebComponent({ ... })`. There is no `static properties` block and no `declare` line. The bare form takes a type constructor (`count: Number`, `label: String`, `open: Boolean`); the `prop()` helper carries options (`count: prop(Number, { reflect: true })`, `name: prop(String, { attribute: 'show-close-button' })`, internal state `prop({ state: true })` with no type), and narrows the TS type (`student: prop<Student>(Object)`, `mode: prop<'a' | 'b'>(String, { reflect: true })`). The types flow automatically to `this.<prop>`. Set defaults in the constructor after `super()` (a declarative `default` option also exists for lit-parity, a function default running per instance for a fresh object / array, but the constructor is the recommended way). NEVER use a class-field initializer (e.g., `count = 0`), which clobbers the reactive accessor (caught by `reactive-props-no-class-field`). Declaring your own `static properties` THROWS at runtime and is flagged by `no-static-properties`. Declare an array-typed prop with the `Array` constructor, not `Object` (`items: prop<Tag[]>(Array)`): the two share one JSON converter so neither crashes, but `Array` states the prop's shape and `array-prop-uses-array-type` flags the `Object` form. Property options: `type` (default `String`), `reflect`, `state`, `hasChanged`, `converter`, `default`, `attribute`.
244
+ **Reactive properties ride HTML attributes, reflect to them, or arrive via SSR hydration.** They are declared ONLY by passing the property shape into the base-class factory `WebComponent({ ... })`. There is no `static properties` block and no `declare` line. The bare form takes a type constructor (`count: Number`, `label: String`, `open: Boolean`); the `prop()` helper carries options (`count: prop(Number, { reflect: true })`, `name: prop(String, { attribute: 'show-close-button' })`, internal state `prop({ state: true })` with no type), and narrows the TS type (`student: prop<Student>(Object)`, `mode: prop<'a' | 'b'>(String, { reflect: true })`). The types flow automatically to `this.<prop>`. Set defaults in the constructor after `super()` (a declarative `default` option also exists for lit-parity, a function default running per instance for a fresh object / array, but the constructor is the recommended way). NEVER use a class-field declaration OR initializer (e.g., `count = 0` or `todos!: Todo[]`), which compiles to define an own property on the instance and clobbers the reactive accessor (caught by `reactive-props-no-class-field`). Declaring your own `static properties` THROWS at runtime and is flagged by `no-static-properties`. Declare an array-typed prop with the `Array` constructor, not `Object` (`items: prop<Tag[]>(Array)`): the two share one JSON converter so neither crashes, but `Array` states the prop's shape and `array-prop-uses-array-type` flags the `Object` form. Property options: `type` (default `String`), `reflect`, `state`, `hasChanged`, `converter`, `default`, `attribute`.
241
245
 
242
246
  **Lifecycle (lit-aligned), in order:** `shouldUpdate`, `willUpdate`, controllers' `hostUpdate()`, `update` (calls `render()` + commits), controllers' `hostUpdated()`, `firstUpdated`, `updated`, `updateComplete`, each receiving a `changedProperties` Map. **SSR runs only the constructor, attribute application, the pre-render hooks (`willUpdate` / `hostUpdate`), `reflect: true` reflection, and `render()`; it does NOT call `connectedCallback`, `firstUpdated`, `updated`, or any browser-only hook.** So defaults for first paint go in the constructor; browser-only data (localStorage, viewport, `navigator.*`) goes in `connectedCallback` writing a signal; server-known data arrives via the page function. Never ship a placeholder first paint that fetches in `connectedCallback`. A browser-only global in the constructor/`render()` throws at SSR (flagged by `no-browser-globals-in-render`; attribute methods and `closest()` are shimmed).
243
247
 
@@ -286,7 +290,7 @@ Optional app-root file default-exporting a **schema object** (env-var names to a
286
290
  | Plain `.ts` | yes | **Lint violation** (`use-server-needs-extension`). Rename to add `.server.`. |
287
291
  | Plain `.ts` | no | Browser-safe; standard. |
288
292
 
289
- The server-only-utility row (`.server.ts`, no `'use server'`) is a runtime trap: its browser stub throws at module load, so a page / layout / component that ends up SHIPPING to the browser and transitively imports one crashes the moment the module loads, while `webjs typecheck` and the rest of `webjs check` pass. `webjs check`'s `no-server-import-in-browser-module` rule catches this statically by reusing the build's elision verdict (it only flags modules that genuinely ship; a display-only page the framework elides is fine, because the framework strips its server import). A `'use server'` action is exempt: its browser stub is a working RPC, which is the intended way to call the server from a shipping module.
293
+ The server-only-utility row (`.server.ts`, no `'use server'`) is a runtime trap: its browser stub throws at module load, so a page / layout / component that ends up SHIPPING to the browser and transitively imports one crashes the moment the module loads, while `webjs typecheck` and the rest of `webjs check` pass. `webjs check`'s `no-server-import-in-browser-module` rule catches this statically by reusing the build's elision verdict (it only flags modules that genuinely ship; a display-only page the framework elides is fine, because the framework strips its server import). A `'use server'` action is exempt: its browser stub is a working RPC, which is the intended way to call the server from a shipping module. A TYPE-ONLY `import type { Row } from './x.server.ts'` is exempt too, because the TypeScript stripper erases it before it can reach the browser, so sharing a derived row type from a `.server.ts` into a shipping component is safe and is not flagged (#805).
290
294
 
291
295
  Server actions export named async functions whose args + returns round-trip through the serializer. **Importing from a client component IS the API** (rewritten to an RPC stub; never hand-write `fetch()`). **REST over HTTP is a `route.ts`** that imports and calls the action (optionally via the `route(action, opts?)` adapter from `@webjsdev/server`, which merges query + route params + JSON body into one input object and JSON-responds). **Input validation (#245)** is declared via the `export const validate` config export (read on the RPC boundary); a `route()` endpoint passes the same validator as its `{ validate }` option. The framework only CALLS the validator (ships no validation library) and reads its return (`{ success: true, data? }` runs the action, `{ success: false, fieldErrors }` returns a `422` without running the body, a THROW is a sanitized error, any other value is transformed input). The validator stays server-side and receives the action's first argument. Full reference in `agent-docs/recipes.md`.
292
296
 
@@ -294,7 +298,7 @@ Server actions export named async functions whose args + returns round-trip thro
294
298
 
295
299
  ### RPC + REST endpoint security
296
300
 
297
- Action RPC CSRF is an **Origin / `Sec-Fetch-Site` check** (the Remix 3 / Go 1.25 model), NOT a token cookie: a state-changing verb passes only when `Sec-Fetch-Site` is `same-origin` / `none`, or (older browsers, no `Sec-Fetch-Site`) the `Origin` host matches the request host, or the source is in `webjs.allowedOrigins`; otherwise 403. **No `Set-Cookie` rides the SSR HTML**, which is why a page that opts into a public `Cache-Control` (via `metadata.cacheControl`, e.g. on a root layout for a whole visitor-identical app) is CDN-edge-cacheable. A safe GET action is CSRF-exempt. Prod errors are sanitized to `message` only. A `route.ts` REST endpoint (hand-written or via the `route()` adapter) is NOT covered by this check: authenticate every mutating endpoint, use `validate`, log without secrets, rate-limit. For CORS use the `cors()` middleware from `@webjsdev/server`; **`credentials: true` REQUIRES an explicit origin allowlist, never `'*'`.** See `agent-docs/advanced.md`.
301
+ Action RPC CSRF is an **Origin / `Sec-Fetch-Site` check** (the Remix 3 / Go 1.25 model), NOT a token cookie: a state-changing verb passes only when `Sec-Fetch-Site` is `same-origin` / `none`, or (older browsers, no `Sec-Fetch-Site`) the `Origin` host matches the request host, or the source is in `webjs.allowedOrigins`; otherwise 403. **No `Set-Cookie` rides the SSR HTML**, which is why a page that opts into a public `Cache-Control` (via `metadata.cacheControl`, e.g. on a root layout for a whole visitor-identical app) is CDN-edge-cacheable. A safe GET action is CSRF-exempt. **Prod action errors are sanitized to a GENERIC message plus a short `digest`** (#749): a thrown action never returns its raw message to the client (a DB-driver message, an internal IP, an fs path are not author-controlled), the full error is logged server-side keyed by that digest, and the streaming error frame is sanitized the same way; `redirect()` / `notFound()` control-flow throws pass through, and a user-facing message belongs on the `ActionResult` `{ success: false, error }` envelope, not on a throw. A `route.ts` REST endpoint (hand-written or via the `route()` adapter) is NOT covered by this check: authenticate every mutating endpoint, use `validate`, log without secrets, rate-limit. For CORS use the `cors()` middleware from `@webjsdev/server`; **`credentials: true` REQUIRES an explicit origin allowlist, never `'*'`.** See `agent-docs/advanced.md`.
298
302
 
299
303
  ### Components (`components/*.{js,ts}`)
300
304
 
@@ -336,9 +340,86 @@ The advanced client-router surface is in `agent-docs/advanced.md`: **link prefet
336
340
 
337
341
  ---
338
342
 
343
+ ## Mutations: default to optimistic UI
344
+
345
+ **Use `optimistic()` from `@webjsdev/core` for every user-facing mutation** (create, update, delete, like, toggle, reorder) where the client can predict the result. The UI should feel instant; roll back automatically on failure.
346
+
347
+ ### Preferred: declarative API
348
+
349
+ ```ts
350
+ import { WebComponent, prop, optimistic, html } from '@webjsdev/core';
351
+ import { createTodo } from '#modules/todos/actions/create-todo.server.ts';
352
+
353
+ class TodoList extends WebComponent({
354
+ todos: prop<Todo[]>(Array),
355
+ }) {
356
+ private optimisticTodos = optimistic(this, {
357
+ source: () => this.todos,
358
+ update: (state, title: string) => [
359
+ ...state,
360
+ { id: crypto.randomUUID() as any, title, completed: false, pending: true },
361
+ ],
362
+ });
363
+
364
+ async handleSubmit(e: SubmitEvent) {
365
+ e.preventDefault();
366
+ const title = new FormData(e.target as HTMLFormElement).get('title') as string;
367
+ if (!title) return;
368
+ (e.target as HTMLFormElement).reset();
369
+
370
+ const promise = createTodo({ title });
371
+ this.optimisticTodos.add(title, promise);
372
+
373
+ const result = await promise;
374
+ if (result.success && result.data) {
375
+ this.todos = [...this.todos.filter(t => t.pending), result.data, ...this.todos.filter(t => !t.pending)];
376
+ }
377
+ }
378
+
379
+ render() {
380
+ return html`<ul>${this.optimisticTodos.value.map(todo => html`
381
+ <li class=${todo.pending ? 'opacity-50' : ''}>${todo.title}</li>
382
+ `)}</ul>`;
383
+ }
384
+ }
385
+ TodoList.register('todo-list');
386
+ ```
387
+
388
+ `.add(payload, promise)` auto-releases when the promise settles (resolve or reject). No try-catch, no manual rollback, no temp-ID bookkeeping.
389
+
390
+ ### Simple flips: imperative API
391
+
392
+ For boolean toggles where the value itself is the mutation:
393
+
394
+ ```ts
395
+ import { signal, optimistic } from '@webjsdev/core';
396
+ import { likePost } from '#actions/like-post.server.ts';
397
+
398
+ const liked = signal(false);
399
+ // in an @click handler:
400
+ const result = await optimistic(liked, true, () => likePost(postId));
401
+ ```
402
+
403
+ ### When to skip optimistic UI
404
+
405
+ - The result is unpredictable (AI-generated content, server-computed values the client cannot guess).
406
+ - The mutation has side effects the user must wait for (payment processing, email sending, OAuth).
407
+ - The action validates against data that may have changed server-side (unique constraints, race conditions).
408
+ - The mutation is destructive and irreversible with no undo (confirm-first UX is better).
409
+
410
+ ### When optimistic UI is appropriate
411
+
412
+ - Todo items, comments, posts, likes, follows, toggles, reorders, renames, status changes.
413
+ - Any mutation where the client can construct the expected result from the input.
414
+ - CRUD operations where the server returns the same shape the client already has.
415
+
416
+ **Never write manual try-catch / cache-and-restore / temp-ID reconciliation** when `optimistic()` covers the pattern. See `agent-docs/advanced.md` for the full reference and `agent-docs/recipes.md` for complete examples.
417
+
418
+ ---
419
+
339
420
  ## Invariants (for both humans and agents)
340
421
 
341
- > Hit one of these as a runtime error? The [Troubleshooting page](https://docs.webjs.com/docs/troubleshooting) is keyed by symptom (the throw-at-load server import, the backtick-in-template 500, the TypeScript strip failure, the SSR browser-global crash, the missing-frame swap) and maps each back to the invariant and the `webjs check` rule below.
422
+ > Hit one of these as a runtime error? The [Troubleshooting page](https://docs.webjs.dev/docs/troubleshooting) is keyed by symptom (the throw-at-load server import, the backtick-in-template 500, the TypeScript strip failure, the SSR browser-global crash, the missing-frame swap) and maps each back to the invariant and the `webjs check` rule below.
342
423
 
343
424
  1. **Server-only code goes in `.server.{js,ts}` files, `route.ts` handlers, or `middleware.ts`. Never in pages, layouts, or components.** The `.server.{js,ts}` extension is the path-level boundary (the file router refuses to serve the source); a `'use server'` directive additionally makes exports RPC-callable, else the file is a server-only utility whose browser import is a throw-at-load stub. Importing a DB driver (`pg`), `node:*`, or any server-only dep from a component or an `app/**` page / layout / loading / error / not-found file crashes the browser at module load.
344
425
  2. **Every `*.server.{js,ts}` file with `'use server'` exports must be `async` functions returning serializer-safe values.** Args and results round-trip via webjs's wire. Files without `'use server'` (server-only utilities) can export anything, including singletons.
@@ -359,7 +440,7 @@ The advanced client-router surface is in `agent-docs/advanced.md`: **link prefet
359
440
 
360
441
  Three scaffolds exist (do not invent template names): `webjs create <name>` (full-stack: layout, page, components, modules, Drizzle+SQLite), `webjs create <name> --template api` (backend-only routes + modules + Drizzle, no SSR), `webjs create <name> --template saas` (auth + login/signup + protected dashboard + User model). The `--db sqlite|postgres` flag (default sqlite) picks the dialect; the schema/queries/actions are identical across dialects (see #563). The `--runtime node|bun` flag (default node, #541) is ORTHOGONAL to the template and re-flavors any of the three for Bun (`bun --bun` dev/start scripts so the SERVER runs on Bun, `bun.lock`, a pure `oven/bun:1` Dockerfile (#595; safe since cli@0.10.20's npx-free `webjs db migrate` (#570) needs no Node in the image) + bun-install CI, bun-command agent docs; the test/db/check tooling stays on Node); `bun create webjs <name>` auto-detects it. Pick from the request: default for any product with UI (todo, blog, dashboard, marketplace, social, e-commerce), `api` for an HTTP/JSON API with no UI, `saas` for accounts/login/signup; default to full-stack when ambiguous.
361
442
 
362
- Rules: **always scaffold via `webjs create`** (never hand-roll). **Default to a real database (Drizzle + SQLite); NEVER use JSON files, in-memory arrays, or localStorage for persistence.** Update `db/schema.server.ts` to real models FIRST, then `webjs db generate` + `webjs db migrate`, then build pages/actions/queries. **Treat the scaffold as REFERENCE, not the final product:** replace the example page / `User` model / components and adapt `app/layout.ts` (brand, nav, content width; the default `<main class="max-w-[760px]">` reading column needs widening for a full-bleed app). ENFORCED: examples carry a `webjs-scaffold-placeholder` comment and `no-scaffold-placeholder` fails until the content is replaced and the marker deleted. Docs at https://docs.webjs.com.
443
+ Rules: **always scaffold via `webjs create`** (never hand-roll). **Default to a real database (Drizzle + SQLite); NEVER use JSON files, in-memory arrays, or localStorage for persistence.** Update `db/schema.server.ts` to real models FIRST, then `webjs db generate` + `webjs db migrate`, then build pages/actions/queries. **Treat the scaffold as REFERENCE, not the final product:** replace the example page / `User` model / components and adapt `app/layout.ts` (brand, nav, content width; the default `<main class="max-w-[760px]">` reading column needs widening for a full-bleed app). ENFORCED: examples carry a `webjs-scaffold-placeholder` comment and `no-scaffold-placeholder` fails until the content is replaced and the marker deleted. **The scaffold IS the primary teaching surface:** every UI scaffold (full-stack AND saas) ships a densely commented feature gallery (one webjs concept per demo under `app/features/`, logic in `modules/`, plus a whole example app under `app/examples/`), and the `api` template ships a backend-features showcase under `app/api/features/` (the `route()` adapter + validation, rate limiting, streaming, file storage, WebSockets + broadcast) listed in the root `app/route.ts` index; read the demos to learn the idioms, then prune per-feature. Docs at https://docs.webjs.dev.
363
444
 
364
445
  ---
365
446
 
@@ -41,10 +41,13 @@ On a client-router navigation to a streamed page, the router applies the respons
41
41
 
42
42
  Five stacked zero-build optimizations:
43
43
 
44
- 1. **`<link rel="modulepreload">` per used component + transitive deps.**
44
+ 1. **`<link rel="modulepreload">` per used component + transitive deps + reached vendors.**
45
45
  The SSR pass knows every custom element in the final HTML. A startup
46
- module-graph scan adds their transitive import dependencies too. All
47
- preload hints are deduplicated and emitted in `<head>`.
46
+ module-graph scan adds their transitive import dependencies too, AND the
47
+ npm vendor URLs those shipped modules import (#754, flattening the
48
+ cross-origin CDN waterfall one level; see the no-build model below for the
49
+ shallow-dependency caveat). All preload hints are deduplicated and emitted
50
+ in `<head>`, vendor hints carrying their SRI `integrity` + `crossorigin`.
48
51
  2. **HTTP/2 multiplex at the edge.** The production server (`npm run start`) speaks plain
49
52
  HTTP/1.1. PaaS edges (Railway, Fly, Render, Vercel, Cloudflare Pages,
50
53
  Heroku) and reverse proxies (nginx, Caddy, Traefik) speak
@@ -132,9 +135,23 @@ production. The Rails 7+ / Hotwire pattern:
132
135
  resource. The browser walks the import graph and fetches each module
133
136
  on demand.
134
137
  - **`<link rel="modulepreload">` hints at SSR time**: for every component
135
- the route uses + its transitive deps from the module graph. The
136
- browser parallelizes fetches instead of waterfalling through nested
137
- imports. This is what eliminates the perceived gap vs a bundle.
138
+ the route uses + its transitive deps from the module graph, AND for the
139
+ npm **vendor** URLs those shipped modules reach (#754). The browser
140
+ parallelizes fetches instead of waterfalling through nested imports. This
141
+ is what closes most of the perceived gap vs a bundle. **The honest caveat:**
142
+ a bundle still wins on a DEEP vendor tree. webjs flattens the FIRST level
143
+ (the vendor entries your code imports are hinted up front, with their SRI
144
+ `integrity`, byte-identical to the importmap target so there is no double
145
+ fetch), but a vendor's OWN transitive deps are still discovered by parsing
146
+ each fetched CDN module in turn, level by level, over the cross-origin CDN
147
+ connection. So the complementary mitigation is **shallow-dependency
148
+ discipline**: prefer few, shallow ESM dependencies (a library with a flat
149
+ or one-level graph fully benefits; a deep tree still waterfalls past level
150
+ one). Only REACHED vendors are hinted: a vendor imported solely by an
151
+ elided display-only component, by a page/layout module dropped from the boot
152
+ (an inert or import-only page whose binding vendor import is used only during
153
+ SSR, the canonical SSR-only-dependency case), by a `.server.*` file, or
154
+ pinned-but-unimported, is never preloaded (no over-fetch).
138
155
  - **HTTP/2 multiplex** is what makes per-file serving competitive: one
139
156
  TCP+TLS handshake, many module fetches in parallel over the same
140
157
  connection. The production server (`npm run start`) speaks plain HTTP/1.1.
@@ -486,10 +503,56 @@ the events + `aria-busy` are a client-only enhancement.
486
503
 
487
504
  ### Optimistic mutations (`optimistic()`)
488
505
 
489
- `optimistic(signal, value, action)` from `@webjsdev/core` shows a mutation's
490
- expected result IMMEDIATELY (the UI feels instant), runs the real server action,
491
- and ROLLS BACK on failure. It is a thin wrapper over the signal primitive, no
492
- state machine.
506
+ `optimistic()` from `@webjsdev/core` shows a mutation's expected result IMMEDIATELY (the UI feels instant), runs the real server action, and ROLLS BACK on failure. **Use it for every user-facing mutation where the client can predict the result** (create, update, delete, like, toggle, reorder). See `agent-docs/recipes.md` for complete examples.
507
+
508
+ #### Declarative API (preferred)
509
+
510
+ `optimistic(host, { source, update })` returns an `OptimisticState<State, Action>` with a `.value` getter and `.add(payload, promise?)` method. The reducer transforms the current state with each payload; `.add()` pushes an update and schedules a re-render. When a `promise` is passed, the update auto-releases on settlement (`.finally()` or a `.then()` fallback for thenables lacking `.finally`).
511
+
512
+ ```ts
513
+ import { WebComponent, prop, optimistic, html } from '@webjsdev/core';
514
+ import { createTodo } from '#modules/todos/actions/create-todo.server.ts';
515
+
516
+ class TodoList extends WebComponent({
517
+ todos: prop<Todo[]>(Array),
518
+ }) {
519
+ private optimisticTodos = optimistic(this, {
520
+ source: () => this.todos,
521
+ update: (state, title: string) => [
522
+ ...state,
523
+ { id: crypto.randomUUID() as any, title, completed: false, pending: true },
524
+ ],
525
+ });
526
+
527
+ async handleSubmit(e: SubmitEvent) {
528
+ e.preventDefault();
529
+ const title = new FormData(e.target as HTMLFormElement).get('title') as string;
530
+ if (!title) return;
531
+ (e.target as HTMLFormElement).reset();
532
+
533
+ const promise = createTodo({ title });
534
+ this.optimisticTodos.add(title, promise);
535
+
536
+ const result = await promise;
537
+ if (result.success && result.data) {
538
+ this.todos = [...this.todos.filter(t => t.pending), result.data, ...this.todos.filter(t => !t.pending)];
539
+ }
540
+ }
541
+
542
+ render() {
543
+ return html`<ul>${this.optimisticTodos.value.map(todo => html`
544
+ <li class=${todo.pending ? 'opacity-50' : ''}>${todo.title}</li>
545
+ `)}</ul>`;
546
+ }
547
+ }
548
+ TodoList.register('todo-list');
549
+ ```
550
+
551
+ Multiple `.add()` calls stack independently; each `release()` removes its own update by ID. When `update` is omitted, the payload replaces the state directly (`Action = State`), matching the simple `useOptimistic(setState)` pattern.
552
+
553
+ #### Imperative API (simple flips)
554
+
555
+ `optimistic(signal, value, action)` is a thin wrapper over the signal primitive for simple boolean flips:
493
556
 
494
557
  ```ts
495
558
  import { signal, optimistic } from '@webjsdev/core';
@@ -505,9 +568,7 @@ const result = await optimistic(liked, true, () => likePost(postId));
505
568
  // to the authoritative value from `result` if you need it.
506
569
  ```
507
570
 
508
- It rolls back on a thrown error OR an `ActionResult` `{ success: false }`
509
- envelope, and never on success. Client-only (it mutates a signal), so a
510
- component importing it is never elided as display-only.
571
+ It rolls back on a thrown error OR an `ActionResult` `{ success: false }` envelope, and never on success. Client-only (it mutates a signal), so a component importing it is never elided as display-only.
511
572
 
512
573
  ### Wire-byte optimization
513
574
 
@@ -642,6 +703,28 @@ document.addEventListener('webjs:prefetch', (e) => {
642
703
  });
643
704
  ```
644
705
 
706
+ ### `webjs:before-cache` (strip transient state before a back/forward snapshot)
707
+
708
+ The router keeps a URL-keyed snapshot cache (Turbo's SnapshotCache pattern) so
709
+ Back/Forward restores instantly. Because a snapshot is a raw `outerHTML` clone of
710
+ the live page, anything OPEN at navigate-away time (a hover-card, a dropdown, a
711
+ toast) is captured open and **restored open** on Forward. To prevent that, the
712
+ router dispatches `webjs:before-cache` on `document` **synchronously, on the page
713
+ being cached, immediately before the `outerHTML` read** (detail `{ url }`). A
714
+ handler's DOM mutations are captured in the snapshot; the live edits are
715
+ invisible because the page is being navigated away from (Turbo's
716
+ `turbo:before-cache` contract). Reset transient state here:
717
+
718
+ ```ts
719
+ document.addEventListener('webjs:before-cache', () => {
720
+ document.querySelectorAll('[data-transient]').forEach((el) => el.remove());
721
+ // close open menus, clear in-progress toasts, reset a wizard step, ...
722
+ });
723
+ ```
724
+
725
+ The kit's overlays (hover-card, tooltip, dropdown-menu, dialog, alert-dialog,
726
+ sonner) already listen and reset their open state, so they come back closed.
727
+
645
728
  ### View Transitions (opt-in, all three swap paths)
646
729
 
647
730
  The router can wrap a client navigation's DOM mutation in the native
@@ -1163,7 +1246,7 @@ prod. A truncated stream (a server crash, a dropped connection) also throws
1163
1246
  rather than completing silently: a healthy stream always ends in an explicit
1164
1247
  terminal frame, so a missing one is an error. For a slow region you want behind a fallback on the FIRST paint, use
1165
1248
  `<webjs-suspense>` instead; streaming RPC is for an imperative stream consumed
1166
- after an interaction. Full reference: the [Data fetching](https://docs.webjs.com/docs/data-fetching) page.
1249
+ after an interaction. Full reference: the [Data fetching](https://docs.webjs.dev/docs/data-fetching) page.
1167
1250
 
1168
1251
  ### Opt out per link
1169
1252
 
@@ -59,6 +59,13 @@ export const listPosts = cache(
59
59
  );
60
60
  ```
61
61
 
62
+ Cached values round-trip through the same rich serializer as the RPC wire
63
+ (`Date` / `Map` / `Set` / `BigInt` / typed arrays / cycles), so a warm cache
64
+ hit returns the exact same value shape as a cold miss (a row's `createdAt`
65
+ stays a `Date`, never degrades to a string). Cache keys are rich-serialized
66
+ too, so a `Map` / `Set` argument is a distinct key per value instead of
67
+ colliding. You do not need cached values to be JSON-plain.
68
+
62
69
  To invalidate a read from an UNRELATED mutation (without importing the
63
70
  wrapper), add `tags`. It is either a static `string[]` or a function
64
71
  `(...args) => string[]` so a per-entity read tags with the id:
@@ -105,13 +112,16 @@ exact id by tag. An untagged `cache()` is untouched by any `revalidateTag`.
105
112
  evicts cached HTML. Together they are the **server cache invalidation
106
113
  surface**, both imported from `@webjsdev/server`.
107
114
 
108
- **Multi-instance note.** The tag index is a thin, non-atomic
109
- read-modify-write of a JSON array in the store. With a shared Redis store,
110
- `revalidateTag` reaches every instance for the keys it can see, but two
111
- instances appending to one tag concurrently can lose an append, so a
112
- freshly-stored key on a peer might miss eviction and live until its TTL.
113
- The index entry carries the cache TTL so it self-prunes. For strict
114
- cross-instance invalidation, prefer a short `ttl` as the floor.
115
+ **Multi-instance note.** On the built-in memory and Redis stores the tag
116
+ index is a real set (a native `Set` in memory, a Redis `SADD` set), so
117
+ adding a cache key to a tag is an atomic insert. Two mutations appending to
118
+ the same tag concurrently (across Redis instances, or interleaved in one
119
+ process) no longer lose an entry, so `revalidateTag` reliably evicts every
120
+ tagged key (#752). A custom store that does not implement the optional
121
+ atomic-set methods (`setAdd` / `setMembers`) falls back to the older
122
+ non-atomic JSON path, where a concurrent append can be lost; there, prefer a
123
+ short `ttl` as the cross-instance floor. The index entry carries the cache
124
+ TTL so it self-prunes either way.
115
125
 
116
126
  ### Server HTML response cache (`export const revalidate`, ISR for no-build)
117
127
 
@@ -0,0 +1,87 @@
1
+ # Component member shadowing: names a WebComponent must not reuse
2
+
3
+ A `WebComponent` inherits two layers of members. In the browser it extends
4
+ `HTMLElement` (so every DOM property and method is present), and it extends the
5
+ framework base that owns the reactivity lifecycle (`render`, `update`,
6
+ `requestUpdate`, and the lit-aligned hooks). SSR swaps `HTMLElement` for an
7
+ `ElementShim`-style stand-in with the same public surface. When app code
8
+ declares a reactive property (via the `WebComponent({ ... })` factory) or a
9
+ method whose NAME collides with one of those inherited members, one of two
10
+ things happens.
11
+
12
+ - **Type-incompatible collision.** TypeScript refuses the class. A reactive
13
+ property that shadows a native property of a different type raises **`TS2415`**
14
+ on the `class X extends WebComponent({ ... })` line ("Class 'X' incorrectly
15
+ extends base class ...; types of property 'y' are incompatible"). A method or
16
+ field in the class body whose signature differs from the inherited one raises
17
+ **`TS2416`** on that member ("Property 'y' in type 'X' is not assignable to the
18
+ same property in base type").
19
+ - **Type-compatible collision.** It compiles, then silently hijacks the native
20
+ member at runtime (a `title` reactive prop overrides the element's tooltip /
21
+ `title`-attribute reflection, a `hidden` prop overrides native hide behavior).
22
+ No error, wrong behavior.
23
+
24
+ The fix for every row below is the same. **Rename the field or method** to a
25
+ name that is not an inherited member (`postTitle` instead of `title`,
26
+ `removeItem` instead of `remove`). Only override an inherited lifecycle method
27
+ deliberately and with its exact signature (override `render` / `connectedCallback`
28
+ on purpose, never repurpose the name for unrelated app logic).
29
+
30
+ ## Members not to shadow
31
+
32
+ | Member | Origin | Base type or signature | Shadow with an app prop / method | Error | Fix (rename to) |
33
+ |---|---|---|---|---|---|
34
+ | `title` | HTMLElement | `string` | `title: prop<Post>(Object)` (a post object) | `TS2415` | `post`, `heading` |
35
+ | `id` | HTMLElement | `string` | `id: prop(Number)` (a numeric row id) | `TS2415` | `postId`, `rowId` |
36
+ | `slot` | HTMLElement | `string` | `slot: prop<Slot>(Object)` | `TS2415` | `bookingSlot` |
37
+ | `role` | HTMLElement | `string \| null` | `role: prop<Role>(Object)` (a user role) | `TS2415` | `userRole` |
38
+ | `hidden` | HTMLElement | `boolean` | `hidden: prop(String)` | `TS2415` | `isHidden` (or keep `boolean`) |
39
+ | `dir` | HTMLElement | `string` | `dir: prop<Direction>(Object)` | `TS2415` | `direction` |
40
+ | `lang` | HTMLElement | `string` | `lang: prop<Lang>(Object)` | `TS2415` | `language` |
41
+ | `translate` | HTMLElement | `boolean` | `translate: prop(String)` | `TS2415` | `translationKey` |
42
+ | `draggable` | HTMLElement | `boolean` | `draggable: prop(String)` | `TS2415` | `isDraggable` |
43
+ | `tabIndex` | HTMLElement | `number` | `tabIndex: prop(String)` | `TS2415` | `tabOrder` |
44
+ | `className` | HTMLElement | `string` | `className: prop<string[]>(Array)` | `TS2415` | `variantClasses` |
45
+ | `dataset` | HTMLElement | `DOMStringMap` | `dataset: prop<Data>(Object)` | `TS2415` | `payload`, `meta` |
46
+ | `remove` | Element | `(): void` | `remove(id: number): Promise<void>` | `TS2416` | `removeItem`, `deleteRow` |
47
+ | `closest` | Element | `(sel: string) => Element \| null` | `closest(n: number)` | `TS2416` | `nearest` |
48
+ | `matches` | Element | `(sel: string) => boolean` | `matches(other: T)` | `TS2416` | `isMatch` |
49
+ | `focus` | HTMLElement | `(opts?) => void` | `focus(field: string)` | `TS2416` | `focusField` |
50
+ | `blur` | HTMLElement | `(): void` | `blur(amount: number)` | `TS2416` | `applyBlur` |
51
+ | `click` | HTMLElement | `(): void` | `click(e: Event)` | `TS2416` | `handleClick` |
52
+ | `append` / `prepend` | Element | `(...nodes) => void` | `append(item: T)` | `TS2416` | `appendItem` |
53
+ | `before` / `after` | Element | `(...nodes) => void` | `after(cb: () => void)` | `TS2416` | `runAfter` |
54
+ | `render` | WebComponent | `() => TemplateResult \| Promise<...>` | `render(data: T)` (arg added) | `TS2416` | override with the real signature |
55
+ | `update` | WebComponent | `(changed: Map) => void` | `update(input: T)` | `TS2416` | `applyUpdate`, `save` |
56
+ | `requestUpdate` | WebComponent | `(name?, old?) => void` | `requestUpdate(payload: T)` | `TS2416` | `queueUpdate` |
57
+ | `updated` / `firstUpdated` | WebComponent | `(changed: Map) => void` | `updated(row: T)` | `TS2416` | `onUpdated` |
58
+ | `willUpdate` / `shouldUpdate` | WebComponent | `(changed: Map) => boolean \| void` | repurposed for app logic | `TS2416` | rename or override correctly |
59
+ | `connectedCallback` | WebComponent | `(): void` | `connectedCallback(user: T)` | `TS2416` | override with no args |
60
+ | `renderError` / `renderFallback` | WebComponent | `(err?) => TemplateResult` | `renderFallback(id: number)` | `TS2416` | override with the real signature |
61
+ | `addController` / `removeController` | WebComponent | `(c: Controller) => void` | app method of the same name | `TS2416` | rename |
62
+ | `updateComplete` | WebComponent | `get(): Promise<boolean>` | `updateComplete: prop(Boolean)` | `TS2415` | `isComplete` |
63
+
64
+ Framework-private fields are all underscore-prefixed (`_renderRoot`,
65
+ `_connected`, `_changedProperties`, `_updatePromise`, `_isUpdating`). Never
66
+ declare a reactive prop or field with a leading underscore that matches one, and
67
+ never write to them from app code. Use a plainly-named reactive prop or a signal
68
+ instead.
69
+
70
+ ## Names that are safe (not inherited)
71
+
72
+ Common component prop names that do NOT collide, so they need no rename.
73
+
74
+ `label`, `open`, `count`, `value` (declare it, native `HTMLElement` has none),
75
+ `name`, `items`, `todos`, `active`, `variant`, `size`, `checked`, `selected`,
76
+ `heading`, `caption`, `message`, `status`.
77
+
78
+ Note. `open`, `value`, `checked`, and `selected` ARE native properties on
79
+ specific built-in elements (`<details>`, `<input>`, `<option>`), but a
80
+ `WebComponent` extends the generic `HTMLElement`, which does not define them, so
81
+ they are free on a custom element. When in doubt, grep the base surface in
82
+ `packages/core/src/component.js` (or `node_modules/@webjsdev/core/src/component.js`
83
+ in an app) rather than guessing.
84
+
85
+ Cross-references. Reactive property declaration is in the root `AGENTS.md`
86
+ (`WebComponent` essentials) and `agent-docs/components.md`. The lit patterns that
87
+ break webjs reactivity are in `agent-docs/lit-muscle-memory-gotchas.md`.
@@ -59,7 +59,7 @@ class Dialog extends WebComponent({
59
59
  }
60
60
  ```
61
61
 
62
- Set defaults in the constructor after `super()`. A declarative `default` option also exists (lit-parity, a function default runs per instance for a fresh object / array), but the constructor is the recommended way. **Never** use a class-field initializer (e.g., `count = 0` or `student: Student = { ... }`): the framework installs reactive getter/setter on `this` inside the constructor via `Object.defineProperty`, and a class-field initializer compiles to an assignment after `super()` that uses `[[Define]]` to overwrite the accessor, silently breaking reactivity. The `reactive-props-no-class-field` rule catches this.
62
+ Set defaults in the constructor after `super()`. A declarative `default` option also exists (lit-parity, a function default runs per instance for a fresh object / array), but the constructor is the recommended way. **Never** use a class-field declaration OR initializer (e.g., `count = 0`, `student: Student = { ... }`, or `todos!: Todo[]`): even a type-only declaration like `todos!: Todo[]` compiles under modern TS configurations (with `useDefineForClassFields: true`) to define a property on the instance after `super()`, which shadows and clobbers the prototype's reactive accessor, silently breaking reactivity. Only declare properties in the factory, and read/write them directly off `this`. The `reactive-props-no-class-field` rule catches this.
63
63
 
64
64
  ```ts
65
65
  // set defaults in the constructor after super()
@@ -96,7 +96,7 @@ The SSR pipeline runs the **pre-render value-deriving hooks** before `render()`:
96
96
 
97
97
  For component-local state, create an instance signal in the constructor and call `signal.set(...)` to mutate. The built-in `SignalWatcher` re-runs `render()` on the next microtask; the same lifecycle hooks fire as for reactive-property changes.
98
98
 
99
- See [`/docs/lifecycle`](https://docs.webjs.com/docs/lifecycle) for per-hook usage examples.
99
+ See [`/docs/lifecycle`](https://docs.webjs.dev/docs/lifecycle) for per-hook usage examples.
100
100
 
101
101
  ## Async render: bare-await data fetch (#469)
102
102
 
@@ -244,10 +244,23 @@ patterns, treat the component as interactive.
244
244
 
245
245
  The detection lists live in `packages/server/src/component-elision.js`
246
246
  and are the single source of truth. They are kept in lockstep with the
247
- lifecycle table above by `packages/server/test/elision/lifecycle-coverage.test.js`,
248
- which fails if a new `WebComponent` hook is added without teaching the
249
- analyser about it. If you add an interactivity feature to the framework,
250
- update that file.
247
+ framework's interactivity surface by two drift guards, which fail the build
248
+ if a new interactivity feature ships without teaching the analyser about it
249
+ (a silent over-elision otherwise). `packages/server/test/elision/lifecycle-coverage.test.js`
250
+ introspects the live `WebComponent` prototype and covers hooks and methods.
251
+ `packages/server/test/elision/sigil-coverage.test.js` covers the two surfaces
252
+ that are not prototype members: template binding sigils and interactivity
253
+ static fields. The renderers' sigil set is single-sourced in core's
254
+ `BINDING_PREFIXES` (`packages/core/src/binding-prefixes.js`); the analyser
255
+ classifies each sigil as a client-behaviour ship signal (`SSR_DROPPED_PREFIXES`,
256
+ `@event`, drops at SSR) or an SSR-safe round-trip (`ROUND_TRIP_PREFIXES`,
257
+ `.prop` / `?bool`, survives into the served HTML). The guard asserts that
258
+ classification partitions `BINDING_PREFIXES` exactly, so a new sigil cannot be
259
+ added without a deliberate ship-or-round-trip decision. Interactivity static
260
+ fields live in the `INTERACTIVITY_STATIC_FIELDS` registry (`shadow`, `refresh`).
261
+ If you add an interactivity feature to the framework, update the matching list
262
+ in `component-elision.js` (and add a new static convention to both the
263
+ `INTERACTIVITY_STATIC_FIELDS` registry and this lifecycle table).
251
264
 
252
265
  ## Pages and layouts: keep them carriers, out of the network tab
253
266
 
@@ -325,19 +338,28 @@ suggests is the three legitimate shapes: gate the route in
325
338
  `'use server'` ACTION (its browser stub is a working RPC, so it is
326
339
  exempt), or register the component in a `layout.{ts,js}` so the page
327
340
  elides again. Server-to-server imports (`.server.ts` importing
328
- `.server.ts`) and `middleware.ts` / `route.ts` are never flagged.
341
+ `.server.ts`) and `middleware.ts` / `route.ts` are never flagged. A
342
+ TYPE-ONLY `import type { Row } from './x.server.ts'` is exempt too,
343
+ because the stripper erases it before it reaches the browser, so sharing
344
+ a derived row type from a `.server.ts` is safe (a mixed
345
+ `import { type Row, value }` still ships the runtime `value` binding, so
346
+ it is still flagged).
329
347
 
330
348
  The rule covers every module the build ships, not just pages: a shipping
331
349
  component, and `error.{ts,js}` / `loading.{ts,js}` / `not-found.{ts,js}`
332
350
  modules, are checked too. Those three boundaries always ship and are
333
351
  never elided (only an elidable component import is ever stripped), so a
334
352
  personalized 404 that does `await auth()` is the same throw-at-load crash
335
- and is flagged. One known gap: a DYNAMIC `import('./x.server.ts')` is not
336
- caught, because the framework's import scanner tracks only static
337
- `import` / `export … from`, not the `import(` call form. That is
338
- consistent with the rest of the framework (a dynamic import is also not
339
- elided framework-wide, and its crash is deferred to call time, not module
340
- load), so the rule leaves it to the runtime.
353
+ and is flagged. Scope note for dynamic imports: a string-literal
354
+ `import('./widget.ts')` IS tracked by the authorization gate (#751), so a
355
+ lazily-imported app module is servable instead of 404ing, but it is kept
356
+ out of elision and the modulepreload set (a dynamic import is lazy by
357
+ author intent, fetched at call time). The `no-server-import-in-browser-module`
358
+ rule still operates on STATIC edges only, so a dynamic
359
+ `import('./x.server.ts')` of a no-`'use server'` utility is not flagged at
360
+ check time, its throw-at-load is deferred to call time; and a computed
361
+ `import(expr)` cannot be resolved statically, so `webjs check` warns on it
362
+ in a shipping module (it would 404 if it targets an app module).
341
363
 
342
364
  ### Turning elision off
343
365