@webjsdev/cli 0.10.29 → 0.10.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/webjs.js +1 -1
- package/lib/api-gallery.js +229 -0
- package/lib/create.js +452 -158
- package/lib/saas-template.js +39 -15
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +70 -2
- package/templates/.claude/hooks/check-server-imports.mjs +86 -0
- package/templates/.claude/hooks/check-server-imports.sh +26 -0
- package/templates/.claude/settings.json +9 -0
- package/templates/.cursorrules +41 -2
- package/templates/.github/copilot-instructions.md +41 -2
- package/templates/AGENTS.md +160 -10
- package/templates/CONVENTIONS.md +150 -11
- package/templates/gallery/app/examples/todo/page.ts +34 -0
- package/templates/gallery/app/features/async-render/page.ts +14 -0
- package/templates/gallery/app/features/broadcast/feed/route.ts +19 -0
- package/templates/gallery/app/features/broadcast/page.ts +24 -0
- package/templates/gallery/app/features/caching/page.ts +39 -0
- package/templates/gallery/app/features/client-router/page.ts +34 -0
- package/templates/gallery/app/features/client-router/second/page.ts +20 -0
- package/templates/gallery/app/features/components/page.ts +14 -0
- package/templates/gallery/app/features/directives/page.ts +14 -0
- package/templates/gallery/app/features/env/page.ts +36 -0
- package/templates/gallery/app/features/file-storage/file/[key]/route.ts +19 -0
- package/templates/gallery/app/features/file-storage/page.ts +62 -0
- package/templates/gallery/app/features/forms/page.ts +72 -0
- package/templates/gallery/app/features/metadata/page.ts +55 -0
- package/templates/gallery/app/features/optimistic-ui/page.ts +14 -0
- package/templates/gallery/app/features/rate-limit/page.ts +29 -0
- package/templates/gallery/app/features/rate-limit/ping/middleware.ts +8 -0
- package/templates/gallery/app/features/rate-limit/ping/route.ts +7 -0
- package/templates/gallery/app/features/route-handler/data/route.ts +8 -0
- package/templates/gallery/app/features/route-handler/page.ts +13 -0
- package/templates/gallery/app/features/routing/[id]/page.ts +25 -0
- package/templates/gallery/app/features/routing/page.ts +47 -0
- package/templates/gallery/app/features/server-actions/page.ts +14 -0
- package/templates/gallery/app/features/service-worker/page.ts +35 -0
- package/templates/gallery/app/features/websockets/echo/route.ts +19 -0
- package/templates/gallery/app/features/websockets/page.ts +25 -0
- package/templates/gallery/modules/async-render/components/server-clock.ts +26 -0
- package/templates/gallery/modules/async-render/queries/server-greeting.server.ts +9 -0
- package/templates/gallery/modules/broadcast/components/broadcast-feed.ts +61 -0
- package/templates/gallery/modules/components/components/counter-card.ts +35 -0
- package/templates/gallery/modules/directives/components/directive-demo.ts +53 -0
- package/templates/gallery/modules/file-storage/actions/store-upload.server.ts +18 -0
- package/templates/gallery/modules/optimistic-ui/actions/like-post.server.ts +9 -0
- package/templates/gallery/modules/optimistic-ui/components/like-button.ts +35 -0
- package/templates/gallery/modules/rate-limit/components/rate-probe.ts +49 -0
- package/templates/gallery/modules/server-actions/actions/greet.server.ts +12 -0
- package/templates/gallery/modules/server-actions/components/greeter.ts +30 -0
- package/templates/gallery/modules/server-actions/utils/format.server.ts +8 -0
- package/templates/gallery/modules/todo/actions/create-todo.server.ts +16 -0
- package/templates/gallery/modules/todo/actions/delete-todo.server.ts +15 -0
- package/templates/gallery/modules/todo/actions/toggle-todo.server.ts +21 -0
- package/templates/gallery/modules/todo/components/todo-app.ts +140 -0
- package/templates/gallery/modules/todo/queries/list-todos.server.ts +19 -0
- package/templates/gallery/modules/todo/types.ts +12 -0
- package/templates/gallery/modules/websockets/components/ws-echo.ts +62 -0
- package/templates/lib/utils/ui.ts +4 -4
- package/templates/test/hello/browser/hello.test.js +12 -6
- package/templates/test/hello/e2e/hello.test.ts +11 -9
- package/templates/test/hello/hello.test.ts +4 -6
- package/templates/web-test-runner.config.js +84 -9
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/forms route), then delete this marker line. webjs check fails while the marker remains.
|
|
2
|
+
// forms: the no-JS write path. A real <form method="post"> posts to this page's
|
|
3
|
+
// `action` export, and the framework re-renders the SAME page with the result on
|
|
4
|
+
// `actionData`. WHY it matters: the form works with JS OFF (server round-trip),
|
|
5
|
+
// and with JS the client router applies the response in place (no full reload).
|
|
6
|
+
// Never reach for fetch() + a click handler where a <form> + page action does.
|
|
7
|
+
// On failure the framework re-renders at 422 with the result; on success it
|
|
8
|
+
// does a 303 Post-Redirect-Get, so we redirect to ?sent=1 to show a confirmation.
|
|
9
|
+
import { html } from '@webjsdev/core';
|
|
10
|
+
import type { Metadata } from '@webjsdev/core';
|
|
11
|
+
|
|
12
|
+
export const metadata: Metadata = { title: 'Forms (no-JS PE) | features' };
|
|
13
|
+
|
|
14
|
+
interface Result {
|
|
15
|
+
success: boolean;
|
|
16
|
+
fieldErrors?: Record<string, string>;
|
|
17
|
+
values?: Record<string, string>;
|
|
18
|
+
redirect?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const field = (label: string, name: string, input: unknown, error?: string) => html`
|
|
22
|
+
<div class="grid gap-1.5">
|
|
23
|
+
<label for=${name} class="text-[13px] font-medium text-muted-foreground">${label}</label>
|
|
24
|
+
${input}
|
|
25
|
+
${error ? html`<p class="m-0 text-[12.5px] text-destructive">${error}</p>` : ''}
|
|
26
|
+
</div>
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const inputCls = 'w-full bg-background border border-border rounded-xl px-3 py-2 text-[15px] text-foreground outline-none transition-colors focus:border-primary placeholder:text-muted-foreground/70';
|
|
30
|
+
|
|
31
|
+
export default function FormsFeature({ searchParams, actionData }: { searchParams: Record<string, string | undefined>; actionData?: Result }) {
|
|
32
|
+
if (searchParams.sent) {
|
|
33
|
+
return html`
|
|
34
|
+
<h1 class="text-h2 font-bold mb-4">Forms</h1>
|
|
35
|
+
<div class="max-w-[460px] grid gap-3 p-6 rounded-2xl bg-card border border-border text-center">
|
|
36
|
+
<span class="mx-auto grid place-items-center w-12 h-12 rounded-2xl bg-primary/15 text-primary">
|
|
37
|
+
<svg viewBox="0 0 24 24" class="w-6 h-6 stroke-current fill-none" style="stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round"><path d="m5 13 4 4L19 7"/></svg>
|
|
38
|
+
</span>
|
|
39
|
+
<p class="m-0 text-lg font-semibold text-foreground">Message sent</p>
|
|
40
|
+
<p class="m-0 text-sm text-muted-foreground">Thanks, we got it. <a class="text-primary" href="/features/forms">Send another</a>.</p>
|
|
41
|
+
</div>
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
const errs = actionData?.fieldErrors ?? {};
|
|
45
|
+
const v = actionData?.values ?? {};
|
|
46
|
+
return html`
|
|
47
|
+
<h1 class="text-h2 font-bold mb-2">Forms</h1>
|
|
48
|
+
<p class="text-muted-foreground mb-5 max-w-[460px]">A real <code><form></code> posting to this page's <code>action</code>. It works with JS off; validation errors come back on <code>actionData</code>.</p>
|
|
49
|
+
<form method="post" action="" class="max-w-[460px] grid gap-4 p-5 rounded-2xl bg-card border border-border">
|
|
50
|
+
${field('Name', 'name', html`<input id="name" name="name" value=${v.name ?? ''} class=${inputCls} placeholder="Ada Lovelace" />`, errs.name)}
|
|
51
|
+
${field('Email', 'email', html`<input id="email" name="email" type="email" value=${v.email ?? ''} class=${inputCls} placeholder="ada@example.com" />`, errs.email)}
|
|
52
|
+
${field('Message', 'message', html`<textarea id="message" name="message" rows="3" class=${inputCls} placeholder="Say hello...">${v.message ?? ''}</textarea>`, errs.message)}
|
|
53
|
+
<button type="submit" class="justify-self-start 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]">Send message</button>
|
|
54
|
+
</form>
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The page action runs on a non-GET submission to this URL (the no-JS write
|
|
59
|
+
// path). Validate, then return a failure (re-renders at 422 with fieldErrors +
|
|
60
|
+
// values) or a success with a same-site `redirect` (a 303 PRG to the confirmation).
|
|
61
|
+
export async function action({ formData }: { formData: FormData }): Promise<Result> {
|
|
62
|
+
const name = String(formData.get('name') ?? '').trim();
|
|
63
|
+
const email = String(formData.get('email') ?? '').trim();
|
|
64
|
+
const message = String(formData.get('message') ?? '').trim();
|
|
65
|
+
const fieldErrors: Record<string, string> = {};
|
|
66
|
+
if (!name) fieldErrors.name = 'Your name is required.';
|
|
67
|
+
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) fieldErrors.email = 'A valid email is required.';
|
|
68
|
+
if (message.length < 5) fieldErrors.message = 'Message must be at least 5 characters.';
|
|
69
|
+
if (Object.keys(fieldErrors).length) return { success: false, fieldErrors, values: { name, email, message } };
|
|
70
|
+
// A real app would persist / email here. We just confirm.
|
|
71
|
+
return { success: true, redirect: '/features/forms?sent=1' };
|
|
72
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/metadata route), then delete this marker line. webjs check fails while the marker remains.
|
|
2
|
+
// Metadata: how a page declares its <title>, description, and Open Graph tags.
|
|
3
|
+
// A static `metadata` export is read as-is. `generateMetadata(ctx)` takes
|
|
4
|
+
// precedence and can read the request (params, searchParams, url) to compute
|
|
5
|
+
// the tags, and may be async. Both run only on the server, so the tags are in
|
|
6
|
+
// the first paint (no JS, crawler-friendly). Whole-page metadata ROUTES
|
|
7
|
+
// (sitemap.ts, robots.ts, opengraph-image.ts, ...) live at the app root.
|
|
8
|
+
import { html } from '@webjsdev/core';
|
|
9
|
+
import type { Metadata } from '@webjsdev/core';
|
|
10
|
+
|
|
11
|
+
// generateMetadata wins over a static `metadata` export when both exist. Here
|
|
12
|
+
// it derives the title from a ?topic= query param to show the dynamic form;
|
|
13
|
+
// with no param it falls back to a default. Change the URL to ?topic=webjs and
|
|
14
|
+
// view source: the <title> follows.
|
|
15
|
+
export function generateMetadata({
|
|
16
|
+
searchParams,
|
|
17
|
+
}: {
|
|
18
|
+
searchParams: Record<string, string | undefined>;
|
|
19
|
+
}): Metadata {
|
|
20
|
+
const topic = (searchParams.topic || '').trim();
|
|
21
|
+
return {
|
|
22
|
+
title: topic ? topic + ' | metadata' : 'Metadata (generateMetadata) | features',
|
|
23
|
+
description: 'How a webjs page declares title, description, and Open Graph tags.',
|
|
24
|
+
openGraph: { title: topic || 'Metadata', type: 'article' },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function MetadataExample({
|
|
29
|
+
searchParams,
|
|
30
|
+
}: {
|
|
31
|
+
searchParams: Record<string, string | undefined>;
|
|
32
|
+
}) {
|
|
33
|
+
const topic = (searchParams.topic || '').trim();
|
|
34
|
+
return html`
|
|
35
|
+
<h1 class="text-h2 font-bold mb-4">Metadata</h1>
|
|
36
|
+
<p class="text-muted-foreground mb-4">
|
|
37
|
+
<code>generateMetadata(ctx)</code> runs on the server and can read the
|
|
38
|
+
request, so the <code><title></code> is computed per URL. View source
|
|
39
|
+
to see the tag this page produced.
|
|
40
|
+
</p>
|
|
41
|
+
<p class="mb-4">
|
|
42
|
+
Current title source:
|
|
43
|
+
<code class="font-mono text-sm">${topic ? '?topic=' + topic : '(default, no ?topic=)'}</code>
|
|
44
|
+
</p>
|
|
45
|
+
<ul class="list-disc pl-5 mb-4">
|
|
46
|
+
<li><a class="text-primary" href="/features/metadata?topic=webjs">?topic=webjs</a></li>
|
|
47
|
+
<li><a class="text-primary" href="/features/metadata?topic=Routing">?topic=Routing</a></li>
|
|
48
|
+
<li><a class="text-primary" href="/features/metadata">clear the param</a></li>
|
|
49
|
+
</ul>
|
|
50
|
+
<p class="text-muted-foreground text-sm">
|
|
51
|
+
Site-wide metadata (sitemap, robots, Open Graph images) lives in metadata
|
|
52
|
+
ROUTES at the app root, e.g. <code class="font-mono">app/sitemap.ts</code>.
|
|
53
|
+
</p>
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/optimistic-ui route AND modules/optimistic-ui), 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/optimistic-ui/components/like-button.ts';
|
|
5
|
+
|
|
6
|
+
export const metadata: Metadata = { title: 'Optimistic UI (imperative flip) | features' };
|
|
7
|
+
|
|
8
|
+
export default function OptimisticUiFeature() {
|
|
9
|
+
return html`
|
|
10
|
+
<h1 class="text-h2 font-bold mb-4">Optimistic UI</h1>
|
|
11
|
+
<p class="text-muted-foreground mb-4">The imperative <code>optimistic(signal, value, action)</code> form: the UI flips instantly and rolls back if the action fails. For the declarative list form (add / remove with rollback) in a full app, see <a class="text-primary" href="/examples/todo">/examples/todo</a>.</p>
|
|
12
|
+
<like-button></like-button>
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/rate-limit route AND modules/rate-limit), then delete this marker line. webjs check fails while the marker remains.
|
|
2
|
+
// Rate limiting: rateLimit() from '@webjsdev/server' is a middleware. It lives in
|
|
3
|
+
// a middleware.ts scoped to the endpoint it protects (here app/features/
|
|
4
|
+
// rate-limit/ping/middleware.ts, so it limits /features/rate-limit/ping WITHOUT
|
|
5
|
+
// touching this page). It is backed by the pluggable cache store, in-memory by
|
|
6
|
+
// default; point the store at Redis to share the window across instances.
|
|
7
|
+
import { html } from '@webjsdev/core';
|
|
8
|
+
import type { Metadata } from '@webjsdev/core';
|
|
9
|
+
import '#modules/rate-limit/components/rate-probe.ts';
|
|
10
|
+
|
|
11
|
+
export const metadata: Metadata = { title: 'Rate limiting (rateLimit middleware) | features' };
|
|
12
|
+
|
|
13
|
+
export default function RateLimitExample() {
|
|
14
|
+
return html`
|
|
15
|
+
<h1 class="text-h2 font-bold mb-4">Rate limiting</h1>
|
|
16
|
+
<p class="text-muted-foreground mb-4">
|
|
17
|
+
The <code class="font-mono">/ping</code> endpoint is limited to five
|
|
18
|
+
requests per ten seconds by a scoped <code class="font-mono">middleware.ts</code>.
|
|
19
|
+
Ping past the limit to get a <code class="font-mono">429</code> with a
|
|
20
|
+
<code class="font-mono">Retry-After</code> header.
|
|
21
|
+
</p>
|
|
22
|
+
<rate-probe></rate-probe>
|
|
23
|
+
<p class="text-muted-foreground text-sm mt-4">
|
|
24
|
+
With JavaScript off, hit
|
|
25
|
+
<a class="text-primary" href="/features/rate-limit/ping">/features/rate-limit/ping</a>
|
|
26
|
+
directly (refresh past five times in ten seconds).
|
|
27
|
+
</p>
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Per-segment middleware. It sits in the ping/ folder, so it applies ONLY to
|
|
2
|
+
// /features/rate-limit/ping (its route.ts), not to the demo page one level up.
|
|
3
|
+
// rateLimit() returns a standard webjs middleware: return a Response to
|
|
4
|
+
// short-circuit (the 429), or call next() to continue. Keyed by client IP by
|
|
5
|
+
// default; pass `key` to key by user id, API key, etc.
|
|
6
|
+
import { rateLimit } from '@webjsdev/server';
|
|
7
|
+
|
|
8
|
+
export default rateLimit({ window: '10s', max: 5, message: 'Slow down: five requests per ten seconds.' });
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The rate-limited endpoint. A plain GET route handler: it just reports success.
|
|
2
|
+
// The rateLimit() middleware in this folder runs BEFORE it and stamps the
|
|
3
|
+
// X-RateLimit-* headers onto the response (and returns a 429 once the window is
|
|
4
|
+
// exhausted, so this handler never runs on a limited request).
|
|
5
|
+
export function GET() {
|
|
6
|
+
return Response.json({ ok: true, at: new Date().toISOString() });
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// A route.ts is a server-only HTTP handler (named GET / POST / PUT / PATCH /
|
|
2
|
+
// DELETE exports). It is NOT isomorphic and never ships to the client, the webjs
|
|
3
|
+
// equivalent of a Next route handler. Each handler returns a Response (a plain
|
|
4
|
+
// value auto-JSONs). A folder cannot have BOTH page.ts and route.ts, so this
|
|
5
|
+
// endpoint lives one segment deeper, at /features/route-handler/data.
|
|
6
|
+
export async function GET() {
|
|
7
|
+
return Response.json({ ok: true, at: new Date().toISOString() });
|
|
8
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/route-handler route, including its data/route.ts handler), 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
|
+
|
|
5
|
+
export const metadata: Metadata = { title: 'Route handlers (route.ts) | features' };
|
|
6
|
+
|
|
7
|
+
export default function RouteHandlerExample() {
|
|
8
|
+
return html`
|
|
9
|
+
<h1 class="text-h2 font-bold mb-4">Route handlers</h1>
|
|
10
|
+
<p class="text-muted-foreground mb-4">A <code>route.ts</code> is a server-only HTTP endpoint (named <code>GET</code>/<code>POST</code>/... exports), the webjs equivalent of a Next route handler. It never ships to the client.</p>
|
|
11
|
+
<p><a class="text-primary" href="/features/route-handler/data">GET /features/route-handler/data</a> returns JSON.</p>
|
|
12
|
+
`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// A dynamic route: `params.id` is the URL segment. The default export is a
|
|
2
|
+
// (possibly async) function receiving { params, searchParams, url }; it runs
|
|
3
|
+
// ONLY on the server. Throw notFound() / redirect() to short-circuit.
|
|
4
|
+
//
|
|
5
|
+
// Type-safe routes: instead of hand-typing `{ params: { id: string } }`, type
|
|
6
|
+
// the props with PageProps<'<route>'>. `webjs types` (run automatically by
|
|
7
|
+
// `webjs dev`) generates .webjs/routes.d.ts with a Route union and per-route
|
|
8
|
+
// params, so PageProps<'/features/routing/[id]'>['params'] narrows to
|
|
9
|
+
// { id: string } automatically. Rename the folder and the type follows; pass a
|
|
10
|
+
// route literal that does not exist and it is a compile error.
|
|
11
|
+
import { html } from '@webjsdev/core';
|
|
12
|
+
import type { PageProps } from '@webjsdev/core';
|
|
13
|
+
|
|
14
|
+
export default function RoutingParam({ params }: PageProps<'/features/routing/[id]'>) {
|
|
15
|
+
return html`
|
|
16
|
+
<h1 class="text-h2 font-bold mb-4">Route param</h1>
|
|
17
|
+
<p>The <code>[id]</code> segment is: <strong>${params.id}</strong></p>
|
|
18
|
+
<p class="text-muted-foreground text-sm mt-3">
|
|
19
|
+
Typed with <code class="font-mono">PageProps<'/features/routing/[id]'></code>,
|
|
20
|
+
so <code class="font-mono">params.id</code> is a checked
|
|
21
|
+
<code class="font-mono">string</code> from the generated route union.
|
|
22
|
+
</p>
|
|
23
|
+
<p class="mt-3"><a class="text-primary" href="/features/routing">Back</a></p>
|
|
24
|
+
`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/routing route), then delete this marker line. webjs check fails while the marker remains.
|
|
2
|
+
// Routing basics: a static page that links to a dynamic route. app/ is routing
|
|
3
|
+
// only; a folder maps to a URL segment, and [id] is a dynamic segment read from
|
|
4
|
+
// `params`. See app/features/routing/[id]/page.ts.
|
|
5
|
+
import { html } from '@webjsdev/core';
|
|
6
|
+
import type { Metadata } from '@webjsdev/core';
|
|
7
|
+
|
|
8
|
+
export const metadata: Metadata = { title: 'Routing (dynamic params) | features' };
|
|
9
|
+
|
|
10
|
+
export default function RoutingExample() {
|
|
11
|
+
return html`
|
|
12
|
+
<h1 class="text-h2 font-bold mb-4">Routing</h1>
|
|
13
|
+
<p class="text-muted-foreground mb-4">A folder is a URL segment; a <code>[id]</code> folder is a dynamic param.</p>
|
|
14
|
+
<ul class="list-disc pl-5 mb-4">
|
|
15
|
+
<li><a class="text-primary" href="/features/routing/42">/features/routing/42</a></li>
|
|
16
|
+
<li><a class="text-primary" href="/features/routing/hello">/features/routing/hello</a></li>
|
|
17
|
+
</ul>
|
|
18
|
+
<p class="text-muted-foreground text-sm mb-2">
|
|
19
|
+
Routes are type-safe: <code class="font-mono">webjs types</code> (run by
|
|
20
|
+
<code class="font-mono">webjs dev</code>) generates a
|
|
21
|
+
<code class="font-mono">Route</code> union, and the
|
|
22
|
+
<code class="font-mono">[id]</code> page types its props with
|
|
23
|
+
<code class="font-mono">PageProps<'/features/routing/[id]'></code>
|
|
24
|
+
so <code class="font-mono">params</code> is checked against the real routes.
|
|
25
|
+
</p>
|
|
26
|
+
<p class="text-muted-foreground text-sm mb-2">
|
|
27
|
+
Programmatic navigation is checked too:
|
|
28
|
+
<code class="font-mono">navigate(url)</code> takes that
|
|
29
|
+
<code class="font-mono">Route</code> union, so
|
|
30
|
+
<code class="font-mono">navigate('/random/42')</code> is a compile error in
|
|
31
|
+
your editor and <code class="font-mono">webjs typecheck</code>. (Plain
|
|
32
|
+
<code class="font-mono"><a href></code> strings are not checked, so
|
|
33
|
+
prefer <code class="font-mono">navigate()</code> for internal links you want
|
|
34
|
+
verified.)
|
|
35
|
+
</p>
|
|
36
|
+
<p class="text-muted-foreground text-sm">
|
|
37
|
+
Changing route in code, two sides:
|
|
38
|
+
<code class="font-mono">navigate(url)</code> runs in the browser (a soft,
|
|
39
|
+
in-place client-router nav from an event handler, no reload), while
|
|
40
|
+
<code class="font-mono">redirect(url)</code> runs on the server (throw it in
|
|
41
|
+
a page or an <code class="font-mono">action</code> to bail before render and
|
|
42
|
+
return an HTTP 3xx). Throw
|
|
43
|
+
<code class="font-mono">redirect()</code> on the server, call
|
|
44
|
+
<code class="font-mono">navigate()</code> on the client.
|
|
45
|
+
</p>
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/server-actions route AND modules/server-actions), 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/server-actions/components/greeter.ts';
|
|
5
|
+
|
|
6
|
+
export const metadata: Metadata = { title: 'Server actions (.server vs use server) | features' };
|
|
7
|
+
|
|
8
|
+
export default function ServerActionsExample() {
|
|
9
|
+
return html`
|
|
10
|
+
<h1 class="text-h2 font-bold mb-4">Server actions</h1>
|
|
11
|
+
<p class="text-muted-foreground mb-4">A 'use server' action is RPC-callable from the client; a plain .server.ts is a server-only utility you never import into a component.</p>
|
|
12
|
+
<server-greeter></server-greeter>
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/service-worker route), then delete this marker line. webjs check fails while the marker remains.
|
|
2
|
+
// Service worker: opt-in progressive enhancement. webjs does NOT register one by
|
|
3
|
+
// default (an active SW caches aggressively and would confuse dev). You opt in
|
|
4
|
+
// by adding public/sw.js and registering it from a component's connectedCallback
|
|
5
|
+
// (browser-only). This page is a guided reference, not an active registration,
|
|
6
|
+
// so the scaffold stays predictable until you deliberately turn it on. See
|
|
7
|
+
// agent-docs/service-worker.md for the full recipe.
|
|
8
|
+
import { html } from '@webjsdev/core';
|
|
9
|
+
import type { Metadata } from '@webjsdev/core';
|
|
10
|
+
|
|
11
|
+
export const metadata: Metadata = { title: 'Service worker (opt-in) | features' };
|
|
12
|
+
|
|
13
|
+
export default function ServiceWorkerExample() {
|
|
14
|
+
return html`
|
|
15
|
+
<h1 class="text-h2 font-bold mb-4">Service worker</h1>
|
|
16
|
+
<p class="text-muted-foreground mb-4">
|
|
17
|
+
Opt-in offline/caching enhancement. Nothing is registered until you add
|
|
18
|
+
<code class="font-mono">public/sw.js</code> and register it from a
|
|
19
|
+
browser-only lifecycle hook.
|
|
20
|
+
</p>
|
|
21
|
+
<p class="mb-2 text-sm font-medium">Register inside a component (never in a page or layout):</p>
|
|
22
|
+
<pre class="bg-card border border-border rounded-xl p-4 overflow-x-auto text-sm font-mono mb-4"><code>connectedCallback() {
|
|
23
|
+
super.connectedCallback();
|
|
24
|
+
if ('serviceWorker' in navigator) {
|
|
25
|
+
// Register at the site root so the worker's scope is the whole origin.
|
|
26
|
+
navigator.serviceWorker.register('/sw.js');
|
|
27
|
+
}
|
|
28
|
+
}</code></pre>
|
|
29
|
+
<p class="text-muted-foreground text-sm">
|
|
30
|
+
Registration lives in a component because it is browser-only work.
|
|
31
|
+
A page or layout never hydrates, so it is the wrong home for it. Full
|
|
32
|
+
recipe: <code class="font-mono">agent-docs/service-worker.md</code>.
|
|
33
|
+
</p>
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The WebSocket endpoint for the websockets feature demo. A route.ts is a
|
|
2
|
+
// server-only HTTP file; exporting WS(ws, req, ctx) upgrades it to a WebSocket
|
|
3
|
+
// endpoint. `ws` is a standard ws-library socket: listen with ws.on('message'),
|
|
4
|
+
// reply with ws.send(). In dev the module is re-imported per connection, so keep
|
|
5
|
+
// shared state on globalThis if you need it (this echo endpoint is stateless).
|
|
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
|
+
// Echo the text back to just this client. For a fan-out to ALL connected
|
|
16
|
+
// clients, use broadcast() from '@webjsdev/server' (see the broadcast demo).
|
|
17
|
+
ws.send('echo: ' + data.toString());
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/websockets route AND modules/websockets), then delete this marker line. webjs check fails while the marker remains.
|
|
2
|
+
// WebSockets: a route.ts exports WS(ws, req) for the server endpoint, and a
|
|
3
|
+
// component uses connectWS() to talk to it. WebSockets are inherently JS-only
|
|
4
|
+
// (there is no no-JS fallback for a live socket), so the component degrades to a
|
|
5
|
+
// clear "requires JavaScript" note at SSR and enhances on hydration. The echo
|
|
6
|
+
// endpoint lives at app/features/websockets/echo/route.ts (a sibling folder, so
|
|
7
|
+
// it does not collide with this page).
|
|
8
|
+
import { html } from '@webjsdev/core';
|
|
9
|
+
import type { Metadata } from '@webjsdev/core';
|
|
10
|
+
import '#modules/websockets/components/ws-echo.ts';
|
|
11
|
+
|
|
12
|
+
export const metadata: Metadata = { title: 'WebSockets (connectWS + WS) | features' };
|
|
13
|
+
|
|
14
|
+
export default function WebSocketsExample() {
|
|
15
|
+
return html`
|
|
16
|
+
<h1 class="text-h2 font-bold mb-4">WebSockets</h1>
|
|
17
|
+
<p class="text-muted-foreground mb-4">
|
|
18
|
+
A <code class="font-mono">WS(ws, req)</code> export in
|
|
19
|
+
<code class="font-mono">route.ts</code> is the server endpoint;
|
|
20
|
+
<code class="font-mono">connectWS()</code> (auto-reconnect, JSON
|
|
21
|
+
encode/decode, queued sends) is the client. This echoes each message back.
|
|
22
|
+
</p>
|
|
23
|
+
<ws-echo></ws-echo>
|
|
24
|
+
`;
|
|
25
|
+
}
|