@webjsdev/cli 0.10.30 → 0.10.32

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 (65) hide show
  1. package/bin/webjs.js +1 -1
  2. package/lib/api-gallery.js +229 -0
  3. package/lib/create.js +462 -161
  4. package/lib/saas-template.js +39 -15
  5. package/package.json +1 -1
  6. package/templates/.agents/rules/workflow.md +78 -1
  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/hooks/cleanup-merged-worktree.sh +129 -0
  10. package/templates/.claude/hooks/commit-before-stop.sh +52 -0
  11. package/templates/.claude/settings.json +28 -0
  12. package/templates/.cursorrules +50 -1
  13. package/templates/.github/copilot-instructions.md +50 -1
  14. package/templates/AGENTS.md +180 -13
  15. package/templates/CLAUDE.md +22 -0
  16. package/templates/CONVENTIONS.md +165 -12
  17. package/templates/gallery/app/examples/todo/page.ts +34 -0
  18. package/templates/gallery/app/features/async-render/page.ts +14 -0
  19. package/templates/gallery/app/features/broadcast/feed/route.ts +19 -0
  20. package/templates/gallery/app/features/broadcast/page.ts +24 -0
  21. package/templates/gallery/app/features/caching/page.ts +39 -0
  22. package/templates/gallery/app/features/client-router/page.ts +34 -0
  23. package/templates/gallery/app/features/client-router/second/page.ts +20 -0
  24. package/templates/gallery/app/features/components/page.ts +14 -0
  25. package/templates/gallery/app/features/directives/page.ts +14 -0
  26. package/templates/gallery/app/features/env/page.ts +36 -0
  27. package/templates/gallery/app/features/file-storage/file/[key]/route.ts +19 -0
  28. package/templates/gallery/app/features/file-storage/page.ts +62 -0
  29. package/templates/gallery/app/features/forms/page.ts +78 -0
  30. package/templates/gallery/app/features/metadata/page.ts +55 -0
  31. package/templates/gallery/app/features/optimistic-ui/page.ts +14 -0
  32. package/templates/gallery/app/features/rate-limit/page.ts +29 -0
  33. package/templates/gallery/app/features/rate-limit/ping/middleware.ts +8 -0
  34. package/templates/gallery/app/features/rate-limit/ping/route.ts +7 -0
  35. package/templates/gallery/app/features/route-handler/data/route.ts +8 -0
  36. package/templates/gallery/app/features/route-handler/page.ts +13 -0
  37. package/templates/gallery/app/features/routing/[id]/page.ts +25 -0
  38. package/templates/gallery/app/features/routing/page.ts +47 -0
  39. package/templates/gallery/app/features/server-actions/page.ts +14 -0
  40. package/templates/gallery/app/features/service-worker/page.ts +36 -0
  41. package/templates/gallery/app/features/websockets/echo/route.ts +19 -0
  42. package/templates/gallery/app/features/websockets/page.ts +25 -0
  43. package/templates/gallery/modules/async-render/components/server-clock.ts +26 -0
  44. package/templates/gallery/modules/async-render/queries/server-greeting.server.ts +9 -0
  45. package/templates/gallery/modules/broadcast/components/broadcast-feed.ts +61 -0
  46. package/templates/gallery/modules/components/components/browser/counter-card.test.js +36 -0
  47. package/templates/gallery/modules/components/components/counter-card.ts +35 -0
  48. package/templates/gallery/modules/directives/components/directive-demo.ts +53 -0
  49. package/templates/gallery/modules/file-storage/actions/store-upload.server.ts +18 -0
  50. package/templates/gallery/modules/optimistic-ui/actions/like-post.server.ts +9 -0
  51. package/templates/gallery/modules/optimistic-ui/components/like-button.ts +35 -0
  52. package/templates/gallery/modules/rate-limit/components/rate-probe.ts +49 -0
  53. package/templates/gallery/modules/server-actions/actions/greet.server.ts +12 -0
  54. package/templates/gallery/modules/server-actions/components/greeter.ts +30 -0
  55. package/templates/gallery/modules/server-actions/utils/format.server.ts +8 -0
  56. package/templates/gallery/modules/todo/actions/create-todo.server.ts +16 -0
  57. package/templates/gallery/modules/todo/actions/delete-todo.server.ts +15 -0
  58. package/templates/gallery/modules/todo/actions/toggle-todo.server.ts +21 -0
  59. package/templates/gallery/modules/todo/components/todo-app.ts +140 -0
  60. package/templates/gallery/modules/todo/queries/list-todos.server.ts +25 -0
  61. package/templates/gallery/modules/todo/types.ts +12 -0
  62. package/templates/gallery/modules/websockets/components/ws-echo.ts +62 -0
  63. package/templates/lib/utils/ui.ts +4 -4
  64. package/templates/test/hello/browser/hello.test.js +6 -0
  65. package/templates/web-test-runner.config.js +9 -1
