diff --git a/agent-docs/framework-dev.md b/agent-docs/framework-dev.md index 0db3ee2e..ab018b86 100644 --- a/agent-docs/framework-dev.md +++ b/agent-docs/framework-dev.md @@ -46,7 +46,7 @@ The scaffold is webjs's primary teaching surface for AI agents, so a new framewo - **Tier 2 (CI gate):** `test/scaffolds/gallery-coverage.test.js` reconciles the LIVE framework surface against `test/scaffolds/gallery-coverage.json` and FAILS when something new is neither demoed nor exempted. It gates three surfaces: `@webjsdev/core` exports (a `{ demo }` gallery-file pointer), `@webjsdev/server` exports (`{ demoed: true }`, verified by a generated app importing it), and routing convention files (the stems DERIVED from `packages/server/src/router.js`, each demonstrated by a file in a generated app). It runs under `npm test`, so a local `--no-verify` cannot skip it: a new export or convention turns CI red until classified. The `reconcile()` / `reconcileSet()` cores are pure and their failure modes (new name, stale key, missing/over-claimed demo, empty reason) are proven with synthetic inputs alongside the real-surface assertions. The deferred backlog is tracked in #859. -**When you add or rename a `@webjsdev/core` export, update the manifest** the same way you write a test: add a demo pointer, or an honest exemption. The gate covers the export surface (where the #848-class throwers live); a new routing convention FILE type (a `*.ts` boundary) is not an export, so it stays tier-1-only for now. +**When you add or rename a `@webjsdev/core` or `@webjsdev/server` export, or add a routing convention file the router parses, update the manifest** the same way you write a test: add a demo pointer / `{ demoed: true }`, or an honest exemption. All three surfaces are gated (the convention stems are derived from `packages/server/src/router.js`, so a new `stem === '...'` branch auto-appears and must be classified). --- diff --git a/packages/cli/templates/gallery/app/features/boundaries/crash/page.ts b/packages/cli/templates/gallery/app/features/boundaries/crash/page.ts new file mode 100644 index 00000000..506117d3 --- /dev/null +++ b/packages/cli/templates/gallery/app/features/boundaries/crash/page.ts @@ -0,0 +1,7 @@ +// A page that throws a real render error, so the nearest error.ts boundary +// (../error.ts) catches it. In a real app an unexpected throw (a failed query, +// a bug) lands here; expected failures should return an ActionResult or throw +// notFound() / forbidden() instead. +export default function Crash() { + throw new Error('demo: this page threw during render'); +} diff --git a/packages/cli/templates/gallery/app/features/boundaries/error.ts b/packages/cli/templates/gallery/app/features/boundaries/error.ts new file mode 100644 index 00000000..4804de68 --- /dev/null +++ b/packages/cli/templates/gallery/app/features/boundaries/error.ts @@ -0,0 +1,16 @@ +// error.ts is the error boundary for this segment's subtree. A render-time +// exception in a sibling or deeper page (see crash/page.ts) is caught here and +// rendered scoped to this boundary, so outer layouts stay alive. The default +// export receives { error, ...ctx }; in production only error.message is sent. +import { html } from '@webjsdev/core'; + +export default function BoundariesError({ error }: { error: Error }) { + return html` +
+ This segment's error.ts boundary caught a
+ render error: ${error?.message ?? 'unknown'}.
+
+ This segment's not-found.ts boundary rendered,
+ because a page threw notFound() or the URL
+ matched nothing under this segment.
+
unauthorized(), caught by
private/unauthorized.ts (401).
+ error.ts (500).
+ not-found.ts (404).
+
forbidden() is for an authenticated user who
diff --git a/packages/cli/templates/gallery/app/features/client-router/page.ts b/packages/cli/templates/gallery/app/features/client-router/page.ts
index 40087c04..a741b208 100644
--- a/packages/cli/templates/gallery/app/features/client-router/page.ts
+++ b/packages/cli/templates/gallery/app/features/client-router/page.ts
@@ -8,6 +8,7 @@
// JS off, every link is a normal full-page navigation.
import { html } from '@webjsdev/core';
import type { Metadata } from '@webjsdev/core';
+import '#modules/client-router/components/router-controls.ts';
export const metadata: Metadata = { title: 'Client router (soft nav) | features' };
@@ -24,6 +25,8 @@ export default function ClientRouterExample() {
Go to page two
Home
+
Or drive it from JS with navigate() / revalidate():
Opt out app-wide with { "webjs": { "clientRouter": false } },
or per-link with data-no-router (use it for
diff --git a/packages/cli/templates/gallery/app/features/components/page.ts b/packages/cli/templates/gallery/app/features/components/page.ts
index 4968c89f..e3fe7822 100644
--- a/packages/cli/templates/gallery/app/features/components/page.ts
+++ b/packages/cli/templates/gallery/app/features/components/page.ts
@@ -2,6 +2,7 @@
import { html } from '@webjsdev/core';
import type { Metadata } from '@webjsdev/core';
import '#modules/components/components/counter-card.ts';
+import '#modules/components/components/reactive-meter.ts';
export const metadata: Metadata = { title: 'Components (signals + slots) | features' };
@@ -10,5 +11,7 @@ export default function ComponentsExample() {
The WebComponent factory, a reactive prop, an instance signal, and a slot.
Shadow DOM (scoped css) plus the rest of the signals API (computed, effect, batch):
${this.lastLogged.get()}
+ `; + } +} +ReactiveMeter.register('reactive-meter'); diff --git a/packages/cli/templates/gallery/modules/directives/components/directive-demo.ts b/packages/cli/templates/gallery/modules/directives/components/directive-demo.ts index 4851dd2d..96ce0569 100644 --- a/packages/cli/templates/gallery/modules/directives/components/directive-demo.ts +++ b/packages/cli/templates/gallery/modules/directives/components/directive-demo.ts @@ -2,9 +2,13 @@ // `repeat` keys a list so DOM nodes are REUSED across reorders instead of being // recreated (use it for keyed lists that reorder; plain `.map()` is fine for // static lists). `watch(signal)` does a fine-grained DOM swap of ONE node when -// the signal changes, without re-running the whole template. +// the signal changes, without re-running the whole template. The second card +// shows more of the set: `live` (a controlled input), `ref` + `createRef` (a +// handle to a DOM node), `until` (a pending fallback for a promise), +// `unsafeHTML` (trusted raw HTML, NEVER user input), and `keyed` (force a fresh +// subtree when a key changes). import { WebComponent, signal, html } from '@webjsdev/core'; -import { repeat, watch } from '@webjsdev/core/directives'; +import { repeat, watch, live, until, keyed, unsafeHTML, ref, createRef } from '@webjsdev/core/directives'; interface Item { id: number; label: string } @@ -17,6 +21,14 @@ export class DirectiveDemo extends WebComponent { { id: 3, label: 'Charlie' }, ]); private ticks = signal(0); + // A controlled value (for `live`) and a key (for `keyed`). + private text = signal('type here'); + private variant = signal(0); + // A handle to the input node, attached by `ref` in the browser. + private inputRef = createRefasync: ${until(this.asyncValue, 'loading...')}
+ + + ${keyed(variant, html`