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` +

Something went wrong

+

+ This segment's error.ts boundary caught a + render error: ${error?.message ?? 'unknown'}. +

+

Back to boundaries

+ `; +} diff --git a/packages/cli/templates/gallery/app/features/boundaries/loading.ts b/packages/cli/templates/gallery/app/features/boundaries/loading.ts new file mode 100644 index 00000000..5de85bce --- /dev/null +++ b/packages/cli/templates/gallery/app/features/boundaries/loading.ts @@ -0,0 +1,15 @@ +// loading.ts auto-wraps the sibling page in a Suspense boundary: its default +// export is the fallback shown while an async page (or its streamed regions) +// resolves. The boundaries index is fast, so you rarely see this, but any async +// page in this subtree gets this skeleton for free during navigation. +import { html } from '@webjsdev/core'; + +export default function BoundariesLoading() { + return html` +
+
+
+
+
+ `; +} diff --git a/packages/cli/templates/gallery/app/features/boundaries/not-found.ts b/packages/cli/templates/gallery/app/features/boundaries/not-found.ts new file mode 100644 index 00000000..2f6b5b58 --- /dev/null +++ b/packages/cli/templates/gallery/app/features/boundaries/not-found.ts @@ -0,0 +1,17 @@ +// not-found.ts is the nearest 404 boundary for this subtree. It renders both +// for a thrown notFound() in a page below here AND for an unmatched URL under +// /features/boundaries/ (try /features/boundaries/does-not-exist). Nearest wins, +// so this beats the root not-found for anything in this segment. +import { html } from '@webjsdev/core'; + +export default function BoundariesNotFound() { + return html` +

404: Not here

+

+ This segment's not-found.ts boundary rendered, + because a page threw notFound() or the URL + matched nothing under this segment. +

+

Back to boundaries