@@ -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,35 @@ 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, and redesign means more than recolor (UI apps).**
377
+ Give the app a design of its own (palette, typography, LAYOUT, spacing,
378
+ and chrome) chosen from what the app IS. Recoloring the scaffold and
379
+ swapping the logo while keeping its skeleton (a fixed top header with a
380
+ Home link and a theme toggle, the centered ~760px reading column, the
381
+ "Built with webjs" footer) is NOT a unique design. Decide from scratch
382
+ whether this app even needs a header or footer, what nav (if any), and
383
+ what layout fits (a centered board, a full-bleed dashboard, a split, a
384
+ single card). The scaffold ships a `webjs-scaffold-placeholder` marker on
385
+ its footer, so `webjs check` fails until you remove or replace the
386
+ "Built with webjs" branding. Self-audit before finishing: nothing should
387
+ read as the scaffold example (no "Built with webjs" footer, no leftover
388
+ example nav, no default reading column unless it truly fits). The design
389
+ tokens and theme wiring in `app/layout.ts` are infrastructure to keep and
390
+ restyle on top of, not the example look to preserve. Style with Tailwind
391
+ utilities wherever they reach, and use custom CSS only for what utilities
392
+ cannot express (@theme tokens, @keyframes, scrollbar, complex color-mix
393
+ or gradients). The `api` template has no UI, so this does not apply there.
394
+ 7. **Keep:** the Drizzle setup, the test config, the agent config files
337
395
  (`AGENTS.md`, `CONVENTIONS.md`, `CLAUDE.md`, `.cursorrules`, etc.),
338
396
  `db/connection.server.ts` + `db/columns.server.ts`, the directory
339
397
  conventions, the design tokens in `app/layout.ts`. These are the
340
398
  infrastructure, not the example app.
341
399
 
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
400
+ This is enforced, not just advised. The example `app/page.ts`,
401
+ `app/layout.ts`, and each `app/features/<name>/page.ts` +
402
+ `app/examples/<name>/page.ts` carry a
403
+ `webjs-scaffold-placeholder` marker comment, and the
404
+ `no-scaffold-placeholder` check fails while any marker remains, so a
345
405
  freshly scaffolded app fails `webjs check` until you address each
346
406
  placeholder. The marker is acknowledge-and-remove: replace the example
347
407
  content, or deliberately keep it, and in either case delete the marker
@@ -352,6 +412,19 @@ The scaffold exists so the agent doesn't reinvent the directory layout,
352
412
  the Drizzle wiring, the test runner config, or the convention files. It
353
413
  does NOT exist so the agent ships the example homepage.
354
414
 
415
+ ### Prune what the app does not use
416
+
417
+ The scaffold is reference, so keep the infrastructure the app actually
418
+ USES and delete the rest, both files AND their folders. No persistence
419
+ means delete `db/`, `drizzle.config.ts`, and the `db:*` scripts. No UI
420
+ kit used means delete `components/ui/`, `components.json`, and
421
+ `lib/utils/cn.ts`. No PWA means delete `public/sw.js` and `offline.html`.
422
+ Always KEEP the durable knowledge (`AGENTS.md`, `CONVENTIONS.md`, the
423
+ per-agent rule files, the MCP wiring), and never prune it, so removing
424
+ example code never removes your context. Prune AFTER you have used the
425
+ features and examples as reference, never blindly up front. This is a no-op for the
426
+ `api` template, which ships no UI kit and no PWA files.
427
+
355
428
  ---
