@webjsdev/mcp 0.1.0 → 0.1.1

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.0",
3
+ "version": "0.1.1",
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": {
@@ -107,7 +107,7 @@ A page/layout module still **loads** in the browser for its top-level side effec
107
107
 
108
108
  ## Framework source: where to find it
109
109
 
110
- Plain JS with JSDoc lives in `node_modules/@webjsdev/` (`core/`, `server/`, `cli/`, `mcp/`, `ts-plugin/`, `ui/`); what you read is what runs. Starting points: SSR `@webjsdev/server/src/ssr.js`, client hydration `@webjsdev/core/src/render-client.js`, client router `@webjsdev/core/src/router-client.js`, convention rules `@webjsdev/server/src/check.js`. For UI debugging use the Playwright MCP server; for live introspection the scaffold wires the read-only `@webjsdev/mcp` server (`npx @webjsdev/mcp`, also reachable as `webjs mcp`): `list_routes`, `list_actions`, `list_components`, `check`, plus a knowledge layer (docs / recipes / framework source).
110
+ Plain JS with JSDoc lives in `node_modules/@webjsdev/` (`core/`, `server/`, `cli/`, `mcp/`, `intellisense/`, `ui/`); what you read is what runs. Starting points: SSR `@webjsdev/server/src/ssr.js`, client hydration `@webjsdev/core/src/render-client.js`, client router `@webjsdev/core/src/router-client.js`, convention rules `@webjsdev/server/src/check.js`. For UI debugging use the Playwright MCP server; for live introspection the scaffold wires the read-only `@webjsdev/mcp` server (`npx @webjsdev/mcp`, also reachable as `webjs mcp`): `list_routes`, `list_actions`, `list_components`, `check`, plus a knowledge layer (docs / recipes / framework source).
111
111
 
112
112
  ---
113
113
 
@@ -158,10 +158,11 @@ The bare `@webjsdev/core` specifier resolves to a BROWSER bundle dropping server
158
158
  | `register(tag, C)` | Tag binding. Auto-called by `Class.register('tag')`. |
159
159
  | `render(v, el)` | Client-side render into a DOM element. |
160
160
  | `renderToString` | Server-side async render to HTML with DSD (from `/server`). |
161
- | `notFound()` / `redirect(url)` | Throw to return 404, or 307/308 redirect. |
161
+ | `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 })`. |
162
162
  | `expose(p, fn)` | Tag a server action ALSO reachable at a REST path. Server-side only. |
