@webjsdev/mcp 0.1.3 → 0.1.4
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 +1 -1
- package/resources/AGENTS.md +50 -10
- package/resources/agent-docs/advanced.md +29 -12
- package/resources/agent-docs/components.md +49 -18
- package/resources/agent-docs/lit-muscle-memory-gotchas.md +21 -27
- package/resources/agent-docs/recipes.md +49 -6
- package/resources/agent-docs/testing.md +2 -2
- package/resources/agent-docs/typescript.md +1 -1
- package/src/mcp-docs.js +2 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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": {
|
package/resources/AGENTS.md
CHANGED
|
@@ -37,6 +37,18 @@ reference there.
|
|
|
37
37
|
|
|
38
38
|
Claude Code enforces step 1 via `.claude/hooks/guard-branch-context.sh`. Other agents check manually.
|
|
39
39
|
|
|
40
|
+
### One task per git worktree when agents run concurrently
|
|
41
|
+
|
|
42
|
+
webjs is worked by MULTIPLE agents at once. If more than one agent (or more than one in-flight task) shares ONE working checkout, they collide: a `git checkout` in one moves `HEAD` under the other, so the next commit lands on the WRONG branch. This has happened (a `chore: release` commit landed on an unrelated `feat/` branch, with a contaminated changelog). So give each task its own worktree:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
git worktree add -b <prefix>/<slug> ../<repo>-<slug> origin/main
|
|
46
|
+
cd ../<repo>-<slug> # do ALL work for the task here
|
|
47
|
+
# ... commit + push from this worktree; after merge: git worktree remove ../<repo>-<slug>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
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.
|
|
51
|
+
|
|
40
52
|
### Skills are routed deterministically, never skipped
|
|
41
53
|
|
|
42
54
|
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.
|
|
@@ -75,7 +87,7 @@ See `agent-docs/framework-dev.md` for monorepo commands, workspace layout, per-f
|
|
|
75
87
|
|
|
76
88
|
## What webjs is
|
|
77
89
|
|
|
78
|
-
An **AI-first, web-components-first** framework inspired by NextJs, Lit, and Rails. The component runtime API matches lit (reactive
|
|
90
|
+
An **AI-first, web-components-first** framework inspired by NextJs, Lit, and Rails. The component runtime API matches lit (reactive properties, the lit lifecycle hooks, ReactiveControllers, the `lit-html` directive set, `html` / `css` templates) so lit training data transfers directly, but webjs ships its own no-build implementation under `packages/core/src/`. Reactive properties are the one deliberate divergence (invariant 10): instead of lit's `@property()` decorator or a `static properties` block, webjs declares them through the declare-free base-class factory `extends WebComponent({ count: Number })`.
|
|
79
91
|
|
|
80
92
|
- **No build step.** Source files are served as native ES modules. JSDoc `.js` is default; `.ts` / `.mts` is stripped through a pluggable stripper (#508): Node 24+'s built-in `module.stripTypeScriptTypes`, or `amaro` on Bun (byte-identical, position-preserving) (invariant 10 + `agent-docs/typescript.md`). **Runs on Node 24+ or Bun** (run a Bun app with `bun --bun run dev` / `start`); the early `assertNodeVersion()` preflight enforces the Node floor and admits Bun. On Bun, `startServer` selects a native `Bun.serve` listener shell instead of the node:http one (skipping the compat bridge for ~1.9x more req/s on the listening path, at near-complete feature parity, the one node-only gap being 103 Early Hints since `Bun.serve` has no informational-response API), via a runtime-neutral seam that also sets up future `Deno.serve` / embedded adapters. Edge runtimes (no filesystem) are a separate, later target.
|
|
81
93
|
- **SSR + CSR by default.** Pages are server-rendered HTML; components render light DOM by default, shadow DOM opt-in via `static shadow = true` with DSD SSR.
|
|
@@ -139,12 +151,30 @@ db/*.server.{js,ts} data layer (Drizzle: schema, columns, connection)
|
|
|
139
151
|
|
|
140
152
|
Every file is a plain ES module.
|
|
141
153
|
|
|
154
|
+
### Imports: prefer the `#` root alias over deep relatives (#555)
|
|
155
|
+
|
|
156
|
+
App-internal imports use the **`#` path alias** instead of deep `../../../`
|
|
157
|
+
relatives: `import { db } from '#db/connection.server.ts'`, `import { Button }
|
|
158
|
+
from '#components/ui/button.ts'`, `#lib/...`, `#modules/...`. It is Node's native
|
|
159
|
+
`package.json "imports"` field (the scaffold ships the single catch-all `"#*":
|
|
160
|
+
"./*"`, so any top-level folder is aliased with no config change), resolved at
|
|
161
|
+
runtime by Node 24+ AND Bun with **no build step and no tsconfig `paths`**. The
|
|
162
|
+
sigil is `#` (not `@`) and there is **no slash after it** (`#lib/...`, not
|
|
163
|
+
`#/lib/...`): a `#/`-prefixed key does not resolve on Bun. The webjs server
|
|
164
|
+
expands the same map for the import graph / auth gate / elision / browser
|
|
165
|
+
importmap, so a `#`-aliased `.server.ts` still trips the server-only boundary.
|
|
166
|
+
A same-directory import stays relative (`./sibling.ts`); only deep relatives
|
|
167
|
+
become `#`. The alias addresses a top-level SUBDIRECTORY (`#lib/...`,
|
|
168
|
+
`#components/...`); a bare root-level file (`./env.ts`) is imported relatively
|
|
169
|
+
(the browser importmap scope is per-directory, so a `#`-imported root file has
|
|
170
|
+
no browser mapping in dev). Opt out anywhere by writing a plain relative import.
|
|
171
|
+
|
|
142
172
|
---
|
|
143
173
|
|
|
144
174
|
## Public API of `@webjsdev/core`
|
|
145
175
|
|
|
146
176
|
```js
|
|
147
|
-
import { html, css, WebComponent, render } from '@webjsdev/core';
|
|
177
|
+
import { html, css, WebComponent, prop, render } from '@webjsdev/core';
|
|
148
178
|
import { renderToString } from '@webjsdev/core/server';
|
|
149
179
|
```
|
|
150
180
|
|
|
@@ -153,7 +183,8 @@ The bare `@webjsdev/core` specifier resolves to a BROWSER bundle dropping server
|
|
|
153
183
|
| Export | Purpose |
|
|
154
184
|
|---|---|
|
|
155
185
|
| `html` / `css` | Tagged template literals. `css` goes in `static styles`. |
|
|
156
|
-
| `WebComponent` | Base class for interactive components. |
|
|
186
|
+
| `WebComponent` | Base class for interactive components. Called as `WebComponent({ ... })` it returns a typed base declaring reactive properties. |
|
|
187
|
+
| `prop(type?, opts?)` | Declares one reactive property inside the `WebComponent({ ... })` factory with options (`reflect`, `state`, `attribute`, `default`, `converter`, `hasChanged`) and a narrowable TS type (`prop<Student>(Object)`). |
|
|
157
188
|
| `register(tag, C)` | Tag binding. Auto-called by `Class.register('tag')`. |
|
|
158
189
|
| `render(v, el)` | Client-side render into a DOM element. |
|
|
159
190
|
| `renderToString` | Server-side async render to HTML with DSD (from `/server`). |
|
|
@@ -179,18 +210,27 @@ lit-html parity: `repeat` (keyed lists), `unsafeHTML(str)` (trusted raw HTML, **
|
|
|
179
210
|
## `WebComponent` essentials
|
|
180
211
|
|
|
181
212
|
```ts
|
|
182
|
-
|
|
213
|
+
// Reactive properties are declared in the base-class factory (declare-free)
|
|
214
|
+
class MyThing extends WebComponent({
|
|
215
|
+
count: prop(Number, { reflect: true })
|
|
216
|
+
}) {
|
|
183
217
|
static shadow = false; // default light DOM; true = scoped shadow DOM
|
|
184
218
|
static lazy = false; // true = load module on viewport entry
|
|
185
|
-
static properties = { count: { type: Number, reflect: true } };
|
|
186
|
-
declare count: number; // TS only, typed accessor
|
|
187
219
|
static styles = css`…`;
|
|
220
|
+
|
|
221
|
+
constructor() {
|
|
222
|
+
super();
|
|
223
|
+
this.count = 0; // Fully typed, no declare!
|
|
224
|
+
}
|
|
225
|
+
|
|
188
226
|
render() { return html`…`; }
|
|
189
227
|
}
|
|
190
228
|
MyThing.register('my-thing');
|
|
191
229
|
```
|
|
192
230
|
|
|
193
|
-
**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.
|
|
231
|
+
**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.
|
|
232
|
+
|
|
233
|
+
**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 via the `default` option (`prop(Number, { default: 0 })`, a function default for a fresh object / array per instance) OR in the constructor after `super()`. 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`. Property options: `type` (default `String`), `reflect`, `state`, `hasChanged`, `converter`, `default`, `attribute`.
|
|
194
234
|
|
|
195
235
|
**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).
|
|
196
236
|
|
|
@@ -285,7 +325,7 @@ type ActionResult<T> =
|
|
|
285
325
|
|
|
286
326
|
Nested layouts auto-emit `<!--wj:children:<segment-path>-->` markers; the client router walks both DOMs and replaces only the deepest shared layout's children slot, preserving outer-layout DOM identity. Form submissions ride the same pipeline (`data-no-router` opts out). Wire bytes are minimized via the `X-Webjs-Have` header (the server returns only the divergent fragment); scroll is restored on back/forward. A non-GET `<form>` whose target page exports an `action` is the no-JS write-path (with JS the router applies the response in place: a `422` swaps without reload, a `303` is followed via fetch). A failed navigation recovers in place (a cancelable `webjs:navigation-error` event, else a minimal in-place alert), never a destructive full reload.
|
|
287
327
|
|
|
288
|
-
The advanced client-router surface is in `agent-docs/advanced.md`: **link prefetch** (on by default, `intent`
|
|
328
|
+
The advanced client-router surface is in `agent-docs/advanced.md`: **link prefetch** (on by default, device-adaptive default: `intent` on a hover pointer, `viewport` (dwell-gated, cancel-on-scroll-out) on touch, per-link `data-prefetch` override), **`<webjs-frame>`** partial-swap regions, **View Transitions** (opt-in via `<meta name="view-transition">`, plus `data-webjs-permanent` to persist a live element), and **stream actions** (`<webjs-stream>` element-level updates, #248). Production benefits from HTTP/2 at the edge; `npm run start` speaks plain HTTP/1.1 (put a reverse proxy in front for TLS + HTTP/2).
|
|
289
329
|
|
|
290
330
|
---
|
|
291
331
|
|
|
@@ -297,7 +337,7 @@ The advanced client-router surface is in `agent-docs/advanced.md`: **link prefet
|
|
|
297
337
|
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.
|
|
298
338
|
3. **Custom element tag names must contain a hyphen** (HTML spec). Pass the tag to `Class.register('tag-name')`, not a static field. Any short-string quote works: `'tag-name'`, `"tag-name"`, or `` `tag-name` `` (single-line, no interpolation).
|
|
299
339
|
4. **Event (`@`), property (`.`), boolean (`?`) holes in `html` must be unquoted**, e.g. `@click=${fn}`, never `@click="${fn}"`.
|
|
300
|
-
5. **Signals are the default state primitive.** Import `signal` / `computed` from `@webjsdev/core` and read via `signal.get()` inside `render()`; the built-in SignalWatcher tracks the reads and re-renders. Module-scope signals share state across components; instance-scope signals (constructor) are component-local.
|
|
340
|
+
5. **Signals are the default state primitive.** Import `signal` / `computed` from `@webjsdev/core` and read via `signal.get()` inside `render()`; the built-in SignalWatcher tracks the reads and re-renders. Module-scope signals share state across components; instance-scope signals (constructor) are component-local. The factory `WebComponent({ ... })` is reserved for values riding an HTML attribute, reflected to one, or arriving via `.prop=${value}` SSR hydration. For fine-grained DOM swap use `${watch(signal)}` from `@webjsdev/core/directives`.
|
|
301
341
|
6. **Page and layout default exports must be functions.** They return a value (usually `TemplateResult`). They do not call `render()` themselves.
|
|
302
342
|
7. **Light-DOM components with custom CSS MUST prefix every class selector with their tag name.** Tailwind utilities are unique by construction, so prefer them.
|
|
303
343
|
8. **Non-root layouts and pages MUST NOT** write `<!doctype>` / `<html>` / `<head>` / `<body>`. Only the root layout may.
|
|
@@ -310,7 +350,7 @@ The advanced client-router surface is in `agent-docs/advanced.md`: **link prefet
|
|
|
310
350
|
|
|
311
351
|
## Scaffolding
|
|
312
352
|
|
|
313
|
-
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). 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.
|
|
353
|
+
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, `trustedDependencies`, `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.
|
|
314
354
|
|
|
315
355
|
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.
|
|
316
356
|
|
|
@@ -570,20 +570,30 @@ affects a cached page.
|
|
|
570
570
|
### Link prefetch
|
|
571
571
|
|
|
572
572
|
Same-origin in-app links are prefetched speculatively so a click
|
|
573
|
-
resolves from a warm cache with no round-trip. On by default
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
573
|
+
resolves from a warm cache with no round-trip. On by default (no per-link
|
|
574
|
+
opt-in needed), the way Next / Nuxt / SvelteKit ship auto-prefetch. The
|
|
575
|
+
prefetch request carries the same `X-Webjs-Have` header a real navigation
|
|
576
|
+
sends, so the server returns the same divergent fragment; that fragment
|
|
577
|
+
lands in a dedicated prefetch cache (separate from the back/forward
|
|
578
|
+
snapshot cache) and `fetchAndApply` consumes it via `prefetchTake` before
|
|
579
|
+
falling back to the network.
|
|
580
|
+
|
|
581
|
+
The default strategy is DEVICE-ADAPTIVE, because one strategy cannot serve
|
|
582
|
+
both input modalities. On a hover-capable fine pointer (mouse / trackpad)
|
|
583
|
+
the default is `intent` (warm on hover / focus, a real head-start before
|
|
584
|
+
the click). On touch the default is `viewport` (warm as links settle
|
|
585
|
+
on-screen), because a touch device has no hover and `touchstart` fires at
|
|
586
|
+
tap time, too late to front-run the navigation. The modality is detected
|
|
587
|
+
with `matchMedia('(hover: hover) and (pointer: fine)')`, not a user-agent
|
|
588
|
+
sniff. A per-link `data-prefetch` always overrides the adaptive default.
|
|
580
589
|
|
|
581
590
|
Per link, set `data-prefetch` (a valid-HTML `data-*` attribute, the shape
|
|
582
591
|
SvelteKit and Astro use; Next / Nuxt / Remix use a component prop, which
|
|
583
592
|
webjs has no equivalent for since links are plain `<a href>`):
|
|
584
593
|
|
|
585
594
|
```html
|
|
586
|
-
<a href="/dashboard">
|
|
595
|
+
<a href="/dashboard">adaptive: intent on pointer, viewport on touch (default)</a>
|
|
596
|
+
<a href="/dashboard" data-prefetch="intent">hover / focus / touch</a>
|
|
587
597
|
<a href="/dashboard" data-prefetch="render">eager on insert</a>
|
|
588
598
|
<a href="/dashboard" data-prefetch="viewport">on scroll-into-view</a>
|
|
589
599
|
<a href="/dashboard" data-prefetch="none">never</a>
|
|
@@ -591,9 +601,14 @@ webjs has no equivalent for since links are plain `<a href>`):
|
|
|
591
601
|
|
|
592
602
|
Next-style aliases are accepted: `true` = `render`, `auto` = `viewport`,
|
|
593
603
|
`false` = `none`. `intent` waits a short dwell (~100ms) after hover/focus
|
|
594
|
-
so a pointer passing over a link does not fetch it
|
|
595
|
-
IntersectionObserver at threshold 0.5
|
|
596
|
-
|
|
604
|
+
so a pointer passing over a link does not fetch it. `viewport` uses an
|
|
605
|
+
IntersectionObserver at threshold 0.5 and waits a ~250ms dwell, cancelled
|
|
606
|
+
the instant the link scrolls back out, so a fast scroll through a long
|
|
607
|
+
list spends no requests (the same gate Astro / Next / Nuxt / Remix /
|
|
608
|
+
TanStack / Turbo apply). On touch, `touchstart` additionally warms the
|
|
609
|
+
tapped link itself (a single request for a link about to be navigated).
|
|
610
|
+
`render` and `viewport` are applied by a document scan on enable and after
|
|
611
|
+
each navigation.
|
|
597
612
|
|
|
598
613
|
Only internal links qualify, using the same eligibility as a click:
|
|
599
614
|
cross-origin, `download`, `target` other than `_self`, non-HTML
|
|
@@ -602,7 +617,9 @@ Opt out with `data-prefetch="none"`, `data-no-prefetch`, or
|
|
|
602
617
|
`rel="external"`. Speculation is bounded by a concurrency cap (excess
|
|
603
618
|
requests queue and drain as slots free, rather than being dropped),
|
|
604
619
|
in-flight de-dupe, and an LRU + TTL cache, and is disabled entirely under
|
|
605
|
-
`Save-Data
|
|
620
|
+
`Save-Data`, `prefers-reduced-data`, or a 2g `effectiveType` connection.
|
|
621
|
+
The guiding rule: snappy, but never at the cost of bloating the client
|
|
622
|
+
network tab; when the two conflict, the gate under-fetches. A mutating form submission and
|
|
606
623
|
`revalidate(url)` both evict the prefetch cache alongside the snapshot
|
|
607
624
|
cache, so a fragment prefetched before a mutation is never served stale.
|
|
608
625
|
|
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
| Option | Type | Default | Meaning |
|
|
6
6
|
|---|---|---|---|
|
|
7
|
-
|
|
7
|
+
These options are passed to the `prop()` helper inside the `WebComponent({ ... })` factory (e.g. `count: prop(Number, { reflect: true })`); the bare form `count: Number` is shorthand for `prop(Number)`.
|
|
8
|
+
|
|
9
|
+
| Option | Type | Default | Meaning |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| `type` | `Number\|String\|Boolean\|Object\|Array` | `String` | Used by the default attribute converter (the first `prop()` argument) |
|
|
8
12
|
| `reflect` | `boolean` | `false` | Property changes write back to the HTML attribute |
|
|
9
13
|
| `state` | `boolean` | `false` | Internal-only. No attribute, not in `observedAttributes` |
|
|
14
|
+
| `attribute` | `string` | derived from the prop name | The HTML attribute name the property rides |
|
|
15
|
+
| `default` | value or `() => value` | none | Declarative initial value (a function runs per instance for a fresh object / array) |
|
|
10
16
|
| `hasChanged` | `(newVal, oldVal) => boolean` | strict `!==` | Custom change detection |
|
|
11
17
|
| `converter` | `{ fromAttribute?, toAttribute? }` | type-based | Custom attribute ↔ property serialization |
|
|
12
18
|
|
|
@@ -14,18 +20,47 @@ Built-in constructors (`String`, `Number`, `Boolean`, `Array`, `Object`) feed
|
|
|
14
20
|
the default attribute coercion. For anything the default can't parse correctly
|
|
15
21
|
(Date, Map, Set, discriminated unions) supply a custom `converter`.
|
|
16
22
|
|
|
17
|
-
##
|
|
23
|
+
## Declaring reactive properties: the base-class factory
|
|
24
|
+
|
|
25
|
+
Reactive properties are declared by passing the property shape into the **base-class factory** `WebComponent({ ... })`. The types flow automatically to `this.<prop>`, so there is no `static properties` block and no `declare` line. A direct `static properties` block throws at runtime (caught statically by the `no-static-properties` rule).
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
class Counter extends WebComponent({
|
|
29
|
+
count: Number
|
|
30
|
+
}) {
|
|
31
|
+
constructor() {
|
|
32
|
+
super();
|
|
33
|
+
this.count = 0; // fully typed, no declare needed
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
18
37
|
|
|
19
|
-
The
|
|
20
|
-
constructor via `Object.defineProperty`. Without `declare`, TypeScript
|
|
21
|
-
emits `student = undefined` after `super()`, which under modern class-
|
|
22
|
-
field semantics uses `[[Define]]` to overwrite the accessor. Result:
|
|
23
|
-
`this.student = …` no longer goes through the setter, no `requestUpdate`,
|
|
24
|
-
no `hasChanged`, no reflect, and reactivity silently breaks.
|
|
38
|
+
The bare form takes a type constructor (`count: Number`, `label: String`, `open: Boolean`). The `prop()` helper carries options and narrows the TS type:
|
|
25
39
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
40
|
+
```ts
|
|
41
|
+
class Dialog extends WebComponent({
|
|
42
|
+
open: prop(Boolean, { reflect: true }), // reflects to the `open` attribute
|
|
43
|
+
showClose: prop(Boolean, { attribute: 'show-close-button' }), // custom attribute name
|
|
44
|
+
variant: prop<'info' | 'danger'>(String, { reflect: true }), // narrowed union type
|
|
45
|
+
student: prop<Student>(Object), // narrowed object type
|
|
46
|
+
internal: prop({ state: true }), // internal state, no attribute, no type
|
|
47
|
+
}) {
|
|
48
|
+
constructor() {
|
|
49
|
+
super();
|
|
50
|
+
this.student = { name: '', email: '' };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Set defaults via the `default` option (a function default runs per instance for a fresh object / array) or in the constructor after `super()`. **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.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// default via the option (no constructor needed)
|
|
59
|
+
class Counter extends WebComponent({
|
|
60
|
+
count: prop(Number, { default: 0 }),
|
|
61
|
+
items: prop(Array, { default: () => [] }), // function default = fresh array per instance
|
|
62
|
+
}) {}
|
|
63
|
+
```
|
|
29
64
|
|
|
30
65
|
## Lifecycle hooks (lit-aligned)
|
|
31
66
|
|
|
@@ -55,9 +90,7 @@ See [`/docs/lifecycle`](https://docs.webjs.com/docs/lifecycle) for per-hook usag
|
|
|
55
90
|
A component can fetch its own server data into the first paint. `render()` may be `async`, so you write the natural line directly:
|
|
56
91
|
|
|
57
92
|
```ts
|
|
58
|
-
class UserProfile extends WebComponent {
|
|
59
|
-
static properties = { uid: { type: String } };
|
|
60
|
-
declare uid: string;
|
|
93
|
+
class UserProfile extends WebComponent({ uid: String }) {
|
|
61
94
|
async render() {
|
|
62
95
|
const u = await getUser(this.uid); // a 'use server' action: real fn at SSR, RPC stub on the client
|
|
63
96
|
return html`<h3>${u.name}</h3>`;
|
|
@@ -75,9 +108,7 @@ Writing `await` makes the function async by the JS rule, and every render path a
|
|
|
75
108
|
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
109
|
|
|
77
110
|
```ts
|
|
78
|
-
class UserActivity extends WebComponent {
|
|
79
|
-
static properties = { uid: { type: String } };
|
|
80
|
-
declare uid: string;
|
|
111
|
+
class UserActivity extends WebComponent({ uid: String }) {
|
|
81
112
|
renderFallback() { return html`<div class="skeleton h-24"></div>`; } // shown only while a re-fetch is in flight
|
|
82
113
|
async render() {
|
|
83
114
|
const items = await getActivity(this.uid);
|
|
@@ -153,7 +184,7 @@ server/client split to reason about. A component stays elidable as long
|
|
|
153
184
|
as it has none of the following.
|
|
154
185
|
|
|
155
186
|
- An `@event` binding in a template (`@click=${...}`), or a native event-handler property (`.onclick=${...}`).
|
|
156
|
-
- A reactive property
|
|
187
|
+
- A factory-declared reactive property (`WebComponent({ ... })`) that is not `{ state: true }`. Attribute-driven or `.prop`-driven values are the channel a parent uses to push client updates.
|
|
157
188
|
- An overridden lifecycle hook (anything in the table above), as a method or an arrow class field.
|
|
158
189
|
- A `signal` / `computed` / `watch` / `Task` / `ref` / `live` / streaming directive imported from `@webjsdev/core`, OR a transitive import of a module that reads shared module-scope signal state.
|
|
159
190
|
- An `addController(...)` or `requestUpdate()` call.
|
|
@@ -77,14 +77,14 @@ lit form unless the vanilla API is genuinely unavoidable.
|
|
|
77
77
|
|
|
78
78
|
| Vanilla muscle memory | Lit-style webjs form |
|
|
79
79
|
|---|---|
|
|
80
|
-
| `this.getAttribute('x')` / `this.hasAttribute('x')` for own config | a reactive prop: `
|
|
80
|
+
| `this.getAttribute('x')` / `this.hasAttribute('x')` for own config | a reactive prop declared in the factory: `extends WebComponent({ x: String })`, read `this.x` (the prop rides the `x` attribute) |
|
|
81
81
|
| `this.setAttribute('x', v)` / `removeAttribute` to reflect own state | a reactive prop with `reflect: true`, or for non-attribute state a `signal` |
|
|
82
82
|
| `state: true` reactive prop for internal state | a `signal` (instance signal in the constructor, or module-scope) |
|
|
83
83
|
| `this.classList.add/toggle(...)` on self | a `class=${...}` binding in `render()` |
|
|
84
84
|
| `this.innerHTML = ...` / `appendChild` / `document.createElement` | return the markup from `render()` as `` html`...` `` |
|
|
85
85
|
| `this.addEventListener('click', ...)` on own/child elements | a `@click=${...}` binding in the template |
|
|
86
86
|
| `this.querySelector(...)` to reach own rendered DOM | the `ref()` directive + `createRef()`, or read a `<form>` with `new FormData(form)` |
|
|
87
|
-
| manual `observedAttributes` + `attributeChangedCallback` | `
|
|
87
|
+
| manual `observedAttributes` + `attributeChangedCallback` | a factory-declared reactive prop `WebComponent({ ... })` (the framework derives both) |
|
|
88
88
|
| manual `customElements.define('x', C)` | `C.register('x')` |
|
|
89
89
|
|
|
90
90
|
Emitting an event with `this.dispatchEvent(new CustomEvent(...))` is the
|
|
@@ -244,8 +244,8 @@ the event methods (`addEventListener` / `removeEventListener` /
|
|
|
244
244
|
`dispatchEvent`), and `this.attachInternals()` ARE backed by a server shim,
|
|
245
245
|
so reading an attribute in `render()`, wiring a delegated listener in the
|
|
246
246
|
constructor, or reflecting a property during the SSR update cycle all work.
|
|
247
|
-
Reading attributes that drive render through a reactive
|
|
248
|
-
(`
|
|
247
|
+
Reading attributes that drive render through a factory-declared reactive
|
|
248
|
+
property (`WebComponent({ ... })`) is still the idiomatic path, but
|
|
249
249
|
`this.hasAttribute(...)` no longer crashes.
|
|
250
250
|
|
|
251
251
|
Two guards catch the browser-only cases. `webjs check` flags browser
|
|
@@ -278,20 +278,20 @@ connectedCallback() {
|
|
|
278
278
|
|
|
279
279
|
### 5. Class-field initializers for reactive properties
|
|
280
280
|
|
|
281
|
-
|
|
282
|
-
accessor.
|
|
281
|
+
In Lit, class-field initializers (like `student: Student = { name: '', email: '' }`) are commonly used. In webjs, the base class installs reactive accessors on `this` inside the constructor via `Object.defineProperty` (to support SSR property hydration and signals). Under modern V8 class-field semantics, a class-field initializer compiles to an assignment after `super()`, which uses `[[Define]]` and overwrites the accessor, silently breaking reactivity. The footgun is still live with factory-declared props, so it stays a gotcha.
|
|
283
282
|
|
|
284
|
-
|
|
285
|
-
// wrong (the initializer overwrites the framework accessor after super())
|
|
286
|
-
class StudentCard extends WebComponent {
|
|
287
|
-
static properties = { student: { type: Object } };
|
|
288
|
-
student: Student = { name: '', email: '' };
|
|
289
|
-
}
|
|
283
|
+
The fix is to declare the prop in the base-class factory `WebComponent({ ... })` (which types it automatically, no `declare` line) and set its default via the `default` option or in the constructor after `super()`, never as a class-field initializer:
|
|
290
284
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
285
|
+
```ts
|
|
286
|
+
// default via the option (a function default = fresh object per instance)
|
|
287
|
+
class StudentCard extends WebComponent({
|
|
288
|
+
student: prop<Student>(Object, { default: () => ({ name: '', email: '' }) }),
|
|
289
|
+
}) {}
|
|
290
|
+
|
|
291
|
+
// or set the default in the constructor (fully typed, no declare needed)
|
|
292
|
+
class StudentCard extends WebComponent({
|
|
293
|
+
student: prop<Student>(Object),
|
|
294
|
+
}) {
|
|
295
295
|
constructor() {
|
|
296
296
|
super();
|
|
297
297
|
this.student = { name: '', email: '' };
|
|
@@ -299,17 +299,11 @@ class StudentCard extends WebComponent {
|
|
|
299
299
|
}
|
|
300
300
|
```
|
|
301
301
|
|
|
302
|
-
`webjs check` flags
|
|
303
|
-
AI agents emit the broken form on autopilot. The convention check is
|
|
304
|
-
the safety net, not the primary defense. Authoring code should use
|
|
305
|
-
`declare` plus constructor defaults from the start.
|
|
302
|
+
`webjs check` flags a class-field initializer on a factory-declared prop via the `reactive-props-no-class-field` rule.
|
|
306
303
|
|
|
307
|
-
### 6. The `@property()` decorator
|
|
304
|
+
### 6. The `@property()` decorator and a direct `static properties` block
|
|
308
305
|
|
|
309
|
-
|
|
310
|
-
`static properties = { ... }` plus a matching `declare` for the typed
|
|
311
|
-
accessor, as shown above. Decorators are non-erasable, so they would
|
|
312
|
-
force the framework to depend on a build step.
|
|
306
|
+
The `@property()` decorator is banned by framework invariant 10 (erasable TS): decorators are non-erasable, so they would force the framework to depend on a build step. A direct `static properties = { ... }` block is also gone, and webjs THROWS at runtime if a class body declares one (flagged by the `no-static-properties` rule). The single replacement for both is the declare-free base-class factory `WebComponent({ ... })` (the `prop()` helper carries options), as shown above.
|
|
313
307
|
|
|
314
308
|
## Patterns that produce different visual output
|
|
315
309
|
|
|
@@ -463,8 +457,8 @@ must move with it, use `repeat()` with a stable key.
|
|
|
463
457
|
| `Task` for client-time async | `Task` (no change, that's its job) |
|
|
464
458
|
| `window.X` or `document.X` in constructor or `render()` | Move to `connectedCallback` |
|
|
465
459
|
| Top-level `import` of browser-only library | Dynamic `import()` inside `connectedCallback` |
|
|
466
|
-
| `student: Student = { ... }` field initializer | `
|
|
467
|
-
| `@property()` decorator
|
|
460
|
+
| `student: Student = { ... }` field initializer | Base-class factory `WebComponent({ student: Object })`, default via the `default` option or the constructor |
|
|
461
|
+
| `@property()` decorator / `static properties` block | Base-class factory `WebComponent({ ... })` (the `prop()` helper carries options) |
|
|
468
462
|
| `static styles = css` / inline `<style>` with semantic class names in a light-DOM component | Tailwind utilities (the default); or `static shadow = true` for genuinely scoped CSS |
|
|
469
463
|
| Plain `.map()` for an interactive/stateful list | Works (reconciles in place, keeps node identity); use `repeat(items, key, t)` only when the list **reorders** |
|
|
470
464
|
| `willUpdate` for SSR-visible derived state | Works (runs at SSR); keep it a pure derivation |
|
|
@@ -200,6 +200,53 @@ option: `export const POST = route(createPost, { validate })`. A
|
|
|
200
200
|
validator that THROWS (the classic `Schema.parse` style) becomes a 400, and a
|
|
201
201
|
non-envelope return transforms the input.
|
|
202
202
|
|
|
203
|
+
## HTTP verbs, caching, streaming, and cancellation (#488, #489, #490, #492)
|
|
204
|
+
|
|
205
|
+
A `'use server'` action is a POST by default. Reserved sibling exports, read
|
|
206
|
+
statically (the same way a page reads `export const revalidate`), change its
|
|
207
|
+
HTTP semantics WITHOUT changing the call site (you still write `await
|
|
208
|
+
getUser(7)`). One callable function per configured file (a second callable is a
|
|
209
|
+
`webjs check` error).
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
// modules/users/queries/get-user.server.ts: a cached, tagged GET read
|
|
213
|
+
'use server';
|
|
214
|
+
export const method = 'GET'; // absent = POST
|
|
215
|
+
export const cache = 60; // seconds, or { maxAge, swr, public }
|
|
216
|
+
export const tags = (id: number) => ['user:' + id];
|
|
217
|
+
export async function getUser(id: number) { return db.query.users.findFirst({ where: { id } }); }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
// a mutation evicts the tags it touches
|
|
222
|
+
'use server';
|
|
223
|
+
export const invalidates = (id: number) => ['user:' + id];
|
|
224
|
+
export const middleware = [requireAuth]; // #490: async (ctx, next) => result; actionContext()
|
|
225
|
+
export async function updateUser(id: number, patch: Partial<User>) { /* ... */ }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
- A **GET** rides args in the URL (POST fallback over a 4KB cap), is CSRF-exempt,
|
|
229
|
+
and carries `Cache-Control` + a weak `ETag` (304 on `If-None-Match`) +
|
|
230
|
+
`X-Webjs-Tags`. A **mutation** (POST/PUT/PATCH/DELETE) sends the rich body
|
|
231
|
+
(DELETE rides the URL), is CSRF-protected, and on success evicts its
|
|
232
|
+
`invalidates` tags and reports them via `X-Webjs-Invalidate`. A method mismatch
|
|
233
|
+
is a `405` + `Allow`.
|
|
234
|
+
- **SAFETY:** `cache` with `public: true` SHARES one response across all users,
|
|
235
|
+
keyed only by URL + args. Use it ONLY for data identical for every visitor (the
|
|
236
|
+
same rule as a page's `export const revalidate`).
|
|
237
|
+
- **Streaming (#489):** return a `ReadableStream` / async iterable / async
|
|
238
|
+
generator (any verb) and the call site does `for await (const c of await
|
|
239
|
+
streamTokens(8))`. Each chunk is rich-serialized as yielded, back-pressured,
|
|
240
|
+
and the source is cancelled on disconnect. A streamed result is never cached /
|
|
241
|
+
ETagged / seeded.
|
|
242
|
+
- **Cancellation (#492):** read the request `AbortSignal` via `actionSignal()`
|
|
243
|
+
(from `@webjsdev/server`) to stop work on disconnect; a superseded client
|
|
244
|
+
`async render()` auto-aborts the previous in-flight fetch.
|
|
245
|
+
- **SSR seeding (#472):** an action result invoked during a non-streamed SSR
|
|
246
|
+
render is serialized into the page; the stub reads that seed on its first client
|
|
247
|
+
call, so a shipping component does not re-fetch on hydration (consume-once,
|
|
248
|
+
fail-open; opt out with `"webjs": { "seed": false }`).
|
|
249
|
+
|
|
203
250
|
## Add a component
|
|
204
251
|
|
|
205
252
|
```ts
|
|
@@ -217,9 +264,7 @@ A leaf component fetches its own server data into the first paint, co-located, w
|
|
|
217
264
|
|
|
218
265
|
```ts
|
|
219
266
|
// (a) blocking async render: real data in the first paint, the common case
|
|
220
|
-
class UserProfile extends WebComponent {
|
|
221
|
-
static properties = { uid: { type: String } };
|
|
222
|
-
declare uid: string;
|
|
267
|
+
class UserProfile extends WebComponent({ uid: String }) {
|
|
223
268
|
async render() {
|
|
224
269
|
const u = await getUser(this.uid);
|
|
225
270
|
return html`<h3>${u.name}</h3>`;
|
|
@@ -235,9 +280,7 @@ html`
|
|
|
235
280
|
`;
|
|
236
281
|
|
|
237
282
|
// (c) renderFallback() = the CLIENT re-fetch loading state (never the first paint)
|
|
238
|
-
class UserActivity extends WebComponent {
|
|
239
|
-
static properties = { uid: { type: String } };
|
|
240
|
-
declare uid: string;
|
|
283
|
+
class UserActivity extends WebComponent({ uid: String }) {
|
|
241
284
|
renderFallback() { return html`<div class="skeleton h-24"></div>`; }
|
|
242
285
|
async render() {
|
|
243
286
|
const items = await getActivity(this.uid);
|
|
@@ -352,8 +352,8 @@ test/
|
|
|
352
352
|
|
|
353
353
|
App-side runners:
|
|
354
354
|
|
|
355
|
-
- `webjs test` →
|
|
356
|
-
- `webjs test --browser` → web-test-runner against `test/**/browser
|
|
355
|
+
- `webjs test` → the runtime's test runner over everything not under `browser/` or `e2e/` (#570): `node --test` on Node, `bun test` on Bun (dispatched on `process.versions.bun`, since `bun --test` is invalid).
|
|
356
|
+
- `webjs test --browser` → web-test-runner against `test/**/browser/**` (the app's resolved `wtr` bin, spawned with the current runtime, no `npx`).
|
|
357
357
|
- `WEBJS_E2E=1 webjs test` adds e2e.
|
|
358
358
|
|
|
359
359
|
App AI agents read this convention through the scaffold's
|
|
@@ -386,7 +386,7 @@ A single plugin with its OWN in-template intelligence (no Lit dependency; the `w
|
|
|
386
386
|
Gives you, inside `` html`` `` templates:
|
|
387
387
|
- **Go-to-definition** from `<my-counter>` to the class registered via `MyCounter.register('my-counter')`, from an attribute / property / event name to its class member, and from a CSS class in `class="…"` to its `` css`` `` rule.
|
|
388
388
|
- **Completions**: reachable custom-element tag names after `<`, and binding-aware attribute completions: `.` offers property names, plain / `?` offer hyphenated attribute names (`maxLength` → `max-length`), `@event` is permissive.
|
|
389
|
-
- **Diagnostics**: attribute / property value type-checks (`<my-counter .count=${expr}>` assignability-checks `typeof expr` against `
|
|
389
|
+
- **Diagnostics**: attribute / property value type-checks (`<my-counter .count=${expr}>` assignability-checks `typeof expr` against the property's type from the `WebComponent({ count: Number })` factory shape, and `@click=${fn}` must be callable), unquoted `@`/`.`/`?` bindings (invariant 4), and expressionless `.prop` bindings.
|
|
390
390
|
- **Hover**: a tag shows its component class; an attribute / property / event shows its declared type.
|
|
391
391
|
|
|
392
392
|
Every feature is gated on import-graph reachability: a tag is recognized only if the file registering it is reachable from the file you're editing. There is deliberately no blanket "unknown tag / attribute" diagnostic (webjs has no element type map, so it would false-positive on third-party custom elements).
|
package/src/mcp-docs.js
CHANGED
|
@@ -365,16 +365,14 @@ const PROMPT_BODIES = {
|
|
|
365
365
|
'Tag MUST contain a hyphen (invariant 3). Event/property/boolean holes are',
|
|
366
366
|
'unquoted (invariant 4). Read `webjs-docs://lit-muscle-memory-gotchas` first:',
|
|
367
367
|
'a class-field initializer that overwrites a reactive accessor breaks reactivity',
|
|
368
|
-
'(
|
|
368
|
+
'(declare reactive props via the `extends WebComponent({ … })` factory).',
|
|
369
369
|
].join('\n'),
|
|
370
370
|
fetch_data_in_component: [
|
|
371
371
|
'Fetch server data IN the component that needs it, with an async render() (#469).',
|
|
372
372
|
'No prop-drilling: SSR awaits the render, so the DATA is in the first paint.',
|
|
373
373
|
'',
|
|
374
374
|
'```ts',
|
|
375
|
-
'class UserProfile extends WebComponent {',
|
|
376
|
-
' static properties = { uid: { type: String } };',
|
|
377
|
-
' declare uid: string;',
|
|
375
|
+
'class UserProfile extends WebComponent({ uid: String }) {',
|
|
378
376
|
' async render() {',
|
|
379
377
|
' const u = await getUser(this.uid); // a `use server` action: real fn at SSR, RPC stub on the client',
|
|
380
378
|
' return html`<h3>${u.name}</h3>`;',
|