356
429
 
357
430
  ## Sensible defaults
@@ -664,7 +737,7 @@ export class MyWidget extends WebComponent({
664
737
  render() {
665
738
  return html`
666
739
  <div class="p-4 border border-border rounded-lg">
667
- <p class="font-serif text-fg">${this.label}: ${this.count}</p>
740
+ <p class="font-serif text-foreground">${this.label}: ${this.count}</p>
668
741
  </div>
669
742
  `;
670
743
  }
@@ -724,8 +797,27 @@ Both hydrate without flash on the client.
724
797
  The scaffold ships with the **Tailwind CSS browser runtime** + `@theme`
725
798
  design tokens defined in the root layout. Every colour, font family,
726
799
  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`).
800
+ and available everywhere via utility classes (`text-foreground`,
801
+ `bg-card`, `font-serif`, `duration-fast`, `text-display`).
802
+
803
+ **One theme, canonical tokens.** The app has a SINGLE theme, defined
804
+ once in `app/layout.ts` using the standard `@webjsdev/ui`
805
+ (shadcn-compatible) semantic tokens set to the brand palette. Use the
806
+ canonical utility names everywhere, in the page chrome AND inside
807
+ components: `bg-background`, `text-foreground`, `bg-card`, `bg-muted`,
808
+ `text-muted-foreground`, `bg-primary`, `text-primary-foreground`,
809
+ `bg-accent`, `text-accent-foreground`, `border-border`, `ring-ring`.
810
+ These are exactly the tokens a component copied in by
811
+ `webjs ui add <name>` reads, so a scaffolded page and a later-added ui
812
+ component share one coherent theme with no extra wiring. **Never invent a
813
+ parallel token vocabulary** (`--fg`, `--bg`, `text-fg`, `bg-elev`, a
814
+ separate `--brand`): it collides with the ui tokens (the accent once
815
+ flipped to neutral on navigation for exactly this reason) and diverges
816
+ from the shadcn conventions the kit and AI agents both expect. Reach for
817
+ opacity modifiers (`bg-primary/10`, `hover:bg-primary/90`,
818
+ `text-muted-foreground/70`) before adding a token; to ADD one, do it the
819
+ canonical way (a `--x` variable in the `:root` / `.dark` blocks plus a
820
+ `--color-x: var(--x)` line in `@theme inline`, then `bg-x` / `text-x`).
729
821
 
730
822
  **Tailwind-first is the strong default for pages AND light-DOM
731
823
  components (the default DOM mode).** Use utilities for layout, spacing,
@@ -998,6 +1090,12 @@ toggle, the tab switch) requires JS.
998
1090
  - **Don't gate read-paths on hydration.** Never write components whose
999
1091
  SSR'd HTML is empty or wrong on purpose with the expectation that
1000
1092
  JS will fill it in. The first paint must be the right content.
1093
+ - **Label every interactive control.** Give each control an accessible
1094
+ name, and make clickable text a `<label for="control-id">` (or the
1095
+ control itself) so a text click activates the control on BOTH the JS
1096
+ path and the no-JS form-submit path. Use `aria-label` and
1097
+ `aria-pressed` on icon-only controls. `assertNoA11yViolations(el)` in a
1098
+ browser test (see the Testing section) catches missing labels.
1001
1099
 
1002
1100
  **SSR-meaningful component state.** The SSR pipeline constructs the
1003
1101
  component, applies its attributes, runs `willUpdate` and controllers'
@@ -1046,6 +1144,16 @@ Where the data lives, where to read it:
1046
1144
 
1047
1145
  <!-- OVERRIDE -->
1048
1146
 
1147
+ **The `.server.ts` vs `'use server'` decision, in one question.** Will the
1148
+ client call it? Add `'use server'` and the file becomes an RPC action
1149
+ (the browser import is rewritten to a typed stub). Is it server-only
1150
+ infra instead (a DB driver, secrets, `node:*`)? Use NO directive, and
1151
+ never import it into a page, layout, or component. Reach it from a
1152
+ `'use server'` action, a `route.ts` handler, or `middleware.ts`. A
1153
+ `.server.ts` file WITHOUT the directive is a server-only utility whose
1154
+ browser import throws at module load, so a page/component that imports it
1155
+ directly crashes on the client.
1156
+
1049
1157
  ```ts
1050
1158
  // modules/posts/actions/create-post.server.ts
1051
1159
  'use server';
@@ -1070,6 +1178,47 @@ export async function createPost(input: {
1070
1178
 
1071
1179
  ---
1072
1180
 
1181
+ ## Mutations: default to optimistic UI
1182
+
1183
+ <!-- OVERRIDE -->
1184
+
1185
+ Default to optimistic UI for every feasible mutation. Use `optimistic()`
1186
+ from `@webjsdev/core` (create, toggle, like, follow, reorder, rename,
1187
+ status change) so the UI updates instantly and rolls back automatically
1188
+ on failure. No hand-written try-catch, cache-and-restore, or temp-id
1189
+ reconciliation.
1190
+
1191
+ ```ts
1192
+ import { WebComponent, prop, optimistic, html } from '@webjsdev/core';
1193
+ import { createTodo } from '#modules/todos/actions/create-todo.server.ts';
1194
+
1195
+ class TodoList extends WebComponent({ todos: prop<Todo[]>(Array) }) {
1196
+ private optimisticTodos = optimistic(this, {
1197
+ source: () => this.todos,
1198
+ update: (state, title: string) => [...state, { title, pending: true }],
1199
+ });
1200
+
1201
+ async handleSubmit(title: string) {
1202
+ const promise = createTodo({ title });
1203
+ this.optimisticTodos.add(title, promise); // auto-releases on settle
1204
+ await promise;
1205
+ }
1206
+
1207
+ render() {
1208
+ return html`<ul>${this.optimisticTodos.value.map(t => html`
1209
+ <li class=${t.pending ? 'opacity-50' : ''}>${t.title}</li>`)}</ul>`;
1210
+ }
1211
+ }
1212
+ ```
1213
+
1214
+ Do NOT reach for optimistic UI where it hurts: unpredictable or
1215
+ server-computed results (AI output, server-assigned values the client
1216
+ cannot guess), side-effectful mutations the user must wait on (payment,
1217
+ email, OAuth), and destructive irreversible actions (a confirm-first UX
1218
+ is better). See `agent-docs/advanced.md` for the full API.
1219
+
1220
+ ---
1221
+
1073
1222
  ## Code style
1074
1223
 
1075
1224
  <!-- OVERRIDE -->
@@ -1116,8 +1265,12 @@ This project enforces a git workflow via agent-specific config files
1116
1265
  (`Add contact form with email validation`, not `update files`).
1117
1266
  - **NEVER add AI attribution.** No `Co-Authored-By: Claude`, no
1118
1267
  `Generated by AI`, no `AI-assisted` trailers or prefixes.
1119
- - **Committing is automatic.** The user should never have to ask
1120
- "please commit". Commit after completing each logical unit.
1268
+ - **Committing is automatic (this overrides Claude Code's default).** The
1269
+ user should never have to ask "please commit". Commit after completing each
1270
+ logical unit. Claude Code's built-in never-commit default is overridden by
1271
+ `CLAUDE.md`, and the `.claude/hooks/commit-before-stop.sh` Stop hook refuses
1272
+ to end a turn with a pile of uncommitted work on a feature branch (disable
1273
+ with `WEBJS_NO_COMMIT_STOP=1`).
1121
1274
 
1122
1275
  **Branch rules:**
1123
1276
  - **Feature branches.** Never commit directly to main
@@ -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
+ }
@@ -0,0 +1,34 @@
1
+ // webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/client-router route AND its second/ subpage), then delete this marker line. webjs check fails while the marker remains.
2
+ // Client router: automatic. It auto-enables the moment @webjsdev/core loads in
3
+ // the browser (the bundle every component pulls, so any page with a component
4
+ // gets it for free). There is nothing to import. An <a href> to another page
5
+ // does a soft navigation: the framework fetches only the divergent fragment
6
+ // (via the X-Webjs-Have header), swaps it in place, and restores scroll on
7
+ // back/forward. Links prefetch on hover by default. It degrades perfectly: with
8
+ // JS off, every link is a normal full-page navigation.
9
+ import { html } from '@webjsdev/core';
10
+ import type { Metadata } from '@webjsdev/core';
11
+
12
+ export const metadata: Metadata = { title: 'Client router (soft nav) | features' };
13
+
14
+ export default function ClientRouterExample() {
15
+ return html`
16
+ <h1 class="text-h2 font-bold mb-4">Client router</h1>
17
+ <p class="text-muted-foreground mb-4">
18
+ Navigate to the second page and back. With JS on it is a soft swap (no full
19
+ reload, scroll restored); open the network tab to see only a fragment
20
+ fetched, prefetched on hover. With JS off the same links do full-page
21
+ navigations. Nothing was imported to get this.
22
+ </p>
23
+ <div class="flex gap-3 items-center">
24
+ <a href="/features/client-router/second" class="inline-flex items-center px-4 py-2 rounded-xl bg-primary text-primary-foreground font-semibold text-sm no-underline transition-all hover:bg-primary/90 active:scale-[0.97]">Go to page two</a>
25
+ <a href="/" class="text-muted-foreground no-underline font-medium text-sm hover:text-foreground transition-colors">Home</a>
26
+ </div>
27
+ <p class="text-muted-foreground text-sm mt-6">
28
+ Opt out app-wide with <code class="font-mono">{ "webjs": { "clientRouter": false } }</code>,
29
+ or per-link with <code class="font-mono">data-no-router</code> (use it for
30
+ auth flows like <code class="font-mono">/logout</code> that must reset
31
+ in-memory state).
32
+ </p>
33
+ `;
34
+ }
@@ -0,0 +1,20 @@
1
+ // webjs-scaffold-placeholder. Feature gallery route (client-router page two). Pruned together with the parent app/features/client-router route. Delete this marker line once you adapt or remove it. webjs check fails while the marker remains.
2
+ // The soft-navigation target for the client-router demo. A plain page: the
3
+ // router needs no per-page code. The browser Back button restores this page and
4
+ // its scroll position from the client-router snapshot cache.
5
+ import { html } from '@webjsdev/core';
6
+ import type { Metadata } from '@webjsdev/core';
7
+
8
+ export const metadata: Metadata = { title: 'Client router: page two | features' };
9
+
10
+ export default function ClientRouterSecond() {
11
+ return html`
12
+ <h1 class="text-h2 font-bold mb-4">Page two</h1>
13
+ <p class="text-muted-foreground mb-4">
14
+ You arrived here without a full reload. Press the browser Back button (or
15
+ the link below): the previous page and its scroll position are restored
16
+ from the snapshot cache.
17
+ </p>
18
+ <a href="/features/client-router" class="text-primary no-underline font-medium">&larr; Back to page one</a>
19
+ `;
20
+ }
@@ -0,0 +1,14 @@
1
+ // webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/components route AND modules/components), 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/components/components/counter-card.ts';
5
+
6
+ export const metadata: Metadata = { title: 'Components (signals + slots) | features' };
7
+
8
+ export default function ComponentsExample() {
9
+ return html`
10
+ <h1 class="text-h2 font-bold mb-4">Components</h1>
11
+ <p class="text-muted-foreground mb-4">The WebComponent factory, a reactive prop, an instance signal, and a slot.</p>
12
+ <counter-card label="Taps"><strong>A slotted title</strong></counter-card>
13
+ `;
14
+ }
@@ -0,0 +1,14 @@
1
+ // webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/directives route AND modules/directives), 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/directives/components/directive-demo.ts';
5
+
6
+ export const metadata: Metadata = { title: 'Directives (repeat + watch) | features' };
7
+
8
+ export default function DirectivesExample() {
9
+ return html`
10
+ <h1 class="text-h2 font-bold mb-4">Directives</h1>
11
+ <p class="text-muted-foreground mb-4">The lit-html directive set: <code>repeat</code> keys a reordering list so nodes are reused, and <code>watch(signal)</code> swaps one node without a full re-render.</p>
12
+ <directive-demo></directive-demo>
13
+ `;
14
+ }
@@ -0,0 +1,36 @@
1
+ // webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/env route), then delete this marker line. webjs check fails while the marker remains.
2
+ // Environment variables: process.env.X reads are server-only. NODE_ENV is
3
+ // defined on both sides. A name prefixed WEBJS_PUBLIC_ is exposed to the browser
4
+ // through an inline script (no build step); everything else stays server-side
5
+ // so secrets never reach the client. This page reads them during SSR, so the
6
+ // values are in the first paint with no JS. Validate required vars at boot with
7
+ // an app-root env.ts (a schema or a validator fn) that fails fast.
8
+ import { html } from '@webjsdev/core';
9
+ import type { Metadata } from '@webjsdev/core';
10
+
11
+ export const metadata: Metadata = { title: 'Env vars (public vs server) | features' };
12
+
13
+ export default function EnvExample() {
14
+ // Server-only read (this function runs on the server for SSR).
15
+ const nodeEnv = process.env.NODE_ENV || 'development';
16
+ // A WEBJS_PUBLIC_ var is safe to surface to the browser; unset here unless you
17
+ // add WEBJS_PUBLIC_APP_NAME=... to .env, which demonstrates the default.
18
+ const publicName = process.env.WEBJS_PUBLIC_APP_NAME || '(unset, add WEBJS_PUBLIC_APP_NAME to .env)';
19
+ return html`
20
+ <h1 class="text-h2 font-bold mb-4">Environment variables</h1>
21
+ <p class="text-muted-foreground mb-4">
22
+ Read on the server during SSR. Only <code>WEBJS_PUBLIC_</code>-prefixed
23
+ names are exposed to the browser; the rest stay server-side.
24
+ </p>
25
+ <ul class="list-disc pl-5 mb-4 space-y-1">
26
+ <li><code class="font-mono text-sm">NODE_ENV</code> = <span class="text-primary">${nodeEnv}</span> <span class="text-muted-foreground text-sm">(defined both sides)</span></li>
27
+ <li><code class="font-mono text-sm">WEBJS_PUBLIC_APP_NAME</code> = <span class="text-primary">${publicName}</span></li>
28
+ </ul>
29
+ <p class="text-muted-foreground text-sm">
30
+ Never read a secret in a page, layout, or component that ships to the
31
+ browser. Keep secret reads in <code class="font-mono">.server.ts</code>
32
+ files, and validate required vars at boot with
33
+ <code class="font-mono">app/env.ts</code>.
34
+ </p>
35
+ `;
36
+ }
@@ -0,0 +1,19 @@
1
+ // Serves a stored file back by key. getFileStore().get(key) returns the bytes as
2
+ // a web ReadableStream (streamed, never buffered whole into memory) plus the
3
+ // content type recorded at upload. A route.ts is server-only, so importing the
4
+ // storage singleton here is safe. The [key] segment is validated inside the
5
+ // store (traversal-safe), so a crafted key cannot escape the uploads directory.
6
+ import { getFileStore } from '@webjsdev/server';
7
+
8
+ export async function GET(_req: Request, { params }: { params: { key: string } }) {
9
+ const file = await getFileStore().get(params.key);
10
+ if (!file) return new Response('Not found', { status: 404 });
11
+ // file.body is a web ReadableStream at runtime (the diskStore streams the
12
+ // bytes); the store's type is a Node/web union, so narrow it for Response.
13
+ return new Response(file.body as ReadableStream<Uint8Array>, {
14
+ headers: {
15
+ 'content-type': file.contentType,
16
+ 'content-length': String(file.size),
17
+ },
18
+ });
19
+ }
@@ -0,0 +1,62 @@
1
+ // webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/file-storage route AND modules/file-storage), then delete this marker line. webjs check fails while the marker remains.
2
+ // File storage: a no-JS upload. A multipart <form> posts to this page's `action`
3
+ // (the progressive-enhancement write path); the action calls a 'use server'
4
+ // helper that streams the bytes into the FileStore. On success it redirects
5
+ // (PRG) with the new key in the query, and the page renders a download link that
6
+ // streams the file back through file/[key]/route.ts. Works with JS off; the
7
+ // client router applies the same flow in place with JS on.
8
+ import { html } from '@webjsdev/core';
9
+ import type { Metadata } from '@webjsdev/core';
10
+ import { storeUpload } from '#modules/file-storage/actions/store-upload.server.ts';
11
+
12
+ export const metadata: Metadata = { title: 'File storage (upload + serve) | features' };
13
+
14
+ export async function action({ formData }: { formData: FormData }) {
15
+ const file = formData.get('file');
16
+ if (!(file instanceof File) || file.size === 0) {
17
+ return { success: false, error: 'Choose a file to upload.' };
18
+ }
19
+ const result = await storeUpload(file);
20
+ if (!result.success) return result;
21
+ const { key, name, size } = result.data;
22
+ const q = new URLSearchParams({ key, name, size: String(size) });
23
+ return { success: true, redirect: '/features/file-storage?' + q.toString() };
24
+ }
25
+
26
+ export default function FileStorageExample({
27
+ searchParams,
28
+ actionData,
29
+ }: {
30
+ searchParams: Record<string, string | undefined>;
31
+ actionData?: { error?: string };
32
+ }) {
33
+ const key = (searchParams.key || '').trim();
34
+ const name = (searchParams.name || '').trim();
35
+ const size = (searchParams.size || '').trim();
36
+ return html`
37
+ <h1 class="text-h2 font-bold mb-4">File storage</h1>
38
+ <p class="text-muted-foreground mb-4">
39
+ Upload a file: the bytes stream into the FileStore (a local
40
+ <code class="font-mono">.webjs/uploads</code> directory by default,
41
+ gitignored). Swap the backend for S3/R2 with one
42
+ <code class="font-mono">setFileStore()</code> call, no call-site change.
43
+ </p>
44
+ <form method="post" enctype="multipart/form-data" class="flex flex-wrap gap-3 items-center mb-4">
45
+ <input type="file" name="file" required
46
+ class="text-sm text-muted-foreground file:mr-3 file:px-3.5 file:py-2 file:rounded-xl file:border-0 file:bg-card file:border file:border-border file:text-foreground file:text-sm file:cursor-pointer" />
47
+ <button type="submit"
48
+ class="px-4 py-2 rounded-xl bg-primary text-primary-foreground font-semibold text-sm border-0 cursor-pointer transition-all hover:bg-primary/90 active:scale-[0.97]">Upload</button>
49
+ </form>
50
+ ${actionData?.error
51
+ ? html`<p class="text-destructive text-sm mb-4">${actionData.error}</p>`
52
+ : ''}
53
+ ${key
54
+ ? html`
55
+ <div class="px-4 py-3 rounded-xl bg-card border border-border text-sm">
56
+ Stored <span class="text-foreground font-medium">${name}</span>
57
+ <span class="text-muted-foreground/70">(${size} bytes)</span>
58
+ <a class="text-primary no-underline ml-2" href="/features/file-storage/file/${key}">download</a>
59
+ </div>`
60
+ : ''}
61
+ `;
62
+ }