Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/webjs/references/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ A cross-runtime proof is often a plain assert script rather than a test file, so

## Convention validation (`webjs check`)

`npm run check` is the correctness validator. Every rule catches code that is wrong to ship (a crash, a security leak, a type-strip failure), plus the `no-scaffold-placeholder` sentinel for unreplaced scaffold content. Run it and fix every violation before considering the change done (`npm run check -- --json` for an agent loop, `npm run check -- --rules` to list the rules). It is separate from `CONVENTIONS.md`, which carries the customizable project conventions you follow by judgment.
`npm run check` is the correctness validator. Every rule catches code that is wrong to ship, a crash, a security leak, a reactive prop that silently stops re-rendering, or a type-strip failure. Run it and fix every violation before considering the change done (`npm run check -- --json` for an agent loop, `npm run check -- --rules` to list the rules). It is separate from `CONVENTIONS.md`, which carries the customizable project conventions you follow by judgment.

## What NOT to do

Expand Down
17 changes: 11 additions & 6 deletions .claude/skills/webjs-scaffold-sync/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,19 +199,24 @@ Run the mandatory generate + boot + check verifications in PARALLEL when more th
```sh
# generate (files only is enough for structure/typecheck; install to boot)
node -e "import('packages/cli/lib/create.js').then(m => m.scaffoldApp('probe', '/tmp/x', { template: 'full-stack', install: false }))"
# then in the generated app: webjs check (only no-scaffold-placeholder should
# remain), webjs typecheck (clean), and boot it to hit the new route(s).
# then in the generated app: webjs check (clean), webjs typecheck (clean),
# and boot it to hit the new route(s).
```
A scaffold change is NOT done until a freshly generated app of each affected
template BOOTS, serves the new/changed route, passes `webjs check` (only the
intended `no-scaffold-placeholder` markers), and `webjs typecheck` is clean.
template BOOTS, serves the new/changed route, passes `webjs check`, and
`webjs typecheck` is clean.
5. Run the scaffold tests (`node --test 'test/scaffolds/*.test.js'`) and add/adjust
assertions (a new demo in the FEATURES list, a per-template inclusion/exclusion
test, the counterfactual).
6. Respect the prose-punctuation invariant (#11) in every comment and doc, and
keep each demo densely commented (a header stating the webjs concept + the
why, inline comments on the non-obvious idiom, a `webjs-scaffold-placeholder`
marker). The scaffold teaches by its comments; a thin demo is a bug.
why, and inline comments on the non-obvious idiom). Add a prune note where
removal is NOT obvious, which is what the demos that carry one do: a card
spanning several paths (the auth card names its route, its module, its api
routes and its schema column), or one that depends on another (the
server-actions card says to prune it with auth). A self-contained demo needs
none, since `npm run gallery:clear` sheds the lot. The scaffold teaches by its comments, and a thin demo is a
bug.