+ `; +} diff --git a/packages/cli/templates/gallery/app/features/boundaries/page.ts b/packages/cli/templates/gallery/app/features/boundaries/page.ts index d5fc93a7..77e89ea8 100644 --- a/packages/cli/templates/gallery/app/features/boundaries/page.ts +++ b/packages/cli/templates/gallery/app/features/boundaries/page.ts @@ -40,6 +40,16 @@ export default function BoundariesExample() { throws unauthorized(), caught by private/unauthorized.ts (401). +
  • + /features/boundaries/crash + throws a render error, caught by this segment's + error.ts (500). +
  • +
  • + /features/boundaries/does-not-exist + matches nothing, caught by the nearest + 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() {

    Components

    The WebComponent factory, a reactive prop, an instance signal, and a slot.

    A slotted title +

    Shadow DOM (scoped css) plus the rest of the signals API (computed, effect, batch):

    + `; } diff --git a/packages/cli/templates/gallery/app/manifest.ts b/packages/cli/templates/gallery/app/manifest.ts new file mode 100644 index 00000000..10d3c027 --- /dev/null +++ b/packages/cli/templates/gallery/app/manifest.ts @@ -0,0 +1,22 @@ +// webjs-scaffold-placeholder. Metadata route. Keep and adapt it, or prune it +// (delete this file), then delete this marker line. webjs check fails while the +// marker remains. +// +// app/manifest.ts serves /manifest.json (the web app manifest). The default +// export returns an object, serialized to JSON. Adapt the name, colors, and +// icons to your app; pair it with the opt-in service worker for an installable +// PWA. See agent-docs/service-worker.md. (Gallery files are copied verbatim, so +// set the real app name here by hand rather than expecting substitution.) +export default function Manifest() { + return { + name: 'webjs app', + short_name: 'webjs app', + start_url: '/', + display: 'standalone', + background_color: '#ffffff', + theme_color: '#1c1613', + icons: [ + { src: '/favicon.svg', sizes: 'any', type: 'image/svg+xml' }, + ], + }; +} diff --git a/packages/cli/templates/gallery/app/robots.ts b/packages/cli/templates/gallery/app/robots.ts new file mode 100644 index 00000000..d9e8c0b0 --- /dev/null +++ b/packages/cli/templates/gallery/app/robots.ts @@ -0,0 +1,16 @@ +// webjs-scaffold-placeholder. Metadata route. Keep and adapt it, or prune it +// (delete this file), then delete this marker line. webjs check fails while the +// marker remains. +// +// app/robots.ts serves /robots.txt. The default export returns a string (served +// as text/plain) or an object. This example allows all crawlers and points them +// at the sitemap. Tighten the rules (Disallow paths) for a real app. +const SITE_URL = (process.env.SITE_URL || 'http://localhost:8080').replace(/\/$/, ''); + +export default function Robots() { + return [ + 'User-agent: *', + 'Allow: /', + `Sitemap: ${SITE_URL}/sitemap.xml`, + ].join('\n'); +} diff --git a/packages/cli/templates/gallery/app/sitemap.ts b/packages/cli/templates/gallery/app/sitemap.ts new file mode 100644 index 00000000..72278457 --- /dev/null +++ b/packages/cli/templates/gallery/app/sitemap.ts @@ -0,0 +1,23 @@ +// webjs-scaffold-placeholder. Metadata route. Keep and adapt it to your real +// routes, or prune it (delete this file), then delete this marker line. webjs +// check fails while the marker remains. +// +// app/sitemap.ts serves /sitemap.xml. The default export is a (possibly async) +// server function; `sitemap(entries)` from @webjsdev/server serializes a +// spec-valid XML sitemap. In a real app, build the entries from your content +// (a query over posts/products), so new content is discoverable without editing +// this file. Here it lists the gallery's static routes as an example. +import { sitemap } from '@webjsdev/server'; + +const SITE_URL = (process.env.SITE_URL || 'http://localhost:8080').replace(/\/$/, ''); + +export default function Sitemap() { + const routes = ['/', '/features/routing', '/features/boundaries']; + return sitemap( + routes.map((path) => ({ + url: `${SITE_URL}${path}`, + changeFrequency: 'weekly' as const, + priority: path === '/' ? 1.0 : 0.7, + })), + ); +} diff --git a/packages/cli/templates/gallery/modules/client-router/components/router-controls.ts b/packages/cli/templates/gallery/modules/client-router/components/router-controls.ts new file mode 100644 index 00000000..1be9883c --- /dev/null +++ b/packages/cli/templates/gallery/modules/client-router/components/router-controls.ts @@ -0,0 +1,24 @@ +// Programmatic client navigation. `navigate(url)` does the same soft, in-place +// swap an click does, but from an event handler (after a save, a wizard +// step, etc.). `revalidate(url?)` evicts the browser snapshot cache so the next +// visit refetches fresh HTML instead of the cached page. Both are client-only +// (they run in the browser), so a component is the right home; a page/layout +// never hydrates. With JS off this component is inert, so keep real navigation +// on plain and use navigate() only for JS-driven flows. +import { WebComponent, html, navigate, revalidate } from '@webjsdev/core'; + +export class RouterControls extends WebComponent { + render() { + return html` +
    + + +
    + `; + } +} +RouterControls.register('router-controls'); diff --git a/packages/cli/templates/gallery/modules/components/components/reactive-meter.ts b/packages/cli/templates/gallery/modules/components/components/reactive-meter.ts new file mode 100644 index 00000000..c8cb3e9f --- /dev/null +++ b/packages/cli/templates/gallery/modules/components/components/reactive-meter.ts @@ -0,0 +1,64 @@ +// A shadow-DOM component: `static shadow = true` scopes `static styles = css\`\`` +// to this element (bare selectors do not leak out), the one place the lit reflex +// to scope CSS is right in webjs. It also shows the rest of the signals API: +// `computed` (a derived signal), `effect` (a browser-side reaction that runs on +// change), and `batch` (coalesce several writes into ONE re-render). +import { WebComponent, html, css, signal, computed, effect, batch } from '@webjsdev/core'; + +export class ReactiveMeter extends WebComponent { + static shadow = true; + static styles = css` + .row { display: flex; align-items: center; gap: 8px; } + button { + padding: 6px 12px; border-radius: 10px; border: 1px solid #8883; + background: transparent; color: inherit; cursor: pointer; font: inherit; + } + .val { font-variant-numeric: tabular-nums; font-weight: 600; } + .muted { opacity: 0.6; font-size: 13px; } + `; + + private count = signal(0); + // computed: recomputed lazily when count changes, cached otherwise. + private doubled = computed(() => this.count.get() * 2); + private lastLogged = signal(''); + private disposeEffect?: () => void; + + connectedCallback() { + super.connectedCallback(); + // effect: runs now and again whenever a signal it reads changes. Browser-only + // (SSR never calls connectedCallback), so it is the place for reactive + // side effects. + this.disposeEffect = effect(() => { + this.lastLogged.set('count is ' + this.count.get()); + }); + } + disconnectedCallback() { + super.disconnectedCallback?.(); + this.disposeEffect?.(); + } + + private bump(n: number) { + this.count.set(this.count.get() + n); + } + // batch: both writes commit together, so the component re-renders ONCE. + private reset() { + batch(() => { + this.count.set(0); + this.lastLogged.set('reset'); + }); + } + + render() { + return html` +
    + + ${this.count.get()} + + doubled: ${this.doubled.get()} + +
    +

    ${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 = createRef(); + // Created ONCE (not per render), so `until` keeps the resolved value across + // re-renders instead of flashing back to the fallback each time. + private asyncValue: Promise = this.later(); private reverse() { this.items.set(this.items.get().slice().reverse()); @@ -25,27 +37,59 @@ export class DirectiveDemo extends WebComponent { const id = nextId++; this.items.set([...this.items.get(), { id, label: 'Item ' + id }]); } + private focusInput() { + this.inputRef.value?.focus(); + } + // A promise that resolves after a tick, so `until` shows its fallback first. + private later(): Promise { + return new Promise((resolve) => setTimeout(() => resolve('resolved!'), 400)); + } render() { + const variant = this.variant.get(); return html` -
    -
    - - +
    +
    +
    + + +
    + +
      + ${repeat(this.items.get(), (it: Item) => it.id, (it: Item) => html` +
    • + #${it.id}${it.label} +
    • + `)} +
    + + +
    + +
    + +
    + this.text.set((e.target as HTMLInputElement).value)}> + +
    + +

    async: ${until(this.asyncValue, 'loading...')}

    + + + ${keyed(variant, html`
    ${unsafeHTML('fresh subtree')} #${variant}
    `)}
    - -
      - ${repeat(this.items.get(), (it: Item) => it.id, (it: Item) => html` -
    • - #${it.id}${it.label} -
    • - `)} -
    - -
    `; } diff --git a/test/scaffolds/gallery-coverage.json b/test/scaffolds/gallery-coverage.json index bbc44d58..cac01319 100644 --- a/test/scaffolds/gallery-coverage.json +++ b/test/scaffolds/gallery-coverage.json @@ -66,13 +66,13 @@ "exempt": "deferred: asyncReplace directive; agent-docs/components.md (#859)" }, "batch": { - "exempt": "deferred: signal batching; agent-docs/components.md (#859)" + "demo": "modules/components/components/reactive-meter.ts" }, "cache": { "exempt": "deferred: lit cache directive (distinct from the server cache() helper); agent-docs/advanced.md (#859)" }, "computed": { - "exempt": "deferred: derived signal; agent-docs/components.md (#859)" + "demo": "modules/components/components/reactive-meter.ts" }, "connectWS": { "demo": "modules/broadcast/components/broadcast-feed.ts" @@ -87,13 +87,13 @@ "exempt": "internal: frame codec used by the client router" }, "createRef": { - "exempt": "deferred: element ref directive; agent-docs/components.md (#859)" + "demo": "modules/directives/components/directive-demo.ts" }, "cspNonce": { "exempt": "deferred: CSP nonce accessor; agent-docs/configuration.md (#859)" }, "css": { - "exempt": "deferred: shadow-DOM scoped styles; documented in agent-docs/styling.md (#859)" + "demo": "modules/components/components/reactive-meter.ts" }, "deserialize": { "exempt": "internal: wire serializer; app code round-trips via server actions" @@ -102,7 +102,7 @@ "exempt": "deferred: client-router opt-out; agent-docs/configuration.md (#859)" }, "effect": { - "exempt": "deferred: signal effect; agent-docs/components.md (#859)" + "demo": "modules/components/components/reactive-meter.ts" }, "enableClientRouter": { "exempt": "deferred: client-router opt-in; agent-docs/configuration.md (#859)" @@ -192,10 +192,10 @@ "exempt": "internal: directive type guard used by the renderer" }, "keyed": { - "exempt": "deferred: keyed directive; agent-docs/components.md (#859)" + "demo": "modules/directives/components/directive-demo.ts" }, "live": { - "exempt": "deferred: live directive; agent-docs/components.md (#859)" + "demo": "modules/directives/components/directive-demo.ts" }, "lookup": { "exempt": "internal: component-registry plumbing" @@ -207,7 +207,7 @@ "exempt": "internal: stale-while-revalidate plumbing" }, "navigate": { - "exempt": "deferred: programmatic client navigation; mentioned in the routing demo prose (#859)" + "demo": "modules/client-router/components/router-controls.ts" }, "notFound": { "demo": "app/features/routing/[id]/page.ts" @@ -231,7 +231,7 @@ "exempt": "deferred: server redirect throw; mentioned in the boundaries demo prose (#859)" }, "ref": { - "exempt": "deferred: element ref directive; agent-docs/components.md (#859)" + "demo": "modules/directives/components/directive-demo.ts" }, "register": { "demo": "modules/components/components/counter-card.ts" @@ -240,22 +240,22 @@ "exempt": "internal: keyed-list registry plumbing" }, "render": { - "exempt": "internal: client render entry; the framework calls it, app code writes components" + "exempt": "deferred: client render(v, el) into a DOM element; public API, agent-docs/advanced (#859)" }, "renderStream": { - "exempt": "internal: the applier is auto-wired by the client router; app uses the tag / connectWS" + "exempt": "deferred: applier for a connectWS handler; public API, agent-docs/advanced (#859)" }, "renderToStream": { "exempt": "internal: server render entry; used by the framework and tests, not authored in app pages" }, "renderToString": { - "exempt": "internal: server render entry; used by the framework and tests, not authored in app pages" + "exempt": "deferred: SSR-to-string for test assertions; @webjsdev/core/server, agent-docs/testing (#859)" }, "repeat": { "demo": "modules/directives/components/directive-demo.ts" }, "revalidate": { - "exempt": "deferred: client snapshot eviction; agent-docs/built-ins.md (#859)" + "demo": "modules/client-router/components/router-controls.ts" }, "richFetch": { "exempt": "deferred: content-negotiated rich fetch; agent-docs/advanced.md (#859)" @@ -294,10 +294,10 @@ "demo": "app/features/boundaries/private/page.ts" }, "unsafeHTML": { - "exempt": "deferred: trusted raw-HTML directive; agent-docs/components.md (#859)" + "demo": "modules/directives/components/directive-demo.ts" }, "until": { - "exempt": "deferred: until directive; agent-docs/components.md (#859)" + "demo": "modules/directives/components/directive-demo.ts" }, "watch": { "demo": "modules/directives/components/directive-demo.ts" @@ -354,7 +354,7 @@ "exempt": "internal: framework/CLI plumbing" }, "attachWebSocket": { - "exempt": "deferred: WS upgrade helper (#865)" + "exempt": "deferred: WS test-server helper; docs/app/docs/testing (#859)" }, "auditPinned": { "exempt": "internal: framework/CLI plumbing" @@ -372,7 +372,7 @@ "exempt": "internal: framework/CLI plumbing" }, "buildRouteTable": { - "exempt": "internal: framework/CLI plumbing" + "exempt": "deferred: documented test helper (build a route table); docs/app/docs/testing (#859)" }, "cache": { "demoed": true @@ -393,13 +393,13 @@ "exempt": "deferred: cookie session strategy (#865)" }, "cookieSessionStorage": { - "exempt": "deferred: cookie session storage (#865)" + "exempt": "deferred: cookie session storage strategy; documented in docs/app/docs/sessions (#859)" }, "cookies": { "exempt": "deferred: request cookie accessor (#865)" }, "cookiesToHeader": { - "exempt": "deferred: cookie serializer (#865)" + "exempt": "internal: low-level cookie serializer; apps use cookies()" }, "cors": { "demoed": true @@ -471,10 +471,10 @@ "exempt": "deferred: session read (#865)" }, "getSetCookies": { - "exempt": "deferred: Set-Cookie reader (#865)" + "exempt": "internal: low-level Set-Cookie reader" }, "getStore": { - "exempt": "deferred: active store accessor (#865)" + "exempt": "deferred: active-store read API; docs/app/docs/cache + agent-docs/built-ins (#859)" }, "handleApi": { "exempt": "internal: framework/CLI plumbing" @@ -516,10 +516,10 @@ "demoed": true }, "matchApi": { - "exempt": "internal: framework/CLI plumbing" + "exempt": "deferred: documented test helper (match an API route); docs/app/docs/testing (#859)" }, "matchPage": { - "exempt": "internal: framework/CLI plumbing" + "exempt": "deferred: documented test helper (match a page route); docs/app/docs/testing (#859)" }, "memoryStore": { "exempt": "deferred: in-memory store (#865)" @@ -546,10 +546,10 @@ "demoed": true }, "rawActionRequest": { - "exempt": "internal: framework/CLI plumbing" + "exempt": "deferred: documented test helper (build an action request); docs/app/docs/testing (#859)" }, "readBody": { - "exempt": "internal: framework/CLI plumbing" + "exempt": "deferred: route-handler body helper (inverse of json()); docs/app/docs/api-routes (#859)" }, "readPinFile": { "exempt": "internal: framework/CLI plumbing" @@ -588,10 +588,10 @@ "demoed": true }, "runInstrumentation": { - "exempt": "deferred: instrumentation runner (#865)" + "exempt": "internal: the framework runs it; apps write instrumentation.ts register()" }, "runWithActionSignal": { - "exempt": "deferred: action-signal scope helper (#865)" + "exempt": "internal: action-signal scope helper; apps use actionSignal()" }, "satisfiesSemverRange": { "exempt": "internal: framework/CLI plumbing" @@ -630,7 +630,7 @@ "exempt": "deferred: signed URL builder (#865)" }, "sitemap": { - "exempt": "deferred: sitemap serializer (#865)" + "demoed": true }, "sitemapIndex": { "exempt": "deferred: sitemap index serializer (#865)" @@ -642,16 +642,16 @@ "exempt": "internal: framework/CLI plumbing" }, "stampRemoteIp": { - "exempt": "internal: framework/CLI plumbing" + "exempt": "deferred: embedded-adapter IP helper; docs/app/docs/rate-limiting + agent-docs/advanced (#859)" }, "startServer": { "exempt": "deferred: programmatic server boot (#865)" }, "storeSession": { - "exempt": "deferred: session persistence (#865)" + "exempt": "deferred: production session storage (alias of storeSessionStorage); docs/app/docs/sessions (#859)" }, "storeSessionStorage": { - "exempt": "deferred: session storage (#865)" + "exempt": "deferred: production (Redis-backed) session storage; docs/app/docs/sessions + cache (#859)" }, "stream": { "demoed": true @@ -684,7 +684,7 @@ "exempt": "deferred: signed URL verifier (#865)" }, "withCookies": { - "exempt": "deferred: cookie response helper (#865)" + "exempt": "internal: low-level response cookie helper; apps use cookies()" }, "withRequest": { "exempt": "internal: framework/CLI plumbing" @@ -701,16 +701,16 @@ "demoed": true }, "error": { - "exempt": "deferred: no example error file shipped by the scaffold yet (#865)" + "demoed": true }, "loading": { - "exempt": "deferred: no example loading file shipped by the scaffold yet (#865)" + "demoed": true }, "middleware": { "demoed": true }, "not-found": { - "exempt": "deferred: no example not-found file shipped by the scaffold yet (#865)" + "demoed": true }, "forbidden": { "demoed": true @@ -728,13 +728,13 @@ "demoed": true }, "sitemap": { - "exempt": "deferred: no example sitemap file shipped by the scaffold yet (#865)" + "demoed": true }, "robots": { - "exempt": "deferred: no example robots file shipped by the scaffold yet (#865)" + "demoed": true }, "manifest": { - "exempt": "deferred: no example manifest file shipped by the scaffold yet (#865)" + "demoed": true }, "icon": { "exempt": "deferred: no example icon file shipped by the scaffold yet (#865)"