@webjsdev/mcp 0.1.2 → 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 +68 -27
- package/resources/agent-docs/advanced.md +29 -12
- package/resources/agent-docs/built-ins.md +10 -8
- package/resources/agent-docs/components.md +52 -19
- package/resources/agent-docs/configuration.md +31 -0
- package/resources/agent-docs/framework-dev.md +1 -1
- package/resources/agent-docs/lit-muscle-memory-gotchas.md +21 -27
- package/resources/agent-docs/recipes.md +90 -46
- package/resources/agent-docs/testing.md +48 -3
- package/resources/agent-docs/typescript.md +8 -7
- package/src/mcp-docs.js +6 -6
- package/src/mcp.js +3 -3
- package/resources/agent-docs/ssr-partial-nav-design.md +0 -214
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
|
@@ -14,10 +14,9 @@ reference there.
|
|
|
14
14
|
| `agent-docs/components.md` | WebComponent deep-dive (controllers, hooks, light/shadow DOM, slots) |
|
|
15
15
|
| `agent-docs/styling.md` | Tailwind helpers + vanilla-CSS opt-out conventions |
|
|
16
16
|
| `agent-docs/built-ins.md` | Auth, sessions, env vars, caching (cache(), HTTP cache, asset-hash, conditional GET), rate-limit, broadcast, file storage |
|
|
17
|
-
| `agent-docs/configuration.md` | The `package.json` `"webjs"` block (security headers, CSP, redirects, trailing-slash, basePath, ingress caps) + observability |
|
|
17
|
+
| `agent-docs/configuration.md` | The `package.json` `"webjs"` block (security headers, CSP, redirects, trailing-slash, basePath, ingress caps, dev/start task orchestration) + observability |
|
|
18
18
|
| `agent-docs/advanced.md` | Suspense streaming, performance, bundling, client router (prefetch, frames, view transitions, stream actions), WebSockets |
|
|
19
19
|
| `agent-docs/typescript.md` | TS at runtime + full-stack type safety |
|
|
20
|
-
| `agent-docs/deployment.md` | Production, runtime targets, embedded use |
|
|
21
20
|
| `agent-docs/service-worker.md` | The opt-in progressive-enhancement service worker (`public/sw.js`) |
|
|
22
21
|
| `agent-docs/testing.md` | Unit, browser, convention validation, the `handle()` test harness (`@webjsdev/server/testing`) |
|
|
23
22
|
| `agent-docs/framework-dev.md` | Monorepo dev (only when editing webjs itself): commands, repo-health git config, changelog flow, dev error overlay |
|
|
@@ -38,9 +37,21 @@ reference there.
|
|
|
38
37
|
|
|
39
38
|
Claude Code enforces step 1 via `.claude/hooks/guard-branch-context.sh`. Other agents check manually.
|
|
40
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
|
+
|
|
41
52
|
### Skills are routed deterministically, never skipped
|
|
42
53
|
|
|
43
|
-
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. Tests in `test/hooks/route-skills.test.mjs
|
|
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.
|
|
44
55
|
|
|
45
56
|
### Autonomous mode (sandbox / bypass permissions)
|
|
46
57
|
|
|
@@ -76,9 +87,9 @@ See `agent-docs/framework-dev.md` for monorepo commands, workspace layout, per-f
|
|
|
76
87
|
|
|
77
88
|
## What webjs is
|
|
78
89
|
|
|
79
|
-
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 })`.
|
|
80
91
|
|
|
81
|
-
- **No build step.** Source files are served as native ES modules. JSDoc `.js` is default; `.ts` / `.mts` is stripped
|
|
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.
|
|
82
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.
|
|
83
94
|
- **Progressive enhancement is the default architecture.** Pages and components are SSR'd; with JS off, content reads, `<a>` navigates, `<form>` server actions submit, display-only elements render. JS is opt-in *per interactive behaviour* (`@click`, a reactive property assignment, a signal mutation). Never write a first paint that depends on hydration; never use `fetch` + JS where a `<form>` + server action would do.
|
|
84
95
|
- **Display-only components are elided from the browser.** A component with no interactivity signal renders identical HTML with or without its JS, so the framework strips its import (and any vendor reachable only through it, importmap entry included) from the served source. Automatic, conservative, verified differentially. Disable with `"webjs": { "elide": false }` or `WEBJS_ELIDE=0`. See `agent-docs/components.md`.
|
|
@@ -101,7 +112,7 @@ A page/layout module still **loads** in the browser for its top-level side effec
|
|
|
101
112
|
|
|
102
113
|
`route.{js,ts}` is the one routing file that is NOT isomorphic: a server-only HTTP handler (named `GET` / `POST` exports), the webjs equivalent of a Next route handler. It never ships to the client.
|
|
103
114
|
|
|
104
|
-
**`.server.{js,ts}` is the one server boundary, an RPC + source-protection mechanism, NOT an RSC component.** With `'use server'` exports are RPC-callable (the browser import becomes a stub POSTing to `/__webjs/action/<hash>/<fn>`); without it the file is a server-only utility whose browser import **throws at module load** (
|
|
115
|
+
**`.server.{js,ts}` is the one server boundary, an RPC + source-protection mechanism, NOT an RSC component.** With `'use server'` exports are RPC-callable (the browser import becomes a stub POSTing to `/__webjs/action/<hash>/<fn>`); without it the file is a server-only utility whose browser import **throws at module load** (the DB driver, secrets, `node:*`, hashing). Consequence: **never import a no-`'use server'` util directly into a page, layout, or component** (it works at SSR but the client stub crashes on load); use it inside `'use server'` actions, `route.{js,ts}`, or `middleware`, and reach it from a page by importing a `'use server'` action (whose RPC stub loads safely client-side). This boundary, not a component annotation, is how a dependency is kept off the client (a date library used only during SSR belongs in `lib/format.server.ts`). See `agent-docs/components.md`.
|
|
105
116
|
|
|
106
117
|
---
|
|
107
118
|
|
|
@@ -135,17 +146,35 @@ lib/ app-wide code (lib/*.server.js infra, lib/utils/ bro
|
|
|
135
146
|
modules/<feature>/ feature-scoped: actions/ (mutations), queries/ (reads), components/, utils/, types.js
|
|
136
147
|
components/*.js SHARED presentational primitives
|
|
137
148
|
public/* static assets, served at /<name>
|
|
138
|
-
|
|
149
|
+
db/*.server.{js,ts} data layer (Drizzle: schema, columns, connection)
|
|
139
150
|
```
|
|
140
151
|
|
|
141
152
|
Every file is a plain ES module.
|
|
142
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
|
+
|
|
143
172
|
---
|
|
144
173
|
|
|
145
174
|
## Public API of `@webjsdev/core`
|
|
146
175
|
|
|
147
176
|
```js
|
|
148
|
-
import { html, css, WebComponent, render } from '@webjsdev/core';
|
|
177
|
+
import { html, css, WebComponent, prop, render } from '@webjsdev/core';
|
|
149
178
|
import { renderToString } from '@webjsdev/core/server';
|
|
150
179
|
```
|
|
151
180
|
|
|
@@ -154,7 +183,8 @@ The bare `@webjsdev/core` specifier resolves to a BROWSER bundle dropping server
|
|
|
154
183
|
| Export | Purpose |
|
|
155
184
|
|---|---|
|
|
156
185
|
| `html` / `css` | Tagged template literals. `css` goes in `static styles`. |
|
|
157
|
-
| `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)`). |
|
|
158
188
|
| `register(tag, C)` | Tag binding. Auto-called by `Class.register('tag')`. |
|
|
159
189
|
| `render(v, el)` | Client-side render into a DOM element. |
|
|
160
190
|
| `renderToString` | Server-side async render to HTML with DSD (from `/server`). |
|
|
@@ -180,18 +210,27 @@ lit-html parity: `repeat` (keyed lists), `unsafeHTML(str)` (trusted raw HTML, **
|
|
|
180
210
|
## `WebComponent` essentials
|
|
181
211
|
|
|
182
212
|
```ts
|
|
183
|
-
|
|
213
|
+
// Reactive properties are declared in the base-class factory (declare-free)
|
|
214
|
+
class MyThing extends WebComponent({
|
|
215
|
+
count: prop(Number, { reflect: true })
|
|
216
|
+
}) {
|
|
184
217
|
static shadow = false; // default light DOM; true = scoped shadow DOM
|
|
185
218
|
static lazy = false; // true = load module on viewport entry
|
|
186
|
-
static properties = { count: { type: Number, reflect: true } };
|
|
187
|
-
declare count: number; // TS only, typed accessor
|
|
188
219
|
static styles = css`…`;
|
|
220
|
+
|
|
221
|
+
constructor() {
|
|
222
|
+
super();
|
|
223
|
+
this.count = 0; // Fully typed, no declare!
|
|
224
|
+
}
|
|
225
|
+
|
|
189
226
|
render() { return html`…`; }
|
|
190
227
|
}
|
|
191
228
|
MyThing.register('my-thing');
|
|
192
229
|
```
|
|
193
230
|
|
|
194
|
-
**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`.
|
|
195
234
|
|
|
196
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).
|
|
197
236
|
|
|
@@ -259,7 +298,7 @@ One custom element per file; call `Class.register('tag')` at module top level. S
|
|
|
259
298
|
## Modules architecture (preferred for non-trivial apps)
|
|
260
299
|
|
|
261
300
|
- **`modules/<feature>/actions/*.server.{js,ts}`** mutations, **`queries/*.server.{js,ts}`** reads (one function per file), **`components/*.{js,ts}`** feature-owned components (shared UI in top-level `components/`), **`utils/*.{js,ts}`** pure helpers (no `'use server'`, no DB), **`types.{js,ts}`** typedefs.
|
|
262
|
-
- **`lib/`** cross-cutting: `lib/*.server.{js,ts}` server-only infra (
|
|
301
|
+
- **`lib/`** cross-cutting: `lib/*.server.{js,ts}` server-only infra (session, hashing; the DB connection lives in `db/connection.server.ts`), `lib/utils/*` browser-safe helpers, `lib/*.ts` app-wide values.
|
|
263
302
|
|
|
264
303
|
### The `ActionResult<T>` envelope
|
|
265
304
|
|
|
@@ -286,7 +325,7 @@ type ActionResult<T> =
|
|
|
286
325
|
|
|
287
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.
|
|
288
327
|
|
|
289
|
-
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).
|
|
290
329
|
|
|
291
330
|
---
|
|
292
331
|
|
|
@@ -294,11 +333,11 @@ The advanced client-router surface is in `agent-docs/advanced.md`: **link prefet
|
|
|
294
333
|
|
|
295
334
|
> Hit one of these as a runtime error? The [Troubleshooting page](https://docs.webjs.com/docs/troubleshooting) is keyed by symptom (the throw-at-load server import, the backtick-in-template 500, the TypeScript strip failure, the SSR browser-global crash, the missing-frame swap) and maps each back to the invariant and the `webjs check` rule below.
|
|
296
335
|
|
|
297
|
-
1. **Server-only code goes in `.server.{js,ts}` files, `route.ts` handlers, or `middleware.ts`. Never in pages, layouts, or components.** The `.server.{js,ts}` extension is the path-level boundary (the file router refuses to serve the source); a `'use server'` directive additionally makes exports RPC-callable, else the file is a server-only utility whose browser import is a throw-at-load stub. Importing
|
|
336
|
+
1. **Server-only code goes in `.server.{js,ts}` files, `route.ts` handlers, or `middleware.ts`. Never in pages, layouts, or components.** The `.server.{js,ts}` extension is the path-level boundary (the file router refuses to serve the source); a `'use server'` directive additionally makes exports RPC-callable, else the file is a server-only utility whose browser import is a throw-at-load stub. Importing a DB driver (`better-sqlite3` / `pg`), `node:*`, or any server-only dep from a component or an `app/**` page / layout / loading / error / not-found file crashes the browser at module load.
|
|
298
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.
|
|
299
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).
|
|
300
339
|
4. **Event (`@`), property (`.`), boolean (`?`) holes in `html` must be unquoted**, e.g. `@click=${fn}`, never `@click="${fn}"`.
|
|
301
|
-
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`.
|
|
302
341
|
6. **Page and layout default exports must be functions.** They return a value (usually `TemplateResult`). They do not call `render()` themselves.
|
|
303
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.
|
|
304
343
|
8. **Non-root layouts and pages MUST NOT** write `<!doctype>` / `<html>` / `<head>` / `<body>`. Only the root layout may.
|
|
@@ -311,17 +350,17 @@ The advanced client-router surface is in `agent-docs/advanced.md`: **link prefet
|
|
|
311
350
|
|
|
312
351
|
## Scaffolding
|
|
313
352
|
|
|
314
|
-
Three scaffolds exist (do not invent template names): `webjs create <name>` (full-stack: layout, page, components, modules,
|
|
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.
|
|
315
354
|
|
|
316
|
-
Rules: **always scaffold via `webjs create`** (never hand-roll). **Default to a real database (
|
|
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.
|
|
317
356
|
|
|
318
357
|
---
|
|
319
358
|
|
|
320
359
|
## CLI reference
|
|
321
360
|
|
|
322
361
|
```sh
|
|
323
|
-
webjs dev [--port N]
|
|
324
|
-
webjs start [--port N] # prod server; source IS the runtime, plain HTTP/1.1 (reverse-proxy for TLS + HTTP/2)
|
|
362
|
+
webjs dev [--port N] [--no-hot] # dev server with live reload (node --watch on Node, bun --hot on Bun). --no-hot runs in-process. Runs webjs.dev.before + webjs.dev.parallel (#550)
|
|
363
|
+
webjs start [--port N] # prod server; source IS the runtime, plain HTTP/1.1 (reverse-proxy for TLS + HTTP/2). Runs webjs.start.before first (#550)
|
|
325
364
|
webjs test [--server] [--browser] [--watch]
|
|
326
365
|
webjs check [--rules] [--json] # correctness validator (report-only, no autofix); --json for an agent loop
|
|
327
366
|
webjs mcp # read-only MCP: routes, actions (RPC hashes), components, check
|
|
@@ -329,19 +368,19 @@ webjs doctor # project-health checklist; non-zero exit on
|
|
|
329
368
|
webjs types # generate .webjs/routes.d.ts (typed Route union + per-route params, #258)
|
|
330
369
|
webjs typecheck [tsc args...] # the project's own tsc --noEmit
|
|
331
370
|
webjs create <name> [--template api|saas]
|
|
332
|
-
webjs db <
|
|
371
|
+
webjs db <generate|migrate|push|studio|seed> # wraps drizzle-kit (+ runs db/seed.server.ts)
|
|
333
372
|
webjs ui init | add <names...> | list | view <name>
|
|
334
373
|
webjs vendor pin|unpin|list|audit|outdated|update [--from PROVIDER] # importmap pinning, .webjs/vendor/importmap.json
|
|
335
374
|
```
|
|
336
375
|
|
|
337
|
-
`--from PROVIDER` accepts `jspm` (default), `jsdelivr`, `unpkg`, `skypack` and is persisted in the pin file. `PORT` is honoured when `--port` is absent; `webjs dev` emits `routes.d.ts` automatically. Running this repo's own apps (`website/`, `docs/`, `examples/blog/`, `packages/ui/packages/website/`):
|
|
376
|
+
`--from PROVIDER` accepts `jspm` (default), `jsdelivr`, `unpkg`, `skypack` and is persisted in the pin file. `PORT` is honoured when `--port` is absent; `webjs dev` emits `routes.d.ts` automatically. Running this repo's own apps (`website/`, `docs/`, `examples/blog/`, `packages/ui/packages/website/`): `cd` in and use **its** `npm run dev` / `npm start`; as of #550 a bare `webjs dev` / `webjs start` is equivalent (the per-app orchestration moved into the `webjs.dev` / `webjs.start` tasks config, which the primitive runs), so the npm scripts are now just thin aliases.
|
|
338
377
|
|
|
339
378
|
---
|
|
340
379
|
|
|
341
380
|
## Environment, server config, caching, observability
|
|
342
381
|
|
|
343
382
|
- **Env vars.** `process.env.X` reads are server-only; `WEBJS_PUBLIC_`-prefixed names are exposed in the browser via an inline `<script>` (no build); `NODE_ENV` is defined both sides. See `agent-docs/built-ins.md`.
|
|
344
|
-
- **The `package.json` `"webjs"` block.** Security headers (on by default, per-path `webjs.headers` overrides), CSP (opt-in nonce, `webjs.csp`), declarative `webjs.redirects` (#254), `webjs.trailingSlash` (#255), `webjs.basePath` (#256),
|
|
383
|
+
- **The `package.json` `"webjs"` block.** Security headers (on by default, per-path `webjs.headers` overrides), CSP (opt-in nonce, `webjs.csp`), declarative `webjs.redirects` (#254), `webjs.trailingSlash` (#255), `webjs.basePath` (#256), ingress caps (`maxBodyBytes` / `maxMultipartBytes` / server timeouts), and dev/start task orchestration (`webjs.dev.before` / `webjs.dev.parallel` / `webjs.start.before`, #550, the orchestration `webjs dev`/`start` run so they match `npm run dev`/`start`). Type it with `WebjsConfig` + the JSON Schema. See `agent-docs/configuration.md`.
|
|
345
384
|
- **Caching + file storage** (`agent-docs/built-ins.md`). HTTP `Cache-Control`, the `cache()` query helper + `revalidateTag`, the server HTML response cache (`export const revalidate` + `revalidatePath`, #241), content-hash asset URLs (`?v=`, #243), conditional GET (ETag, #240), and `FileStore` + `diskStore` (streaming, traversal-safe, signed URLs, S3-pluggable, #247).
|
|
346
385
|
- **Observability** (`agent-docs/configuration.md`). Access log, `requestId()` + `X-Request-Id`, the `onError` APM hook, `GET /__webjs/version` (#239).
|
|
347
386
|
|
|
@@ -386,11 +425,13 @@ export default async function User({ params }: { params: { id: string } }) {
|
|
|
386
425
|
```ts
|
|
387
426
|
// modules/users/actions/update-profile.server.ts
|
|
388
427
|
'use server';
|
|
389
|
-
import {
|
|
428
|
+
import { eq } from 'drizzle-orm';
|
|
429
|
+
import { db } from '../../../db/connection.server.ts';
|
|
430
|
+
import { users } from '../../../db/schema.server.ts';
|
|
390
431
|
export async function updateProfile(input: { name: string }) {
|
|
391
432
|
const name = String(input?.name || '').trim();
|
|
392
433
|
if (!name) return { success: false, error: 'name required', status: 400 };
|
|
393
|
-
const row = await
|
|
434
|
+
const [row] = await db.update(users).set({ name }).where(eq(users.id, me.id)).returning();
|
|
394
435
|
return { success: true, data: row };
|
|
395
436
|
}
|
|
396
437
|
```
|
|
@@ -406,4 +447,4 @@ Not in v1. Do not implement as part of other tasks:
|
|
|
406
447
|
- **Bundling and per-route code splitting.** webjs is **no-build** (the Rails 7 + importmap model); prod perf comes from HTTP/2 multiplex + `<link rel="modulepreload">` hints, not concatenation. **Do not propose a bundler or `webjs build`.**
|
|
407
448
|
- **Vite-grade HMR with state preservation.** Custom elements only `define` once, so full reload is necessary; data reloads are near-instant via `fs.watch` to SSE.
|
|
408
449
|
- **React Server Components Flight.** Server actions + `Suspense` streaming cover the need.
|
|
409
|
-
- **Edge-runtime bundling / full portability** (
|
|
450
|
+
- **Edge-runtime bundling / full portability** (deployment guidance lives in the docs site at `/docs/deployment`), **i18n, image optimization** (layer libraries on top).
|
|
@@ -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
|
|
|
@@ -13,7 +13,7 @@ layer to debug.
|
|
|
13
13
|
```js
|
|
14
14
|
// route handler
|
|
15
15
|
export async function GET() {
|
|
16
|
-
const posts = await
|
|
16
|
+
const posts = await db.query.posts.findMany();
|
|
17
17
|
return new Response(JSON.stringify(posts), {
|
|
18
18
|
headers: {
|
|
19
19
|
'Cache-Control': 'public, max-age=60, stale-while-revalidate=300',
|
|
@@ -51,10 +51,10 @@ until the TTL expires:
|
|
|
51
51
|
// modules/posts/queries/list-posts.server.ts
|
|
52
52
|
'use server';
|
|
53
53
|
import { cache } from '@webjsdev/server';
|
|
54
|
-
import {
|
|
54
|
+
import { db } from '../../../db/connection.server.ts';
|
|
55
55
|
|
|
56
56
|
export const listPosts = cache(
|
|
57
|
-
async () =>
|
|
57
|
+
async () => db.query.posts.findMany({ orderBy: { createdAt: 'desc' } }),
|
|
58
58
|
{ key: 'posts', ttl: 60 }
|
|
59
59
|
);
|
|
60
60
|
```
|
|
@@ -65,12 +65,12 @@ wrapper), add `tags`. It is either a static `string[]` or a function
|
|
|
65
65
|
|
|
66
66
|
```ts
|
|
67
67
|
export const postById = cache(
|
|
68
|
-
async (id: string) =>
|
|
68
|
+
async (id: string) => db.query.posts.findFirst({ where: { id } }),
|
|
69
69
|
{ key: 'post', ttl: 300, tags: (id) => ['post:' + id] } // per-entity tag
|
|
70
70
|
);
|
|
71
71
|
|
|
72
72
|
export const listPosts = cache(
|
|
73
|
-
async () =>
|
|
73
|
+
async () => db.query.posts.findMany(),
|
|
74
74
|
{ key: 'posts', ttl: 60, tags: ['posts'] } // static tag
|
|
75
75
|
);
|
|
76
76
|
```
|
|
@@ -83,9 +83,11 @@ no import of the wrapper):
|
|
|
83
83
|
// modules/comments/actions/create-comment.server.ts
|
|
84
84
|
'use server';
|
|
85
85
|
import { revalidateTag, revalidatePath } from '@webjsdev/server';
|
|
86
|
+
import { db } from '../../../db/connection.server.ts';
|
|
87
|
+
import { comments } from '../../../db/schema.server.ts';
|
|
86
88
|
|
|
87
89
|
export async function createComment(input) {
|
|
88
|
-
await
|
|
90
|
+
await db.insert(comments).values(input);
|
|
89
91
|
await revalidateTag('post:' + input.postId); // postById(postId) recomputes
|
|
90
92
|
await revalidateTag('posts'); // listPosts recomputes
|
|
91
93
|
await revalidatePath('/blog'); // also evict the cached HTML
|
|
@@ -163,7 +165,7 @@ Evict on a write with `revalidatePath`:
|
|
|
163
165
|
import { revalidatePath } from '@webjsdev/server';
|
|
164
166
|
|
|
165
167
|
export async function publishPost(input) {
|
|
166
|
-
// ... persist via
|
|
168
|
+
// ... persist via Drizzle ...
|
|
167
169
|
await revalidatePath('/blog'); // next /blog request re-renders fresh
|
|
168
170
|
return { success: true };
|
|
169
171
|
}
|
|
@@ -248,7 +250,7 @@ export const { auth, signIn, signOut, handlers } = createAuth({
|
|
|
248
250
|
providers: [
|
|
249
251
|
Credentials({
|
|
250
252
|
async authorize(credentials) {
|
|
251
|
-
const user = await
|
|
253
|
+
const user = await db.query.users.findFirst({ where: { email: credentials.email } });
|
|
252
254
|
if (!user || !verifyPassword(credentials.password, user.passwordHash)) return null;
|
|
253
255
|
return { id: user.id, name: user.name, email: user.email, role: user.role };
|
|
254
256
|
},
|
|
@@ -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
|
+
```
|
|
37
|
+
|
|
38
|
+
The bare form takes a type constructor (`count: Number`, `label: String`, `open: Boolean`). The `prop()` helper carries options and narrows the TS type:
|
|
18
39
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
```
|
|
25
54
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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);
|
|
@@ -116,7 +147,9 @@ class Report extends WebComponent {
|
|
|
116
147
|
|
|
117
148
|
**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
149
|
|
|
119
|
-
**SSR action seeding (#472).** For a SHIPPING async component (one that hydrates because it carries an interactivity signal), the on-hydration re-fetch is eliminated without eliding the module. Each `'use server'` action result invoked during a (non-streamed) SSR render is serialized into the page as one `<script type="application/json" id="__webjs-seeds">` block, keyed by the action's hash + function name + serialized args. The generated client RPC stub reads that seed on its FIRST call: a hit resolves synchronously (no network, no hydration flicker), a later refetch or arg-change misses (consume-once) and goes to RPC as normal. So `const u = await getUser(this.id)` runs once, server-side, and its result is reused on the client's first render. The capture is a transparent server-side facade over the `'use server'` module (a synchronous
|
|
150
|
+
**SSR action seeding (#472).** For a SHIPPING async component (one that hydrates because it carries an interactivity signal), the on-hydration re-fetch is eliminated without eliding the module. Each `'use server'` action result invoked during a (non-streamed) SSR render is serialized into the page as one `<script type="application/json" id="__webjs-seeds">` block, keyed by the action's hash + function name + serialized args. The generated client RPC stub reads that seed on its FIRST call: a hit resolves synchronously (no network, no hydration flicker), a later refetch or arg-change misses (consume-once) and goes to RPC as normal. So `const u = await getUser(this.id)` runs once, server-side, and its result is reused on the client's first render. The capture is a transparent server-side facade over the `'use server'` module (a synchronous load hook wraps each export in a recording `Proxy`), so there is **no source transform and no build step**: the browser source tab and the on-disk files are byte-unchanged, and the RPC stub the browser fetches is the same shape as before (it just consults the seed first). The install mechanism is runtime-neutral (#529): Node uses `module.registerHooks`, Bun uses a `Bun.plugin` `onLoad` (Bun has no `module.registerHooks`), and both emit the identical seed, so a page seeds the same on either runtime. Fail-open by construction: a key miss is always a normal RPC, never wrong data. Default on; opt out with `"webjs": { "seed": false }` or `WEBJS_SEED=0` (then the client re-fetches on hydration, hidden by stale-while-revalidate as before). A soft navigation carries the seed too: the client router ingests the incoming page's payload (`applySwap` -> `scanSeeds`) before its components hydrate, so a navigated async component also skips the refetch. Streamed regions (`<webjs-suspense>`) are NOT seeded (their data resolves after the first flush), so a slow boundary keeps the stale-while-revalidate refetch.
|
|
151
|
+
|
|
152
|
+
**Interactivity during a COLD on-hydration re-fetch (#528).** Seeding matters for more than a wasted round trip. When a SHIPPING async component DOES re-fetch on hydration (seeding off via `WEBJS_SEED=0`, a `static refresh = true` leaf, or a seed miss), its FIRST client commit is deferred until that re-fetch resolves, and that commit is the step that binds the component's event listeners. So for the fetch's duration the SSR'd markup is visible but NOT yet interactive: an `@click` does nothing until the data lands (the window equals the action's latency, not a constant). Seeding erases this by resolving the first render synchronously, so the component hydrates and binds its listeners immediately, which is the default and works on both Node and Bun (#529, seeding is no longer Node-only). The component is fully reactive once that first commit lands: this is purely a cold-start window, NOT a lost signal subscription (a bump after the commit re-renders normally). To keep an async leaf interactive from the first paint regardless, give it a synchronous first render (read request-known data from a prop / the constructor) and move the async work to a signal-driven `Task` or an `@event` handler.
|
|
120
153
|
|
|
121
154
|
### Streaming a slow region with `<webjs-suspense>` (#471)
|
|
122
155
|
|
|
@@ -151,7 +184,7 @@ server/client split to reason about. A component stays elidable as long
|
|
|
151
184
|
as it has none of the following.
|
|
152
185
|
|
|
153
186
|
- An `@event` binding in a template (`@click=${...}`), or a native event-handler property (`.onclick=${...}`).
|
|
154
|
-
- 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.
|
|
155
188
|
- An overridden lifecycle hook (anything in the table above), as a method or an arrow class field.
|
|
156
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.
|
|
157
190
|
- An `addController(...)` or `requestUpdate()` call.
|
|
@@ -208,6 +208,37 @@ Returns JSON describing the live build, alongside the `/__webjs/health` and `/__
|
|
|
208
208
|
|
|
209
209
|
---
|
|
210
210
|
|
|
211
|
+
## Dev/start task orchestration: `webjs.dev` / `webjs.start` (#550)
|
|
212
|
+
|
|
213
|
+
`webjs dev` and `webjs start` run an app's per-environment orchestration
|
|
214
|
+
themselves, read from the `package.json` `"webjs"` block, so the framework
|
|
215
|
+
primitive is not a degraded run versus `npm run dev` / `npm start`. The npm
|
|
216
|
+
scripts become thin aliases (`"dev": "webjs dev"`), and both forms behave
|
|
217
|
+
identically. This replaces the old `predev` / `prestart` npm hooks +
|
|
218
|
+
`concurrently` watchers (which a bare `webjs dev` silently skipped, #452).
|
|
219
|
+
|
|
220
|
+
```jsonc
|
|
221
|
+
"webjs": {
|
|
222
|
+
"dev": {
|
|
223
|
+
"before": ["webjs db migrate"], // one-shot, runs to completion first
|
|
224
|
+
"parallel": ["tailwindcss -i ./public/input.css -o ./public/tailwind.css --watch"] // long-lived, runs alongside the server
|
|
225
|
+
},
|
|
226
|
+
"start": {
|
|
227
|
+
"before": ["webjs db migrate"] // one-shot, before serving
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
- **`before`** (dev and start): commands run sequentially to completion BEFORE the server boots (the old `predev` / `prestart`: `webjs db migrate`, a registry copy). A non-zero exit ABORTS the boot with a clear message, so a failed migration never serves stale code/schema.
|
|
233
|
+
- **`parallel`** (dev only): long-lived child processes that run ALONGSIDE the server (the old `concurrently` watchers: the Tailwind CLI `--watch`). They are spawned once in the parent (not on every hot-reload restart) and TORN DOWN on exit (SIGINT / SIGTERM / server exit), so a watcher cannot leak past the dev server.
|
|
234
|
+
- Each command runs through a shell, so a normal command line works. An empty / absent block means `webjs dev` / `start` run unchanged, so a plain app with no Tailwind/DB needs no config.
|
|
235
|
+
- The scaffold uses the Tailwind browser runtime (no CSS build step), so it ships only `dev.before` / `start.before` (the Drizzle migration apply); an app that adds the Tailwind CLI puts its `--watch` under `webjs.dev.parallel`. The in-repo apps (`examples/blog`, `website`, `docs`, ui-website) show the Tailwind `parallel` watcher pattern.
|
|
236
|
+
- **Prod note:** `before` runs at boot, so `webjs start` runs `webjs db migrate` in-process to apply pending migrations. Drizzle has no client-codegen step (the schema IS the types, inferred at compile time), so there is nothing to run at image-BUILD time. Authoring a new migration from a schema change is a dev-time `webjs db generate`, committed to source control, not a boot step. So `CMD ["npm", "start"]` and `CMD ["webjs", "start"]` are equivalent.
|
|
237
|
+
|
|
238
|
+
Read by `readAppTasks` in `packages/cli/lib/app-tasks.js` (pure, unit-tested); orchestrated in `packages/cli/bin/webjs.js` (`runBeforeSteps` / `startParallelTasks`).
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
211
242
|
## Server port resolution (`--port` / `PORT` / `.env`) (#447)
|
|
212
243
|
|
|
213
244
|
Both `webjs dev` and `webjs start` resolve the listen port with a single
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Framework development (editing webjs itself)
|
|
2
2
|
|
|
3
|
-
Read this only when editing the webjs monorepo (this repo), not a scaffolded app. The repo is buildless: `packages/` is plain `.js` with JSDoc (never add `.ts` there); TypeScript is fine in `examples/`, `docs/`, `website/`. Each in-repo app (`website/`, `docs/`, `examples/blog/`, `packages/ui/packages/website/`) is run
|
|
3
|
+
Read this only when editing the webjs monorepo (this repo), not a scaffolded app. The repo is buildless: `packages/` is plain `.js` with JSDoc (never add `.ts` there); TypeScript is fine in `examples/`, `docs/`, `website/`. Each in-repo app (`website/`, `docs/`, `examples/blog/`, `packages/ui/packages/website/`) is run from its OWN dir via `npm run dev` / `npm start`; as of #550 a bare `webjs dev` / `webjs start` is equivalent (each app's per-environment orchestration, the Tailwind watcher, `webjs db migrate`, the registry copy, moved into its `webjs.dev` / `webjs.start` tasks config, which `webjs dev`/`start` run). The sections below cover the repo-health git config, the changelog flow, and the dev error overlay.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|