## Audit-mode procedure (sweep the scaffold for drift)

Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ Default export receives `{ children, params, searchParams, url }`, must embed `c

### Error / loading / metadata routes

`error.{js,ts}` default-exports `({ error, ...ctx }) => TemplateResult` (catches sibling-page / deeper render errors, innermost wins, prod sends only `error.message`). `loading.{js,ts}` wraps the sibling page in `Suspense` with an immediately-flushed fallback. `forbidden.{js,ts}` / `unauthorized.{js,ts}` render the nearest 403 / 401 boundary for a thrown `forbidden()` / `unauthorized()` (#848). Two **root-only** boundaries (`app/` root exactly): `global-error.{js,ts}` is the app-wide catch-all tried after the nested `error` boundaries are exhausted, and it renders its **own** `<!doctype><html><body>` document (returned verbatim, since a root-layout failure is when it fires). Because it is returned verbatim (no framework `<head>` splice), it ships **no importmap or boot script**, so keep it **static HTML with no components/hydration** (a last-resort page must not depend on the module system that may have just failed); under an opt-in CSP, an inline `<style>`/`<script>` in it must carry the nonce via `cspNonce()`. `global-not-found.{js,ts}` renders for an unmatched-anywhere URL when no `not-found` matches. `not-found` is nearest-wins from the throwing page's chain (#848 fixed the prior root-only behavior). Metadata routes (`sitemap`, `robots`, `manifest`, `icon`, `apple-icon`, `opengraph-image`, `twitter-image`) live at app root or static segments only and default-export a possibly-async function; `sitemap(entries)` / `sitemapIndex(sitemaps)` from `@webjsdev/server` serialize spec-valid XML.
`error.{js,ts}` default-exports `({ error, ...ctx }) => TemplateResult` (catches sibling-page / deeper render errors, innermost wins, prod sends only `error.message`). `loading.{js,ts}` wraps the sibling page in `Suspense` with an immediately-flushed fallback. `forbidden.{js,ts}` / `unauthorized.{js,ts}` render the nearest 403 / 401 boundary for a thrown `forbidden()` / `unauthorized()` (#848). Two **root-only** boundaries (`app/` root exactly): `global-error.{js,ts}` is the app-wide catch-all tried after the nested `error` boundaries are exhausted, and it renders its **own** `<!doctype><html><body>` document (returned verbatim, since a root-layout failure is when it fires). Because it is returned verbatim (no framework `<head>` splice), it ships **no importmap or boot script**, so keep it **static HTML with no components/hydration** (a last-resort page must not depend on the module system that may have just failed); under an opt-in CSP, an inline `<script>` in it must carry the nonce via `cspNonce()` (an inline `<style>` needs one only if you tighten `style-src`, since the default allows inline style outright). `global-not-found.{js,ts}` renders for an unmatched-anywhere URL when no `not-found` matches. `not-found` is nearest-wins from the throwing page's chain (#848 fixed the prior root-only behavior). Metadata routes (`sitemap`, `robots`, `manifest`, `icon`, `apple-icon`, `opengraph-image`, `twitter-image`) live at app root or static segments only and default-export a possibly-async function; `sitemap(entries)` / `sitemapIndex(sitemaps)` from `@webjsdev/server` serialize spec-valid XML.

### Route handlers (`app/**/route.{js,ts}`)

Expand Down Expand Up @@ -369,7 +369,7 @@ Derive the type at every boundary: a DB row from the schema (`typeof todos.$infe