163
163
  | `validateInput(fn, validate)` | Attach an input validator running on BOTH the RPC and `expose()` REST paths (#245). Server-side only. See Server actions. |
164
- | `Suspense({fallback, children})` | Streaming boundary. `repeat` keyed-list directive is also re-exported. |
164
+ | `Suspense({fallback, children})` | Page/region-level streaming boundary (a value in a hole). `repeat` keyed-list directive is also re-exported. |
165
+ | `<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. |
165
166
  | `connectWS(url, handlers)` / `richFetch<T>` | Client WebSocket (auto-reconnect, queued sends); content-negotiated rich-type fetch. |
166
167
  | `navigate(url, opts?)` / `revalidate(url?)` | Programmatic client-router nav; evict the BROWSER snapshot cache. |
167
168
  | `optimistic(signal, value, action)` | Set `signal` immediately, run `action`, roll back on error or `{ success: false }`. |
@@ -196,7 +197,9 @@ MyThing.register('my-thing');
196
197
 
197
198
  **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).
198
199
 
199
- **Light DOM (default) vs Shadow DOM.** Light DOM applies global CSS and Tailwind directly (default; for Tailwind/global CSS + simple composition). Shadow DOM (`static shadow = true`) is for `static styles` scoped CSS and third-party isolation; `<slot>` works in either. A light-DOM component authoring custom CSS MUST prefix every class selector with its tag name (invariant 7); prefer Tailwind. Install the `webjs` VSCode extension (`packages/editors/vscode`, VS Marketplace + Open VSX; also covers Cursor / Antigravity / Windsurf) or `webjs.nvim` (`packages/editors/nvim`, via lazy.nvim) for template highlighting + editor intelligence with no Lit plugin, or add the standalone `@webjsdev/ts-plugin` to `tsconfig.json` `plugins` manually (JetBrains). Full deep-dive in `agent-docs/components.md` + `agent-docs/lit-muscle-memory-gotchas.md`.
200
+ **Async render (`async render()`), bare-await data fetch (#469).** A component may write `async render() { const u = await getUser(this.id); return html\`<h3>${u.name}</h3>\`; }`. Writing `await` makes the function async by JS rule, and every render path awaits a promise-returning `render()` automatically (no flag). This co-locates the fetch in the leaf component (no prop-drilling). The model is decoupled into three separate concerns. (1) **SSR always blocks**, so the resolved DATA is in the first paint with no fallback markup (PE-safe, JS-off reads it). (2) **The client re-fetch default is stale-while-revalidate**: when a prop / dependency change re-runs `async render()`, the current content stays until the new render resolves (no blank, no flash). (3) **`renderFallback()` is the OPTIONAL re-fetch loading UI**, a prop-aware method shown ONLY during a client re-fetch, NEVER on the first paint, and it does NOT trigger SSR streaming. **Errors are isolated per component by default** (no user code): a thrown `await getData()` renders a component-scoped error state while siblings render, and `renderError()` optionally customizes it (dev surfaces the message, prod stays silent). `getData()` is already isomorphic (a `'use server'` action is the real function during SSR and an RPC stub on the client), so the same line works both sides. Use `async render()` for request-time-known SERVER data that should be in the first paint; keep `Task` / signals for genuinely client-only data (a `Task` shows its pending state at SSR, losing first-paint data). A **bare** async-render component (an `async render()` with no other client signal, light DOM) is **elided** like any display-only component (#474): its SSR'd HTML is the complete output, so the framework drops the module AND the redundant on-hydration re-fetch. It SHIPS only when it also carries an independent signal (an `@event`, a non-`state` reactive prop, a signal / reactive import, a lifecycle hook including `renderFallback()`, a `<slot>`, `static shadow = true`, `static refresh = true`, cross-module observation, or a transitively-reachable interactive child). Two carve-outs always ship: `static shadow = true` (Declarative Shadow DOM attaches only during HTML parsing, so a streamed or soft-navigated shadow component needs its module to re-run `attachShadow`) and `static refresh = true` (the explicit opt-in keeping the stale-while-revalidate on-load re-fetch that eliding drops, moot for request-stable data). **For SLOW data where blocking the first byte hurts, wrap the region in `<webjs-suspense .fallback=${html\`…\`}>` to STREAM it** (the fallback flushes on the first byte, the data streams in; multiple boundaries fetch concurrently). This is the only way to show a first-paint fallback, a deliberate choice for slow regions, and it streams progressively on soft navigation too. A throwing component inside a boundary is isolated (renders its error state, siblings stream). (A hydration-seed optimization that would also skip the redundant client re-fetch on load is deferred, #472; stale-while-revalidate already hides it.)
201
+
202
+ **Light DOM (default) vs Shadow DOM.** Light DOM applies global CSS and Tailwind directly (default; for Tailwind/global CSS + simple composition). Shadow DOM (`static shadow = true`) is for `static styles` scoped CSS and third-party isolation; `<slot>` works in either. A light-DOM component authoring custom CSS MUST prefix every class selector with its tag name (invariant 7); prefer Tailwind. Install the `webjs` VSCode extension (`packages/editors/vscode`, VS Marketplace + Open VSX; also covers Cursor / Antigravity / Windsurf) or `webjs.nvim` (`packages/editors/nvim`, via lazy.nvim) for template highlighting + editor intelligence with no Lit plugin, or add the standalone `@webjsdev/intellisense` to `tsconfig.json` `plugins` manually (JetBrains). Full deep-dive in `agent-docs/components.md` + `agent-docs/lit-muscle-memory-gotchas.md`.
200
203
 
201
204
  ---
202
205
 
@@ -239,6 +242,8 @@ Optional app-root file default-exporting a **schema object** (env-var names to a
239
242
  | Plain `.ts` | yes | **Lint violation** (`use-server-needs-extension`). Rename to add `.server.`. |
240
243
  | Plain `.ts` | no | Browser-safe; standard. |
241
244
 
245
+ 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.
246
+
242
247
  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()`). **Expose as REST:** `expose('METHOD /path', fn, { validate? })`. **Input validation runs on BOTH call paths (#245),** declared via `validateInput(fn, validate)` or `expose(..., { validate })`; 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`.
243
248
 
244
249
  ### RPC + expose security
@@ -18,6 +18,25 @@ fallback flushes immediately, and the resolved content streams in as a
18
18
  `<template>` + inline `__webjsResolve('id')` script when the promise
19
19
  lands. Nested Suspense supported.
20
20
 
21
+ ### Component-level streaming: `<webjs-suspense>` (#471)
22
+
23
+ `Suspense({ fallback, children })` above is the page/region-level primitive (a promise passed as `children`). With **async render** (`agent-docs/components.md`), a COMPONENT is the suspending unit: a component doing `async render() { const u = await getUser(this.uid); … }` BLOCKS the first byte by default (real data in the first paint). To STREAM a slow component, wrap it in the renderer-recognized `<webjs-suspense>` element:
24
+
25
+ ```js
26
+ html`
27
+ <webjs-suspense .fallback=${html`<p>Loading section…</p>`}>
28
+ <user-profile uid="42"></user-profile>
29
+ <user-activity uid="42"></user-activity>
30
+ </webjs-suspense>
31
+ `;
32
+ ```
33
+
34
+ `.fallback` is read at SSR as the inline placeholder (`render-server.js`'s `processSuspenseElements` carries it via `data-webjs-fallback`, since a `TemplateResult` is not serializer-safe) and flushed on the first byte; the children push to `ctx.pending` and stream in via the same `<template data-webjs-resolve>` engine. Multiple boundaries resolve via `Promise.all`, so they fetch concurrently (no server waterfall). One boundary groups several components under one fallback (the boundary `.fallback` wins over a contained component's `renderFallback()`), and a throwing component inside is isolated to its own error state while siblings stream. Without a streaming context (`renderToString`) the children render inline (blocking).
35
+
36
+ ### Progressive soft-nav streaming (#473)
37
+
38
+ On a client-router navigation to a streamed page, the router applies the response PROGRESSIVELY: the SSR stream flushes the shell (with fallbacks) plus a `<!--wj-stream-shell-->` sentinel, the router's `readStreamedShell` swaps the shell in immediately and advances the URL, then `streamBoundariesProgressively` applies each resolved boundary into the live DOM as it streams (fast-before-slow), upgrading the custom elements inside. So a soft nav matches the initial-load experience instead of buffering the whole response. A non-streaming page is read to completion and applied once; a navigation superseded mid-stream stops and cancels the reader; a mid-stream transport failure leaves the applied boundaries in place (non-destructive).
39
+
21
40
  ## First-paint performance without a build step
22
41
 
23
42
  Five stacked zero-build optimizations:
@@ -42,10 +61,20 @@ Five stacked zero-build optimizations:
42
61
  `connectedCallback` activation. Ideal for below-the-fold widgets.
43
62
  5. **Auto-vendor via jspm.io (Rails 7 + importmap-rails posture).** At
44
63
  startup the server scans client-reachable source for bare npm import
45
- specifiers. Each `pkg@version` is resolved through `api.jspm.io/generate`
46
- to a CDN URL (`https://ga.jspm.io/npm:<pkg>@<version>/...`) and added
47
- to the import map; the browser fetches each package directly from
48
- the CDN. **SRI integrity (SHA-384) is computed on BOTH paths.** A
64
+ specifiers. The WHOLE scanned set is resolved in ONE
65
+ `api.jspm.io/generate` call (a single `install[]` array) so jspm
66
+ computes one mutually-consistent dependency graph: a directly-imported
67
+ package and a transitive that needs a newer version of the same package
68
+ agree on one CDN URL, instead of skewing (a direct dep pinned to its
69
+ local version while a transitive floats to jspm-latest, which crashed
70
+ the browser with a missing export). Each resolved entry is a CDN URL
71
+ (`https://ga.jspm.io/npm:<pkg>@<version>/...`) added to the import map;
72
+ the browser fetches each package directly from the CDN. If the unified
73
+ call fails because some install is unresolvable (a 401 for a private or
74
+ server-only dep), the resolver falls back to per-package isolation for
75
+ the offending install(s) and re-resolves the resolvable survivors as one
76
+ graph, so one bad dep can never collapse the whole map.
77
+ **SRI integrity (SHA-384) is computed on BOTH paths.** A
49
78
  live-resolved (unpinned) app hashes each cross-origin bundle at warmup
50
79
  and emits `integrity` + `crossorigin` on the importmap and modulepreload
51
80
  tags, so a swapped or compromised CDN response is rejected by the browser
@@ -60,7 +89,19 @@ Five stacked zero-build optimizations:
60
89
  stable boot-time build id with no warmup fetch); `webjs vendor pin
61
90
  --download` also caches the bundle bytes locally under
62
91
  `.webjs/vendor/<pkg>@<version>.js` for air-gapped / strict-CSP
63
- deployments. No bundler runs at any point.
92
+ deployments. No bundler runs at any point. **`webjs doctor` validates
93
+ importmap coherence** (#450): for each resolved package it checks that the
94
+ version pinned for every OTHER resolved package it depends on satisfies the
95
+ declared dependency / peer range, and WARNS naming both packages, the range,
96
+ and the pinned version when they skew (the class of bug where a pinned
97
+ package needs a newer minor of another pinned package than is pinned, so a
98
+ symbol it expects is missing at runtime). It is a validation over the
99
+ produced importmap, not a re-resolution and not bundling, and it runs the
100
+ SAME check with the SAME verdict over the live importmap and the vendored
101
+ `.webjs/vendor/importmap.json` (vendoring freezes the runtime-resolved
102
+ graph, so a coherent live graph stays coherent vendored). It reads
103
+ dependency metadata from the installed `node_modules` (no network of its
104
+ own) and degrades to "could not verify" when a manifest is unavailable.
64
105
 
65
106
  ## No-build production model
66
107
 
@@ -70,9 +111,10 @@ production. The Rails 7+ / Hotwire pattern:
70
111
 
71
112
  - **Importmap-driven**: bare-specifier imports (`from "react"`) are
72
113
  resolved via `<script type="importmap">` emitted into the document
73
- head. By default each package resolves through `api.jspm.io/generate`
74
- to a `https://ga.jspm.io/npm:<pkg>@<version>/...` URL and the browser
75
- fetches it from the CDN directly, with an `integrity` SRI hash on every
114
+ head. By default the whole scanned set resolves through one
115
+ `api.jspm.io/generate` call (one consistent graph, see point 5 above)
116
+ to `https://ga.jspm.io/npm:<pkg>@<version>/...` URLs and the browser
117
+ fetches them from the CDN directly, with an `integrity` SRI hash on every
76
118
  cross-origin entry (computed live at warmup for an unpinned app, or read
77
119
  from the pin file for a pinned one). `webjs vendor pin` commits the
78
120
  resolved URLs + SHA-384 integrity hashes to `.webjs/vendor/importmap.json`
@@ -80,7 +122,12 @@ production. The Rails 7+ / Hotwire pattern:
80
122
  caches each bundle to `.webjs/vendor/<pkg>@<version>.js` and rewrites
81
123
  the importmap to `/__webjs/vendor/<pkg>@<version>.js` so the server
82
124
  serves the bytes from disk (air-gapped / strict-CSP path). No bundler
83
- runs at any point.
125
+ runs at any point. Because the pins are meant to be committed,
126
+ `webjs vendor pin` makes its output committable: the scaffold already
127
+ un-ignores `.webjs/vendor/`, and if a `.gitignore` would swallow the
128
+ pins it heals it (or prints how to commit them when the ignore is not
129
+ in the app's own `.gitignore`). A no-vendor app, which never pins, is
130
+ unaffected.
84
131
  - **Per-file ESM serving**: every app `.js` / `.ts` becomes its own HTTP
85
132
  resource. The browser walks the import graph and fetches each module
86
133
  on demand.
@@ -760,16 +807,24 @@ The action receives `{ request, params, searchParams, url, formData }`
760
807
  and returns an `ActionResult`. The server interprets the result:
761
808
 
762
809
  - **Success** (`{ success: true, redirect? }`, or any non-`false`
763
- result, or a thrown `redirect()`): a `303 See Other` to
764
- `result.redirect` if present, else the page's own path
765
- (Post/Redirect/Get, so a reload does not resubmit).
810
+ result): a `303 See Other` to `result.redirect` if present, else the
811
+ page's own path (Post/Redirect/Get, so a reload does not resubmit).
812
+ This 303 is the success-result path only; a `redirect()` *thrown* from
813
+ the action is a separate case (see below).
766
814
  - **Failure** (`{ success: false, fieldErrors?, values?, status? }`):
767
815
  re-SSR the SAME page with `status` (default `422`) and the result on
768
816
  `ctx.actionData`. The page reads `actionData.fieldErrors.<field>` for
769
817
  messages and `actionData.values.<field>` to repopulate native
770
818
  `<input value=...>`, so the user's typed input survives.
771
- - A thrown `notFound()` yields a 404, a thrown `redirect()` keeps its
772
- own 307/308 status (PRG uses 303 only for the success-result path).
819
+ - A thrown `notFound()` yields a 404. A thrown `redirect()` carries no
820
+ baked-in status; the catching site picks the convention: thrown from an
821
+ action (a POST) it defaults to `307 Temporary Redirect`
822
+ (method-preserving, so the bounce keeps the POST's intent), thrown
823
+ during a GET page/layout render or gate it defaults to `302 Found` (the
824
+ code an auth bounce conventionally wants). An explicit status overrides
825
+ either default, in both the positional `redirect(url, 308)` and the
826
+ options `redirect(url, { status })` forms. PRG's 303 (above) is only
827
+ the success-result path, distinct from these thrown defaults.
773
828
 
774
829
  A page WITHOUT an `action` export keeps the old behavior, a non-GET to
775
830
  it 404s. There is no form library: native input repopulation plus the
@@ -802,6 +857,20 @@ mounted across nav and keep their `scrollTop` natively.
802
857
 
803
858
  ### `<webjs-frame>` escape hatch
804
859
 
860
+ `<webjs-frame>` is webjs's take on **Turbo Frames** (Hotwire Turbo), so the
861
+ mental model and most `<turbo-frame>` muscle memory transfer directly: a lazy,
862
+ URL-addressable region that swaps on its own, driven by a link or form
863
+ targeting its id. It is the unit for a region that loads or refreshes
864
+ INDEPENDENTLY of a full-page navigation (something a page/layout, which
865
+ re-renders only at the route level, cannot express), and it ships zero
866
+ component JS. A frame's route can itself use `<webjs-suspense>`, so a deferred
867
+ (`loading="lazy"`) frame whose data is slow streams that data behind a fallback
868
+ inside the frame (one caveat: a streamed framed route skips the byte-saving
869
+ subtree extraction, so the full page renders and the client slices out the
870
+ region). See the data-fetching doc page (`docs/app/docs/data-fetching/page.ts`)
871
+ for the async-render vs `<webjs-suspense>` vs `<webjs-frame>` vs `<webjs-stream>`
872
+ decision boundary.
873
+
805
874
  For partial-swap regions NOT tied to a folder layout (a marketing-page
806
875
  widget, tabbed UI, etc.), wrap the region in a frame:
807
876
 
@@ -954,6 +1023,13 @@ those fallback children).
954
1023
 
955
1024
  ### Stream actions: surgical element-level updates (#248)
956
1025
 
1026
+ `<webjs-stream>` is webjs's take on **Turbo Streams** (Hotwire Turbo); the
1027
+ action set (`append` / `prepend` / `before` / `after` / `replace` / `update` /
1028
+ `remove`) mirrors `<turbo-stream>`, so that muscle memory transfers directly. It
1029
+ is the ONLY surgical single-element update primitive (and the live-channel
1030
+ applier); a region swap or a `<webjs-frame>` reload redraws a whole region, and
1031
+ `<webjs-suspense>` is for streaming, so none of them cover "append one row".
1032
+
957
1033
  A region swap (a layout marker or a `<webjs-frame>`) is the right tool for "this
958
1034
  part of the page changed". It is too coarse for "append ONE comment", "remove
959
1035
  ONE row", "bump a count", or "insert a toast". For those, a server response can
@@ -277,6 +277,16 @@ if (!session) throw redirect('/login');
277
277
 
278
278
  JWT sessions by default (stateless, scales horizontally). OAuth providers handle the full redirect flow.
279
279
 
280
+ `auth()` resolves `{ user }` where `user` is `Record<string, unknown>` by default. To read the custom fields the callbacks set (`session.user.id`, `session.user.role`) without a cast and with typo-checking, type the session. Augment `AuthUser` for global typing, or use `createAuth<AppUser>(...)` for per-instance typing:
281
+
282
+ ```ts
283
+ declare module '@webjsdev/server' {
284
+ interface AuthUser { id: string; role: 'admin' | 'member'; }
285
+ }
286
+ ```
287
+
288
+ Both are opt-in and types-only. See `agent-docs/typescript.md` and the auth recipe.
289
+
280
290
  ## File storage (`FileStore` + `diskStore`)
281
291
 
282
292
  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.
@@ -351,7 +361,7 @@ See the "Receive and persist an uploaded file" recipe in `agent-docs/recipes.md`
351
361
  | `AUTH_GITHUB_ID` / `AUTH_GITHUB_SECRET` | GitHub OAuth (optional) |
352
362
  | `SESSION_SECRET` | Cookie session signing |
353
363
  | `REDIS_URL` | When set, sessions + rate limit + cache use Redis |
354
- | `PORT` | Server port (default 8080) |
364
+ | `PORT` | Server port. Precedence `--port` > `PORT` (real env or `.env`) > 8080 (#447) |
355
365
 
356
366
  ## Scaling to multiple instances
357
367
 
@@ -50,6 +50,94 @@ For component-local state, create an instance signal in the constructor and call
50
50
 
51
51
  See [`/docs/lifecycle`](https://docs.webjs.com/docs/lifecycle) for per-hook usage examples.
52
52
 
53
+ ## Async render: bare-await data fetch (#469)
54
+
55
+ A component can fetch its own server data into the first paint. `render()` may be `async`, so you write the natural line directly:
56
+
57
+ ```ts
58
+ class UserProfile extends WebComponent {
59
+ static properties = { uid: { type: String } };
60
+ declare uid: string;
61
+ async render() {
62
+ const u = await getUser(this.uid); // a 'use server' action: real fn at SSR, RPC stub on the client
63
+ return html`<h3>${u.name}</h3>`;
64
+ }
65
+ }
66
+ UserProfile.register('user-profile');
67
+ ```
68
+
69
+ Writing `await` makes the function async by the JS rule, and every render path awaits a promise-returning `render()` automatically. There is no flag. Plain sync `render()` stays the zero-cost default.
70
+
71
+ **Three concerns, decoupled. Do not conflate them.**
72
+
73
+ 1. **SSR always blocks by default.** The server awaits `async render()`, so the resolved DATA is baked into the first paint. There is no fallback on first paint, ever. JS-off reads the data (a progressive-enhancement UPGRADE over a client-fetched `Task`, which shows nothing without JS).
74
+ 2. **The client re-fetch default is stale-while-revalidate.** When a prop / dependency change re-runs `async render()`, the previously rendered content stays until the new render resolves. No blank, no flash, no user code.
75
+ 3. **`renderFallback()` is the OPTIONAL re-fetch loading UI.** Define it to OVERRIDE the stale-while-revalidate default with a loading state (skeleton / spinner) shown DURING a client re-fetch. It is shown ONLY on a re-fetch, NEVER on the first paint, and it does NOT create a server-streaming boundary. It is a prop-aware method (not a static field), so it can branch on the component's current state.
76
+
77
+ ```ts
78
+ class UserActivity extends WebComponent {
79
+ static properties = { uid: { type: String } };
80
+ declare uid: string;
81
+ renderFallback() { return html`<div class="skeleton h-24"></div>`; } // shown only while a re-fetch is in flight
82
+ async render() {
83
+ const items = await getActivity(this.uid);
84
+ return html`<ul>${items.map((i) => html`<li>${i.label}</li>`)}</ul>`;
85
+ }
86
+ }
87
+ ```
88
+
89
+ **Errors are isolated per component by default, no user code.** A thrown `await getData()` (or any render throw) renders a component-scoped error state while its siblings render normally; it never bubbles to the route `error.js`. The default surfaces the tag and message in dev and renders a silent empty element in prod (no leak). Override `renderError(error)` only to customize the error UI:
90
+
91
+ ```ts
92
+ class Report extends WebComponent {
93
+ async render() { return html`<pre>${await getReport()}</pre>`; }
94
+ renderError(error) { return html`<p class="error">Could not load the report.</p>`; } // optional
95
+ }
96
+ ```
97
+
98
+ `Task` and a signal cannot replace this: a `Task` renders its pending state at SSR (it loses the first-paint data), and you cannot wrap a signal around your own `await` inside `render()`. So `async render()` plus `renderFallback()` is the only way to get SSR-first-paint data AND a custom re-fetch loading state.
99
+
100
+ **Decision rules (which tool to reach for):**
101
+
102
+ 1. Server data knowable at request time: fetch it IN the component with `async render()`. Co-located, no prop-drilling, data in the first paint. The default, simplest case.
103
+ 2. Client re-fetch where stale content would mislead: add `renderFallback()` for a loading state during the re-fetch.
104
+ 3. Genuinely CLIENT-ONLY data (depends on a click, viewport, localStorage, or live updates, and does NOT need to be in the first paint): use `Task` / signals plus an RPC action.
105
+ 4. Slow server data where blocking the first byte hurts: stream it. Wrap the component in `<webjs-suspense .fallback=${html\`…\`}>` to flush the fallback on the first byte and stream the data in (the only way to show a first-paint fallback; concurrent across boundaries; progressive on soft nav). Do it deliberately for slow regions, not by default.
106
+
107
+ **Anti-patterns (likely footguns):**
108
+
109
+ - Do NOT prop-drill server data through layers when the leaf component can fetch it itself.
110
+ - Do NOT put `await getData()` in a page / layout function if it can live in a component (page / layout fetches run sequentially, a route-level waterfall).
111
+ - Do NOT fetch in `connectedCallback` / `Task` for data that is knowable server-side (that yields a fallback-then-RPC, not first-paint data).
112
+ - Do NOT expect `renderFallback()` to affect the first paint (it is the CLIENT re-fetch loading state).
113
+ - Do NOT add `renderError()` on every component (isolation is automatic).
114
+
115
+ **How it works.** On the server the SSR walker already awaits a promise-returning `render()` and bakes the data in; a throw is caught per component and rendered as the error state. On the client, `update()` detects a thenable from `render()` and routes to a stale-while-revalidate commit: the current DOM stays until the promise resolves, a monotonic render token drops a superseded resolution (an out-of-order fetch never commits stale DOM), and a rejection routes to `renderError()`. `firstUpdated` / `updated` / `updateComplete` fire after the async commit lands. Only signal reads BEFORE the first `await` establish reactive dependencies.
116
+
117
+ **Elision (#474).** A **bare** `async render()` (no other client signal, light DOM) is **elided** like any display-only component: the SSR'd data is the complete first paint, so the framework drops the module and the redundant on-hydration re-fetch (a common content / docs leaf shape). It ships only when it ALSO carries an independent signal (an `@event`, a non-`state` reactive prop, a signal / reactive import, a lifecycle hook including `renderFallback()`, a `<slot>`, cross-module observation, or a transitively-reachable interactive child). Two carve-outs always ship. `static shadow = true` ships because Declarative Shadow DOM attaches only during HTML parsing, so a streamed (`<webjs-suspense>`) or soft-navigated shadow component arrives via a JS DOM insertion and needs its module to re-run `attachShadow`. `static refresh = true` is the explicit opt-in to keep the stale-while-revalidate on-load re-fetch that eliding drops (moot for request-stable data, the default; reach for it only when fresh-on-load matters). The only behaviour eliding removes is that on-load refresh; the first paint is byte-identical, verified by the differential-elision guard.
118
+
119
+ ### Streaming a slow region with `<webjs-suspense>` (#471)
120
+
121
+ `async render()` BLOCKS the first byte by default (the SSR HTML waits for the data). For a SLOW region where that wait hurts time-to-first-byte, wrap it in `<webjs-suspense>` to stream it: the fallback flushes immediately, the resolved content streams in.
122
+
123
+ ```ts
124
+ html`
125
+ <webjs-suspense .fallback=${html`<p>Loading section…</p>`}>
126
+ <user-profile uid="42"></user-profile>
127
+ <user-activity uid="42"></user-activity>
128
+ </webjs-suspense>
129
+ `;
130
+ ```
131
+
132
+ - **The fallback is on the first byte, the content streams in.** This is the ONLY way a fallback appears on the first paint. It is a deliberate choice for slow data (fast TTFB, accepting a fallback-then-content swap), exactly like page-level `Suspense`. PE-critical content stays unwrapped (blocking) so it is in the first paint with no JS.
133
+ - **Grouping + override.** One boundary wraps several components under ONE fallback. The boundary `.fallback` wins over a contained component's `renderFallback()`.
134
+ - **Concurrent.** Multiple `<webjs-suspense>` boundaries on a page fetch their data in parallel (each is a non-blocking boundary resolved together), so they stream fast-before-slow with no server waterfall.
135
+ - **Error-isolated.** A throwing component inside a boundary renders its component-scoped error state (via `renderError()` or the default) while its siblings stream normally.
136
+ - **Progressive on soft navigation (#473).** A client-router navigation to a streamed page applies the shell (with fallbacks) immediately, advances the URL, then streams each boundary in, matching the initial-load experience instead of buffering the whole response.
137
+ - **`.fallback` is special-cased at SSR:** the renderer reads it inline as the placeholder, never through the `data-webjs-prop-*` path (a `TemplateResult` is not serializer-safe). It must be an UNQUOTED property hole (`.fallback=${...}`, invariant 4).
138
+
139
+ Decision: a bare `async render()` for request-time data that should be in the first paint (the default); `<webjs-suspense>` ONLY when the data is slow enough that blocking the first byte is the worse tradeoff.
140
+
53
141
  ## Display-only components are elided from the browser
54
142
 
55
143
  A component that does no client-side work renders the same SSR'd HTML
@@ -113,6 +201,46 @@ which fails if a new `WebComponent` hook is added without teaching the
113
201
  analyser about it. If you add an interactivity feature to the framework,
114
202
  update that file.
115
203
 
204
+ ### Elision is what keeps a server import off a display-only page (and `webjs check` guards the seam)
205
+
206
+ Elision is also why a page can call a server-only utility and stay
207
+ browser-safe, and why the same code crashes once the page gains client
208
+ work. A page that does `const s = await auth()` (where `auth` comes from
209
+ a `lib/auth.server.ts` UTILITY, no `'use server'`) is fine while the page
210
+ is display-only: the framework elides the page, strips the server import,
211
+ and the browser never sees it. The moment the page also imports a
212
+ component to register it (`import '../components/workspace.ts'`), enables
213
+ the client router, or uses a reactive primitive, the page stops being
214
+ display-only, must load in the browser to do that work, and drags the
215
+ server import with it. In the browser that import is a throw-at-load
216
+ stub, so the page crashes the instant its module loads. `webjs typecheck`
217
+ and the rest of `webjs check` pass; only the running page fails. This was
218
+ the single biggest source of extra AI iterations when porting a real app.
219
+
220
+ `webjs check`'s `no-server-import-in-browser-module` rule catches it
221
+ statically. It reuses the SAME elision verdict described above (over the
222
+ module graph, scanned components, and route table), so it flags ONLY a
223
+ module that genuinely ships: a display-only page the framework elides is
224
+ never flagged, because its server import really is stripped. The fix it
225
+ suggests is the three legitimate shapes: gate the route in
226
+ `middleware.ts` (server-only, never shipped), call the server through a
227
+ `'use server'` ACTION (its browser stub is a working RPC, so it is
228
+ exempt), or register the component in a `layout.{ts,js}` so the page
229
+ elides again. Server-to-server imports (`.server.ts` importing
230
+ `.server.ts`) and `middleware.ts` / `route.ts` are never flagged.
231
+
232
+ The rule covers every module the build ships, not just pages: a shipping
233
+ component, and `error.{ts,js}` / `loading.{ts,js}` / `not-found.{ts,js}`
234
+ modules, are checked too. Those three boundaries always ship and are
235
+ never elided (only an elidable component import is ever stripped), so a
236
+ personalized 404 that does `await auth()` is the same throw-at-load crash
237
+ and is flagged. One known gap: a DYNAMIC `import('./x.server.ts')` is not
238
+ caught, because the framework's import scanner tracks only static
239
+ `import` / `export … from`, not the `import(` call form. That is
240
+ consistent with the rest of the framework (a dynamic import is also not
241
+ elided framework-wide, and its crash is deferred to call time, not module
242
+ load), so the rule leaves it to the runtime.
243
+
116
244
  ### Turning elision off
117
245
 
118
246
  Elision is on by default. To disable it app-wide, set `elide` to `false`
@@ -205,3 +205,21 @@ Returns JSON describing the live build, alongside the `/__webjs/health` and `/__
205
205
  ```
206
206
 
207
207
  `version` is the `@webjsdev/server` framework version (read from its own `package.json`), `build` is the published importmap build id (the same fingerprint the client router reads from `data-webjs-build`; empty until the vendor map resolves), `node` is the running Node version, `uptime` is process uptime in seconds. Mechanism: `requestId()` / `setRequestId` in `packages/server/src/context.js`, `buildInfo` / `buildInfoResponse` in `packages/server/src/build-info.js`, all wired in `packages/server/src/dev.js`.
208
+
209
+ ---
210
+
211
+ ## Server port resolution (`--port` / `PORT` / `.env`) (#447)
212
+
213
+ Both `webjs dev` and `webjs start` resolve the listen port with a single
214
+ precedence: **`--port` flag > `PORT` (a real exported env var OR a `PORT`
215
+ in the app's `.env`) > `8080`**. A real exported `PORT` still wins over a
216
+ `.env` `PORT`, matching the `.env` auto-load's shell-wins-over-file rule
217
+ (`process.loadEnvFile` does not clobber an already-set var).
218
+
219
+ The CLI loads `<appDir>/.env` BEFORE resolving the port, so a `PORT` set
220
+ only in `.env` is honored (it previously was not: the port was computed
221
+ from `process.env.PORT || 8080` before the server's own `.env` load ran,
222
+ so the file's `PORT` never reached the comparison and the server always
223
+ bound 8080). Mechanism: `loadAppEnv(appDir)` + the pure
224
+ `resolvePort(portFlag, env)` in `packages/cli/lib/port.js`, called by the
225
+ `dev` and `start` bins before listening.
@@ -181,7 +181,32 @@ client then renders the resolved state, causing a flash.
181
181
 
182
182
  `Task` is still useful for client-time async (interaction-triggered
183
183
  mutations, polling, websocket reactions). For initial-paint data, fetch
184
- in the page function instead.
184
+ in the page function OR in the component itself with an `async render()`
185
+ (see the next entry), which Lit does not have.
186
+
187
+ ### 2b. Lit has no async render; webjs does (#469)
188
+
189
+ Lit keeps `render()` synchronous (its async-SSR work signals a promise the
190
+ renderer awaits BEFORE a still-sync render). webjs lets `render()` itself be
191
+ `async`, so you write `const u = await getUser(this.id)` directly in the
192
+ component and SSR bakes the resolved data into the first paint. Three things
193
+ trip up Lit muscle memory:
194
+
195
+ - **SSR blocks by default. Streaming is NOT automatic.** A bare `async
196
+ render()` (no wrapper) renders real data in the first paint with no
197
+ fallback. There is no skeleton flash. To STREAM slow data (fallback on
198
+ first byte), wrap the region in `<webjs-suspense .fallback=${html`…`}>`.
199
+ Do not reach for a fallback expecting it to show on first load; the
200
+ unwrapped default is block-real-data, and streaming is the deliberate
201
+ opt-in for slow regions.
202
+ - **`renderFallback()` is NOT a first-paint concern.** It is the OPTIONAL
203
+ client re-fetch loading UI, shown only when a prop / dependency change
204
+ re-runs `async render()`, never on the first paint. The first-paint and
205
+ re-fetch defaults are both no-flash: SSR has the data, and a re-fetch keeps
206
+ the stale content (stale-while-revalidate) until the new render resolves.
207
+ - **You do not need an error boundary.** A thrown `await getData()` is
208
+ isolated to that component automatically (siblings render, the page does
209
+ not blank). Add `renderError()` only to customize the error UI.
185
210
 
186
211
  ### 3. Browser-only APIs in the constructor or `render()`
187
212
 
@@ -32,6 +32,12 @@ transition agents most often get wrong, so it is the first recipe.
32
32
  }
33
33
  ```
34
34
 
35
+ Start the dev server with `npm run dev` (the canonical command), NOT a bare
36
+ `webjs dev`: `npm run dev` runs the `predev` hook (`prisma generate`) first,
37
+ while `webjs dev` skips it and boots against an ungenerated client. A bare
38
+ `webjs dev` will now warn and point you back at `npm run dev`, but use the
39
+ npm script from the start.
40
+
35
41
  2. **Migrate.** Run the npm script (not the `webjs`/`prisma` binary directly,
36
42
  so the `predev` / `db:*` hooks fire):
37
43
 
@@ -210,6 +216,48 @@ export class HelloWorld extends WebComponent {
210
216
  HelloWorld.register('hello-world');
211
217
  ```
212
218
 
219
+ ## Fetch data in a component (async render, #469)
220
+
221
+ A leaf component fetches its own server data into the first paint, co-located, with no prop-drilling. Make `render()` async and call a `'use server'` action directly (real fn at SSR, RPC stub on the client). SSR awaits it, so the data is in the first paint.
222
+
223
+ ```ts
224
+ // (a) blocking async render: real data in the first paint, the common case
225
+ class UserProfile extends WebComponent {
226
+ static properties = { uid: { type: String } };
227
+ declare uid: string;
228
+ async render() {
229
+ const u = await getUser(this.uid);
230
+ return html`<h3>${u.name}</h3>`;
231
+ }
232
+ }
233
+ UserProfile.register('user-profile');
234
+
235
+ // (b) stream a SLOW region (fallback on the first byte, content streams in)
236
+ html`
237
+ <webjs-suspense .fallback=${html`<p>Loading…</p>`}>
238
+ <user-profile uid="42"></user-profile>
239
+ </webjs-suspense>
240
+ `;
241
+
242
+ // (c) renderFallback() = the CLIENT re-fetch loading state (never the first paint)
243
+ class UserActivity extends WebComponent {
244
+ static properties = { uid: { type: String } };
245
+ declare uid: string;
246
+ renderFallback() { return html`<div class="skeleton h-24"></div>`; }
247
+ async render() {
248
+ const items = await getActivity(this.uid);
249
+ return html`<ul>${items.map((i) => html`<li>${i.label}</li>`)}</ul>`;
250
+ }
251
+ }
252
+
253
+ // (d) errors are isolated by default: NO renderError() needed (add it only to customize)
254
+ class Report extends WebComponent {
255
+ async render() { return html`<pre>${await getReport()}</pre>`; }
256
+ }
257
+ ```
258
+
259
+ The client re-fetch default is stale-while-revalidate (the current content stays until the new render resolves). Use `<webjs-suspense>` only for genuinely slow data; keep `Task` / signals for client-only data that should NOT be in the first paint. Full decision guide in `agent-docs/components.md` and the `data-fetching` doc page.
260
+
213
261
  ## Form mutation with server-side validation (no JS required)
214
262
 
215
263
  This is webjs's progressive-enhancement write-path. A `<form method="POST">`
@@ -265,7 +313,7 @@ export default function Contact({ actionData }: {
265
313
  | Action outcome | HTTP response |
266
314
  |---|---|
267
315
  | success result (see the failure rule below) | `303 See Other` to a same-site `redirect` if present, else the page's own path (Post/Redirect/Get) |
268
- | thrown `redirect('/x')` | `307`/`308` (keeps the status `redirect()` was called with) |
316
+ | thrown `redirect('/x')` | `307` (method-preserving, the action default since it is a POST), or the explicit status if `redirect('/x', 308)` / `redirect('/x', { status })` was used. A redirect thrown during a plain GET page render instead defaults to `302`. |
269
317
  | thrown `notFound()` | `404` rendered via `not-found.{js,ts}` |
270
318
  | failure result (`success: false`, or `fieldErrors`, or an `error`) | re-SSR the SAME page with `status` (default `422`) and the result on `ctx.actionData` |
271
319
 
@@ -171,7 +171,7 @@ Naming convention: derive the scope class from the file path. Slashes
171
171
 
172
172
  Styles colocate with the markup as `const STYLES = css\`…\`` and
173
173
  interpolate via `<style>${STYLES.text}</style>`. The standalone
174
- `@webjsdev/ts-plugin` (and the `webjs` editor extension) resolves class
174
+ `@webjsdev/intellisense` (and the `webjs` editor extension) resolves class
175
175
  go-to-definition inside those blocks.
176
176
 
177
177
  ### Example: a page
@@ -71,7 +71,7 @@ packages/
71
71
  cli/test/ scaffold validation only
72
72
  ui/test/ schema, resolver, project-detect, components
73
73
  components/browser/ real-browser tests for the kit
74
- ts-plugin/test/plugin/ tsserver plugin tests (.test.mjs)
74
+ intellisense/test/plugin/ tsserver plugin tests (.test.mjs)
75
75
  test/ cross-package only
76
76
  ssr/ SSR pipeline (core + server)
77
77
  actions/ expose() (core + server)
@@ -131,7 +131,7 @@ ui-website ship no test suite yet, so they are not in the job.
131
131
  3. Drop the test in directly when it's a node test; nest it
132
132
  inside `browser/` / `e2e/` / `smoke/` when it's that kind.
133
133
  4. Use `.test.js` for ESM packages, `.test.mjs` when the
134
- surrounding package is CJS (e.g. `@webjsdev/ts-plugin`).
134
+ surrounding package is CJS (e.g. `@webjsdev/intellisense`).
135
135
 
136
136
  ---
137
137
 
@@ -162,6 +162,12 @@ assert.ok(el.innerHTML.includes('10'));
162
162
 
163
163
  **Hydration-mismatch pattern.** To assert SSR and the hydrated DOM agree, normalise the SSR string (strip the `<!--webjs-hydrate-->` marker, `data-webjs-prop-*` attributes, part comments) and compare against the live `el.innerHTML`. The counterfactual is a component whose `render()` is non-deterministic across the SSR call and the hydration render; `ssrFixture` returns the live hydrated element, so the divergence is detectable. The worked tests live in `packages/core/test/testing/browser/ssr-fixture.test.js`, alongside the broader SSR-vs-client parity corpus in `packages/core/test/rendering/browser/ssr-client-parity.test.js`.
164
164
 
165
+ **Testing async render + streaming + error isolation (#469 / #471 / #473).** Split by layer:
166
+ - **SSR async render + error isolation** (unit, node, `renderToString`): assert an `async render()` bakes the resolved DATA into the HTML with no fallback markup, and that a throwing component renders a component-scoped error state while siblings render (worked in `packages/core/test/suspense/async-render-ssr.test.js`).
167
+ - **`<webjs-suspense>` streaming** (unit, node, `renderToStream` + a `suspenseCtx`): collect the stream and assert the fallback flushes first, then the `<template data-webjs-resolve>` boundaries, and that multiple boundaries fetch concurrently via a timing assertion that fails if serial (worked in `packages/core/test/suspense/webjs-suspense-ssr.test.js`).
168
+ - **Client async render** (browser, WTR): mount a component and `await el.updateComplete`, then assert stale-while-revalidate keeps prior content during a gated re-fetch, `renderFallback()` shows only on a re-fetch (never first paint), a rejected render commits `renderError()`, and the race guard drops a superseded resolution. Use a gate promise inside `render()` to control timing (worked in `packages/core/test/suspense/browser/async-render-client.test.js`).
169
+ - **Progressive soft-nav streaming** (e2e, `WEBJS_E2E=1`): soft-navigate to a streamed page and assert the fallback is live in the DOM at the moment the URL advances (a buffered swap would only advance the URL once the boundary already resolved), then that the boundary streams in. The DOM-free reader helpers are unit-tested in `packages/core/test/routing/progressive-stream.test.js`.
170
+
165
171
  ### `assertNoA11yViolations(el, opts?)` (opt-in)
166
172
 
167
173
  An OPT-IN accessibility assertion that runs the standard axe-core engine against an element's subtree in the WTR Chromium session. Nothing calls it for you, it is never a forced gate.
@@ -304,13 +304,47 @@ core prop / metadata types rather than redefining them. The runtime stays plain
304
304
  `.js` + JSDoc; the overlay is types-only with zero runtime cost. A drift test
305
305
  keeps `index.d.ts` in lockstep with `index.js`'s runtime exports.
306
306
 
307
+ ### Typing the `auth()` session user
308
+
309
+ `auth()` resolves `{ user }`, and by default `user` is `Record<string,
310
+ unknown>`, so reading a custom field your `session`/`jwt` callbacks set
311
+ (such as `session.user.id`) needs a cast and a typo is not caught. Two
312
+ opt-in ways make `user` typed, both types-only.
313
+
314
+ Augment the `AuthUser` interface (NextAuth/Auth.js style) to type every
315
+ `auth()` call across the app:
316
+
317
+ ```ts
318
+ declare module '@webjsdev/server' {
319
+ interface AuthUser {
320
+ id: string;
321
+ role: 'admin' | 'member';
322
+ }
323
+ }
324
+ ```
325
+
326
+ Or parameterise the factory to type one instance without a global
327
+ augmentation. `createAuth<AppUser>(...)` returns an `auth()` whose `user`
328
+ is `AppUser`:
329
+
330
+ ```ts
331
+ const { auth } = createAuth<AppUser>({ secret, providers });
332
+ const session = await auth();
333
+ session?.user.role; // typed, no cast
334
+ ```
335
+
336
+ Un-augmented and un-parameterised, `AuthUser` is empty and resolves back
337
+ to `Record<string, unknown>`, so pre-existing untyped code keeps
338
+ compiling. The declared fields should mirror what the callbacks write
339
+ onto `session.user`.
340
+
307
341
  ### TypeScript is not required
308
342
 
309
343
  JS + JSDoc gets the same call-site type safety. The TypeScript language
310
344
  server reads `@typedef` / `@param` / `@returns` identically to `.ts`
311
345
  syntax. Add `"checkJs": true` to enforce types in editor + CI.
312
346
 
313
- ## Editor plugin: `@webjsdev/ts-plugin`
347
+ ## Editor plugin: `@webjsdev/intellisense`
314
348
 
315
349
  **Editor-only. Not required for the framework to run.** The runtime
316
350
  has no dependency on it.
@@ -319,7 +353,7 @@ A single plugin with its OWN in-template intelligence (no Lit dependency; the `w
319
353
 
320
354
  ```jsonc
321
355
  "plugins": [
322
- { "name": "@webjsdev/ts-plugin" }
356
+ { "name": "@webjsdev/intellisense" }
323
357
  ]
324
358
  ```
325
359
 
package/src/mcp-docs.js CHANGED
@@ -284,6 +284,7 @@ export const PROMPTS = [
284
284
  { name: 'add_dynamic_route', description: 'Scaffold a dynamic page reading params, e.g. app/users/[id]/page.ts.', arguments: [{ name: 'route', description: 'URL path with a [param], e.g. /users/[id]', required: false }] },
285
285
  { name: 'add_server_action', description: 'Scaffold a server action (.server.ts + use server) called from a component.', arguments: [{ name: 'feature', description: 'Feature/module name', required: false }] },
286
286
  { name: 'add_component', description: 'Scaffold an interactive WebComponent (signals, light DOM, register).', arguments: [{ name: 'tag', description: 'Custom-element tag, e.g. my-thing', required: false }] },
287
+ { name: 'fetch_data_in_component', description: 'Fetch server data IN a component with async render() (co-located, first-paint data), plus webjs-suspense streaming and renderFallback.', arguments: [{ name: 'tag', description: 'Custom-element tag, e.g. user-profile', required: false }] },
287
288
  { name: 'add_module', description: 'Scaffold a modules/<feature>/ slice (actions/queries/components/utils).', arguments: [{ name: 'feature', description: 'Feature name', required: false }] },
288
289
  ];
289
290
 
@@ -364,6 +365,30 @@ const PROMPT_BODIES = {
364
365
  'a class-field initializer that overwrites a reactive accessor breaks reactivity',
365
366
  '(use `declare` + `static properties`).',
366
367
  ].join('\n'),
368
+ fetch_data_in_component: [
369
+ 'Fetch server data IN the component that needs it, with an async render() (#469).',
370
+ 'No prop-drilling: SSR awaits the render, so the DATA is in the first paint.',
371
+ '',
372
+ '```ts',
373
+ 'class UserProfile extends WebComponent {',
374
+ ' static properties = { uid: { type: String } };',
375
+ ' declare uid: string;',
376
+ ' async render() {',
377
+ ' const u = await getUser(this.uid); // a `use server` action: real fn at SSR, RPC stub on the client',
378
+ ' return html`<h3>${u.name}</h3>`;',
379
+ ' }',
380
+ '}',
381
+ "UserProfile.register('user-profile');",
382
+ '```',
383
+ '',
384
+ 'Decoupled model: SSR BLOCKS by default (real data, no fallback). The client',
385
+ 're-fetch default is stale-while-revalidate. `renderFallback()` is the OPTIONAL',
386
+ 're-fetch loading state (re-fetch only, NEVER first paint). Errors are isolated',
387
+ 'per component by default (add `renderError()` only to customize). For SLOW data,',
388
+ 'wrap in `<webjs-suspense .fallback=${html`...`}>` to STREAM it (fallback first).',
389
+ 'Keep `Task` / signals for client-only data (it shows pending at SSR). Read',
390
+ '`webjs-docs://components` for the full decision guide and anti-patterns.',
391
+ ].join('\n'),
367
392
  add_module: [
368
393
  'Add a feature module. `modules/<feature>/` holds `actions/*.server.ts` (mutations),',
369
394
  '`queries/*.server.ts` (reads), `components/*.ts` (feature UI), `utils/*.ts` (pure),',
package/src/mcp-source.js CHANGED
@@ -28,7 +28,7 @@ import { createRequire } from 'node:module';
28
28
  import { join, resolve, sep, relative } from 'node:path';
29
29
 
30
30
  /** The published framework packages whose source an agent may want to read. */
31
- export const FRAMEWORK_PACKAGES = ['core', 'server', 'cli', 'ts-plugin', 'ui'];
31
+ export const FRAMEWORK_PACKAGES = ['core', 'server', 'cli', 'intellisense', 'ui'];
32
32
 
33
33
  /** Source file extensions worth grepping / reading (text, not assets). */
34
34
  const TEXT_EXT = /\.(?:js|ts|mjs|mts|cjs|cts|json|md)$/i;
package/src/mcp.js CHANGED
@@ -87,7 +87,7 @@ const SOURCE_SCHEMA = {
87
87
  },
88
88
  package: {
89
89
  type: 'string',
90
- description: 'Limit a no-args listing to one package (core, server, cli, ts-plugin, ui).',
90
+ description: 'Limit a no-args listing to one package (core, server, cli, intellisense, ui).',
91
91
  },
92
92
  },
93
93
  required: [],