@webjsdev/cli 0.10.44 → 0.10.45
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/README.md +2 -3
- package/bin/webjs.js +18 -18
- package/lib/api-gallery.js +1 -1
- package/lib/create.js +51 -108
- package/package.json +1 -1
- package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +10 -2
- package/templates/.agents/skills/webjs/references/components.md +19 -4
- package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +5 -1
- package/templates/.agents/skills/webjs/references/runtime.md +1 -1
- package/templates/.agents/skills/webjs/references/service-worker.md +1 -1
- package/templates/gallery/app/api/auth/[...path]/route.ts +7 -0
- package/templates/gallery/app/examples/layout.ts +11 -0
- package/templates/gallery/app/features/auth/dashboard/layout.ts +20 -0
- package/templates/gallery/app/features/auth/dashboard/middleware.ts +14 -0
- package/templates/gallery/app/features/auth/dashboard/page.ts +18 -0
- package/templates/gallery/app/features/auth/dashboard/settings/page.ts +21 -0
- package/templates/gallery/app/features/auth/login/page.ts +40 -0
- package/templates/gallery/app/features/auth/page.ts +33 -0
- package/templates/gallery/app/features/auth/signup/page.ts +58 -0
- package/templates/gallery/app/features/frames/page.ts +10 -2
- package/templates/gallery/app/features/layout.ts +12 -0
- package/templates/gallery/app/features/server-actions/page.ts +4 -2
- package/templates/gallery/app/features/stream/page.ts +45 -0
- package/templates/gallery/app/features/streaming/page.ts +31 -0
- package/templates/gallery/app/features/suspense/page.ts +34 -0
- package/templates/gallery/app/features/view-transitions/page.ts +41 -0
- package/templates/gallery/app/features/view-transitions/second/page.ts +28 -0
- package/templates/gallery/modules/auth/actions/signup.server.ts +19 -0
- package/templates/gallery/modules/auth/auth.server.ts +53 -0
- package/templates/gallery/modules/auth/password.server.ts +20 -0
- package/templates/gallery/modules/auth/queries/current-user.server.ts +12 -0
- package/templates/gallery/modules/auth/types.ts +9 -0
- package/templates/gallery/modules/server-actions/actions/greet.server.ts +2 -2
- package/templates/gallery/modules/server-actions/actions/greet.test.ts +39 -36
- package/templates/gallery/modules/server-actions/components/greeter.ts +5 -8
- package/templates/gallery/modules/server-actions/middleware/require-auth.server.ts +14 -9
- package/templates/gallery/modules/stream/components/stream-demo.ts +76 -0
- package/templates/gallery/modules/streaming/actions/stream-tokens.server.ts +17 -0
- package/templates/gallery/modules/streaming/components/token-stream.ts +46 -0
- package/templates/gallery/modules/suspense/components/slow-fact.ts +19 -0
- package/templates/gallery/test/auth/auth.test.ts +81 -0
- package/templates/scripts/clear-api-gallery.mjs +55 -0
- package/templates/scripts/clear-gallery.mjs +23 -11
- package/lib/lean-copy.js +0 -43
- package/lib/saas-template.js +0 -568
package/README.md
CHANGED
|
@@ -38,9 +38,8 @@ Both `webjs create` and `create-webjs-app` auto-install dependencies in the new
|
|
|
38
38
|
## Commands
|
|
39
39
|
|
|
40
40
|
```sh
|
|
41
|
-
webjs create <name> # scaffold a full-stack app (default)
|
|
42
|
-
webjs create <name> --template api # backend-only API app
|
|
43
|
-
webjs create <name> --template saas # auth + dashboard + Drizzle User model
|
|
41
|
+
webjs create <name> # scaffold a full-stack app (default; auth ships as a gallery card)
|
|
42
|
+
webjs create <name> --template api # backend-only API app (routes + modules + Drizzle)
|
|
44
43
|
|
|
45
44
|
webjs dev # dev server with live reload (runs webjs.dev.before, e.g. webjs db migrate, then serves; npm run dev is a thin alias)
|
|
46
45
|
webjs start # production server (no build step, serves source directly)
|
package/bin/webjs.js
CHANGED
|
@@ -43,10 +43,10 @@ if (cmd !== 'help' && cmd !== undefined && !wantsHelp && !wantsVersion) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Exactly
|
|
46
|
+
// Exactly two scaffolds exist. Keep this list as the single source of
|
|
47
47
|
// truth. AI-agent docs in README.md / AGENTS.md / .cursorrules /
|
|
48
48
|
// .agents/rules/workflow.md / .github/copilot-instructions.md mirror it.
|
|
49
|
-
const TEMPLATES = ['full-stack', 'api'
|
|
49
|
+
const TEMPLATES = ['full-stack', 'api'];
|
|
50
50
|
|
|
51
51
|
const USAGE = `webjs commands:
|
|
52
52
|
webjs dev [--port 8080] [--no-hot] Start dev server with live reload
|
|
@@ -60,8 +60,8 @@ const USAGE = `webjs commands:
|
|
|
60
60
|
--json emits the structured results (with stable codes). --strict also fails the exit on warnings
|
|
61
61
|
webjs types Generate .webjs/routes.d.ts (typed Route union + per-route params)
|
|
62
62
|
webjs typecheck [tsc args...] Type-check the app with the project's tsc --noEmit (non-zero on errors)
|
|
63
|
-
webjs create <name> [--template full-stack|api
|
|
64
|
-
(only
|
|
63
|
+
webjs create <name> [--template full-stack|api] [--db sqlite|postgres] [--runtime node|bun] [--no-install] Scaffold a new webjs app
|
|
64
|
+
(only 2 templates exist. default: full-stack, Drizzle, --db sqlite, --runtime node)
|
|
65
65
|
--runtime bun emits a Bun-flavored app (bun.lock, bun Dockerfile/CI, bun docs);
|
|
66
66
|
also auto-detected when run via "bun create webjs".
|
|
67
67
|
Auto-runs the detected package manager's install in the new dir
|
|
@@ -160,10 +160,10 @@ const HELP = {
|
|
|
160
160
|
examples: ['webjs typecheck', 'webjs typecheck --watch'],
|
|
161
161
|
},
|
|
162
162
|
create: {
|
|
163
|
-
usage: 'webjs create <name> [--template full-stack|api
|
|
163
|
+
usage: 'webjs create <name> [--template full-stack|api] [--db sqlite|postgres] [--runtime node|bun] [--no-install]',
|
|
164
164
|
summary: 'Scaffold a new app. Defaults: full-stack template, Drizzle + SQLite, Node runtime.',
|
|
165
165
|
options: [
|
|
166
|
-
{ flag: '--template <t>', description: 'full-stack (default)
|
|
166
|
+
{ flag: '--template <t>', description: 'full-stack (default) or api (backend-only, no UI).' },
|
|
167
167
|
{ flag: '--db <d>', description: 'sqlite (default) or postgres.' },
|
|
168
168
|
{ flag: '--runtime <r>', description: 'node (default) or bun.' },
|
|
169
169
|
{ flag: '--no-install', description: 'Skip the package-manager install step.' },
|
|
@@ -171,7 +171,7 @@ const HELP = {
|
|
|
171
171
|
examples: [
|
|
172
172
|
'webjs create my-app',
|
|
173
173
|
'webjs create my-api --template api',
|
|
174
|
-
'webjs create my-
|
|
174
|
+
'webjs create my-api --template api --db postgres',
|
|
175
175
|
'webjs create my-app --runtime bun',
|
|
176
176
|
],
|
|
177
177
|
},
|
|
@@ -846,7 +846,7 @@ async function main() {
|
|
|
846
846
|
case 'create': {
|
|
847
847
|
const name = rest[0];
|
|
848
848
|
if (!name || name.startsWith('-')) {
|
|
849
|
-
console.error('Usage: webjs create <app-name> [--template full-stack|api
|
|
849
|
+
console.error('Usage: webjs create <app-name> [--template full-stack|api]');
|
|
850
850
|
process.exit(1);
|
|
851
851
|
}
|
|
852
852
|
const template = flag(rest, '--template', 'full-stack');
|
|
@@ -856,16 +856,16 @@ async function main() {
|
|
|
856
856
|
// on which scaffold to pick for which kind of app.
|
|
857
857
|
console.error(`Error: unknown template '${template}'.
|
|
858
858
|
|
|
859
|
-
Only
|
|
860
|
-
full-stack (default): pages + components + API + Drizzle/SQLite
|
|
861
|
-
|
|
862
|
-
(
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
859
|
+
Only two scaffolds exist:
|
|
860
|
+
full-stack (default): pages + components + API + Drizzle/SQLite, plus a
|
|
861
|
+
browsable feature gallery. Auth is one of the gallery cards
|
|
862
|
+
(login + session + a protected route), so a full-stack app
|
|
863
|
+
already carries a real auth baseline. Pick this for any app
|
|
864
|
+
the user describes in product terms (todo, blog, dashboard,
|
|
865
|
+
marketplace, social feed, a SaaS with accounts, …).
|
|
866
|
+
api backend-only: route handlers + modules + Drizzle/SQLite, no
|
|
867
|
+
pages/SSR. Pick this only if the user explicitly asks for an
|
|
868
|
+
HTTP/JSON API with no UI.
|
|
869
869
|
|
|
870
870
|
The scaffold is a starting point. Replace the example layout/page/
|
|
871
871
|
components/schema with the actual app the user requested. Use Drizzle +
|
package/lib/api-gallery.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Backend-features showcase for `webjs create --template api`.
|
|
3
3
|
* A set of JSON/HTTP endpoints under `app/api/features/` that demonstrate the
|
|
4
4
|
* backend capabilities an API app uses (the api counterpart of the UI gallery).
|
|
5
|
-
* Extracted here
|
|
5
|
+
* Extracted here to keep create.js readable and dodge
|
|
6
6
|
* nested-template-literal escaping: files are built from arrays of
|
|
7
7
|
* double-quoted strings, so `${...}` and backticks are emitted literally.
|
|
8
8
|
*/
|
package/lib/create.js
CHANGED
|
@@ -18,7 +18,6 @@ import { existsSync } from 'node:fs';
|
|
|
18
18
|
import { createRequire } from 'node:module';
|
|
19
19
|
import { spawnSync } from 'node:child_process';
|
|
20
20
|
import { bunifyProse, bunifyDockerfile, bunifyCompose, bunifyCi } from './runtime-rewrite.js';
|
|
21
|
-
import { leanComponentSource } from './lean-copy.js';
|
|
22
21
|
|
|
23
22
|
/**
|
|
24
23
|
* Detect which package manager invoked us. Reads `npm_config_user_agent`,
|
|
@@ -106,60 +105,6 @@ function resolveUiRegistryRoot() {
|
|
|
106
105
|
}
|
|
107
106
|
const UI_REGISTRY_ROOT = resolveUiRegistryRoot();
|
|
108
107
|
|
|
109
|
-
/**
|
|
110
|
-
* Read a single @webjsdev/ui registry component, rewrite its relative import
|
|
111
|
-
* of `../lib/utils.ts` to the scaffolded app's aliased path so it resolves
|
|
112
|
-
* when written to `components/ui/<name>.ts`. The scaffold puts cn() at
|
|
113
|
-
* `lib/utils/cn.ts` (folder-grouped with other browser-safe helpers), so the
|
|
114
|
-
* alias form is `#lib/utils/cn.ts` (#555/#556).
|
|
115
|
-
*
|
|
116
|
-
* @param {string} name component name without `.ts` (e.g. 'button')
|
|
117
|
-
* @returns {Promise<string>} source with import rewritten
|
|
118
|
-
*/
|
|
119
|
-
async function readUiComponent(name) {
|
|
120
|
-
const src = join(UI_REGISTRY_ROOT, 'components', `${name}.ts`);
|
|
121
|
-
const raw = await readFile(src, 'utf8');
|
|
122
|
-
// The registry component imports cn() via a relative `../lib/utils.ts`; rewrite
|
|
123
|
-
// it to the scaffolded app's aliased path (cn lives at lib/utils/cn.ts).
|
|
124
|
-
const rewritten = raw
|
|
125
|
-
.replaceAll("'../lib/utils.ts'", "'#lib/utils/cn.ts'")
|
|
126
|
-
.replaceAll('"../lib/utils.ts"', '"#lib/utils/cn.ts"')
|
|
127
|
-
// onBeforeCache lives in its own client-only module so cn() stays pure (#819).
|
|
128
|
-
.replaceAll("'../lib/dom.ts'", "'#lib/utils/dom.ts'")
|
|
129
|
-
.replaceAll('"../lib/dom.ts"', '"#lib/utils/dom.ts"');
|
|
130
|
-
// Strip the worked @example from a Tier-1 helper (same as `webjs ui add`), so
|
|
131
|
-
// the scaffolded component is lean and the example is served on demand. The
|
|
132
|
-
// shared helper is used by the saas-template copier too, so they cannot drift.
|
|
133
|
-
return leanComponentSource(rewritten, name);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Copy a list of @webjsdev/ui registry components into the scaffolded app
|
|
138
|
-
* under `components/ui/`. Throws if any name is missing from the registry,
|
|
139
|
-
* since the scaffold's generated pages import these by name and a missing
|
|
140
|
-
* file would produce ERR_MODULE_NOT_FOUND at first request. Caller must
|
|
141
|
-
* have already invoked assertUiRegistryAvailable().
|
|
142
|
-
*
|
|
143
|
-
* @param {string} appDir destination app root
|
|
144
|
-
* @param {string[]} names list of component file basenames (without `.ts`)
|
|
145
|
-
*/
|
|
146
|
-
async function copyUiComponents(appDir, names) {
|
|
147
|
-
const uiDir = join(appDir, 'components', 'ui');
|
|
148
|
-
await mkdir(uiDir, { recursive: true });
|
|
149
|
-
for (const n of names) {
|
|
150
|
-
const src = join(UI_REGISTRY_ROOT, 'components', `${n}.ts`);
|
|
151
|
-
if (!existsSync(src)) {
|
|
152
|
-
throw new Error(
|
|
153
|
-
`@webjsdev/ui registry is missing component '${n}.ts' at ${src}. ` +
|
|
154
|
-
`The scaffold's example pages import this component by name. ` +
|
|
155
|
-
`Either the registry was published incompletely or the scaffold's ` +
|
|
156
|
-
`component list is out of sync with the registry.`,
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
await writeFile(join(uiDir, `${n}.ts`), await readUiComponent(n));
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
108
|
/**
|
|
164
109
|
* Copy the example gallery (idiomatic, densely-commented working examples) into
|
|
165
110
|
* the scaffolded app. Merges `templates/gallery/{app,modules}` over the app so
|
|
@@ -177,7 +122,9 @@ async function copyUiComponents(appDir, names) {
|
|
|
177
122
|
*/
|
|
178
123
|
async function copyGallery(appDir) {
|
|
179
124
|
const galleryDir = join(TEMPLATES, 'gallery');
|
|
180
|
-
|
|
125
|
+
// `test` carries the auth card's real request-pipeline test (test/auth); it
|
|
126
|
+
// ships with the gallery and is pruned by gallery:clear alongside the card.
|
|
127
|
+
for (const sub of ['app', 'modules', 'test']) {
|
|
181
128
|
await cp(join(galleryDir, sub), join(appDir, sub), { recursive: true });
|
|
182
129
|
}
|
|
183
130
|
}
|
|
@@ -312,21 +259,19 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
312
259
|
const shouldInstall = opts.install === true;
|
|
313
260
|
// Defence in depth. The CLI already validates this, but library
|
|
314
261
|
// callers (tests, programmatic use) might pass anything.
|
|
315
|
-
const VALID_TEMPLATES = ['full-stack', 'api'
|
|
262
|
+
const VALID_TEMPLATES = ['full-stack', 'api'];
|
|
316
263
|
if (!VALID_TEMPLATES.includes(template)) {
|
|
317
264
|
throw new Error(
|
|
318
265
|
`Unknown template '${template}'. Only ${VALID_TEMPLATES.join(' / ')} exist.`,
|
|
319
266
|
);
|
|
320
267
|
}
|
|
321
268
|
const isApi = template === 'api';
|
|
322
|
-
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
// that differ (its own home page and the create.js-written schema).
|
|
329
|
-
const isFullStack = !isApi && !isSaas;
|
|
269
|
+
// The example gallery ships in the one UI template (not api, which has no UI),
|
|
270
|
+
// so the copyGallery gate below is !isApi. Auth is one of the gallery cards
|
|
271
|
+
// (app/features/auth + modules/auth), so a UI app ships a real, prunable auth
|
|
272
|
+
// baseline. `isFullStack` names the UI template for the parts that differ from
|
|
273
|
+
// api (the todos table + the passwordHash column the auth card needs).
|
|
274
|
+
const isFullStack = !isApi;
|
|
330
275
|
|
|
331
276
|
// Database dialect (#563): sqlite (default) or postgres. Drizzle is the ORM;
|
|
332
277
|
// the schema/queries/actions are identical across dialects, only db/columns
|
|
@@ -420,8 +365,13 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
420
365
|
// app runs the compiler under Bun (its image has no Node), a Node app runs
|
|
421
366
|
// it directly.
|
|
422
367
|
...(isApi ? {} : { 'css:build': cssBuildCmd }),
|
|
423
|
-
// Shed the demo gallery to a clean, buildable
|
|
424
|
-
|
|
368
|
+
// Shed the demo gallery / backend-features showcase to a clean, buildable
|
|
369
|
+
// base. The UI template runs clear-gallery.mjs; the api template runs
|
|
370
|
+
// clear-api-gallery.mjs (its showcase is app/api/features, not app/features).
|
|
371
|
+
'gallery:clear': (() => {
|
|
372
|
+
const script = isApi ? 'clear-api-gallery.mjs' : 'clear-gallery.mjs';
|
|
373
|
+
return isBun ? `bun scripts/${script}` : `node scripts/${script}`;
|
|
374
|
+
})(),
|
|
425
375
|
dev: isBun ? 'bun --bun webjs dev' : 'webjs dev',
|
|
426
376
|
start: isBun ? 'bun --bun webjs start' : 'webjs start',
|
|
427
377
|
test: 'webjs test',
|
|
@@ -763,7 +713,10 @@ export const users = table('users', {
|
|
|
763
713
|
name: text(),
|
|
764
714
|
// JSON column: a structured value persisted as JSON, typed via json<T>().
|
|
765
715
|
// Same helper works on SQLite and Postgres. Delete if you do not need it.
|
|
766
|
-
settings: json<{ theme?: string }>()
|
|
716
|
+
settings: json<{ theme?: string }>(),${isFullStack ? `
|
|
717
|
+
// The auth gallery card (app/features/auth) signs credentials against this.
|
|
718
|
+
// gallery:clear removes this column with the rest of the auth surface.
|
|
719
|
+
passwordHash: text(),` : ''}
|
|
767
720
|
createdAt: createdAt(),
|
|
768
721
|
});
|
|
769
722
|
${isFullStack ? `
|
|
@@ -1040,10 +993,18 @@ export type ActionResult<T> =
|
|
|
1040
993
|
// counterpart of the UI gallery. Prune what you skip.
|
|
1041
994
|
const { writeApiGallery } = await import('./api-gallery.js');
|
|
1042
995
|
await writeApiGallery(appDir);
|
|
996
|
+
|
|
997
|
+
// The showcase-reset script (wired as `gallery:clear` for the api template).
|
|
998
|
+
// It sheds app/api/features + its modules back to the health + users base.
|
|
999
|
+
const apiClearSrc = join(TEMPLATES, 'scripts', 'clear-api-gallery.mjs');
|
|
1000
|
+
if (existsSync(apiClearSrc)) {
|
|
1001
|
+
await mkdir(join(appDir, 'scripts'), { recursive: true });
|
|
1002
|
+
await cp(apiClearSrc, join(appDir, 'scripts', 'clear-api-gallery.mjs'));
|
|
1003
|
+
}
|
|
1043
1004
|
}
|
|
1044
1005
|
|
|
1045
1006
|
if (!isApi) {
|
|
1046
|
-
//
|
|
1007
|
+
// The UI template: layout + page + theme toggle + Tailwind
|
|
1047
1008
|
|
|
1048
1009
|
// The Tailwind stylesheet is compiled from public/input.css (written below)
|
|
1049
1010
|
// to a STATIC public/tailwind.css by css:build, and lib/utils/ui.ts helpers
|
|
@@ -1053,8 +1014,8 @@ export type ActionResult<T> =
|
|
|
1053
1014
|
const publicDir = join(appDir, 'public');
|
|
1054
1015
|
await mkdir(publicDir, { recursive: true });
|
|
1055
1016
|
// Progressive-enhancement service worker (#271): ship the opt-in offline
|
|
1056
|
-
// primitive (the worker + its offline fallback) into the UI
|
|
1057
|
-
// (
|
|
1017
|
+
// primitive (the worker + its offline fallback) into the UI scaffold
|
|
1018
|
+
// (this block is api-excluded since api has no UI).
|
|
1058
1019
|
// Dormant until the app registers it (see the skill's references/service-worker.md);
|
|
1059
1020
|
// it never changes the JS-disabled baseline.
|
|
1060
1021
|
for (const swFile of ['sw.js', 'offline.html']) {
|
|
@@ -1087,13 +1048,9 @@ export type ActionResult<T> =
|
|
|
1087
1048
|
// styles/globals.css (the @webjsdev/ui theme).
|
|
1088
1049
|
await writeUiBootstrap(appDir);
|
|
1089
1050
|
|
|
1090
|
-
// The
|
|
1091
|
-
//
|
|
1092
|
-
|
|
1093
|
-
await copyUiComponents(appDir, [
|
|
1094
|
-
'button', 'card', 'alert', 'badge', 'separator', 'label', 'input',
|
|
1095
|
-
]);
|
|
1096
|
-
}
|
|
1051
|
+
// The gallery cards style with plain Tailwind (the app is pre-initialised for
|
|
1052
|
+
// `webjs ui add <name>` via writeUiBootstrap above, but ships no components
|
|
1053
|
+
// until you add them on demand).
|
|
1097
1054
|
|
|
1098
1055
|
// The @webjsdev/ui theme (`--color-primary`, `--color-card`, the @theme maps,
|
|
1099
1056
|
// @custom-variant, @keyframes) plus the app @theme mappings are compiled from
|
|
@@ -1136,10 +1093,10 @@ ${uiThemeRaw}
|
|
|
1136
1093
|
|
|
1137
1094
|
// The gallery: idiomatic, densely-commented single-feature demos under
|
|
1138
1095
|
// app/features/ plus one whole example app under app/examples/, with logic
|
|
1139
|
-
// in modules/, all linked from the home page below. Shipped in
|
|
1140
|
-
// scaffold
|
|
1141
|
-
//
|
|
1142
|
-
// the
|
|
1096
|
+
// in modules/, all linked from the home page below. Shipped in the UI
|
|
1097
|
+
// scaffold so an agent gains context by browsing real working code; prune
|
|
1098
|
+
// per-feature (delete the route + its module) for what the app does not use,
|
|
1099
|
+
// or shed the whole gallery at once with `gallery:clear`.
|
|
1143
1100
|
await copyGallery(appDir);
|
|
1144
1101
|
|
|
1145
1102
|
await writeFile(join(appDir, 'app', 'layout.ts'), `import { html, cspNonce } from '@webjsdev/core';
|
|
@@ -1322,11 +1279,7 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1322
1279
|
// demo and the example app, and a footer with the docs + source links. Treat it
|
|
1323
1280
|
// as a starting point: prune the demos you do not use (delete the
|
|
1324
1281
|
// app/features/<x> route AND its modules/<x>), then reshape this page into the
|
|
1325
|
-
// app's real landing page.
|
|
1326
|
-
// spliced under the tagline.
|
|
1327
|
-
const homeAuthLinks = isSaas
|
|
1328
|
-
? '\n <div class="flex flex-wrap gap-3 items-center justify-center mt-2"><a href="/login" class="inline-flex items-center px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium no-underline hover:opacity-90">Log in</a><a href="/signup" class="inline-flex items-center px-4 py-2 rounded-lg border border-border text-foreground text-sm font-medium no-underline hover:bg-accent">Create an account</a></div>'
|
|
1329
|
-
: '';
|
|
1282
|
+
// app's real landing page.
|
|
1330
1283
|
await writeFile(join(appDir, 'app', 'page.ts'), `import { html } from '@webjsdev/core';
|
|
1331
1284
|
|
|
1332
1285
|
export const metadata = {
|
|
@@ -1340,10 +1293,15 @@ export const metadata = {
|
|
|
1340
1293
|
const FEATURES = [
|
|
1341
1294
|
{ href: '/features/routing', title: 'Routing', blurb: 'A static route plus a dynamic [id] segment that reads params. The file-based router in miniature.' },
|
|
1342
1295
|
{ href: '/features/boundaries', title: 'Boundaries', blurb: 'The control-flow throws (forbidden / unauthorized / notFound) and the nearest boundary file that catches each.' },
|
|
1296
|
+
{ href: '/features/auth', title: 'Auth', blurb: 'Password login on createAuth, a signed session cookie, and a real protected route that redirects anonymous visitors to login.' },
|
|
1343
1297
|
{ href: '/features/components', title: 'Components', blurb: 'The WebComponent factory, reactive props, instance signals, and slot projection in light DOM.' },
|
|
1344
1298
|
{ 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.' },
|
|
1345
1299
|
{ href: '/features/optimistic-ui', title: 'Optimistic UI', blurb: 'The imperative optimistic(signal, value, action) flip: instant update, automatic rollback on failure.' },
|
|
1346
1300
|
{ href: '/features/async-render', title: 'Async render', blurb: 'A component that awaits server data in async render(), so the resolved value is in the first paint.' },
|
|
1301
|
+
{ href: '/features/streaming', title: 'Streaming actions', blurb: 'A use-server action that returns an async generator, streamed to the call site token by token with for await.' },
|
|
1302
|
+
{ href: '/features/stream', title: 'Stream updates', blurb: 'The <webjs-stream> element: renderStream() applies surgical append / replace / remove DOM updates by target id, no region redraw.' },
|
|
1303
|
+
{ href: '/features/suspense', title: 'Suspense boundary', blurb: 'The <webjs-suspense> element: a first-paint fallback for a SLOW component, with the resolved content streamed in.' },
|
|
1304
|
+
{ href: '/features/view-transitions', title: 'View transitions', blurb: 'The opt-in view-transition meta cross-fades a soft navigation, with a data-webjs-permanent element persisted across the swap.' },
|
|
1347
1305
|
{ href: '/features/directives', title: 'Directives', blurb: 'The lit-html directive set: repeat for keyed lists, watch(signal) for a fine-grained node swap.' },
|
|
1348
1306
|
{ href: '/features/route-handler', title: 'Route handlers', blurb: 'A server-only route.ts HTTP endpoint returning JSON, the WebJs equivalent of a Next route handler.' },
|
|
1349
1307
|
{ href: '/features/forms', title: 'Forms', blurb: 'A no-JS progressive-enhancement form posting to the page action, with server-side validation errors.' },
|
|
@@ -1376,7 +1334,7 @@ export default function Home() {
|
|
|
1376
1334
|
</h1>
|
|
1377
1335
|
<p class="text-base sm:text-lg text-muted-foreground max-w-lg leading-relaxed m-0">
|
|
1378
1336
|
AI-first and web-components-first. Server-rendered, progressively enhanced, and buildless.
|
|
1379
|
-
</p
|
|
1337
|
+
</p>
|
|
1380
1338
|
</section>
|
|
1381
1339
|
|
|
1382
1340
|
<!-- Gallery: every feature demo + the example app -->
|
|
@@ -1505,12 +1463,6 @@ ThemeToggle.register('theme-toggle');
|
|
|
1505
1463
|
`);
|
|
1506
1464
|
} // end if (!isApi)
|
|
1507
1465
|
|
|
1508
|
-
// --- SaaS template extras: auth, dashboard, drizzle User model ---
|
|
1509
|
-
if (isSaas) {
|
|
1510
|
-
const { writeSaasFiles } = await import('./saas-template.js');
|
|
1511
|
-
await writeSaasFiles(appDir, { runtime });
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
1466
|
// AGENTS.md is already in place via the shared `templateFiles` loop
|
|
1515
1467
|
// earlier in this function, so no framework-root fallback needed.
|
|
1516
1468
|
|
|
@@ -1531,20 +1483,11 @@ ThemeToggle.register('theme-toggle');
|
|
|
1531
1483
|
modules/users/{actions,queries,types.ts} ← routes over server actions
|
|
1532
1484
|
db/{schema,columns,connection}.server.ts ← Drizzle (User model)
|
|
1533
1485
|
${guide}
|
|
1534
|
-
`);
|
|
1535
|
-
} else if (isSaas) {
|
|
1536
|
-
console.log(` ${name}/
|
|
1537
|
-
app/{layout,page}.ts, login/, signup/
|
|
1538
|
-
app/dashboard/{page,settings,middleware}.ts ← protected
|
|
1539
|
-
app/api/auth/[...path]/route.ts ← auth API
|
|
1540
|
-
components/ui/*, components/theme-toggle.ts
|
|
1541
|
-
modules/auth/*, lib/{auth,password}.server.ts
|
|
1542
|
-
db/{schema,columns,connection}.server.ts ← Drizzle (User model)
|
|
1543
|
-
${guide}
|
|
1544
1486
|
`);
|
|
1545
1487
|
} else {
|
|
1546
1488
|
console.log(` ${name}/
|
|
1547
|
-
app/{layout,page}.ts ←
|
|
1489
|
+
app/{layout,page}.ts ← gallery home; gallery:clear grows in place
|
|
1490
|
+
app/features/*, modules/* ← browsable demos (incl. auth: login + protected route)
|
|
1548
1491
|
components/theme-toggle.ts
|
|
1549
1492
|
public/input.css ← Tailwind entry (compiles to public/tailwind.css)
|
|
1550
1493
|
db/{schema,columns,connection}.server.ts ← Drizzle
|
|
@@ -1593,9 +1536,9 @@ ThemeToggle.register('theme-toggle');
|
|
|
1593
1536
|
// Next-steps banner prints LAST so the actionable command is the
|
|
1594
1537
|
// final thing on screen, never buried above the AI-agent guidance.
|
|
1595
1538
|
// Single copy-paste line so the user can move from "scaffold done"
|
|
1596
|
-
// to "dev server up" in one command. The full-stack
|
|
1597
|
-
//
|
|
1598
|
-
//
|
|
1539
|
+
// to "dev server up" in one command. The full-stack template ships
|
|
1540
|
+
// with @webjsdev/ui already initialised; the api template has no UI
|
|
1541
|
+
// but may add one later.
|
|
1599
1542
|
const installSegment = installed ? '' : `${pm} install && `;
|
|
1600
1543
|
// The shipped schema is applied on the first `run dev` (webjs.*.before runs
|
|
1601
1544
|
// `db migrate`), but only if a migration FILE exists. When we installed, we
|
package/package.json
CHANGED
|
@@ -17,7 +17,15 @@ Read this when a task touches client navigation, prefetch, partial-page swaps, s
|
|
|
17
17
|
|
|
18
18
|
The router auto-enables the moment `@webjsdev/core` loads in the browser, which is any page that ships a component. There is nothing to import or opt into. It intercepts same-origin `<a>` clicks (including inside shadow DOM), fetches the target HTML, and replaces only the inside of the deepest shared layout. Outer header, sidenav, and footer DOM is never re-rendered, so scroll positions, input values, and `<details>` state survive a navigation.
|
|
19
19
|
|
|
20
|
-
**
|
|
20
|
+
**The nav parse must preserve comments.** SSR wraps each layout's children AND the page itself in a KEYED boundary comment pair (open `<!--wj:children:<segment>:<route-key>-->`, close `<!--/wj:children:<segment>-->`, #1015). The route-key is the region's resolved concrete path with each substituted param value percent-encoded (so a user-controlled value can never terminate the comment or collide with the `:` delimiter). The router STRICTLY scans both the live and incoming DOM into segment maps: a close must id-match its innermost open, and ANY truncation, mispair, duplicate, or legacy anonymous open poisons the whole scan. The swap decision is two-tier with Next.js remount parity: a CHANGED route-key REPLACES (a fresh remount, permanents regrafted) at the PARENT of the shallowest changed boundary (a layout's boundary wraps only its children, so its own param-derived markup lives in the parent's range; anchoring there remounts the layout chrome too, exactly like Next re-rendering the layout with new params), else MORPH (the keyed state-preserving reconcile) at the deepest shared boundary when it is the leaf on both sides. The X-Webjs-Have header carries `segment:route-key` entries so the server re-renders (and re-ships) a dynamic layout the client holds for other params instead of short-circuiting past it. A poisoned scan or no shared segment degrades to a FULL PAGE LOAD (dev logs the cause), never a guessed recovery, so silent DOM corruption is structurally impossible. Hydration keys off another comment (`<!--webjs-hydrate-->`, which `__isHydrating()` reads as a component's first child). So the router and hydration both ride on comments SURVIVING the parse that turns a navigation response into a Document, which makes that parse a load-bearing correctness boundary rather than an implementation detail.
|
|
21
|
+
|
|
22
|
+
`Document.parseHTMLUnsafe` STRIPS every comment in Chromium 150 (#1007). No other parse API does: `DOMParser`, `setHTMLUnsafe`, `template.innerHTML`, and plain `innerHTML` all preserve them, and so does the document's own navigation parser, which is why a hard refresh always looked correct and only soft nav broke. With the boundaries gone the router degrades to a full page load (correct, just not soft); with `webjs-hydrate` gone a slotted light-DOM component misses the hydration adopt path. `parseHTML` therefore PROBES `parseHTMLUnsafe` once for losslessness instead of sniffing versions, uses it when it is lossless (it is the only single-pass API that also processes Declarative Shadow DOM), and otherwise parses with `DOMParser`, which preserves comments. A fixed browser silently returns to the fast path.
|
|
23
|
+
|
|
24
|
+
On that fallback, Declarative Shadow DOM is left UNPROCESSED (`DOMParser` does not attach it), a deliberate limitation tracked in #1011, because both ways of adding it back are worse than the gap. Re-serializing via `body.setHTMLUnsafe(body.innerHTML)` is not idempotent (Chromium omits the spec's LF-compensation, so a leading newline in `pre` / `textarea` is silently eaten, which in a `textarea` is form-data corruption), and attaching each root by hand yields a NON-declarative root, which makes any element whose constructor unconditionally calls `attachShadow()` throw `NotSupportedError` on upgrade. The gap costs a JS-less DSD-dependent element its shadow content on a full-body-swap nav, on a stripping browser only; a `static shadow = true` component attaches and renders its own root on upgrade, and a soft nav runs JS by definition.
|
|
25
|
+
|
|
26
|
+
Note for anyone testing this: **the Chromium web-test-runner currently resolves (148) is LOSSLESS, so CI cannot observe the bug at all** (and `playwright` is a caret range, so that version moves on any dependency refresh). A test that merely asserts "markers survive" passes there whether or not the fix exists. The guard in `packages/core/test/routing/browser/comment-preserving-parse.test.js` SIMULATES a stripping parser so it is provable on every engine.
|
|
27
|
+
|
|
28
|
+
**There is NO dropped-marker recovery (#1015 replaced #994's).** The pre-#1015 router "recovered" an orphaned open marker by guessing where its children ended (bounded by the other side's trailing-sibling count), which could guess wrong and corrupt silently. Keyed closes make a mispair DETECTABLE instead, and every integrity violation now degrades to a bounded, correct full page load. The historical producers of lost comments (our own comment-stripping parse #1007, mid-parse soft navs #1008) are fixed upstream, so the degradation is a rare backstop, not a common path. Wrapping `${children}` in a container element (the shipped idiom, `<main>${children}</main>` with the footer a sibling outside it) remains a fine layout pattern, though no correctness now depends on it.
|
|
21
29
|
|
|
22
30
|
**Opting out.** App-wide with config, or per moment at runtime.
|
|
23
31
|
|
|
@@ -105,7 +113,7 @@ The router can wrap a navigation's DOM mutation in the native View Transitions A
|
|
|
105
113
|
<meta name="view-transition" content="same-origin">
|
|
106
114
|
```
|
|
107
115
|
|
|
108
|
-
The accepted value is `same-origin`. When enabled it wraps
|
|
116
|
+
The accepted value is `same-origin`. When enabled it wraps every swap path (the two-tier boundary swap, the `<webjs-frame>` swap, and the background-revalidation full-body path). When `startViewTransition` is unavailable the swap runs synchronously with no flash and no throw. To persist a live element (a playing `<audio>`, an open menu) across a swap by node identity, mark it `data-webjs-permanent` and give it an `id`.
|
|
109
117
|
|
|
110
118
|
## `<webjs-stream>` Surgical Updates
|
|
111
119
|
|
|
@@ -103,7 +103,7 @@ class Panel extends WebComponent({ label: String }) {
|
|
|
103
103
|
|
|
104
104
|
## Slots
|
|
105
105
|
|
|
106
|
-
The full `<slot>` surface works in light DOM with shadow-DOM parity; migrating modes never requires a template rewrite.
|
|
106
|
+
The full `<slot>` surface works in light DOM with shadow-DOM parity; migrating modes never requires a template rewrite. A forwarded slot projects its content everywhere (client, SSR, hydration).
|
|
107
107
|
|
|
108
108
|
```ts
|
|
109
109
|
class MyCard extends WebComponent {
|
|
@@ -116,7 +116,21 @@ class MyCard extends WebComponent {
|
|
|
116
116
|
}
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
Named slots, the default slot (unnamed children, text, comments), fallback content (a slot's inner markup when nothing matches), first-wins resolution
|
|
119
|
+
Named slots, the default slot (unnamed children, text, comments), fallback content (a slot's inner markup when nothing matches), and first-wins resolution all behave per spec. The DOM API mirrors shadow slots: `assignedNodes` / `assignedElements` (with `{ flatten: true }`), `element.assignedSlot`, and the `slotchange` event. Both modes are SSR'd (light DOM places children into `<slot data-webjs-light data-projection="actual">`, shadow DOM via Declarative Shadow DOM), so slotted content renders with no JS.
|
|
120
|
+
|
|
121
|
+
**Light-DOM slots ARE the native DOM slot API (#1021, full shadow parity).** There is no WebJs-specific slot API. Post-mount writes are live exactly as in shadow DOM, and moving a component between `static shadow = false` and `true` never needs a rewrite:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const card = document.querySelector('my-card');
|
|
125
|
+
card.appendChild(node); // live, projected
|
|
126
|
+
card.querySelector('[slot=old]').slot = 'new'; // flip re-projects
|
|
127
|
+
card.innerHTML = '<p>replaced</p>'; // replaces slotted content
|
|
128
|
+
card.querySelector('slot').assignedNodes(); // read, mirrors shadow
|
|
129
|
+
node.assignedSlot;
|
|
130
|
+
card.querySelector('slot').addEventListener('slotchange', ...); // async + coalesced
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Things to internalize. (1) Every native mutation is live: `appendChild` / `insertBefore` / `removeChild` / `el.remove()` / `innerHTML` / `el.slot=` flip / `HTMLSlotElement.assign()`. Reorder-by-append moves a child to the end (native semantics), a fragment expands and drains, and `insertBefore` against a renderer/non-child ref throws `NotFoundError`. One caveat rides `assign()`: the light-DOM version is an EXTENSION (an element-bound overlay while name matching keeps working), and native shadow `assign()` needs `slotAssignment: 'manual'` which WebJs does not set, so `assign()` is the one write that does NOT survive flipping to `static shadow = true`; avoid it in mode-portable components. (2) Four inherent gaps (from light DOM having no shadow boundary). The gaps: structural host reads (`host.children` / `host.childNodes` / `querySelector(':scope > ...')` / the `innerHTML` GETTER read the rendered template, not the authored children, so read slotted content with `assignedNodes()`); `assignedChild.parentNode` is the `<slot>`; `::slotted()` CSS is shadow-only (style slotted content with normal selectors / Tailwind); and initial-projection lifecycle timing (`firstUpdated` sees the `<slot>` element with EMPTY `assignedNodes()`, because the first light-DOM projection lands one microtask after the first render, where shadow DOM projects natively before it; read assigned content from a `slotchange` listener or after a microtask). (3) Conditional-on-slot at render time does not exist in EITHER mode (a shadow template can't branch on light-child presence at render time either); use CSS `:has()` / `slot:empty` or a `slotchange` listener. (4) The name `default` is a reserved alias for the default slot; do not name a slot `default`. (5) A display-only slotted wrapper still elides; a component whose slots are mutated at runtime is already shipped because a consumer references its tag (force a ship with `static interactive = true` only for a dynamically-resolved reference the analyser cannot see). (6) A generic DOM library should operate on the assigned nodes, never on the host element itself; writes into an ACTIVELY ASSIGNED slot container are folded into the record (self-heal), while a fallback-mode slot's content is renderer-owned and out of contract. (7) A FORWARDED slot projects its content everywhere (#1023): a template may forward a slot into a nested component (html`<inner-shell><slot></slot></inner-shell>`), and the outer component's content projects through it on a client-only mount, in the SSR first paint, and across hydration (no flash back to fallback). The renderer stamps each slot with its template owner (carried across SSR as `data-wj-slot-owner`), so a forwarded slot routes to the outer host that rendered it, not the child it nests in. (8) A LAYOUT's named slots stay in sync across soft navigation (#1024): when a layout renders its `${children}` inside a slotted shell and a page emits top-level `slot=`-attributed children, the named-slot slices update on a soft-nav boundary swap just as the default slice does (the swap resyncs every own slot of the enclosing shell from the incoming page).
|
|
120
134
|
|
|
121
135
|
A compound child reads its parent at the first server paint via `closest('ui-tabs')` (only tag-name selectors resolve at SSR, and the compound parent must be light DOM). Genuine live-DOM reads (`querySelector`, `classList`, geometry) still throw at SSR, so keep them in `connectedCallback` / `firstUpdated`.
|
|
122
136
|
|
|
@@ -153,7 +167,8 @@ A component that does no client-side work renders the same SSR'd HTML with or wi
|
|
|
153
167
|
- an overridden lifecycle hook (including `renderFallback` / `renderError`)
|
|
154
168
|
- an imported `signal` / `computed` / `watch` / `Task` / `ref` / streaming directive, or `addController` / `requestUpdate`
|
|
155
169
|
- code that runs at module load (a top-level call, non-data `new`, dynamic `import(...)`, top-level `await`); only declarations and `X.register(...)` are allowed
|
|
156
|
-
- a
|
|
170
|
+
- the dynamic slot READ surface (`slotchange`, `assignedNodes` / `assignedElements` / `assignedSlot`); merely RENDERING a `<slot>` does not ship (the SSR output carries the placed children, so a display-only slotted wrapper is byte-identical without its JS; native-write liveness is consumer-driven and the consumer's tag reference forces the ship)
|
|
171
|
+
- being rendered by a component that itself ships
|
|
157
172
|
|
|
158
173
|
A bare `async render()` (no other signal, light DOM) is elided too: the SSR'd data is the complete first paint. Force shipping with `static interactive = true` when interactivity is invisible to static analysis (a dynamically-built tag string, a `:defined` rule in an external stylesheet). `static shadow = true` always ships (Declarative Shadow DOM re-attaches only during parsing). Turn elision off app-wide with `{ "webjs": { "elide": false } }` or `WEBJS_ELIDE=0`.
|
|
159
174
|
|
|
@@ -162,6 +177,6 @@ A bare `async render()` (no other signal, light DOM) is elided too: the SSR'd da
|
|
|
162
177
|
A `WebComponent` inherits `HTMLElement` (browser) or an `ElementShim` (SSR) plus the framework reactivity base. A reactive prop or method whose NAME collides either fails to compile (`TS2415` for a type-incompatible property, `TS2416` for a method signature) or silently hijacks the native member at runtime. The fix is always to rename.
|
|
163
178
|
|
|
164
179
|
- HTMLElement / Element: `title`, `id`, `slot`, `role`, `hidden`, `dir`, `lang`, `translate`, `draggable`, `tabIndex`, `className`, `dataset`, `remove`, `closest`, `matches`, `focus`, `blur`, `click`, `append` / `prepend`, `before` / `after`. Rename (`postTitle`, `removeItem`, `handleClick`).
|
|
165
|
-
- WebComponent base: `render`, `update`, `requestUpdate`, `updated` / `firstUpdated`, `willUpdate` / `shouldUpdate`, `connectedCallback`, `renderError` / `renderFallback`, `addController` / `removeController`, `updateComplete
|
|
180
|
+
- WebComponent base: `render`, `update`, `requestUpdate`, `updated` / `firstUpdated`, `willUpdate` / `shouldUpdate`, `connectedCallback`, `renderError` / `renderFallback`, `addController` / `removeController`, `updateComplete` (#1021: there is no WebJs slot API to override; slots are native). Only override one deliberately, with its exact signature; never repurpose the name for app logic.
|
|
166
181
|
|
|
167
182
|
Framework-private fields are underscore-prefixed (`_renderRoot`, `_connected`, `_changedProperties`, `_updatePromise`, `_isUpdating`); never declare a prop or field that matches one. Safe, non-inherited names: `label`, `open`, `count`, `value`, `name`, `items`, `todos`, `active`, `variant`, `size`, `checked`, `selected`, `heading`, `message`, `status`. When in doubt, grep the base surface in `node_modules/@webjsdev/core/src/component.js`.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## What This Covers
|
|
4
4
|
|
|
5
5
|
- The Next.js patterns that LOOK right in WebJs but break, because WebJs borrows Next's file-based routing shape but not its execution model (no RSC, no `'use client'` split): `redirect()` in a route handler, `fetch()` in a page, `<Link>`, `NEXT_PUBLIC_`, `await params`.
|
|
6
|
-
- The Lit patterns that break WebJs SSR or reactivity, because WebJs is HTML-first (real HTML first paint, JS opt-in per behaviour) not JS-first: `static properties` / the `@property()` decorator, class-field initializers, browser globals in `render()`, fetching in `connectedCallback`, interpolation into `<style
|
|
6
|
+
- The Lit patterns that break WebJs SSR or reactivity, because WebJs is HTML-first (real HTML first paint, JS opt-in per behaviour) not JS-first: `static properties` / the `@property()` decorator, class-field initializers, browser globals in `render()`, fetching in `connectedCallback`, interpolation into `<style>`, reading `assignedNodes()` in `firstUpdated` of a light-DOM component.
|
|
7
7
|
- The WebJs-shaped fix for each, with short code.
|
|
8
8
|
|
|
9
9
|
Read this when a pattern feels familiar from Next.js or Lit but you are not sure it transfers. For the component runtime see `components.md`; for the routing surface see `routing-and-pages.md`. The one difference underneath everything: pages and layouts render server-only and never hydrate, and the one client boundary is a `WebComponent` custom element.
|
|
@@ -149,6 +149,10 @@ The `@property()` decorator is banned by the erasable-TS invariant (decorators a
|
|
|
149
149
|
|
|
150
150
|
Lit defaults to shadow DOM, so `static styles = css` scopes automatically. WebJs defaults to light DOM. A `static styles` block without `static shadow = true` does nothing useful and any inline `<style>` with bare class names leaks globally. The webjs-shaped fix is Tailwind utilities, which apply directly in light DOM. Reach for `static shadow = true` plus `static styles` only when scoped CSS genuinely belongs in a shadow root, or prefix every selector with the tag name if authoring vanilla light-DOM CSS.
|
|
151
151
|
|
|
152
|
+
### Reading `assignedNodes()` in `firstUpdated` of a light-DOM component
|
|
153
|
+
|
|
154
|
+
In shadow DOM the browser projects slotted content natively before `firstUpdated`, so Lit muscle memory says `this.shadowRoot.querySelector('slot').assignedNodes()` is populated there. In light DOM the first projection lands one microtask AFTER the first render, so `firstUpdated` sees the `<slot>` element with an EMPTY `assignedNodes()`. The webjs-shaped fix: read assigned content from a `slotchange` listener (fires once projection lands, and on every later change), or wait a microtask. Every later read and every mutation-driven update behaves identically in both modes; only the first-render read differs.
|
|
155
|
+
|
|
152
156
|
### `:host { display: block }` on a light-DOM component
|
|
153
157
|
|
|
154
158
|
A custom element is `display: inline` by default, so a block container collapses. In Lit you fix this with `:host { display: block }`, which works because Lit is shadow-DOM-first. A light-DOM WebJs component has no shadow root, so there is no `:host` to write. There is nothing to do: the framework already defaults every light-DOM host to `display: block` via a low-priority `@layer webjs-host` rule, overridable by any Tailwind utility (`class="flex"` wins). A shadow-DOM component (`static shadow = true`) still sets `:host { display: block }` in `static styles` itself, exactly like Lit.
|
|
@@ -46,7 +46,7 @@ The 103 Early Hints gap costs only a small first-load latency edge where an edge
|
|
|
46
46
|
webjs create my-app --runtime bun
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
`--runtime` is orthogonal to `--template`, so it re-flavors
|
|
49
|
+
`--runtime` is orthogonal to `--template`, so it re-flavors either full-stack or api. A Bun scaffold emits a `bun.lock`, a pure `oven/bun:1` Dockerfile plus a bun-install CI, and bun-command agent docs. The test, db, and check tooling still runs on Node.
|
|
50
50
|
|
|
51
51
|
## Running on Bun
|
|
52
52
|
|
|
@@ -11,7 +11,7 @@ Read this when you want an offline experience or an asset cache in a WebJs app,
|
|
|
11
11
|
|
|
12
12
|
## What ships and why it is safe
|
|
13
13
|
|
|
14
|
-
WebJs's UI
|
|
14
|
+
WebJs's UI scaffold (full-stack, not the api template) ships a hand-authored service worker at `public/sw.js` and an offline fallback at `public/offline.html`. Both ship **dormant**: they do nothing until the app registers the worker, and the worker only ever registers from JavaScript. So with JS off no worker exists, and pages, links, and forms behave exactly as before. It is opt-in and adds an offline experience plus an asset cache without changing the no-JS baseline.
|
|
15
15
|
|
|
16
16
|
This is a thin, hand-readable worker built directly on the native Service Worker and Cache Storage APIs. There is no Workbox, no precache framework, and no bundler step, matching WebJs's no-build, close-to-web-standards posture. The file is yours to edit, not a framework internal.
|
|
17
17
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The createAuth HTTP endpoints (signin, signout, OAuth callbacks). This route
|
|
2
|
+
// stays at the app root, NOT under app/features/auth/, because createAuth
|
|
3
|
+
// hardcodes /api/auth/signin/* and /api/auth/callback/* for its form posts and
|
|
4
|
+
// OAuth redirect URIs. The rest of the auth card lives under app/features/auth/.
|
|
5
|
+
import { handlers } from '#modules/auth/auth.server.ts';
|
|
6
|
+
export const GET = handlers.GET;
|
|
7
|
+
export const POST = handlers.POST;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { html } from '@webjsdev/core';
|
|
2
|
+
|
|
3
|
+
// Shared layout for every gallery example app under /examples/*. It adds the same
|
|
4
|
+
// slim "back to the gallery" link the feature demos get, so an example is never a
|
|
5
|
+
// dead end. A non-root layout, so it never writes the document shell.
|
|
6
|
+
export default function ExamplesLayout({ children }: { children: unknown }) {
|
|
7
|
+
return html`
|
|
8
|
+
<a href="/" class="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors no-underline mb-6">← Gallery</a>
|
|
9
|
+
${children}
|
|
10
|
+
`;
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { html } from '@webjsdev/core';
|
|
2
|
+
|
|
3
|
+
// Nested layout for the protected dashboard subtree. Logout is a plain
|
|
4
|
+
// <form method="POST"> posting to the createAuth signout route: it clears the
|
|
5
|
+
// session cookie and 302s home, and works with JS off (progressive-enhancement
|
|
6
|
+
// default). signOut is server-only (modules/auth/auth.server.ts), so we POST to
|
|
7
|
+
// its route rather than import it into a browser-shipping page. After signout the
|
|
8
|
+
// dashboard middleware bounces any later visit to login.
|
|
9
|
+
export default function DashboardLayout({ children }: { children: unknown }) {
|
|
10
|
+
return html`
|
|
11
|
+
<nav class="flex items-center gap-4 mb-6 pb-4 border-b border-border">
|
|
12
|
+
<a href="/features/auth/dashboard" class="text-sm font-medium text-foreground hover:underline">Dashboard</a>
|
|
13
|
+
<a href="/features/auth/dashboard/settings" class="text-sm font-medium text-foreground hover:underline">Settings</a>
|
|
14
|
+
<form method="POST" action="/api/auth/signout" class="ml-auto">
|
|
15
|
+
<button type="submit" class="px-3 py-1.5 rounded-lg border border-border text-sm text-foreground bg-transparent cursor-pointer transition-colors hover:bg-accent">Log out</button>
|
|
16
|
+
</form>
|
|
17
|
+
</nav>
|
|
18
|
+
${children}
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { auth } from '#modules/auth/auth.server.ts';
|
|
2
|
+
|
|
3
|
+
// The protected-route gate. A per-segment middleware.ts runs for every request
|
|
4
|
+
// under /features/auth/dashboard/*. It reads the signed session off the request
|
|
5
|
+
// with auth(req); with no valid session it 302s to login BEFORE the page renders,
|
|
6
|
+
// so an anonymous visitor never sees the protected content. This needs no DB
|
|
7
|
+
// query (only a cookie read), so the gate is real the moment the app boots.
|
|
8
|
+
export default async function requireAuth(req: Request, next: () => Promise<Response>) {
|
|
9
|
+
const session = await auth(req);
|
|
10
|
+
if (!session?.user) {
|
|
11
|
+
return new Response(null, { status: 302, headers: { location: '/features/auth/login' } });
|
|
12
|
+
}
|
|
13
|
+
return next();
|
|
14
|
+
}
|