@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.
Files changed (63) hide show
  1. package/bin/webjs.js +1 -1
  2. package/lib/api-gallery.js +229 -0
  3. package/lib/create.js +452 -158
  4. package/lib/saas-template.js +39 -15
  5. package/package.json +1 -1
  6. package/templates/.agents/rules/workflow.md +70 -2
  7. package/templates/.claude/hooks/check-server-imports.mjs +86 -0
  8. package/templates/.claude/hooks/check-server-imports.sh +26 -0
  9. package/templates/.claude/settings.json +9 -0
  10. package/templates/.cursorrules +41 -2
  11. package/templates/.github/copilot-instructions.md +41 -2
  12. package/templates/AGENTS.md +160 -10
  13. package/templates/CONVENTIONS.md +150 -11
  14. package/templates/gallery/app/examples/todo/page.ts +34 -0
  15. package/templates/gallery/app/features/async-render/page.ts +14 -0
  16. package/templates/gallery/app/features/broadcast/feed/route.ts +19 -0
  17. package/templates/gallery/app/features/broadcast/page.ts +24 -0
  18. package/templates/gallery/app/features/caching/page.ts +39 -0
  19. package/templates/gallery/app/features/client-router/page.ts +34 -0
  20. package/templates/gallery/app/features/client-router/second/page.ts +20 -0
  21. package/templates/gallery/app/features/components/page.ts +14 -0
  22. package/templates/gallery/app/features/directives/page.ts +14 -0
  23. package/templates/gallery/app/features/env/page.ts +36 -0
  24. package/templates/gallery/app/features/file-storage/file/[key]/route.ts +19 -0
  25. package/templates/gallery/app/features/file-storage/page.ts +62 -0
  26. package/templates/gallery/app/features/forms/page.ts +72 -0
  27. package/templates/gallery/app/features/metadata/page.ts +55 -0
  28. package/templates/gallery/app/features/optimistic-ui/page.ts +14 -0
  29. package/templates/gallery/app/features/rate-limit/page.ts +29 -0
  30. package/templates/gallery/app/features/rate-limit/ping/middleware.ts +8 -0
  31. package/templates/gallery/app/features/rate-limit/ping/route.ts +7 -0
  32. package/templates/gallery/app/features/route-handler/data/route.ts +8 -0
  33. package/templates/gallery/app/features/route-handler/page.ts +13 -0
  34. package/templates/gallery/app/features/routing/[id]/page.ts +25 -0
  35. package/templates/gallery/app/features/routing/page.ts +47 -0
  36. package/templates/gallery/app/features/server-actions/page.ts +14 -0
  37. package/templates/gallery/app/features/service-worker/page.ts +35 -0
  38. package/templates/gallery/app/features/websockets/echo/route.ts +19 -0
  39. package/templates/gallery/app/features/websockets/page.ts +25 -0
  40. package/templates/gallery/modules/async-render/components/server-clock.ts +26 -0
  41. package/templates/gallery/modules/async-render/queries/server-greeting.server.ts +9 -0
  42. package/templates/gallery/modules/broadcast/components/broadcast-feed.ts +61 -0
  43. package/templates/gallery/modules/components/components/counter-card.ts +35 -0
  44. package/templates/gallery/modules/directives/components/directive-demo.ts +53 -0
  45. package/templates/gallery/modules/file-storage/actions/store-upload.server.ts +18 -0
  46. package/templates/gallery/modules/optimistic-ui/actions/like-post.server.ts +9 -0
  47. package/templates/gallery/modules/optimistic-ui/components/like-button.ts +35 -0
  48. package/templates/gallery/modules/rate-limit/components/rate-probe.ts +49 -0
  49. package/templates/gallery/modules/server-actions/actions/greet.server.ts +12 -0
  50. package/templates/gallery/modules/server-actions/components/greeter.ts +30 -0
  51. package/templates/gallery/modules/server-actions/utils/format.server.ts +8 -0
  52. package/templates/gallery/modules/todo/actions/create-todo.server.ts +16 -0
  53. package/templates/gallery/modules/todo/actions/delete-todo.server.ts +15 -0
  54. package/templates/gallery/modules/todo/actions/toggle-todo.server.ts +21 -0
  55. package/templates/gallery/modules/todo/components/todo-app.ts +140 -0
  56. package/templates/gallery/modules/todo/queries/list-todos.server.ts +19 -0
  57. package/templates/gallery/modules/todo/types.ts +12 -0
  58. package/templates/gallery/modules/websockets/components/ws-echo.ts +62 -0
  59. package/templates/lib/utils/ui.ts +4 -4
  60. package/templates/test/hello/browser/hello.test.js +12 -6
  61. package/templates/test/hello/e2e/hello.test.ts +11 -9
  62. package/templates/test/hello/hello.test.ts +4 -6
  63. package/templates/web-test-runner.config.js +84 -9
@@ -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.com**. Treat this file as the app-scoped
8
- companion and reach for docs.webjs.com whenever you need more detail.
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. This is ENFORCED:
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.com → Editor setup](https://docs.webjs.com/docs/editor-setup)
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
- page.ts → /
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.com Deployment → CSP](https://docs.webjs.com/docs/deployment#csp).
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.com/docs/configuration).
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.com/docs/server-actions
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
@@ -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` "Hello from …" homepage, the example `User`
314
- model, the `theme-toggle` component, the example users module in api /
315
- saas templates) is a **starting point**.
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. **Keep:** the Drizzle setup, the test config, the agent config files
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` and
343
- `app/layout.ts` carry a `webjs-scaffold-placeholder` marker comment, and
344
- the `no-scaffold-placeholder` check fails while any marker remains, so a
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-fg">${this.label}: ${this.count}</p>
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-fg`, `bg-bg-elev`,
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
+ }