@webjsdev/cli 0.10.35 → 0.10.37
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/lib/create.js +18 -11
- package/lib/doctor.js +1 -1
- package/lib/saas-template.js +27 -7
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +1 -1
- package/templates/.github/copilot-instructions.md +1 -1
- package/templates/AGENTS.md +7 -6
- package/templates/CONVENTIONS.md +4 -4
- package/templates/gallery/app/apple-icon.ts +17 -0
- package/templates/gallery/app/features/async-render/page.ts +11 -1
- package/templates/gallery/app/features/boundaries/crash/page.ts +7 -0
- package/templates/gallery/app/features/boundaries/error.ts +16 -0
- package/templates/gallery/app/features/boundaries/gated/forbidden.ts +18 -0
- package/templates/gallery/app/features/boundaries/gated/page.ts +10 -0
- package/templates/gallery/app/features/boundaries/loading.ts +15 -0
- package/templates/gallery/app/features/boundaries/not-found.ts +17 -0
- package/templates/gallery/app/features/boundaries/page.ts +77 -0
- package/templates/gallery/app/features/boundaries/private/page.ts +10 -0
- package/templates/gallery/app/features/boundaries/private/unauthorized.ts +17 -0
- package/templates/gallery/app/features/caching/page.ts +9 -0
- package/templates/gallery/app/features/client-router/page.ts +3 -0
- package/templates/gallery/app/features/components/page.ts +11 -0
- package/templates/gallery/app/features/file-storage/file/[key]/route.ts +10 -1
- package/templates/gallery/app/features/file-storage/page.ts +2 -2
- package/templates/gallery/app/features/forms/page.ts +1 -1
- package/templates/gallery/app/features/rate-limit/page.ts +1 -1
- package/templates/gallery/app/features/route-handler/data/route.ts +34 -5
- package/templates/gallery/app/features/route-handler/page.ts +5 -2
- package/templates/gallery/app/features/routing/[id]/page.ts +13 -1
- package/templates/gallery/app/features/routing/legacy/page.ts +12 -0
- package/templates/gallery/app/features/routing/page.ts +9 -0
- package/templates/gallery/app/features/sessions/count/route.ts +12 -0
- package/templates/gallery/app/features/sessions/middleware.ts +7 -0
- package/templates/gallery/app/features/sessions/page.ts +19 -0
- package/templates/gallery/app/global-error.ts +46 -0
- package/templates/gallery/app/global-not-found.ts +22 -0
- package/templates/gallery/app/icon.ts +19 -0
- package/templates/gallery/app/manifest.ts +22 -0
- package/templates/gallery/app/opengraph-image.ts +20 -0
- package/templates/gallery/app/robots.ts +16 -0
- package/templates/gallery/app/sitemap.ts +23 -0
- package/templates/gallery/app/sitemaps/route.ts +17 -0
- package/templates/gallery/app/twitter-image.ts +19 -0
- package/templates/gallery/modules/caching/actions/bust-caches.server.ts +25 -0
- package/templates/gallery/modules/caching/components/cache-buster.ts +30 -0
- package/templates/gallery/modules/client-router/components/router-controls.ts +52 -0
- package/templates/gallery/modules/components/components/browser/counter-card.test.js +21 -1
- package/templates/gallery/modules/components/components/counter-card.ts +1 -1
- package/templates/gallery/modules/components/components/reactive-meter.ts +64 -0
- package/templates/gallery/modules/components/components/server-render.test.ts +20 -0
- package/templates/gallery/modules/components/components/task-loader.ts +48 -0
- package/templates/gallery/modules/components/components/theme-context.ts +67 -0
- package/templates/gallery/modules/directives/components/directive-demo.ts +162 -20
- package/templates/gallery/modules/file-storage/actions/store-upload.server.ts +3 -0
- package/templates/gallery/modules/file-storage/store.server.ts +29 -0
- package/templates/gallery/modules/rate-limit/components/rate-probe.ts +1 -1
- package/templates/gallery/modules/route-handler/components/rich-data.ts +31 -0
- package/templates/gallery/modules/server-actions/actions/greet.server.ts +9 -1
- package/templates/gallery/modules/server-actions/actions/greet.test.ts +37 -0
- package/templates/gallery/modules/server-actions/components/greeter.ts +1 -1
- package/templates/gallery/modules/sessions/session-config.server.ts +31 -0
- package/templates/gallery/modules/todo/components/todo-app.ts +5 -5
- package/templates/gallery/modules/websockets/components/ws-echo.ts +19 -1
- package/templates/gallery/modules/websockets/echo.test.ts +21 -0
- package/templates/instrumentation.ts +16 -0
package/lib/create.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `webjs create <name>`: scaffold a new
|
|
2
|
+
* `webjs create <name>`: scaffold a new WebJs app with opinionated defaults.
|
|
3
3
|
*
|
|
4
4
|
* Creates a directory with:
|
|
5
5
|
* - app/ with a root layout + page
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - components/ with a theme toggle
|
|
8
8
|
* - test/unit/ and test/e2e/ with example tests
|
|
9
9
|
* - CONVENTIONS.md, AGENTS.md, CLAUDE.md
|
|
10
|
-
* - package.json with
|
|
10
|
+
* - package.json with WebJs deps + test scripts
|
|
11
11
|
* - tsconfig.json for editor support
|
|
12
12
|
*/
|
|
13
13
|
|
|
@@ -160,7 +160,7 @@ async function copyUiComponents(appDir, names) {
|
|
|
160
160
|
* the scaffolded app. Merges `templates/gallery/{app,modules}` over the app so
|
|
161
161
|
* single-feature demos land under `app/features/<name>/`, whole example apps
|
|
162
162
|
* under `app/examples/<name>/`, and their logic under `modules/<name>/`, the
|
|
163
|
-
* app-thin + modules-logic split
|
|
163
|
+
* app-thin + modules-logic split WebJs prescribes.
|
|
164
164
|
*
|
|
165
165
|
* Ships verbatim (no `{{APP_NAME}}` substitution): the examples are self-
|
|
166
166
|
* contained and reference only `@webjsdev/*`, drizzle, `#db/*`, and each other.
|
|
@@ -392,7 +392,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
392
392
|
// Bun runtime (#541): the long-running server scripts (`dev` / `start`)
|
|
393
393
|
// are prefixed `bun --bun` so the app SERVES on Bun. The `--bun` overrides
|
|
394
394
|
// the `webjs` bin's `#!/usr/bin/env node` shebang (without it `bun run dev`
|
|
395
|
-
// would exec
|
|
395
|
+
// would exec WebJs under Node, silently running the "bun" app on Node).
|
|
396
396
|
// Baking it into the script body means a plain `bun run dev` (or even
|
|
397
397
|
// `npm run dev`) starts on Bun, so a user never has to remember the flag.
|
|
398
398
|
// The runtime-neutral tooling scripts below (test / db / check / typecheck
|
|
@@ -487,7 +487,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
487
487
|
noEmit: true,
|
|
488
488
|
allowImportingTsExtensions: true,
|
|
489
489
|
skipLibCheck: true,
|
|
490
|
-
//
|
|
490
|
+
// WebJs uses Node's built-in type-stripping (`process.features.
|
|
491
491
|
// typescript === 'strip'`) which preserves source positions
|
|
492
492
|
// byte-exactly. The constraint is that TypeScript must be
|
|
493
493
|
// "erasable": no `enum`, no `namespace` with values, no
|
|
@@ -539,6 +539,8 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
539
539
|
'test/hello/browser/hello.test.js',
|
|
540
540
|
'test/hello/e2e/hello.test.ts',
|
|
541
541
|
'web-test-runner.config.js',
|
|
542
|
+
// Optional boot-time APM hook (setOnError). Delete if unused.
|
|
543
|
+
'instrumentation.ts',
|
|
542
544
|
// Environment variables
|
|
543
545
|
'.env.example',
|
|
544
546
|
// Project-level gitignore (node_modules, .webjs, .env, OS junk).
|
|
@@ -581,7 +583,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
581
583
|
'.github/pull_request_template.md',
|
|
582
584
|
// CI is the test gate (the pre-commit hook only blocks main). Runs
|
|
583
585
|
// webjs check + the unit / browser / e2e layers on every PR and push
|
|
584
|
-
// to main, mirroring the
|
|
586
|
+
// to main, mirroring the WebJs framework's own CI.
|
|
585
587
|
'.github/workflows/ci.yml',
|
|
586
588
|
'.editorconfig',
|
|
587
589
|
// VS Code: associate the published webjs-config JSON Schema with the
|
|
@@ -1179,8 +1181,9 @@ ${UI_THEME}
|
|
|
1179
1181
|
Add a new design token the canonical way, a --x variable below plus a
|
|
1180
1182
|
--color-x: var(--x) line in the @theme inline block, then use it as bg-x /
|
|
1181
1183
|
text-x. Reach for opacity modifiers
|
|
1182
|
-
(bg-primary/10, hover:bg-primary/90,
|
|
1183
|
-
|
|
1184
|
+
(bg-primary/10, hover:bg-primary/90, border-border/60) before inventing a
|
|
1185
|
+
new token, but keep body text at full opacity so it stays above the AA
|
|
1186
|
+
contrast floor (a faded text-muted-foreground/70 measured 3.83:1). */
|
|
1184
1187
|
:root {
|
|
1185
1188
|
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1186
1189
|
--font-serif: ui-serif, 'Iowan Old Style', Palatino, Georgia, serif;
|
|
@@ -1358,11 +1361,12 @@ export const metadata = {
|
|
|
1358
1361
|
};
|
|
1359
1362
|
|
|
1360
1363
|
// Two kinds of reference the scaffold ships. FEATURES are single-concept demos
|
|
1361
|
-
// (one
|
|
1364
|
+
// (one WebJs feature each, under app/features/, logic in modules/). EXAMPLES are
|
|
1362
1365
|
// whole apps that compose several features (under app/examples/). Prune what you
|
|
1363
1366
|
// do not use (delete the route AND its modules/<name>), then reshape this page.
|
|
1364
1367
|
const features = [
|
|
1365
1368
|
{ href: '/features/routing', title: 'Routing', blurb: 'A static route plus a dynamic [id] segment that reads params. The file-based router in miniature.' },
|
|
1369
|
+
{ href: '/features/boundaries', title: 'Boundaries', blurb: 'The control-flow throws (forbidden / unauthorized / notFound) and the nearest boundary file that catches each.' },
|
|
1366
1370
|
{ href: '/features/components', title: 'Components', blurb: 'The WebComponent factory, reactive props, instance signals, and slot projection in light DOM.' },
|
|
1367
1371
|
{ href: '/features/server-actions', title: 'Server actions', blurb: 'A use-server RPC action next to a server-only .server.ts utility, and why the boundary matters.' },
|
|
1368
1372
|
{ href: '/features/optimistic-ui', title: 'Optimistic UI', blurb: 'The imperative optimistic(signal, value, action) flip: instant update, automatic rollback on failure.' },
|
|
@@ -1379,6 +1383,7 @@ const features = [
|
|
|
1379
1383
|
{ href: '/features/broadcast', title: 'Broadcast', blurb: 'Fan a message out to every connected client on a WebSocket path, so all open tabs stay in sync.' },
|
|
1380
1384
|
{ href: '/features/rate-limit', title: 'Rate limiting', blurb: 'The rateLimit() middleware scoped to one endpoint, returning a 429 with Retry-After past the window.' },
|
|
1381
1385
|
{ href: '/features/file-storage', title: 'File storage', blurb: 'A no-JS multipart upload streamed into the FileStore, then served back through a streaming route.' },
|
|
1386
|
+
{ href: '/features/sessions', title: 'Sessions', blurb: 'A signed-cookie session applied by a segment middleware, read and written per visitor with getSession() in a route.' },
|
|
1382
1387
|
];
|
|
1383
1388
|
const examples = [
|
|
1384
1389
|
{ href: '/examples/todo', title: 'Optimistic todo', blurb: 'A whole app composing several features: the declarative optimistic() list API, progressive-enhancement forms, accessible labels, the modules split, and SQLite.' },
|
|
@@ -1437,7 +1442,7 @@ export default function Home() {
|
|
|
1437
1442
|
`);
|
|
1438
1443
|
} else {
|
|
1439
1444
|
// saas home: the auth landing (hero + login/signup/dashboard) stays the
|
|
1440
|
-
// headline, and the
|
|
1445
|
+
// headline, and the WebJs feature gallery sits BELOW it, so a saas app is
|
|
1441
1446
|
// also a learning surface. Keep the `features` list in sync with the
|
|
1442
1447
|
// full-stack home above (both ship the same gallery).
|
|
1443
1448
|
await writeFile(join(appDir, 'app', 'page.ts'), `// webjs-scaffold-placeholder. This is the example homepage. Replace it with your app's real landing page, then delete this line. webjs check fails while the marker remains.
|
|
@@ -1454,12 +1459,13 @@ export const metadata = {
|
|
|
1454
1459
|
title: '${displayName}: built with webjs',
|
|
1455
1460
|
};
|
|
1456
1461
|
|
|
1457
|
-
// The
|
|
1462
|
+
// The WebJs feature gallery, shown below the auth landing. FEATURES are
|
|
1458
1463
|
// single-concept demos (under app/features/, logic in modules/); EXAMPLES are
|
|
1459
1464
|
// whole apps (under app/examples/). Prune what you do not use (delete the route
|
|
1460
1465
|
// AND its modules/<name>). Keep this list in sync with the full-stack home.
|
|
1461
1466
|
const features = [
|
|
1462
1467
|
{ href: '/features/routing', title: 'Routing', blurb: 'A static route plus a dynamic [id] segment that reads params. The file-based router in miniature.' },
|
|
1468
|
+
{ href: '/features/boundaries', title: 'Boundaries', blurb: 'The control-flow throws (forbidden / unauthorized / notFound) and the nearest boundary file that catches each.' },
|
|
1463
1469
|
{ href: '/features/components', title: 'Components', blurb: 'The WebComponent factory, reactive props, instance signals, and slot projection in light DOM.' },
|
|
1464
1470
|
{ href: '/features/server-actions', title: 'Server actions', blurb: 'A use-server RPC action next to a server-only .server.ts utility, and why the boundary matters.' },
|
|
1465
1471
|
{ href: '/features/optimistic-ui', title: 'Optimistic UI', blurb: 'The imperative optimistic(signal, value, action) flip: instant update, automatic rollback on failure.' },
|
|
@@ -1476,6 +1482,7 @@ const features = [
|
|
|
1476
1482
|
{ href: '/features/broadcast', title: 'Broadcast', blurb: 'Fan a message out to every connected client on a WebSocket path, so all open tabs stay in sync.' },
|
|
1477
1483
|
{ href: '/features/rate-limit', title: 'Rate limiting', blurb: 'The rateLimit() middleware scoped to one endpoint, returning a 429 with Retry-After past the window.' },
|
|
1478
1484
|
{ href: '/features/file-storage', title: 'File storage', blurb: 'A no-JS multipart upload streamed into the FileStore, then served back through a streaming route.' },
|
|
1485
|
+
{ href: '/features/sessions', title: 'Sessions', blurb: 'A signed-cookie session applied by a segment middleware, read and written per visitor with getSession() in a route.' },
|
|
1479
1486
|
];
|
|
1480
1487
|
const examples = [
|
|
1481
1488
|
{ href: '/examples/todo', title: 'Optimistic todo', blurb: 'A whole app composing several features: the declarative optimistic() list API, progressive-enhancement forms, accessible labels, the modules split, and SQLite.' },
|
package/lib/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `webjs doctor`: a project-health checklist runner (issue #266).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* WebJs has unusually many fragile preconditions, each an independent failure
|
|
5
5
|
* mode a contributor onboarding to an existing repo only hits at runtime: the
|
|
6
6
|
* Node 24+ strip-types floor, the `erasableSyntaxOnly` TS flag, importmap pin
|
|
7
7
|
* freshness, env drift vs `.env.example`, `@webjsdev/*` version coherence, and
|
package/lib/saas-template.js
CHANGED
|
@@ -27,7 +27,12 @@ async function readUiComponent(name) {
|
|
|
27
27
|
// it to the scaffolded app's aliased path (cn lives at lib/utils/cn.ts).
|
|
28
28
|
return raw
|
|
29
29
|
.replaceAll("'../lib/utils.ts'", "'#lib/utils/cn.ts'")
|
|
30
|
-
.replaceAll('"../lib/utils.ts"', '"#lib/utils/cn.ts"')
|
|
30
|
+
.replaceAll('"../lib/utils.ts"', '"#lib/utils/cn.ts"')
|
|
31
|
+
// onBeforeCache lives in its own client-only module so cn() stays pure (#819).
|
|
32
|
+
// Without this rewrite dialog.ts keeps the registry-relative `../lib/dom.ts`
|
|
33
|
+
// (which resolves to a nonexistent components/lib/dom.ts) and fails typecheck.
|
|
34
|
+
.replaceAll("'../lib/dom.ts'", "'#lib/utils/dom.ts'")
|
|
35
|
+
.replaceAll('"../lib/dom.ts"', '"#lib/utils/dom.ts"');
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
/** Copy named registry components into `<appDir>/components/ui/`. */
|
|
@@ -83,10 +88,19 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
83
88
|
|
|
84
89
|
// lib/auth.server.ts
|
|
85
90
|
await writeFile(join(appDir, 'lib', 'auth.server.ts'), [
|
|
86
|
-
"import { createAuth, Credentials } from '@webjsdev/server';",
|
|
91
|
+
"import { createAuth, Credentials, GitHub, Google } from '@webjsdev/server';",
|
|
87
92
|
"import { db } from '#db/connection.server.ts';",
|
|
88
93
|
"import { compare } from './password.server.ts';",
|
|
89
94
|
"",
|
|
95
|
+
"// AUTH_SECRET signs session tokens. Set a strong value in .env for any real",
|
|
96
|
+
"// deployment. The dev fallback keeps a fresh scaffold booting, but is NOT",
|
|
97
|
+
"// safe for production, so we fail fast if it is missing or blank there.",
|
|
98
|
+
"const trimmedSecret = process.env.AUTH_SECRET?.trim();",
|
|
99
|
+
"if (process.env.NODE_ENV === 'production' && !trimmedSecret) {",
|
|
100
|
+
" throw new Error('AUTH_SECRET must be set in production');",
|
|
101
|
+
"}",
|
|
102
|
+
"const authSecret = trimmedSecret || 'dev-insecure-secret-change-me';",
|
|
103
|
+
"",
|
|
90
104
|
"export const { auth, signIn, signOut, handlers } = createAuth({",
|
|
91
105
|
" providers: [",
|
|
92
106
|
" Credentials({",
|
|
@@ -96,8 +110,14 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
96
110
|
" return { id: String(user.id), name: user.name, email: user.email };",
|
|
97
111
|
" },",
|
|
98
112
|
" }),",
|
|
113
|
+
" // OAuth providers: add GitHub / Google sign-in by setting the matching",
|
|
114
|
+
" // env vars. Each preset (GitHub(), Google()) reads AUTH_<PROVIDER>_ID /",
|
|
115
|
+
" // _SECRET, so they only activate once configured and a fresh scaffold",
|
|
116
|
+
" // still boots with just Credentials.",
|
|
117
|
+
" ...(process.env.AUTH_GITHUB_ID ? [GitHub({ clientId: process.env.AUTH_GITHUB_ID, clientSecret: process.env.AUTH_GITHUB_SECRET })] : []),",
|
|
118
|
+
" ...(process.env.AUTH_GOOGLE_ID ? [Google({ clientId: process.env.AUTH_GOOGLE_ID, clientSecret: process.env.AUTH_GOOGLE_SECRET })] : []),",
|
|
99
119
|
" ],",
|
|
100
|
-
" secret:
|
|
120
|
+
" secret: authSecret,",
|
|
101
121
|
"});",
|
|
102
122
|
"",
|
|
103
123
|
].join('\n'));
|
|
@@ -324,7 +344,7 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
324
344
|
" <div class=\"max-w-sm mx-auto mt-12\">",
|
|
325
345
|
" <div class=${cardClass()}>",
|
|
326
346
|
" <div class=${cardHeaderClass()}>",
|
|
327
|
-
" <
|
|
347
|
+
" <h1 class=${cardTitleClass()}>Sign in</h1>",
|
|
328
348
|
" <p class=${cardDescriptionClass()}>Welcome back: log in to continue.</p>",
|
|
329
349
|
" </div>",
|
|
330
350
|
" <div class=${cardContentClass()}>",
|
|
@@ -393,7 +413,7 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
393
413
|
" <div class=\"max-w-sm mx-auto mt-12\">",
|
|
394
414
|
" <div class=${cardClass()}>",
|
|
395
415
|
" <div class=${cardHeaderClass()}>",
|
|
396
|
-
" <
|
|
416
|
+
" <h1 class=${cardTitleClass()}>Create an account</h1>",
|
|
397
417
|
" <p class=${cardDescriptionClass()}>Get started with your new workspace.</p>",
|
|
398
418
|
" </div>",
|
|
399
419
|
" <div class=${cardContentClass()}>",
|
|
@@ -460,7 +480,7 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
460
480
|
" </div>",
|
|
461
481
|
" <div class=${cardClass()}>",
|
|
462
482
|
" <div class=${cardHeaderClass()}>",
|
|
463
|
-
" <
|
|
483
|
+
" <h2 class=${cardTitleClass()}>Welcome, ${user?.name || user?.email}!</h2>",
|
|
464
484
|
" <p class=${cardDescriptionClass()}>You're authenticated. Replace this scaffold with your real app.</p>",
|
|
465
485
|
" </div>",
|
|
466
486
|
" <div class=${cardContentClass()}>",
|
|
@@ -486,7 +506,7 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
486
506
|
" <h1 class=\"text-2xl font-semibold mb-6\">Settings</h1>",
|
|
487
507
|
" <div class=${cardClass()}>",
|
|
488
508
|
" <div class=${cardHeaderClass()}>",
|
|
489
|
-
" <
|
|
509
|
+
" <h2 class=${cardTitleClass()}>Account</h2>",
|
|
490
510
|
" <p class=${cardDescriptionClass()}>Your basic profile information.</p>",
|
|
491
511
|
" </div>",
|
|
492
512
|
" <div class=${cardContentClass()}>",
|
package/package.json
CHANGED
|
@@ -157,7 +157,7 @@ self-review loop.
|
|
|
157
157
|
explicit fields plus constructor body assignments instead of parameter
|
|
158
158
|
properties. If `erasableSyntaxOnly` is disabled and non-erasable syntax is
|
|
159
159
|
used, the dev server fails at strip time and returns a 500 pointing at the
|
|
160
|
-
`no-non-erasable-typescript` lint rule.
|
|
160
|
+
`no-non-erasable-typescript` lint rule. WebJs is buildless end-to-end and
|
|
161
161
|
has no bundler fallback.
|
|
162
162
|
- Web components render into light DOM by default (so Tailwind / global CSS
|
|
163
163
|
apply directly). Opt in to shadow DOM per component with
|
|
@@ -149,7 +149,7 @@ each change must include.
|
|
|
149
149
|
|
|
150
150
|
- No build step: source files are served as ES modules. Don't introduce
|
|
151
151
|
build tools or bundlers in the critical path.
|
|
152
|
-
- **Erasable TypeScript only.** The runtime strips types via `module.stripTypeScriptTypes` (Node's built-in, or `amaro` on Bun) (whitespace replacement, byte-exact position preservation, no sourcemap). The scaffold's tsconfig.json sets `erasableSyntaxOnly: true`, so the TS compiler rejects `enum`, `namespace` with values, constructor parameter properties, legacy decorators with `emitDecoratorMetadata`, and `import = require`. Use erasable equivalents: `const X = { ... } as const` plus a derived union type instead of `enum`; explicit fields plus constructor body assignments instead of parameter properties. If `erasableSyntaxOnly` is disabled and non-erasable syntax is used, the dev server fails at strip time and returns a 500 pointing at the `no-non-erasable-typescript` lint rule.
|
|
152
|
+
- **Erasable TypeScript only.** The runtime strips types via `module.stripTypeScriptTypes` (Node's built-in, or `amaro` on Bun) (whitespace replacement, byte-exact position preservation, no sourcemap). The scaffold's tsconfig.json sets `erasableSyntaxOnly: true`, so the TS compiler rejects `enum`, `namespace` with values, constructor parameter properties, legacy decorators with `emitDecoratorMetadata`, and `import = require`. Use erasable equivalents: `const X = { ... } as const` plus a derived union type instead of `enum`; explicit fields plus constructor body assignments instead of parameter properties. If `erasableSyntaxOnly` is disabled and non-erasable syntax is used, the dev server fails at strip time and returns a 500 pointing at the `no-non-erasable-typescript` lint rule. WebJs is buildless end-to-end and has no bundler fallback.
|
|
153
153
|
- Tagged template: html`<div>${value}</div>` with css`...` for styles.
|
|
154
154
|
- **Tailwind-first styling.** Tailwind utilities are the strong default for pages AND light-DOM components (the default DOM mode): layout, spacing, color (via `@theme` tokens), typography, borders, radius, shadows, interaction states. Light DOM does not scope, so utilities apply directly. The lit reflex to scope CSS (`static styles = css\`...\``) or write an inline `<style>` with semantic class names (`.hero`, `.card`) in a light-DOM component is wrong: the scoped block needs `static shadow = true`, and inline class names leak globally. When a utility bundle repeats, extract a `lib/utils/ui.ts` helper returning an `html` fragment, not a CSS class. Reserve raw CSS for the allowlist (design tokens / `@theme`, `@property` + `@keyframes`, `::-webkit-scrollbar`, `prefers-reduced-motion`, complex `color-mix()` / gradients); when unavoidable in a light-DOM component, prefix every class selector with the component tag. Shadow-DOM components (`static shadow = true`) legitimately author `static styles = css\`...\`` for scoped CSS; don't use inline `style="..."` there.
|
|
155
155
|
- **One theme, canonical tokens.** The app has a SINGLE theme, defined once in `app/layout.ts` using the standard `@webjsdev/ui` (shadcn-compatible) semantic tokens set to the brand palette. Use the canonical utility names everywhere, in the page chrome AND inside components: `bg-background`, `text-foreground`, `bg-card`, `bg-muted`, `text-muted-foreground`, `bg-primary`, `text-primary-foreground`, `bg-accent`, `text-accent-foreground`, `border-border`, `ring-ring`. These are exactly the tokens a component copied in by `webjs ui add <name>` reads, so a scaffolded page and a later-added ui component share one theme with no wiring. NEVER invent a parallel token vocabulary (`--fg`, `--bg`, `text-fg`, `bg-elev`, a separate `--brand`): it collides with the ui tokens (the accent once flipped to neutral on navigation for exactly this reason) and diverges from the shadcn conventions the kit and AI agents expect. Reach for opacity modifiers (`bg-primary/10`, `hover:bg-primary/90`) before adding a token; add one the canonical way (a `--x` var plus a `--color-x: var(--x)` line in `@theme inline`).
|
package/templates/AGENTS.md
CHANGED
|
@@ -342,7 +342,7 @@ app/ ROUTING ONLY: thin route adapters (import from modules/
|
|
|
342
342
|
styles/, lib/utils/, and modules/. globals.css is at
|
|
343
343
|
styles/, NOT app/.
|
|
344
344
|
page.ts → / (the scaffold home links to the gallery)
|
|
345
|
-
features/<name>/ single-feature demos (routing, components,
|
|
345
|
+
features/<name>/ single-feature demos (routing, boundaries, components,
|
|
346
346
|
server-actions, optimistic-ui, async-render,
|
|
347
347
|
directives, route-handler, forms, metadata, caching,
|
|
348
348
|
env, client-router, service-worker); prune what you skip
|
|
@@ -405,9 +405,10 @@ instrumentation-client.ts optional client boot hook, runs first before app modu
|
|
|
405
405
|
The scaffold ships a gallery organized by KIND, so features and whole apps are
|
|
406
406
|
not mixed:
|
|
407
407
|
- `app/features/<name>/` are single-feature demos, one webjs concept each
|
|
408
|
-
(routing, components, server-actions, optimistic-ui, async-render,
|
|
409
|
-
route-handler, forms, metadata, caching, env, client-router,
|
|
410
|
-
plus the infra demos websockets, file-storage, rate-limit,
|
|
408
|
+
(routing, boundaries, components, server-actions, optimistic-ui, async-render,
|
|
409
|
+
directives, route-handler, forms, metadata, caching, env, client-router,
|
|
410
|
+
service-worker, plus the infra demos websockets, file-storage, rate-limit,
|
|
411
|
+
broadcast).
|
|
411
412
|
- `app/examples/<name>/` are whole example apps that compose several features
|
|
412
413
|
(todo: optimistic UI + progressive enhancement + a11y + db + modules).
|
|
413
414
|
|
|
@@ -501,7 +502,7 @@ an npm `prestart` hook.
|
|
|
501
502
|
|
|
502
503
|
### Running on Bun instead of Node
|
|
503
504
|
|
|
504
|
-
|
|
505
|
+
WebJs runs on **Node 24+ or Bun**. The same `package.json` scripts work on
|
|
505
506
|
either; to run under Bun, force it with `--bun` so the server executes on Bun
|
|
506
507
|
rather than the `webjs` bin's Node shebang:
|
|
507
508
|
|
|
@@ -1384,7 +1385,7 @@ composition, so a nested shell ends up dropped by the HTML parser.
|
|
|
1384
1385
|
If you turn `erasableSyntaxOnly` off and use non-erasable syntax,
|
|
1385
1386
|
the dev server fails at strip time and returns a 500 naming the
|
|
1386
1387
|
file and pointing at the `no-non-erasable-typescript` lint rule.
|
|
1387
|
-
|
|
1388
|
+
WebJs is buildless end-to-end and has no bundler fallback. The
|
|
1388
1389
|
`erasable-typescript-only` convention check warns when the flag
|
|
1389
1390
|
is missing or set to false.
|
|
1390
1391
|
9. **No em-dashes (U+2014) anywhere, and no hyphen or semicolon used
|
package/templates/CONVENTIONS.md
CHANGED
|
@@ -317,7 +317,7 @@ api / saas templates) is a **starting point**.
|
|
|
317
317
|
|
|
318
318
|
The full-stack and saas scaffolds ship a **gallery** organized by kind so features
|
|
319
319
|
and whole apps are not mixed. `app/features/<name>/` are single-feature
|
|
320
|
-
demos, one webjs concept each (routing, components, server-actions,
|
|
320
|
+
demos, one webjs concept each (routing, boundaries, components, server-actions,
|
|
321
321
|
optimistic-ui, async-render, directives, route-handler). `app/examples/<name>/`
|
|
322
322
|
are whole example apps that compose several features (todo: optimistic UI
|
|
323
323
|
+ progressive enhancement + a11y + db + modules). Both keep their logic in
|
|
@@ -430,7 +430,7 @@ features and examples as reference, never blindly up front. This is a no-op for
|
|
|
430
430
|
## Sensible defaults
|
|
431
431
|
|
|
432
432
|
<!-- OVERRIDE -->
|
|
433
|
-
|
|
433
|
+
WebJs uses sensible defaults. Environment
|
|
434
434
|
variables control infrastructure (no config files needed):
|
|
435
435
|
|
|
436
436
|
| Environment variable | Effect |
|
|
@@ -1066,7 +1066,7 @@ auth middleware.
|
|
|
1066
1066
|
|
|
1067
1067
|
<!-- OVERRIDE -->
|
|
1068
1068
|
|
|
1069
|
-
|
|
1069
|
+
WebJs pages work without JavaScript by design. Read-paths render to
|
|
1070
1070
|
real HTML on the server. Write-paths run through plain `<form>` plus
|
|
1071
1071
|
server actions, and navigation is a real `<a href>`. Every web component
|
|
1072
1072
|
is SSR'd too. Its `render()` runs on the server, so the component's
|
|
@@ -1248,7 +1248,7 @@ is better). See `agent-docs/advanced.md` for the full API.
|
|
|
1248
1248
|
constructor(x: number) { this.x = x; }
|
|
1249
1249
|
}
|
|
1250
1250
|
```
|
|
1251
|
-
If you turn `erasableSyntaxOnly` off and use non-erasable syntax, the dev server fails at strip time and returns a 500 pointing at the `no-non-erasable-typescript` lint rule.
|
|
1251
|
+
If you turn `erasableSyntaxOnly` off and use non-erasable syntax, the dev server fails at strip time and returns a 500 pointing at the `no-non-erasable-typescript` lint rule. WebJs is buildless end-to-end and has no bundler fallback. The `erasable-typescript-only` convention check warns when the flag is off.
|
|
1252
1252
|
- No semicolons (or with semicolons, pick one and stay consistent)
|
|
1253
1253
|
- `const` by default, `let` when needed, never `var`
|
|
1254
1254
|
- Prefer `async/await` over `.then()` chains
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// webjs-scaffold-placeholder. Metadata route. Keep and adapt it, or prune it
|
|
2
|
+
// (delete this file), then delete this marker line. webjs check fails while the
|
|
3
|
+
// marker remains.
|
|
4
|
+
//
|
|
5
|
+
// app/apple-icon.ts serves /apple-icon (the Apple touch icon iOS uses when a
|
|
6
|
+
// visitor adds the site to their home screen). Apple expects a 180x180 square
|
|
7
|
+
// with no rounded corners (iOS rounds them). Same shape as icon.ts: return a
|
|
8
|
+
// Response with the exact content type. Swap the inline SVG for your real mark.
|
|
9
|
+
export default function AppleIcon() {
|
|
10
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="180" height="180" viewBox="0 0 180 180">
|
|
11
|
+
<rect width="180" height="180" fill="#1c1613"/>
|
|
12
|
+
<text x="90" y="120" font-family="system-ui, sans-serif" font-size="104" font-weight="700" fill="#ff8a3d" text-anchor="middle">w</text>
|
|
13
|
+
</svg>`;
|
|
14
|
+
return new Response(svg, {
|
|
15
|
+
headers: { 'content-type': 'image/svg+xml', 'cache-control': 'public, max-age=3600' },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -1,14 +1,24 @@
|
|
|
1
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';
|
|
2
|
+
import { html, Suspense } from '@webjsdev/core';
|
|
3
3
|
import type { Metadata } from '@webjsdev/core';
|
|
4
4
|
import '#modules/async-render/components/server-clock.ts';
|
|
5
5
|
|
|
6
6
|
export const metadata: Metadata = { title: 'Async render (server data in first paint) | features' };
|
|
7
7
|
|
|
8
|
+
// A slow server region. Suspense flushes the fallback on the first byte and
|
|
9
|
+
// streams the resolved content in when it settles, so a slow query does not
|
|
10
|
+
// block the whole page's first paint. Multiple boundaries stream concurrently.
|
|
11
|
+
async function slowRegion() {
|
|
12
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
13
|
+
return html`<p class="text-foreground">Streamed in after the first byte.</p>`;
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
export default function AsyncRenderExample() {
|
|
9
17
|
return html`
|
|
10
18
|
<h1 class="text-h2 font-bold mb-4">Async render</h1>
|
|
11
19
|
<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
20
|
<server-clock></server-clock>
|
|
21
|
+
<p class="text-muted-foreground mt-6 mb-2">For a SLOW region where blocking the first byte hurts, wrap it in <code class="font-mono">Suspense</code> to stream it instead:</p>
|
|
22
|
+
${Suspense({ fallback: html`<p class="text-muted-foreground">loading slow region…</p>`, children: slowRegion() })}
|
|
13
23
|
`;
|
|
14
24
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// A page that throws a real render error, so the nearest error.ts boundary
|
|
2
|
+
// (../error.ts) catches it. In a real app an unexpected throw (a failed query,
|
|
3
|
+
// a bug) lands here; expected failures should return an ActionResult or throw
|
|
4
|
+
// notFound() / forbidden() instead.
|
|
5
|
+
export default function Crash() {
|
|
6
|
+
throw new Error('demo: this page threw during render');
|
|
7
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// error.ts is the error boundary for this segment's subtree. A render-time
|
|
2
|
+
// exception in a sibling or deeper page (see crash/page.ts) is caught here and
|
|
3
|
+
// rendered scoped to this boundary, so outer layouts stay alive. The default
|
|
4
|
+
// export receives { error, ...ctx }; in production only error.message is sent.
|
|
5
|
+
import { html } from '@webjsdev/core';
|
|
6
|
+
|
|
7
|
+
export default function BoundariesError({ error }: { error: Error }) {
|
|
8
|
+
return html`
|
|
9
|
+
<h1 class="text-h2 font-bold mb-4">Something went wrong</h1>
|
|
10
|
+
<p class="text-muted-foreground mb-4">
|
|
11
|
+
This segment's <code class="font-mono">error.ts</code> boundary caught a
|
|
12
|
+
render error: <code class="font-mono">${error?.message ?? 'unknown'}</code>.
|
|
13
|
+
</p>
|
|
14
|
+
<p><a class="text-primary" href="/features/boundaries">Back to boundaries</a></p>
|
|
15
|
+
`;
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The nearest 403 boundary for a thrown forbidden(). A forbidden.ts default-
|
|
2
|
+
// exports a function returning a TemplateResult, rendered at status 403 in place
|
|
3
|
+
// of the page that threw. Nearest wins: this one (inside gated/) beats any
|
|
4
|
+
// forbidden.ts higher up the tree. Keep the message actionable for an
|
|
5
|
+
// authenticated user who lacks permission.
|
|
6
|
+
import { html } from '@webjsdev/core';
|
|
7
|
+
|
|
8
|
+
export default function Forbidden() {
|
|
9
|
+
return html`
|
|
10
|
+
<h1 class="text-h2 font-bold mb-4">403 Forbidden</h1>
|
|
11
|
+
<p class="text-muted-foreground mb-4">
|
|
12
|
+
You are signed in but do not have permission to view this page. This is the
|
|
13
|
+
nearest <code class="font-mono">forbidden.ts</code> boundary, rendered
|
|
14
|
+
because the page threw <code class="font-mono">forbidden()</code>.
|
|
15
|
+
</p>
|
|
16
|
+
<p><a class="text-primary" href="/features/boundaries">Back to boundaries</a></p>
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// A page that throws forbidden(). In a real app you would call this only when
|
|
2
|
+
// an authenticated user lacks permission (checked against the session). Here it
|
|
3
|
+
// throws unconditionally so the demo always shows the 403 boundary. The nearest
|
|
4
|
+
// forbidden.ts (this folder's sibling) renders in place of this page at status
|
|
5
|
+
// 403; without a nearest one the framework renders a default 403 page.
|
|
6
|
+
import { forbidden } from '@webjsdev/core';
|
|
7
|
+
|
|
8
|
+
export default function Gated() {
|
|
9
|
+
forbidden();
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// loading.ts auto-wraps the sibling page in a Suspense boundary: its default
|
|
2
|
+
// export is the fallback shown while an async page (or its streamed regions)
|
|
3
|
+
// resolves. The boundaries index is fast, so you rarely see this, but any async
|
|
4
|
+
// page in this subtree gets this skeleton for free during navigation.
|
|
5
|
+
import { html } from '@webjsdev/core';
|
|
6
|
+
|
|
7
|
+
export default function BoundariesLoading() {
|
|
8
|
+
return html`
|
|
9
|
+
<div class="animate-pulse">
|
|
10
|
+
<div class="h-7 w-40 rounded bg-muted mb-4"></div>
|
|
11
|
+
<div class="h-4 w-full rounded bg-muted mb-2"></div>
|
|
12
|
+
<div class="h-4 w-3/4 rounded bg-muted"></div>
|
|
13
|
+
</div>
|
|
14
|
+
`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// not-found.ts is the nearest 404 boundary for this subtree. It renders both
|
|
2
|
+
// for a thrown notFound() in a page below here AND for an unmatched URL under
|
|
3
|
+
// /features/boundaries/ (try /features/boundaries/does-not-exist). Nearest wins,
|
|
4
|
+
// so this beats the root not-found for anything in this segment.
|
|
5
|
+
import { html } from '@webjsdev/core';
|
|
6
|
+
|
|
7
|
+
export default function BoundariesNotFound() {
|
|
8
|
+
return html`
|
|
9
|
+
<h1 class="text-h2 font-bold mb-4">404: Not here</h1>
|
|
10
|
+
<p class="text-muted-foreground mb-4">
|
|
11
|
+
This segment's <code class="font-mono">not-found.ts</code> boundary rendered,
|
|
12
|
+
because a page threw <code class="font-mono">notFound()</code> or the URL
|
|
13
|
+
matched nothing under this segment.
|
|
14
|
+
</p>
|
|
15
|
+
<p><a class="text-primary" href="/features/boundaries">Back to boundaries</a></p>
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// webjs-scaffold-placeholder. Feature gallery route. Keep and adapt it, or prune it (delete this app/features/boundaries route), then delete this marker line. webjs check fails while the marker remains.
|
|
2
|
+
// Boundaries: the four control-flow throws and their boundary files. A page (or
|
|
3
|
+
// a page `action`) can THROW to short-circuit rendering, and the framework
|
|
4
|
+
// renders the NEAREST matching boundary file in the route's ancestor chain
|
|
5
|
+
// (innermost wins), exactly like an error boundary:
|
|
6
|
+
// notFound() -> the nearest not-found.ts (404)
|
|
7
|
+
// forbidden() -> the nearest forbidden.ts (403, authenticated but not allowed)
|
|
8
|
+
// unauthorized() -> the nearest unauthorized.ts (401, not authenticated)
|
|
9
|
+
// redirect(url) -> an HTTP 3xx (no boundary file; it sends a Location)
|
|
10
|
+
// Two boundaries are ROOT-ONLY, so they live at the app root, not here under a
|
|
11
|
+
// feature folder: global-error.ts (the app-wide catch-all, owns its own <html>)
|
|
12
|
+
// and global-not-found.ts (a 404 for a URL that matches nothing anywhere).
|
|
13
|
+
//
|
|
14
|
+
// This demo ships two live sub-routes. Visit each and the page throws, so you
|
|
15
|
+
// see the nearest boundary render in place of the page:
|
|
16
|
+
// /features/boundaries/gated throws forbidden() -> gated/forbidden.ts
|
|
17
|
+
// /features/boundaries/private throws unauthorized() -> private/unauthorized.ts
|
|
18
|
+
import { html } from '@webjsdev/core';
|
|
19
|
+
import type { Metadata } from '@webjsdev/core';
|
|
20
|
+
|
|
21
|
+
export const metadata: Metadata = { title: 'Boundaries (throws + boundary files) | features' };
|
|
22
|
+
|
|
23
|
+
export default function BoundariesExample() {
|
|
24
|
+
return html`
|
|
25
|
+
<h1 class="text-h2 font-bold mb-4">Boundaries</h1>
|
|
26
|
+
<p class="text-muted-foreground mb-4">
|
|
27
|
+
Throw a control-flow function from a page (or a page
|
|
28
|
+
<code class="font-mono">action</code>) to short-circuit the render. The
|
|
29
|
+
framework catches it and renders the nearest matching boundary file in the
|
|
30
|
+
route's chain, innermost wins.
|
|
31
|
+
</p>
|
|
32
|
+
<ul class="list-disc pl-5 mb-4">
|
|
33
|
+
<li>
|
|
34
|
+
<a class="text-primary" href="/features/boundaries/gated">/features/boundaries/gated</a>
|
|
35
|
+
throws <code class="font-mono">forbidden()</code>, caught by
|
|
36
|
+
<code class="font-mono">gated/forbidden.ts</code> (403).
|
|
37
|
+
</li>
|
|
38
|
+
<li>
|
|
39
|
+
<a class="text-primary" href="/features/boundaries/private">/features/boundaries/private</a>
|
|
40
|
+
throws <code class="font-mono">unauthorized()</code>, caught by
|
|
41
|
+
<code class="font-mono">private/unauthorized.ts</code> (401).
|
|
42
|
+
</li>
|
|
43
|
+
<li>
|
|
44
|
+
<a class="text-primary" href="/features/boundaries/crash">/features/boundaries/crash</a>
|
|
45
|
+
throws a render error, caught by this segment's
|
|
46
|
+
<code class="font-mono">error.ts</code> (500).
|
|
47
|
+
</li>
|
|
48
|
+
<li>
|
|
49
|
+
<a class="text-primary" href="/features/boundaries/does-not-exist">/features/boundaries/does-not-exist</a>
|
|
50
|
+
matches nothing, caught by the nearest
|
|
51
|
+
<code class="font-mono">not-found.ts</code> (404).
|
|
52
|
+
</li>
|
|
53
|
+
</ul>
|
|
54
|
+
<p class="text-muted-foreground text-sm mb-2">
|
|
55
|
+
<code class="font-mono">forbidden()</code> is for an authenticated user who
|
|
56
|
+
lacks permission (403); <code class="font-mono">unauthorized()</code> is for
|
|
57
|
+
a request that is not authenticated at all (401). Both import from
|
|
58
|
+
<code class="font-mono">@webjsdev/core</code> and are thrown, never returned.
|
|
59
|
+
</p>
|
|
60
|
+
<p class="text-muted-foreground text-sm mb-2">
|
|
61
|
+
Same throw model as <code class="font-mono">notFound()</code> (renders the
|
|
62
|
+
nearest <code class="font-mono">not-found.ts</code>) and
|
|
63
|
+
<code class="font-mono">redirect(url)</code> (sends an HTTP 3xx). Inside a
|
|
64
|
+
<code class="font-mono">'use server'</code> RPC action, return an
|
|
65
|
+
<code class="font-mono">ActionResult</code> for an auth failure instead of
|
|
66
|
+
throwing, since a raw throw there is a generic 500.
|
|
67
|
+
</p>
|
|
68
|
+
<p class="text-muted-foreground text-sm">
|
|
69
|
+
Two boundaries are root-only and live at the app root:
|
|
70
|
+
<code class="font-mono">app/global-error.ts</code> (the app-wide catch-all,
|
|
71
|
+
which renders its own <code class="font-mono"><html></code> document)
|
|
72
|
+
and <code class="font-mono">app/global-not-found.ts</code> (a 404 for a URL
|
|
73
|
+
that matches nothing anywhere).
|
|
74
|
+
</p>
|
|
75
|
+
<p class="mt-3"><a class="text-primary" href="/">Back to the gallery</a></p>
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// A page that throws unauthorized(). In a real app you would call this when the
|
|
2
|
+
// request is not authenticated (no valid session), typically to prompt a sign-in.
|
|
3
|
+
// Here it throws unconditionally so the demo always shows the 401 boundary. The
|
|
4
|
+
// nearest unauthorized.ts (this folder's sibling) renders in place of this page
|
|
5
|
+
// at status 401; without a nearest one the framework renders a default 401 page.
|
|
6
|
+
import { unauthorized } from '@webjsdev/core';
|
|
7
|
+
|
|
8
|
+
export default function Private() {
|
|
9
|
+
unauthorized();
|
|
10
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The nearest 401 boundary for a thrown unauthorized(). An unauthorized.ts
|
|
2
|
+
// default-exports a function returning a TemplateResult, rendered at status 401
|
|
3
|
+
// in place of the page that threw. Nearest wins: this one (inside private/) beats
|
|
4
|
+
// any unauthorized.ts higher up the tree. A real one usually links to sign-in.
|
|
5
|
+
import { html } from '@webjsdev/core';
|
|
6
|
+
|
|
7
|
+
export default function Unauthorized() {
|
|
8
|
+
return html`
|
|
9
|
+
<h1 class="text-h2 font-bold mb-4">401 Unauthorized</h1>
|
|
10
|
+
<p class="text-muted-foreground mb-4">
|
|
11
|
+
You need to sign in to view this page. This is the nearest
|
|
12
|
+
<code class="font-mono">unauthorized.ts</code> boundary, rendered because the
|
|
13
|
+
page threw <code class="font-mono">unauthorized()</code>.
|
|
14
|
+
</p>
|
|
15
|
+
<p><a class="text-primary" href="/features/boundaries">Back to boundaries</a></p>
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// use HTTP Cache-Control + ETag (conditional GET).
|
|
9
9
|
import { html } from '@webjsdev/core';
|
|
10
10
|
import type { Metadata } from '@webjsdev/core';
|
|
11
|
+
import '#modules/caching/components/cache-buster.ts';
|
|
11
12
|
|
|
12
13
|
export const metadata: Metadata = { title: 'Caching (revalidate) | features' };
|
|
13
14
|
|
|
@@ -35,5 +36,13 @@ export default function CachingExample() {
|
|
|
35
36
|
<code>revalidateTag</code>, or a GET action's
|
|
36
37
|
<code>export const cache</code>.
|
|
37
38
|
</p>
|
|
39
|
+
<p class="text-muted-foreground text-sm mt-6 mb-2">
|
|
40
|
+
A mutation evicts the cache on demand. Click below (it calls
|
|
41
|
+
<code class="font-mono">revalidatePath('/features/caching')</code>), then refresh:
|
|
42
|
+
the timestamp updates immediately, even inside the 10s window, because the
|
|
43
|
+
cached HTML was dropped. Without clicking, the refresh serves the cached
|
|
44
|
+
copy until the window elapses.
|
|
45
|
+
</p>
|
|
46
|
+
<cache-buster></cache-buster>
|
|
38
47
|
`;
|
|
39
48
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// JS off, every link is a normal full-page navigation.
|
|
9
9
|
import { html } from '@webjsdev/core';
|
|
10
10
|
import type { Metadata } from '@webjsdev/core';
|
|
11
|
+
import '#modules/client-router/components/router-controls.ts';
|
|
11
12
|
|
|
12
13
|
export const metadata: Metadata = { title: 'Client router (soft nav) | features' };
|
|
13
14
|
|
|
@@ -24,6 +25,8 @@ export default function ClientRouterExample() {
|
|
|
24
25
|
<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
26
|
<a href="/" class="text-muted-foreground no-underline font-medium text-sm hover:text-foreground transition-colors">Home</a>
|
|
26
27
|
</div>
|
|
28
|
+
<p class="text-muted-foreground text-sm mt-6 mb-2">Or drive it from JS with <code class="font-mono">navigate()</code> / <code class="font-mono">revalidate()</code>:</p>
|
|
29
|
+
<router-controls></router-controls>
|
|
27
30
|
<p class="text-muted-foreground text-sm mt-6">
|
|
28
31
|
Opt out app-wide with <code class="font-mono">{ "webjs": { "clientRouter": false } }</code>,
|
|
29
32
|
or per-link with <code class="font-mono">data-no-router</code> (use it for
|