@webjsdev/cli 0.10.29 → 0.10.31
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/bin/webjs.js +1 -1
- package/lib/api-gallery.js +229 -0
- package/lib/create.js +452 -158
- package/lib/saas-template.js +39 -15
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +70 -2
- package/templates/.claude/hooks/check-server-imports.mjs +86 -0
- package/templates/.claude/hooks/check-server-imports.sh +26 -0
- package/templates/.claude/settings.json +9 -0
- package/templates/.cursorrules +41 -2
- package/templates/.github/copilot-instructions.md +41 -2
- package/templates/AGENTS.md +160 -10
- package/templates/CONVENTIONS.md +150 -11
- package/templates/gallery/app/examples/todo/page.ts +34 -0
- package/templates/gallery/app/features/async-render/page.ts +14 -0
- package/templates/gallery/app/features/broadcast/feed/route.ts +19 -0
- package/templates/gallery/app/features/broadcast/page.ts +24 -0
- package/templates/gallery/app/features/caching/page.ts +39 -0
- package/templates/gallery/app/features/client-router/page.ts +34 -0
- package/templates/gallery/app/features/client-router/second/page.ts +20 -0
- package/templates/gallery/app/features/components/page.ts +14 -0
- package/templates/gallery/app/features/directives/page.ts +14 -0
- package/templates/gallery/app/features/env/page.ts +36 -0
- package/templates/gallery/app/features/file-storage/file/[key]/route.ts +19 -0
- package/templates/gallery/app/features/file-storage/page.ts +62 -0
- package/templates/gallery/app/features/forms/page.ts +72 -0
- package/templates/gallery/app/features/metadata/page.ts +55 -0
- package/templates/gallery/app/features/optimistic-ui/page.ts +14 -0
- package/templates/gallery/app/features/rate-limit/page.ts +29 -0
- package/templates/gallery/app/features/rate-limit/ping/middleware.ts +8 -0
- package/templates/gallery/app/features/rate-limit/ping/route.ts +7 -0
- package/templates/gallery/app/features/route-handler/data/route.ts +8 -0
- package/templates/gallery/app/features/route-handler/page.ts +13 -0
- package/templates/gallery/app/features/routing/[id]/page.ts +25 -0
- package/templates/gallery/app/features/routing/page.ts +47 -0
- package/templates/gallery/app/features/server-actions/page.ts +14 -0
- package/templates/gallery/app/features/service-worker/page.ts +35 -0
- package/templates/gallery/app/features/websockets/echo/route.ts +19 -0
- package/templates/gallery/app/features/websockets/page.ts +25 -0
- package/templates/gallery/modules/async-render/components/server-clock.ts +26 -0
- package/templates/gallery/modules/async-render/queries/server-greeting.server.ts +9 -0
- package/templates/gallery/modules/broadcast/components/broadcast-feed.ts +61 -0
- package/templates/gallery/modules/components/components/counter-card.ts +35 -0
- package/templates/gallery/modules/directives/components/directive-demo.ts +53 -0
- package/templates/gallery/modules/file-storage/actions/store-upload.server.ts +18 -0
- package/templates/gallery/modules/optimistic-ui/actions/like-post.server.ts +9 -0
- package/templates/gallery/modules/optimistic-ui/components/like-button.ts +35 -0
- package/templates/gallery/modules/rate-limit/components/rate-probe.ts +49 -0
- package/templates/gallery/modules/server-actions/actions/greet.server.ts +12 -0
- package/templates/gallery/modules/server-actions/components/greeter.ts +30 -0
- package/templates/gallery/modules/server-actions/utils/format.server.ts +8 -0
- package/templates/gallery/modules/todo/actions/create-todo.server.ts +16 -0
- package/templates/gallery/modules/todo/actions/delete-todo.server.ts +15 -0
- package/templates/gallery/modules/todo/actions/toggle-todo.server.ts +21 -0
- package/templates/gallery/modules/todo/components/todo-app.ts +140 -0
- package/templates/gallery/modules/todo/queries/list-todos.server.ts +19 -0
- package/templates/gallery/modules/todo/types.ts +12 -0
- package/templates/gallery/modules/websockets/components/ws-echo.ts +62 -0
- package/templates/lib/utils/ui.ts +4 -4
- package/templates/test/hello/browser/hello.test.js +12 -6
- package/templates/test/hello/e2e/hello.test.ts +11 -9
- package/templates/test/hello/hello.test.ts +4 -6
- package/templates/web-test-runner.config.js +84 -9
package/templates/AGENTS.md
CHANGED
|
@@ -4,8 +4,8 @@ Read this before editing any file. This is a webjs app: AI-first, web-
|
|
|
4
4
|
components-first, no build step. The framework's own full API reference
|
|
5
5
|
lives at https://github.com/webjsdev/webjs/blob/main/AGENTS.md and the
|
|
6
6
|
full hosted documentation (every API, recipe, and example) lives at
|
|
7
|
-
**https://docs.webjs.
|
|
8
|
-
companion and reach for docs.webjs.
|
|
7
|
+
**https://docs.webjs.dev**. Treat this file as the app-scoped
|
|
8
|
+
companion and reach for docs.webjs.dev whenever you need more detail.
|
|
9
9
|
|
|
10
10
|
## If you just scaffolded this app (AI agents, read first)
|
|
11
11
|
|
|
@@ -20,7 +20,13 @@ example `Home` nav, and pick a content-width container that fits. The
|
|
|
20
20
|
default `<main class="max-w-[760px]">` is a reading column for prose and
|
|
21
21
|
forms, so for a full-bleed app, dashboard, or board, widen the cap or
|
|
22
22
|
remove it (keep the theme tokens). A wide layout left in the 760px
|
|
23
|
-
reading column overflows into a horizontal scrollbar.
|
|
23
|
+
reading column overflows into a horizontal scrollbar. **Give the app a
|
|
24
|
+
unique design.** When it has a UI, choose its palette, layout, typography,
|
|
25
|
+
and chrome to fit what the user asked for, rather than mimicking the
|
|
26
|
+
scaffold's example look (the warm accent, the reading column, the serif
|
|
27
|
+
display, the example header/nav) or just recoloring the same layout. The
|
|
28
|
+
`api` template has no UI, so this does not apply there. The design tokens
|
|
29
|
+
and theme wiring are infrastructure to keep and restyle on top of. This is ENFORCED:
|
|
24
30
|
the example `app/page.ts` and `app/layout.ts` carry a
|
|
25
31
|
`webjs-scaffold-placeholder` marker comment, and `webjs check` fails
|
|
26
32
|
while any marker remains, so this freshly scaffolded app fails the check
|
|
@@ -51,6 +57,17 @@ user asked for, never leftover scaffold code.
|
|
|
51
57
|
app's real domain models (delete the example `User` model unless the
|
|
52
58
|
app actually needs users), run `webjs db generate` then
|
|
53
59
|
`webjs db migrate`, then build pages / actions / queries against them.
|
|
60
|
+
4. **Prune what the app does not use.** The scaffold is reference, so keep
|
|
61
|
+
the infrastructure the app USES and delete the rest, both files AND
|
|
62
|
+
folders. No persistence means delete `db/`, `drizzle.config.ts`, and
|
|
63
|
+
the `db:*` scripts. No UI kit used means delete `components/ui/`,
|
|
64
|
+
`components.json`, and `lib/utils/cn.ts`. No PWA means delete
|
|
65
|
+
`public/sw.js` and `offline.html`. Always KEEP the durable knowledge
|
|
66
|
+
(`AGENTS.md`, `CONVENTIONS.md`, the per-agent rule files, the MCP
|
|
67
|
+
wiring), never prune it, so removing example code never removes your
|
|
68
|
+
context. Prune AFTER you have used the features and examples as reference, never
|
|
69
|
+
blindly up front. This is a no-op for the `api` template (no UI kit,
|
|
70
|
+
no PWA files).
|
|
54
71
|
|
|
55
72
|
**Picking the right scaffold from the user's prompt** (you do this BEFORE
|
|
56
73
|
running `webjs create`; if you're reading this you've already scaffolded.
|
|
@@ -171,7 +188,7 @@ entry, its own template parser. Inside `` html`…` `` templates you get:
|
|
|
171
188
|
In VS Code / Cursor / Windsurf, the **`webjs` extension** bundles this
|
|
172
189
|
automatically (no `tsconfig.json` edit, no separate Lit extension).
|
|
173
190
|
|
|
174
|
-
See [docs.webjs.
|
|
191
|
+
See [docs.webjs.dev → Editor setup](https://docs.webjs.dev/docs/editor-setup)
|
|
175
192
|
for the full walkthrough.
|
|
176
193
|
|
|
177
194
|
**Config validation in `package.json`.** The scaffold ships
|
|
@@ -296,11 +313,32 @@ wrapper adds zero value and obscures the real element from inspection,
|
|
|
296
313
|
form submission, and screen readers. Custom elements are reserved for
|
|
297
314
|
behavior the browser can't deliver natively.
|
|
298
315
|
|
|
316
|
+
### Accessible control labeling
|
|
317
|
+
|
|
318
|
+
Give every interactive control an accessible name, and make clickable
|
|
319
|
+
text a `<label for="control-id">` (or the control itself) so a text click
|
|
320
|
+
activates the control on BOTH the JS path and the no-JS form-submit path.
|
|
321
|
+
Use `aria-label` and `aria-pressed` on icon-only controls (a toggle
|
|
322
|
+
button, an icon-only close/menu button). A native `<input>` under a
|
|
323
|
+
`<label>` gets this for free, which is another reason to reach for the
|
|
324
|
+
Tier-1 class helpers on real elements. In a browser test,
|
|
325
|
+
`assertNoA11yViolations(el)` from `@webjsdev/core/testing` catches
|
|
326
|
+
missing labels.
|
|
327
|
+
|
|
299
328
|
## File conventions
|
|
300
329
|
|
|
301
330
|
```
|
|
302
|
-
app/ thin route adapters (import from modules/)
|
|
303
|
-
|
|
331
|
+
app/ ROUTING ONLY: thin route adapters (import from modules/).
|
|
332
|
+
No CSS, helpers, or constants here; those live in
|
|
333
|
+
styles/, lib/utils/, and modules/. globals.css is at
|
|
334
|
+
styles/, NOT app/.
|
|
335
|
+
page.ts → / (the scaffold home links to the gallery)
|
|
336
|
+
features/<name>/ single-feature demos (routing, components,
|
|
337
|
+
server-actions, optimistic-ui, async-render,
|
|
338
|
+
directives, route-handler, forms, metadata, caching,
|
|
339
|
+
env, client-router, service-worker); prune what you skip
|
|
340
|
+
examples/<name>/ whole example apps that compose features (todo);
|
|
341
|
+
prune what you skip
|
|
304
342
|
layout.ts root layout, wraps every page
|
|
305
343
|
error.ts error boundary (render failures → user-friendly)
|
|
306
344
|
loading.ts Suspense fallback for sibling page
|
|
@@ -324,6 +362,8 @@ modules/<feature>/
|
|
|
324
362
|
types.ts feature types
|
|
325
363
|
lib/
|
|
326
364
|
... cross-cutting infra (session, auth config, etc.)
|
|
365
|
+
styles/
|
|
366
|
+
globals.css @webjsdev/ui theme tokens (NOT in app/; app/ is routing-only)
|
|
327
367
|
db/
|
|
328
368
|
schema.server.ts Drizzle models + relations (your data layer)
|
|
329
369
|
columns.server.ts column helpers (dialect-specific; the only file to swap for Postgres)
|
|
@@ -341,6 +381,41 @@ test/<feature>/ feature-scoped tests, one folder per concern
|
|
|
341
381
|
middleware.ts root middleware (optional, outermost)
|
|
342
382
|
```
|
|
343
383
|
|
|
384
|
+
### The gallery (reference content, prune it)
|
|
385
|
+
|
|
386
|
+
The scaffold ships a gallery organized by KIND, so features and whole apps are
|
|
387
|
+
not mixed:
|
|
388
|
+
- `app/features/<name>/` are single-feature demos, one webjs concept each
|
|
389
|
+
(routing, components, server-actions, optimistic-ui, async-render, directives,
|
|
390
|
+
route-handler, forms, metadata, caching, env, client-router, service-worker,
|
|
391
|
+
plus the infra demos websockets, file-storage, rate-limit, broadcast).
|
|
392
|
+
- `app/examples/<name>/` are whole example apps that compose several features
|
|
393
|
+
(todo: optimistic UI + progressive enhancement + a11y + db + modules).
|
|
394
|
+
|
|
395
|
+
Both keep their logic in `modules/<name>/`. Each route is small, idiomatic, and
|
|
396
|
+
heavily commented, and the gallery is your PRIMARY reference for how webjs works.
|
|
397
|
+
|
|
398
|
+
**Study the whole gallery FIRST, prune SECOND.** Before you write or delete
|
|
399
|
+
anything, read every feature demo and the example app end to end (the code AND
|
|
400
|
+
the comments) to absorb the idioms you will reuse: the modules split, signals,
|
|
401
|
+
the `optimistic()` API, `async render()`, the `.server.ts` vs `'use server'`
|
|
402
|
+
boundary, progressive-enhancement forms, `<label for>` a11y, dynamic routes, and
|
|
403
|
+
`route.ts` handlers. Only AFTER you have internalised the patterns should you
|
|
404
|
+
prune. Never delete the examples blindly up front (that throws away your context
|
|
405
|
+
before you have read it), and never prune the durable knowledge surfaces
|
|
406
|
+
(`AGENTS.md`, `CONVENTIONS.md`, the per-agent rule files), which stay as context
|
|
407
|
+
for every future iteration.
|
|
408
|
+
|
|
409
|
+
Then prune: the examples are REFERENCE, not your app, so keep and adapt the ones
|
|
410
|
+
you need and delete the rest. Pruning a route means deleting its
|
|
411
|
+
`app/features/<name>` or `app/examples/<name>` folder AND its `modules/<name>`
|
|
412
|
+
folder (and, for the todo app, the `todos` table in `db/schema.server.ts`), then
|
|
413
|
+
removing its link from `app/page.ts`. Each route page carries a
|
|
414
|
+
`webjs-scaffold-placeholder` marker so `webjs check` fails until you have
|
|
415
|
+
consciously kept-and-adapted or pruned it. After pruning, delete any now-empty
|
|
416
|
+
directories (an empty `lib/utils/` or `modules/<name>/` is leftover scaffolding,
|
|
417
|
+
not structure).
|
|
418
|
+
|
|
344
419
|
### Typed page / layout / route-handler props
|
|
345
420
|
|
|
346
421
|
Type page / layout / route-handler arguments with the exported helpers so a
|
|
@@ -517,7 +592,7 @@ git commit -m "vendor + download dayjs"
|
|
|
517
592
|
Bundle files land in `.webjs/vendor/<pkg>@<version>.js`. importmap
|
|
518
593
|
points at local `/__webjs/vendor/` paths. Browser fetches from your
|
|
519
594
|
own origin. Suitable for `script-src 'self'` CSP, air-gapped deploys,
|
|
520
|
-
or compliance environments. See [docs.webjs.
|
|
595
|
+
or compliance environments. See [docs.webjs.dev Deployment → CSP](https://docs.webjs.dev/docs/deployment#csp).
|
|
521
596
|
|
|
522
597
|
**Other CLI commands:**
|
|
523
598
|
|
|
@@ -593,7 +668,7 @@ const url = process.env.WEBJS_PUBLIC_API_URL; // works
|
|
|
593
668
|
const secret = process.env.AUTH_SECRET; // undefined (fail-closed)
|
|
594
669
|
```
|
|
595
670
|
|
|
596
|
-
`process.env.NODE_ENV` is also defined in the browser (`'development'` in `webjs dev`, `'production'` in `webjs start`), so vendor bundles that probe it work without setup. Full docs: [Configuration](https://docs.webjs.
|
|
671
|
+
`process.env.NODE_ENV` is also defined in the browser (`'development'` in `webjs dev`, `'production'` in `webjs start`), so vendor bundles that probe it work without setup. Full docs: [Configuration](https://docs.webjs.dev/docs/configuration).
|
|
597
672
|
|
|
598
673
|
## Component pattern
|
|
599
674
|
|
|
@@ -751,6 +826,31 @@ globally. Prefer Tailwind. When a utility bundle repeats, extract it into
|
|
|
751
826
|
a `lib/utils/ui.ts` helper returning an `` html`...` `` fragment, not a
|
|
752
827
|
CSS class.
|
|
753
828
|
|
|
829
|
+
#### Design tokens: ONE theme, shadcn-canonical
|
|
830
|
+
|
|
831
|
+
The app has a SINGLE theme, defined once in `app/layout.ts`. It uses the
|
|
832
|
+
standard `@webjsdev/ui` (shadcn-compatible) semantic tokens, set to this app's
|
|
833
|
+
brand palette. Use the canonical utility names everywhere, in the page chrome
|
|
834
|
+
AND inside components: `bg-background`, `text-foreground`, `bg-card`, `bg-muted`,
|
|
835
|
+
`text-muted-foreground`, `bg-primary`, `text-primary-foreground`, `bg-accent`,
|
|
836
|
+
`text-accent-foreground`, `border-border`, `ring-ring`. These are exactly the
|
|
837
|
+
tokens the components copied in by `webjs ui add <name>` read, so a scaffolded
|
|
838
|
+
page and a later-added ui component share one coherent theme automatically.
|
|
839
|
+
|
|
840
|
+
- **Never invent a parallel token vocabulary** (`--fg`, `--bg`, `text-fg`,
|
|
841
|
+
`bg-elev`, a separate `--brand`). It collides with the ui tokens (the accent
|
|
842
|
+
once flipped to neutral on navigation for exactly this reason) and diverges
|
|
843
|
+
from the shadcn conventions the ui kit and AI agents both expect.
|
|
844
|
+
- **Reach for opacity modifiers before a new token**: `bg-primary/10` for a
|
|
845
|
+
tint, `hover:bg-primary/90` for a hover, `text-muted-foreground/70` for a
|
|
846
|
+
subtler text level.
|
|
847
|
+
- **Edit the palette in one place** (`app/layout.ts`). To ADD a token, do it
|
|
848
|
+
the canonical way: a `--x` variable in the `:root` / `.dark` blocks plus a
|
|
849
|
+
`--color-x: var(--x)` line in the `@theme inline` block, then use it as
|
|
850
|
+
`bg-x` / `text-x`.
|
|
851
|
+
- Dark mode is a `.dark` class the theme toggle sets. Tokens switch by theme
|
|
852
|
+
automatically, so a component written with these names works in both.
|
|
853
|
+
|
|
754
854
|
Reserve raw CSS for what utilities cannot express: design-token `:root` /
|
|
755
855
|
`@theme` definitions, `@property` + `@keyframes` animations,
|
|
756
856
|
`::-webkit-scrollbar`, `prefers-reduced-motion` blocks, and complex
|
|
@@ -761,6 +861,16 @@ legitimately use `static styles = css\`\`` for scoped CSS.
|
|
|
761
861
|
|
|
762
862
|
## Server action pattern
|
|
763
863
|
|
|
864
|
+
**The `.server.ts` vs `'use server'` decision, in one question.** Will the
|
|
865
|
+
client call it? Add `'use server'` and the file becomes an RPC action
|
|
866
|
+
(the browser import is rewritten to a typed stub). Is it server-only
|
|
867
|
+
infra instead (a DB driver, secrets, `node:*`)? Use NO directive, and
|
|
868
|
+
never import it into a page, layout, or component. Reach it from a
|
|
869
|
+
`'use server'` action, a `route.ts` handler, or `middleware.ts`. A
|
|
870
|
+
`.server.ts` file WITHOUT the directive is a server-only utility whose
|
|
871
|
+
browser import throws at module load (invariant 2 below), so a
|
|
872
|
+
page/component that imports it directly crashes on the client.
|
|
873
|
+
|
|
764
874
|
```ts
|
|
765
875
|
// modules/posts/actions/create-post.server.ts
|
|
766
876
|
'use server';
|
|
@@ -788,7 +898,43 @@ that RETURNS a `ReadableStream` / async generator streams its chunks (consume
|
|
|
788
898
|
with `for await`); read the request `AbortSignal` via `actionSignal()` to cancel
|
|
789
899
|
on disconnect. **SAFETY:** a `cache` with `public: true` shares one response
|
|
790
900
|
across all users, so use it only for data identical for every visitor. Full
|
|
791
|
-
reference: https://docs.webjs.
|
|
901
|
+
reference: https://docs.webjs.dev/docs/server-actions
|
|
902
|
+
|
|
903
|
+
## Mutations: default to optimistic UI
|
|
904
|
+
|
|
905
|
+
Default to optimistic UI for every feasible mutation. Use `optimistic()`
|
|
906
|
+
from `@webjsdev/core` (create, toggle, like, follow, reorder, rename,
|
|
907
|
+
status change) so the UI updates instantly and rolls back automatically
|
|
908
|
+
on failure. The declarative form queues an update on a component with
|
|
909
|
+
auto-release when the action promise settles, no hand-written try-catch,
|
|
910
|
+
cache-and-restore, or temp-id bookkeeping.
|
|
911
|
+
|
|
912
|
+
```ts
|
|
913
|
+
import { WebComponent, prop, optimistic, html } from '@webjsdev/core';
|
|
914
|
+
import { createTodo } from '#modules/todos/actions/create-todo.server.ts';
|
|
915
|
+
|
|
916
|
+
class TodoList extends WebComponent({ todos: prop<Todo[]>(Array) }) {
|
|
917
|
+
private optimisticTodos = optimistic(this, {
|
|
918
|
+
source: () => this.todos,
|
|
919
|
+
update: (state, title: string) => [...state, { title, pending: true }],
|
|
920
|
+
});
|
|
921
|
+
async handleSubmit(title: string) {
|
|
922
|
+
const promise = createTodo({ title });
|
|
923
|
+
this.optimisticTodos.add(title, promise); // auto-releases on settle
|
|
924
|
+
await promise;
|
|
925
|
+
}
|
|
926
|
+
render() {
|
|
927
|
+
return html`<ul>${this.optimisticTodos.value.map(t => html`
|
|
928
|
+
<li class=${t.pending ? 'opacity-50' : ''}>${t.title}</li>`)}</ul>`;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
Do NOT use optimistic UI where it hurts: unpredictable or server-computed
|
|
934
|
+
results (AI output, server-assigned values the client cannot guess),
|
|
935
|
+
side-effectful mutations the user must wait on (payment, email, OAuth),
|
|
936
|
+
and destructive irreversible actions (a confirm-first UX is better). Full
|
|
937
|
+
reference: https://docs.webjs.com/docs/optimistic-ui
|
|
792
938
|
|
|
793
939
|
## Client navigation patterns (auto-magic)
|
|
794
940
|
|
|
@@ -1166,7 +1312,11 @@ composition, so a nested shell ends up dropped by the HTML parser.
|
|
|
1166
1312
|
`lib/utils/cn.ts` with `cn`, design-
|
|
1167
1313
|
system helpers). Server-only `lib/*` files must only be imported
|
|
1168
1314
|
from `.server.ts`/`route.ts`/`middleware.ts`; browser-safe `lib/*`
|
|
1169
|
-
files (like `lib/utils/cn.ts`) can be imported anywhere.
|
|
1315
|
+
files (like `lib/utils/cn.ts`) can be imported anywhere. A TYPE-ONLY
|
|
1316
|
+
import is the exception: `import type { Todo } from
|
|
1317
|
+
'#db/schema.server.ts'` is fine in a page or component, because the
|
|
1318
|
+
TypeScript stripper erases it before it reaches the browser, so
|
|
1319
|
+
sharing a derived row type is safe and is not flagged.
|
|
1170
1320
|
3. Event / property / boolean holes in `` html`` `` are unquoted:
|
|
1171
1321
|
`@click=${fn}`, not `@click="${fn}"`.
|
|
1172
1322
|
4. Component state lives in signals. Import `signal` from
|
package/templates/CONVENTIONS.md
CHANGED
|
@@ -310,9 +310,45 @@ production workloads.
|
|
|
310
310
|
<!-- OVERRIDE -->
|
|
311
311
|
|
|
312
312
|
This project was created with `webjs create`. Every file you see right
|
|
313
|
-
now (the `app/page.ts`
|
|
314
|
-
|
|
315
|
-
|
|
313
|
+
now (the `app/page.ts` homepage, the example `User` model, the
|
|
314
|
+
`theme-toggle` component, the gallery under `app/features/` and
|
|
315
|
+
`app/examples/` in the full-stack template, the example users module in
|
|
316
|
+
api / saas templates) is a **starting point**.
|
|
317
|
+
|
|
318
|
+
The full-stack and saas scaffolds ship a **gallery** organized by kind so features
|
|
319
|
+
and whole apps are not mixed. `app/features/<name>/` are single-feature
|
|
320
|
+
demos, one webjs concept each (routing, components, server-actions,
|
|
321
|
+
optimistic-ui, async-render, directives, route-handler). `app/examples/<name>/`
|
|
322
|
+
are whole example apps that compose several features (todo: optimistic UI
|
|
323
|
+
+ progressive enhancement + a11y + db + modules). Both keep their logic in
|
|
324
|
+
`modules/`, are small and heavily commented, and are the PRIMARY reference
|
|
325
|
+
for how webjs works.
|
|
326
|
+
|
|
327
|
+
**Study the whole gallery FIRST, prune SECOND.** Before writing or deleting
|
|
328
|
+
anything, read every feature demo and the example app end to end (code AND
|
|
329
|
+
comments) to absorb the idioms you will reuse. Only AFTER you have
|
|
330
|
+
internalised the patterns should you prune. Never delete the examples
|
|
331
|
+
blindly up front (that discards your context before you have read it), and
|
|
332
|
+
never prune the durable knowledge surfaces (`AGENTS.md`, `CONVENTIONS.md`,
|
|
333
|
+
the per-agent rule files), which stay as context for every future
|
|
334
|
+
iteration.
|
|
335
|
+
|
|
336
|
+
Then prune: the examples are REFERENCE, not the app, so keep and adapt the
|
|
337
|
+
ones you need and **delete the rest**. Pruning a route means deleting its
|
|
338
|
+
`app/features/<name>` or `app/examples/<name>` folder AND its
|
|
339
|
+
`modules/<name>` folder (for the todo app, also the `todos` table in
|
|
340
|
+
`db/schema.server.ts` and its link in `app/page.ts`). Each route page
|
|
341
|
+
carries a `webjs-scaffold-placeholder` marker, so `webjs check` fails until
|
|
342
|
+
you consciously keep-and-adapt or prune it. After pruning, delete any
|
|
343
|
+
now-empty directories, an empty `lib/utils/` or `modules/<name>/` is
|
|
344
|
+
leftover scaffolding, not structure.
|
|
345
|
+
|
|
346
|
+
**`app/` is routing-only.** Only routing files belong in `app/` (page,
|
|
347
|
+
layout, route, middleware, and metadata routes). CSS, helpers, and
|
|
348
|
+
constants do NOT: the theme lives at `styles/globals.css` (NOT
|
|
349
|
+
`app/globals.css`), browser-safe helpers at `lib/utils/`, and feature
|
|
350
|
+
logic in `modules/`. If you add a stylesheet or a helper, put it outside
|
|
351
|
+
`app/`.
|
|
316
352
|
|
|
317
353
|
When the user asks the agent to build their actual app:
|
|
318
354
|
|
|
@@ -325,6 +361,10 @@ When the user asks the agent to build their actual app:
|
|
|
325
361
|
need a theme picker.
|
|
326
362
|
4. **Delete the example users module** (api/saas templates) if the app
|
|
327
363
|
doesn't use it.
|
|
364
|
+
4b. **Prune the gallery** (full-stack template). Keep and adapt the
|
|
365
|
+
`app/features/` demos and the `app/examples/` app the real app uses,
|
|
366
|
+
delete the rest (route + module + any table), and remove their links
|
|
367
|
+
from `app/page.ts`.
|
|
328
368
|
5. **Adapt `app/layout.ts` to the app, not just the page.** Set the real
|
|
329
369
|
brand, replace the example `Home` nav with the app's navigation, and
|
|
330
370
|
pick a content-width container that fits. The default
|
|
@@ -333,15 +373,25 @@ When the user asks the agent to build their actual app:
|
|
|
333
373
|
dashboard, or board, or a wide layout overflows into an unnecessary
|
|
334
374
|
horizontal scrollbar. Keep the design tokens and theme setup, those
|
|
335
375
|
are infrastructure.
|
|
336
|
-
6. **
|
|
376
|
+
6. **Use a unique design (UI apps).** Give the app a look of its own that
|
|
377
|
+
fits what the user asked for. Choose the palette, layout, typography,
|
|
378
|
+
spacing, and chrome deliberately. Do NOT mimic the scaffold's example
|
|
379
|
+
look (its warm accent, the 760px reading column, the serif display, the
|
|
380
|
+
example header/nav), and do not just recolor the same layout. The
|
|
381
|
+
`api` template has no UI, so this does not apply there. The design
|
|
382
|
+
tokens and theme wiring in `app/layout.ts` are infrastructure to keep
|
|
383
|
+
and restyle on top of, not the example look to preserve.
|
|
384
|
+
7. **Keep:** the Drizzle setup, the test config, the agent config files
|
|
337
385
|
(`AGENTS.md`, `CONVENTIONS.md`, `CLAUDE.md`, `.cursorrules`, etc.),
|
|
338
386
|
`db/connection.server.ts` + `db/columns.server.ts`, the directory
|
|
339
387
|
conventions, the design tokens in `app/layout.ts`. These are the
|
|
340
388
|
infrastructure, not the example app.
|
|
341
389
|
|
|
342
|
-
This is enforced, not just advised. The example `app/page.ts
|
|
343
|
-
`app/layout.ts
|
|
344
|
-
|
|
390
|
+
This is enforced, not just advised. The example `app/page.ts`,
|
|
391
|
+
`app/layout.ts`, and each `app/features/<name>/page.ts` +
|
|
392
|
+
`app/examples/<name>/page.ts` carry a
|
|
393
|
+
`webjs-scaffold-placeholder` marker comment, and the
|
|
394
|
+
`no-scaffold-placeholder` check fails while any marker remains, so a
|
|
345
395
|
freshly scaffolded app fails `webjs check` until you address each
|
|
346
396
|
placeholder. The marker is acknowledge-and-remove: replace the example
|
|
347
397
|
content, or deliberately keep it, and in either case delete the marker
|
|
@@ -352,6 +402,19 @@ The scaffold exists so the agent doesn't reinvent the directory layout,
|
|
|
352
402
|
the Drizzle wiring, the test runner config, or the convention files. It
|
|
353
403
|
does NOT exist so the agent ships the example homepage.
|
|
354
404
|
|
|
405
|
+
### Prune what the app does not use
|
|
406
|
+
|
|
407
|
+
The scaffold is reference, so keep the infrastructure the app actually
|
|
408
|
+
USES and delete the rest, both files AND their folders. No persistence
|
|
409
|
+
means delete `db/`, `drizzle.config.ts`, and the `db:*` scripts. No UI
|
|
410
|
+
kit used means delete `components/ui/`, `components.json`, and
|
|
411
|
+
`lib/utils/cn.ts`. No PWA means delete `public/sw.js` and `offline.html`.
|
|
412
|
+
Always KEEP the durable knowledge (`AGENTS.md`, `CONVENTIONS.md`, the
|
|
413
|
+
per-agent rule files, the MCP wiring), and never prune it, so removing
|
|
414
|
+
example code never removes your context. Prune AFTER you have used the
|
|
415
|
+
features and examples as reference, never blindly up front. This is a no-op for the
|
|
416
|
+
`api` template, which ships no UI kit and no PWA files.
|
|
417
|
+
|
|
355
418
|
---
|
|
356
419
|
|
|
357
420
|
## Sensible defaults
|
|
@@ -401,7 +464,7 @@ modules/
|
|
|
401
464
|
- One exported function per server action/query file
|
|
402
465
|
- Server actions need BOTH the `.server.{js,ts}` extension AND a `'use server'` directive at the top. Extension alone marks a server-only utility (source-protected, not RPC-callable). Directive alone is a lint violation (`use-server-needs-extension`).
|
|
403
466
|
- Components must call `Class.register('tag')`
|
|
404
|
-
- **Server-only code goes in `.server.{js,ts}` files, `route.ts` handlers, or `middleware.ts`. Never in pages, layouts, or components.** Direct imports of a DB driver (`pg`) or `node:*` from pages, layouts, or components crash the browser at module load. Wrap in a `.server.{js,ts}` file; the framework rewrites that import to an RPC stub on the browser side. The DB lives in `db/*.server.ts`; `lib/` holds other server-only infra and browser-safe utilities (`lib/utils/cn.ts` with `cn`); the convention is "if a `lib/` file needs Node APIs, only import it from server-only files."
|
|
467
|
+
- **Server-only code goes in `.server.{js,ts}` files, `route.ts` handlers, or `middleware.ts`. Never in pages, layouts, or components.** Direct imports of a DB driver (`pg`) or `node:*` from pages, layouts, or components crash the browser at module load. Wrap in a `.server.{js,ts}` file; the framework rewrites that import to an RPC stub on the browser side. The DB lives in `db/*.server.ts`; `lib/` holds other server-only infra and browser-safe utilities (`lib/utils/cn.ts` with `cn`); the convention is "if a `lib/` file needs Node APIs, only import it from server-only files." A TYPE-ONLY `import type { Todo } from '#db/schema.server.ts'` is the exception, fine in a page or component because the stripper erases it before it reaches the browser.
|
|
405
468
|
- Routes (`app/**/page.ts`, `app/**/route.ts`) must be thin: import logic from modules
|
|
406
469
|
- **Fetch server data in the component that needs it, with an `async render()`, not by prop-drilling.** A leaf component can write `const u = await getUser(this.uid)` directly in `render()`; SSR awaits it so the data is in the first paint, and the client uses stale-while-revalidate on a re-fetch. Reach for `renderFallback()` only to show a re-fetch loading state, and `Task` / signals only for genuinely client-only data (a `Task` shows its pending state at SSR, losing first-paint data). Do not put `await getData()` in a page / layout when a leaf component can own it (page fetches run sequentially, a route-level waterfall).
|
|
407
470
|
- **Keep pages and layouts as pure carriers, so their modules stay out of the network tab.** A page/layout never hydrates; the framework drops its module from the browser as long as its only browser-relevant job is registering the components it imports. It starts shipping its own module (invisible in tests) the moment its closure does any OTHER client work. So do not give a page/layout module-scope client work (a top-level call, a `window` / `document` / `customElements` access, a bare side-effect import, or a `@webjsdev/core/client-router` import: routing is automatic), and do not import a client-global-touching non-component util into it. Put client behaviour in a component, server-only code in `.server.{js,ts}`. Self-check: `page.ts` / `layout.ts` should not appear in the browser's network tab.
|
|
@@ -664,7 +727,7 @@ export class MyWidget extends WebComponent({
|
|
|
664
727
|
render() {
|
|
665
728
|
return html`
|
|
666
729
|
<div class="p-4 border border-border rounded-lg">
|
|
667
|
-
<p class="font-serif text-
|
|
730
|
+
<p class="font-serif text-foreground">${this.label}: ${this.count}</p>
|
|
668
731
|
</div>
|
|
669
732
|
`;
|
|
670
733
|
}
|
|
@@ -724,8 +787,27 @@ Both hydrate without flash on the client.
|
|
|
724
787
|
The scaffold ships with the **Tailwind CSS browser runtime** + `@theme`
|
|
725
788
|
design tokens defined in the root layout. Every colour, font family,
|
|
726
789
|
fluid type scale value, and motion duration is declared once in `@theme`
|
|
727
|
-
and available everywhere via utility classes (`text-
|
|
728
|
-
`font-serif`, `duration-fast`, `text-display`).
|
|
790
|
+
and available everywhere via utility classes (`text-foreground`,
|
|
791
|
+
`bg-card`, `font-serif`, `duration-fast`, `text-display`).
|
|
792
|
+
|
|
793
|
+
**One theme, canonical tokens.** The app has a SINGLE theme, defined
|
|
794
|
+
once in `app/layout.ts` using the standard `@webjsdev/ui`
|
|
795
|
+
(shadcn-compatible) semantic tokens set to the brand palette. Use the
|
|
796
|
+
canonical utility names everywhere, in the page chrome AND inside
|
|
797
|
+
components: `bg-background`, `text-foreground`, `bg-card`, `bg-muted`,
|
|
798
|
+
`text-muted-foreground`, `bg-primary`, `text-primary-foreground`,
|
|
799
|
+
`bg-accent`, `text-accent-foreground`, `border-border`, `ring-ring`.
|
|
800
|
+
These are exactly the tokens a component copied in by
|
|
801
|
+
`webjs ui add <name>` reads, so a scaffolded page and a later-added ui
|
|
802
|
+
component share one coherent theme with no extra wiring. **Never invent a
|
|
803
|
+
parallel token vocabulary** (`--fg`, `--bg`, `text-fg`, `bg-elev`, a
|
|
804
|
+
separate `--brand`): it collides with the ui tokens (the accent once
|
|
805
|
+
flipped to neutral on navigation for exactly this reason) and diverges
|
|
806
|
+
from the shadcn conventions the kit and AI agents both expect. Reach for
|
|
807
|
+
opacity modifiers (`bg-primary/10`, `hover:bg-primary/90`,
|
|
808
|
+
`text-muted-foreground/70`) before adding a token; to ADD one, do it the
|
|
809
|
+
canonical way (a `--x` variable in the `:root` / `.dark` blocks plus a
|
|
810
|
+
`--color-x: var(--x)` line in `@theme inline`, then `bg-x` / `text-x`).
|
|
729
811
|
|
|
730
812
|
**Tailwind-first is the strong default for pages AND light-DOM
|
|
731
813
|
components (the default DOM mode).** Use utilities for layout, spacing,
|
|
@@ -998,6 +1080,12 @@ toggle, the tab switch) requires JS.
|
|
|
998
1080
|
- **Don't gate read-paths on hydration.** Never write components whose
|
|
999
1081
|
SSR'd HTML is empty or wrong on purpose with the expectation that
|
|
1000
1082
|
JS will fill it in. The first paint must be the right content.
|
|
1083
|
+
- **Label every interactive control.** Give each control an accessible
|
|
1084
|
+
name, and make clickable text a `<label for="control-id">` (or the
|
|
1085
|
+
control itself) so a text click activates the control on BOTH the JS
|
|
1086
|
+
path and the no-JS form-submit path. Use `aria-label` and
|
|
1087
|
+
`aria-pressed` on icon-only controls. `assertNoA11yViolations(el)` in a
|
|
1088
|
+
browser test (see the Testing section) catches missing labels.
|
|
1001
1089
|
|
|
1002
1090
|
**SSR-meaningful component state.** The SSR pipeline constructs the
|
|
1003
1091
|
component, applies its attributes, runs `willUpdate` and controllers'
|
|
@@ -1046,6 +1134,16 @@ Where the data lives, where to read it:
|
|
|
1046
1134
|
|
|
1047
1135
|
<!-- OVERRIDE -->
|
|
1048
1136
|
|
|
1137
|
+
**The `.server.ts` vs `'use server'` decision, in one question.** Will the
|
|
1138
|
+
client call it? Add `'use server'` and the file becomes an RPC action
|
|
1139
|
+
(the browser import is rewritten to a typed stub). Is it server-only
|
|
1140
|
+
infra instead (a DB driver, secrets, `node:*`)? Use NO directive, and
|
|
1141
|
+
never import it into a page, layout, or component. Reach it from a
|
|
1142
|
+
`'use server'` action, a `route.ts` handler, or `middleware.ts`. A
|
|
1143
|
+
`.server.ts` file WITHOUT the directive is a server-only utility whose
|
|
1144
|
+
browser import throws at module load, so a page/component that imports it
|
|
1145
|
+
directly crashes on the client.
|
|
1146
|
+
|
|
1049
1147
|
```ts
|
|
1050
1148
|
// modules/posts/actions/create-post.server.ts
|
|
1051
1149
|
'use server';
|
|
@@ -1070,6 +1168,47 @@ export async function createPost(input: {
|
|
|
1070
1168
|
|
|
1071
1169
|
---
|
|
1072
1170
|
|
|
1171
|
+
## Mutations: default to optimistic UI
|
|
1172
|
+
|
|
1173
|
+
<!-- OVERRIDE -->
|
|
1174
|
+
|
|
1175
|
+
Default to optimistic UI for every feasible mutation. Use `optimistic()`
|
|
1176
|
+
from `@webjsdev/core` (create, toggle, like, follow, reorder, rename,
|
|
1177
|
+
status change) so the UI updates instantly and rolls back automatically
|
|
1178
|
+
on failure. No hand-written try-catch, cache-and-restore, or temp-id
|
|
1179
|
+
reconciliation.
|
|
1180
|
+
|
|
1181
|
+
```ts
|
|
1182
|
+
import { WebComponent, prop, optimistic, html } from '@webjsdev/core';
|
|
1183
|
+
import { createTodo } from '#modules/todos/actions/create-todo.server.ts';
|
|
1184
|
+
|
|
1185
|
+
class TodoList extends WebComponent({ todos: prop<Todo[]>(Array) }) {
|
|
1186
|
+
private optimisticTodos = optimistic(this, {
|
|
1187
|
+
source: () => this.todos,
|
|
1188
|
+
update: (state, title: string) => [...state, { title, pending: true }],
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
async handleSubmit(title: string) {
|
|
1192
|
+
const promise = createTodo({ title });
|
|
1193
|
+
this.optimisticTodos.add(title, promise); // auto-releases on settle
|
|
1194
|
+
await promise;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
render() {
|
|
1198
|
+
return html`<ul>${this.optimisticTodos.value.map(t => html`
|
|
1199
|
+
<li class=${t.pending ? 'opacity-50' : ''}>${t.title}</li>`)}</ul>`;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
Do NOT reach for optimistic UI where it hurts: unpredictable or
|
|
1205
|
+
server-computed results (AI output, server-assigned values the client
|
|
1206
|
+
cannot guess), side-effectful mutations the user must wait on (payment,
|
|
1207
|
+
email, OAuth), and destructive irreversible actions (a confirm-first UX
|
|
1208
|
+
is better). See `agent-docs/advanced.md` for the full API.
|
|
1209
|
+
|
|
1210
|
+
---
|
|
1211
|
+
|
|
1073
1212
|
## Code style
|
|
1074
1213
|
|
|
1075
1214
|
<!-- OVERRIDE -->
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// webjs-scaffold-placeholder. Example gallery route. Keep and adapt it, or prune it (delete this app/examples/todo route, modules/todo, AND the todos table in db/schema.server.ts), then delete this marker line. webjs check fails while the marker remains.
|
|
2
|
+
// A THIN route adapter: app/ is routing only. It fetches the initial data
|
|
3
|
+
// (server-side) via the 'use server' query and renders the interactive
|
|
4
|
+
// component, plus a page `action` for the no-JS write path. All the real logic
|
|
5
|
+
// lives in modules/todo/. This is the idiomatic app-thin + modules-logic split.
|
|
6
|
+
import { html } from '@webjsdev/core';
|
|
7
|
+
import type { Metadata } from '@webjsdev/core'; // Metadata is a @webjsdev/core type
|
|
8
|
+
import { listTodos } from '#modules/todo/queries/list-todos.server.ts';
|
|
9
|
+
import { createTodo } from '#modules/todo/actions/create-todo.server.ts';
|
|
10
|
+
import { toggleTodo } from '#modules/todo/actions/toggle-todo.server.ts';
|
|
11
|
+
import { deleteTodo } from '#modules/todo/actions/delete-todo.server.ts';
|
|
12
|
+
import '#modules/todo/components/todo-app.ts';
|
|
13
|
+
|
|
14
|
+
export const metadata: Metadata = { title: 'Todo (optimistic UI) | examples' };
|
|
15
|
+
|
|
16
|
+
export default async function TodoExample() {
|
|
17
|
+
// SSR-fetched and seeded, so <todo-app> paints the real list on first byte.
|
|
18
|
+
const todos = await listTodos();
|
|
19
|
+
return html`
|
|
20
|
+
<h1 class="text-h2 font-bold mb-4">Optimistic todo</h1>
|
|
21
|
+
<todo-app .todos=${todos}></todo-app>
|
|
22
|
+
`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// No-JS write path: the component's <form>s post here; with JS the component
|
|
26
|
+
// intercepts and mutates optimistically instead. Success is a 303 PRG.
|
|
27
|
+
export async function action({ formData }: { formData: FormData }) {
|
|
28
|
+
const intent = String(formData.get('intent') ?? '');
|
|
29
|
+
const id = String(formData.get('id') ?? '');
|
|
30
|
+
if (intent === 'create') return createTodo({ title: String(formData.get('title') ?? '') });
|
|
31
|
+
if (intent === 'toggle') return toggleTodo({ id });
|
|
32
|
+
if (intent === 'delete') return deleteTodo({ id });
|
|
33
|
+
return { success: false as const, error: 'Unknown action.', status: 400 };
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/async-render route AND modules/async-render), then delete this marker line. webjs check fails while the marker remains.
|
|
2
|
+
import { html } from '@webjsdev/core';
|
|
3
|
+
import type { Metadata } from '@webjsdev/core';
|
|
4
|
+
import '#modules/async-render/components/server-clock.ts';
|
|
5
|
+
|
|
6
|
+
export const metadata: Metadata = { title: 'Async render (server data in first paint) | features' };
|
|
7
|
+
|
|
8
|
+
export default function AsyncRenderExample() {
|
|
9
|
+
return html`
|
|
10
|
+
<h1 class="text-h2 font-bold mb-4">Async render</h1>
|
|
11
|
+
<p class="text-muted-foreground mb-4">A component's <code>async render()</code> awaits server data. SSR blocks, so the resolved value is in the first paint (no fallback, readable with JS off).</p>
|
|
12
|
+
<server-clock></server-clock>
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The WebSocket endpoint for the broadcast demo. Unlike the echo endpoint, this
|
|
2
|
+
// fans each incoming message out to EVERY client on the path with broadcast()
|
|
3
|
+
// from '@webjsdev/server'. The framework auto-registers each connection to its
|
|
4
|
+
// route path, so broadcast('/features/broadcast/feed', ...) reaches all of them.
|
|
5
|
+
import { broadcast } from '@webjsdev/server';
|
|
6
|
+
|
|
7
|
+
// Structural type for the socket, so the demo needs no `@types/ws` dependency.
|
|
8
|
+
type WSLike = {
|
|
9
|
+
on(event: 'message' | 'close', cb: (data: Buffer) => void): void;
|
|
10
|
+
send(msg: string): void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function WS(ws: WSLike) {
|
|
14
|
+
ws.on('message', (data) => {
|
|
15
|
+
// Fan out to every connected client (the sender included, so all open tabs
|
|
16
|
+
// stay in sync). Pass { except: ws } if you want to skip the sender.
|
|
17
|
+
broadcast('/features/broadcast/feed', data.toString());
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/broadcast route AND modules/broadcast), then delete this marker line. webjs check fails while the marker remains.
|
|
2
|
+
// Broadcast: fan a message out to EVERY client connected to a WebSocket path,
|
|
3
|
+
// not just the sender. The framework auto-registers each connection to its path,
|
|
4
|
+
// so broadcast(path, data) from '@webjsdev/server' reaches all of them. This is
|
|
5
|
+
// the difference from the plain websockets demo (which echoes to one socket).
|
|
6
|
+
// Open this page in two browser tabs and send: both see every message.
|
|
7
|
+
import { html } from '@webjsdev/core';
|
|
8
|
+
import type { Metadata } from '@webjsdev/core';
|
|
9
|
+
import '#modules/broadcast/components/broadcast-feed.ts';
|
|
10
|
+
|
|
11
|
+
export const metadata: Metadata = { title: 'Broadcast (fan-out to all clients) | features' };
|
|
12
|
+
|
|
13
|
+
export default function BroadcastExample() {
|
|
14
|
+
return html`
|
|
15
|
+
<h1 class="text-h2 font-bold mb-4">Broadcast</h1>
|
|
16
|
+
<p class="text-muted-foreground mb-4">
|
|
17
|
+
Every message is fanned out to all connected clients via
|
|
18
|
+
<code class="font-mono">broadcast()</code>. Open this page in a second tab
|
|
19
|
+
and watch messages appear in both. Single-instance by default; wire Redis
|
|
20
|
+
to scale across processes.
|
|
21
|
+
</p>
|
|
22
|
+
<broadcast-feed></broadcast-feed>
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/caching route), then delete this marker line. webjs check fails while the marker remains.
|
|
2
|
+
// Caching: `export const revalidate = N` opts the page into the server HTML
|
|
3
|
+
// response cache, keyed by URL for N seconds. The rendered timestamp below only
|
|
4
|
+
// changes once per window: reload inside 10s and it is identical, reload after
|
|
5
|
+
// and it refreshes. SAFETY: only cache a page that is identical for every
|
|
6
|
+
// visitor (no cookies(), no session, no per-user data), since the key is the URL
|
|
7
|
+
// alone. For per-query reads use cache() + tags with revalidateTag; for assets
|
|
8
|
+
// use HTTP Cache-Control + ETag (conditional GET).
|
|
9
|
+
import { html } from '@webjsdev/core';
|
|
10
|
+
import type { Metadata } from '@webjsdev/core';
|
|
11
|
+
|
|
12
|
+
export const metadata: Metadata = { title: 'Caching (revalidate) | features' };
|
|
13
|
+
|
|
14
|
+
// Cache this page's SSR HTML for 10 seconds.
|
|
15
|
+
export const revalidate = 10;
|
|
16
|
+
|
|
17
|
+
export default function CachingExample() {
|
|
18
|
+
// Runs at render time, then the whole response is cached for `revalidate`
|
|
19
|
+
// seconds, so this value is frozen until the window elapses.
|
|
20
|
+
const renderedAt = new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
21
|
+
return html`
|
|
22
|
+
<h1 class="text-h2 font-bold mb-4">Caching</h1>
|
|
23
|
+
<p class="text-muted-foreground mb-4">
|
|
24
|
+
This page sets <code>export const revalidate = 10</code>, so its
|
|
25
|
+
server-rendered HTML is cached per URL for ten seconds.
|
|
26
|
+
</p>
|
|
27
|
+
<p class="mb-4">
|
|
28
|
+
Rendered at
|
|
29
|
+
<code class="font-mono text-primary">${renderedAt}</code>.
|
|
30
|
+
Reload within 10s and this is unchanged; after 10s it re-renders.
|
|
31
|
+
</p>
|
|
32
|
+
<p class="text-muted-foreground text-sm">
|
|
33
|
+
Only for pages identical for every visitor. For per-user or per-query data
|
|
34
|
+
use <code>cache()</code> with <code>tags</code> and
|
|
35
|
+
<code>revalidateTag</code>, or a GET action's
|
|
36
|
+
<code>export const cache</code>.
|
|
37
|
+
</p>
|
|
38
|
+
`;
|
|
39
|
+
}
|