@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.
- package/bin/webjs.js +1 -1
- package/lib/api-gallery.js +229 -0
- package/lib/create.js +462 -161
- package/lib/saas-template.js +39 -15
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +78 -1
- package/templates/.claude/hooks/check-server-imports.mjs +86 -0
- package/templates/.claude/hooks/check-server-imports.sh +26 -0
- package/templates/.claude/hooks/cleanup-merged-worktree.sh +129 -0
- package/templates/.claude/hooks/commit-before-stop.sh +52 -0
- package/templates/.claude/settings.json +28 -0
- package/templates/.cursorrules +50 -1
- package/templates/.github/copilot-instructions.md +50 -1
- package/templates/AGENTS.md +180 -13
- package/templates/CLAUDE.md +22 -0
- package/templates/CONVENTIONS.md +165 -12
- 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 +78 -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 +36 -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/browser/counter-card.test.js +36 -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 +25 -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 +6 -0
- package/templates/web-test-runner.config.js +9 -1
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,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. **
|
|
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
|
|
343
|
-
`app/layout.ts
|
|
344
|
-
|
|
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-
|
|
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-
|
|
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
|
|
1120
|
-
"please commit". Commit after completing each
|
|
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">← 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
|
+
}
|