The router auto-enables when `@webjsdev/core` loads (any page with a component), so there is nothing to opt INTO. An app that wants plain full-page (MPA) navigation can opt OUT app-wide with `{ "webjs": { "clientRouter": false } }` (#629), or per-moment at runtime with `disableClientRouter()`. SSR auto-emits KEYED boundary comment pairs around each layout's children AND the page itself (open `<!--wj:children:<segment>:<route-key>-->`, close `<!--/wj:children:<segment>-->`; the route-key is the resolved path with param values percent-encoded, #1015). The router strictly scans both DOMs (any truncated, mispaired, or duplicated boundary poisons the scan) and applies a two-tier swap with Next.js remount parity: a changed route-key REPLACES (remounts) at the PARENT of the shallowest changed boundary (the range that contains the changed layout's own markup, exact Next remount scope), an unchanged one MORPHS the deepest shared boundary in place (a searchParams-only nav preserves hydrated component state). A poisoned or disjoint scan degrades to a full page load, never a guessed recovery, so silent DOM corruption is structurally impossible; outer-layout DOM identity is preserved on every soft path. **Every degradation dispatches `webjs:navigation-fallback` on `document` in ALL environments** (detail `{ cause, href, willReload }`, not cancelable), so a full page load on a click is observable in production rather than silent (#1114). Form submissions ride the same pipeline (`data-no-router` opts out). Wire bytes are minimized via the `X-Webjs-Have` header (`segment:route-key` entries, so a dynamic layout held for OTHER params is re-rendered rather than short-circuited; the server returns only the divergent fragment, served `private` so a shared cache can never store the reduced body and serve it to a full-page navigation, and additionally marked `Vary: X-Webjs-Have` for caches that honour it (#1140; `Vary` alone was not enough, since Cloudflare honours only `Accept-Encoding`)); scroll is restored on back/forward. **The link-prefetch cache is ANCHOR-VALIDATED** (#1114): a reduced fragment begins at the boundary the server short-circuited on, and on consume the router checks that boundary is still live with the same route-key. A root-anchored fragment therefore survives an unrelated navigation (still a cache hit), while one anchored deeper is discarded once that layout is gone, because applying it would share no boundary with the live DOM and force a full page load. The router also never prefetches the page it is already on (#1106): that request can never serve a later navigation and only occupies a capped cache slot. A non-GET `<form>` that BINDS a server action (`action=${fn}`) is the no-JS write-path (with JS the router posts the same body to the same url and 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.

The advanced client-router surface is in `references/client-router-and-streaming.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), **stream actions** (`<webjs-stream>` element-level updates, #248), and the **opt-in nav-loading indicator** (`<html data-webjs-nav-progress>` exposes a `data-navigating` attribute during a nav so you can style a CSS-only progress affordance, off by default because toggling a root attribute re-resolves `oklch()` tokens to a one-frame repaint flash on iOS WebKit, #610). 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).
The advanced client-router surface is in `references/client-router-and-streaming.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" content="same-origin">`, where the router checks that exact `content` value and a bare tag enables nothing, plus `data-webjs-permanent` to persist a live element), **stream actions** (`<webjs-stream>` element-level updates, #248), and the **opt-in nav-loading indicator** (`<html data-webjs-nav-progress>` exposes a `data-navigating` attribute during a nav so you can style a CSS-only progress affordance, off by default because toggling a root attribute re-resolves `oklch()` tokens to a one-frame repaint flash on iOS WebKit, #610). 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).

---

Expand Down Expand Up @@ -534,7 +534,7 @@ webjs vendor pin|unpin|list|audit|outdated|update [--from PROVIDER] # importma

Every app ships the **conventions** as prose, in `.agents/skills/webjs/SKILL.md` (how to build) and `.agents/rules/workflow.md` (git, tests, review). These are guidance a reasonable project could do differently (modules layout, action placement, one-function-per-file, testing, styling, git workflow), followed by judgment, not tool-enforced.

**`webjs check` is a separate, narrower tool: correctness checks only.** Every rule catches code that is wrong to ship (a crash, a security leak, a build/type-strip failure). They run unconditionally with no per-project disabling. The dividing line: could a sensible app legitimately want this to pass? If yes it is a convention (prose), if no it is a check (the tool). `webjs check --rules` lists them.
**`webjs check` is a separate, narrower tool: correctness checks only.** Every rule catches code that is wrong to ship (a crash, a security leak, a reactive prop that silently stops re-rendering, a build/type-strip failure). They run unconditionally with no per-project disabling. The dividing line: could a sensible app legitimately want this to pass? If yes it is a convention (prose), if no it is a check (the tool). `webjs check --rules` lists them.

So: read the skill and follow its conventions by judgment; run `webjs check` and fix every violation (correctness bugs, not style).

Expand Down
11 changes: 4 additions & 7 deletions blog/ai-first-is-plumbing.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,18 @@ What this gets us is that an agent in autonomous mode (running with permission t

# The convention validator

`webjs check` runs a set of lint rules over the project:
`webjs check` runs a set of lint rules over the project. A few real ones:

- `tests-exist`: every server action / query has a corresponding test
- `no-server-imports-in-components`: components do not import `node:*` or a DB driver (`pg`)
- `no-server-import-in-browser-module`: a module that genuinely ships to the browser does not import a server-only file, whose client stub throws at load
- `use-server-needs-extension`: a `'use server'` directive requires a `.server.{js,ts}` filename
- `no-static-properties`: reactive properties are declared via the `extends WebComponent({ … })` factory, not a hand-written `static properties`
- `erasable-typescript-only`: `tsconfig.json` has `erasableSyntaxOnly: true`
- `shell-in-non-root-layout`: non-root layouts and pages don't write `<!doctype>` / `<html>` / `<head>` / `<body>`
- `no-json-data-files`: app data lives in the database, not in JSON files
- `no-server-env-in-components`: `process.env.X` in components only reads `WEBJS_PUBLIC_*` or `NODE_ENV`
- `light-dom-css-prefix`: light-DOM components with custom CSS prefix every selector with the tag name

Each rule lives in `packages/server/src/check.js`. New rules are about 20 lines apiece. The agent runs `webjs check` before committing, sees violations as concrete messages, and fixes them.
Each rule lives in `packages/server/src/check.js`. The agent runs `webjs check` before committing, sees violations as concrete messages, and fixes them.

The lint is intentionally narrow. We have ~10 rules, not 100. The rules cover invariants that crash in production. The framework does not lint style preferences.
The lint is intentionally narrow. Every rule catches something that is wrong to ship, a crash, a leaked secret, a reactive prop that silently stops re-rendering, or source that will not strip, and the framework does not lint style preferences. `webjs check --rules` prints the current set, which is the only place worth reading it from, because a list copied into a blog post goes stale the next time a rule lands.

# Hooks beyond pre-commit

Expand Down
Loading
Loading