@webjsdev/cli 0.10.50 → 0.10.52
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 +3 -1
- package/bin/webjs.js +437 -32
- package/lib/api-gallery.js +6 -7
- package/lib/app-name.js +208 -0
- package/lib/create.js +37 -9
- package/lib/doctor.js +566 -21
- package/package.json +2 -2
- package/templates/.agents/rules/workflow.md +9 -1
- package/templates/.agents/skills/webjs/SKILL.md +26 -11
- package/templates/.agents/skills/webjs/references/auth-and-sessions.md +2 -2
- package/templates/.agents/skills/webjs/references/built-ins.md +26 -7
- package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +16 -2
- package/templates/.agents/skills/webjs/references/components.md +59 -2
- package/templates/.agents/skills/webjs/references/data-and-actions.md +92 -7
- package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +75 -19
- package/templates/.agents/skills/webjs/references/optimistic-ui.md +35 -14
- package/templates/.agents/skills/webjs/references/routing-and-pages.md +34 -15
- package/templates/.agents/skills/webjs/references/runtime.md +5 -1
- package/templates/.agents/skills/webjs/references/styling.md +1 -1
- package/templates/.agents/skills/webjs/references/testing.md +80 -3
- package/templates/.agents/skills/webjs/references/typescript.md +71 -2
- package/templates/.agents/skills/webjs/references/ui-kit.md +5 -2
- package/templates/.github/pull_request_template.md +1 -0
- package/templates/.github/workflows/ci.yml +13 -0
- package/templates/AGENTS.md +31 -5
- package/templates/CONVENTIONS.md +4 -1
- package/templates/gallery/app/examples/layout.ts +2 -1
- package/templates/gallery/app/examples/todo/page.ts +3 -16
- package/templates/gallery/app/features/auth/dashboard/layout.ts +2 -1
- package/templates/gallery/app/features/auth/signup/page.ts +4 -23
- package/templates/gallery/app/features/caching/page.ts +6 -6
- package/templates/gallery/app/features/file-storage/page.ts +8 -19
- package/templates/gallery/app/features/forms/page.ts +12 -38
- package/templates/gallery/app/features/layout.ts +6 -2
- package/templates/gallery/app/features/route-handler/data/route.ts +2 -1
- package/templates/gallery/app/features/view-transitions/page.ts +1 -1
- package/templates/gallery/app/global-error.ts +7 -4
- package/templates/gallery/modules/async-render/components/server-clock.ts +3 -2
- package/templates/gallery/modules/auth/actions/signup.server.ts +27 -11
- package/templates/gallery/modules/file-storage/actions/store-upload.server.ts +21 -10
- package/templates/gallery/modules/forms/actions/send-message.server.ts +34 -0
- package/templates/gallery/modules/gallery/nav.ts +1 -1
- package/templates/gallery/modules/server-actions/actions/greet.test.ts +6 -5
- package/templates/gallery/modules/todo/actions/submit-todo.server.ts +33 -0
- package/templates/gallery/modules/todo/components/todo-app.ts +8 -5
- package/templates/gallery/modules/todo/types.ts +15 -10
- package/templates/gallery/test/auth/auth.test.ts +31 -16
- package/templates/partials/agents-playbook-api.md +5 -0
- package/templates/partials/agents-playbook-fullstack.md +5 -0
- package/templates/scripts/clear-gallery.mjs +5 -4
- package/templates/test/hello/e2e/hello.test.ts +18 -1
package/lib/api-gallery.js
CHANGED
|
@@ -13,8 +13,7 @@ import { join } from 'node:path';
|
|
|
13
13
|
/**
|
|
14
14
|
* Write the api backend-features gallery into `<appDir>/app/api/features/**`
|
|
15
15
|
* plus a boot-time env-validation example at `app/env.ts`. Each demo carries a
|
|
16
|
-
*
|
|
17
|
-
* pruned or adapted.
|
|
16
|
+
* comment naming exactly what to delete when it is pruned.
|
|
18
17
|
* @param {string} appDir
|
|
19
18
|
*/
|
|
20
19
|
export async function writeApiGallery(appDir) {
|
|
@@ -35,7 +34,7 @@ export async function writeApiGallery(appDir) {
|
|
|
35
34
|
].join('\n'));
|
|
36
35
|
await mkdir(feat('validate'), { recursive: true });
|
|
37
36
|
await writeFile(feat('validate', 'route.ts'), [
|
|
38
|
-
"//
|
|
37
|
+
"// API backend-features demo. Keep and adapt it, or prune it (delete this app/api/features/validate route AND modules/widgets).",
|
|
39
38
|
"// route() turns a 'use server' action into a REST endpoint: it merges the URL",
|
|
40
39
|
"// query, route params, and JSON body into one input, runs `validate` at the",
|
|
41
40
|
"// boundary (a { success:false, fieldErrors } return is a 422, no action call),",
|
|
@@ -76,7 +75,7 @@ export async function writeApiGallery(appDir) {
|
|
|
76
75
|
"",
|
|
77
76
|
].join('\n'));
|
|
78
77
|
await writeFile(feat('rate-limit', 'route.ts'), [
|
|
79
|
-
"//
|
|
78
|
+
"// API backend-features demo. Keep and adapt it, or prune it (delete this app/api/features/rate-limit route).",
|
|
80
79
|
"// The middleware.ts beside this file stamps X-RateLimit-* headers and returns",
|
|
81
80
|
"// a 429 with Retry-After once the window is exhausted, so this handler never",
|
|
82
81
|
"// runs on a limited request. Call it six times in ten seconds to see the 429.",
|
|
@@ -89,7 +88,7 @@ export async function writeApiGallery(appDir) {
|
|
|
89
88
|
// 3) Streaming response (chunks flushed as produced, no buffering).
|
|
90
89
|
await mkdir(feat('stream'), { recursive: true });
|
|
91
90
|
await writeFile(feat('stream', 'route.ts'), [
|
|
92
|
-
"//
|
|
91
|
+
"// API backend-features demo. Keep and adapt it, or prune it (delete this app/api/features/stream route).",
|
|
93
92
|
"// Streams JSON-per-line chunks as they are produced, so a slow or large",
|
|
94
93
|
"// result is delivered incrementally instead of buffered whole. A hand-written",
|
|
95
94
|
"// route.ts returns a ReadableStream for full control. Served as text/plain so",
|
|
@@ -115,7 +114,7 @@ export async function writeApiGallery(appDir) {
|
|
|
115
114
|
// 4) File storage (upload + serve), both hand-written route.ts.
|
|
116
115
|
await mkdir(feat('files', '[key]'), { recursive: true });
|
|
117
116
|
await writeFile(feat('files', 'route.ts'), [
|
|
118
|
-
"//
|
|
117
|
+
"// API backend-features demo. Keep and adapt it, or prune it (delete this app/api/features/files route).",
|
|
119
118
|
"// POST a multipart file: the bytes stream into the FileStore (a local",
|
|
120
119
|
"// .webjs/uploads dir by default, gitignored; swap for S3/R2 with one",
|
|
121
120
|
"// setFileStore() call). Returns the key + a URL served by [key]/route.ts.",
|
|
@@ -166,7 +165,7 @@ export async function writeApiGallery(appDir) {
|
|
|
166
165
|
// 5) WebSocket endpoint with broadcast fan-out.
|
|
167
166
|
await mkdir(feat('ws'), { recursive: true });
|
|
168
167
|
await writeFile(feat('ws', 'route.ts'), [
|
|
169
|
-
"//
|
|
168
|
+
"// API backend-features demo. Keep and adapt it, or prune it (delete this app/api/features/ws route).",
|
|
170
169
|
"// A WebSocket endpoint: exporting WS(ws, req) upgrades this route to a socket.",
|
|
171
170
|
"// The framework auto-registers each connection to its path, so broadcast()",
|
|
172
171
|
"// fans a message out to every connected client. Connect two clients to",
|
package/lib/app-name.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-name validation for `webjs create <name>` (issue #1066).
|
|
3
|
+
*
|
|
4
|
+
* The name a user passes to `webjs create` is not just a directory name. It is
|
|
5
|
+
* ALSO interpolated into generated source as a template-literal value (the
|
|
6
|
+
* `metadata.title` in `app/page.ts`, the `name` field of the api template's
|
|
7
|
+
* root route handler, the `{{APP_NAME}}` substitution in every copied template
|
|
8
|
+
* file) and written verbatim into the generated `package.json` `name` field. A
|
|
9
|
+
* name carrying a quote, a backtick, a `${`, or a backslash therefore breaks
|
|
10
|
+
* the emitted file at the JS/TS syntax level, and the failure surfaces as a
|
|
11
|
+
* parse error on the very first `npm run dev` of the fresh app, far from its
|
|
12
|
+
* actual cause.
|
|
13
|
+
*
|
|
14
|
+
* Rather than escape at each interpolation site (an open-ended list that grows
|
|
15
|
+
* every time the scaffold emits a new file), the name is validated ONCE at the
|
|
16
|
+
* boundary, before any file is written. The rule is npm's package-name rules
|
|
17
|
+
* MINUS the lowercase-only clause (see `ALLOWED_CHAR`), which is still strictly
|
|
18
|
+
* narrower than every interpolation site needs, so a name that passes is safe
|
|
19
|
+
* everywhere the scaffold puts it. `npm init` and `create-next-app` do refuse
|
|
20
|
+
* uppercase; this deliberately does not, because the scaffold's manifest is
|
|
21
|
+
* private and a capital letter breaks nothing.
|
|
22
|
+
*
|
|
23
|
+
* This module is pure and imports nothing, so both the CLI entry
|
|
24
|
+
* (`bin/webjs.js`) and the programmatic entry (`scaffoldApp` in `lib/create.js`)
|
|
25
|
+
* can share one rule.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** npm's hard cap on a package name. */
|
|
29
|
+
export const APP_NAME_MAX_LENGTH = 214;
|
|
30
|
+
|
|
31
|
+
/** One-line description of the allowed shape, reused by both error surfaces. */
|
|
32
|
+
export const APP_NAME_SHAPE =
|
|
33
|
+
'letters, digits, and the separators "-", "." and "_", starting with a letter or a digit, at most 214 characters, and not a name npm reserves';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* npm rejects these outright, whatever else the name looks like. Matched
|
|
37
|
+
* case-insensitively, since the name is also a directory and `Node_Modules`
|
|
38
|
+
* collides with `node_modules` on a case-insensitive filesystem.
|
|
39
|
+
* @type {string[]}
|
|
40
|
+
*/
|
|
41
|
+
const RESERVED_NAMES = ['node_modules', 'favicon.ico'];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Every character allowed in an app name.
|
|
45
|
+
*
|
|
46
|
+
* npm's own rule for a PUBLISHED package name is lowercase-only, and this guard
|
|
47
|
+
* followed it at first. Uppercase is allowed back deliberately: it never broke
|
|
48
|
+
* anything. The scaffold's `package.json` is `private: true`, so the name is
|
|
49
|
+
* never published, and npm installs a capitalized private package without
|
|
50
|
+
* complaint (checked, not assumed). Every OTHER character this rule refuses
|
|
51
|
+
* corrupts generated source or the manifest, which is what the guard is for, so
|
|
52
|
+
* refusing a capital letter alongside them would have been a naming convention
|
|
53
|
+
* wearing a correctness costume, and `webjs create MyApp` worked before.
|
|
54
|
+
*/
|
|
55
|
+
const ALLOWED_CHAR = /[A-Za-z0-9._-]/;
|
|
56
|
+
|
|
57
|
+
/** What a name may start with, per the shape both error surfaces state. */
|
|
58
|
+
const ALLOWED_FIRST_CHAR = /[A-Za-z0-9]/;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* How much of the rejected name to echo back. The name is attacker-shaped by
|
|
62
|
+
* definition here (it is the thing that just failed validation), and it is
|
|
63
|
+
* echoed into a terminal, so it is capped rather than printed whole.
|
|
64
|
+
*/
|
|
65
|
+
const DISPLAY_MAX_LENGTH = 48;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Render the rejected name for an error message. Two things it must not do:
|
|
69
|
+
* break the message onto a second line (a thrown Error is read programmatically
|
|
70
|
+
* and documented as single-line, and a newline is a name `checkAppName`
|
|
71
|
+
* rejects), and replay a control sequence into the terminal it is printed to (a
|
|
72
|
+
* name of `\x1b[31m...` would otherwise repaint the user's shell). So every
|
|
73
|
+
* character with no safe visible form is named by code point, and the result is
|
|
74
|
+
* capped so a 214-character name cannot produce a 240-column line.
|
|
75
|
+
* @param {unknown} name
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
function describeName(name) {
|
|
79
|
+
const raw = typeof name === 'string' ? name : String(name);
|
|
80
|
+
let out = '';
|
|
81
|
+
for (const ch of raw) {
|
|
82
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
83
|
+
out += isUnprintable(code)
|
|
84
|
+
? `<U+${code.toString(16).toUpperCase().padStart(4, '0')}>`
|
|
85
|
+
: ch;
|
|
86
|
+
}
|
|
87
|
+
return out.length > DISPLAY_MAX_LENGTH ? `${out.slice(0, DISPLAY_MAX_LENGTH)}...` : out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Whether a code point has no safe visible form in an error message. C0 and
|
|
92
|
+
* DEL are the obvious set. Two additions matter here: the C1 range, which is
|
|
93
|
+
* where an 8-bit terminal reads control functions, and U+2028 / U+2029, which
|
|
94
|
+
* are JS LineTerminators, so they break the single-line contract exactly the
|
|
95
|
+
* way `\n` does while sailing past any `split('\n')` that claims to check it.
|
|
96
|
+
* @param {number} code
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
*/
|
|
99
|
+
function isUnprintable(code) {
|
|
100
|
+
return code < 0x20 || (code >= 0x7f && code <= 0x9f) || code === 0x2028 || code === 0x2029;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Render a character for an error message. A control character has no visible
|
|
105
|
+
* form, so name it by code point instead of printing it. The quote delimiter is
|
|
106
|
+
* picked to avoid the character itself, so a rejected apostrophe does not print
|
|
107
|
+
* as the unreadable `'''`.
|
|
108
|
+
* @param {string} ch
|
|
109
|
+
* @returns {string}
|
|
110
|
+
*/
|
|
111
|
+
function describeChar(ch) {
|
|
112
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
113
|
+
if (isUnprintable(code)) {
|
|
114
|
+
return `the control character U+${code.toString(16).toUpperCase().padStart(4, '0')}`;
|
|
115
|
+
}
|
|
116
|
+
if (ch === ' ') return 'a space';
|
|
117
|
+
const q = ch === "'" ? '"' : "'";
|
|
118
|
+
return `${q}${ch}${q}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Pure validator. Returns the first problem it finds, phrased as a sentence
|
|
123
|
+
* fragment that reads after "app name ... because".
|
|
124
|
+
*
|
|
125
|
+
* @param {unknown} name the raw name as typed
|
|
126
|
+
* @returns {{ ok: true } | { ok: false, reason: string }}
|
|
127
|
+
*/
|
|
128
|
+
export function checkAppName(name) {
|
|
129
|
+
// A non-string reaches here only from a programmatic caller, and calling it
|
|
130
|
+
// "empty" states something false about the input (a `123` is not empty), which
|
|
131
|
+
// is worse than no message at all when the reader is debugging their own call.
|
|
132
|
+
if (typeof name !== 'string') {
|
|
133
|
+
return { ok: false, reason: `an app name must be a string (this one is a ${typeof name})` };
|
|
134
|
+
}
|
|
135
|
+
if (name.length === 0) {
|
|
136
|
+
return { ok: false, reason: 'an app name cannot be empty' };
|
|
137
|
+
}
|
|
138
|
+
if (name.trim() !== name) {
|
|
139
|
+
return { ok: false, reason: 'an app name cannot start or end with whitespace' };
|
|
140
|
+
}
|
|
141
|
+
if (name.length > APP_NAME_MAX_LENGTH) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
reason: `an app name cannot be longer than ${APP_NAME_MAX_LENGTH} characters (this one is ${name.length})`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (RESERVED_NAMES.includes(name.toLowerCase())) {
|
|
148
|
+
return { ok: false, reason: `'${name}' is reserved by npm and cannot be a package name` };
|
|
149
|
+
}
|
|
150
|
+
// Report the first offending character, since that is what the user has to
|
|
151
|
+
// change.
|
|
152
|
+
for (const ch of name) {
|
|
153
|
+
if (ALLOWED_CHAR.test(ch)) continue;
|
|
154
|
+
return { ok: false, reason: `${describeChar(ch)} is not allowed in an app name` };
|
|
155
|
+
}
|
|
156
|
+
// Checked after the character scan so a leading quote is reported as the bad
|
|
157
|
+
// character rather than as a leading-separator problem. Every separator is
|
|
158
|
+
// refused here, not just the leading dot and underscore npm itself refuses:
|
|
159
|
+
// the shape both error surfaces state is "starting with a letter or a digit",
|
|
160
|
+
// and a rule the message claims but does not enforce is worse than either
|
|
161
|
+
// rule on its own. A leading hyphen also reads as a flag everywhere the name
|
|
162
|
+
// is later used as a directory.
|
|
163
|
+
if (!ALLOWED_FIRST_CHAR.test(name[0])) {
|
|
164
|
+
return { ok: false, reason: `an app name cannot start with ${describeChar(name[0])}` };
|
|
165
|
+
}
|
|
166
|
+
return { ok: true };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The multi-line message the CLI prints. Kept here so the programmatic throw
|
|
171
|
+
* and the CLI output agree on the rule they state.
|
|
172
|
+
* @param {unknown} name
|
|
173
|
+
* @param {string} reason
|
|
174
|
+
* @returns {string}
|
|
175
|
+
*/
|
|
176
|
+
export function appNameErrorMessage(name, reason) {
|
|
177
|
+
return `Error: invalid app name "${describeName(name)}".
|
|
178
|
+
|
|
179
|
+
${reason[0].toUpperCase()}${reason.slice(1)}.
|
|
180
|
+
|
|
181
|
+
The name becomes the app's directory, its package.json name, AND a value the
|
|
182
|
+
scaffold writes into generated source, so it is restricted to:
|
|
183
|
+
|
|
184
|
+
letters and digits
|
|
185
|
+
the separators "-", "." and "_"
|
|
186
|
+
starting with a letter or a digit
|
|
187
|
+
at most ${APP_NAME_MAX_LENGTH} characters
|
|
188
|
+
not ${RESERVED_NAMES.map((n) => `"${n}"`).join(' or ')} (npm reserves both)
|
|
189
|
+
|
|
190
|
+
Example: webjs create my-app`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Convenience wrapper for callers that want an exception. Throws with a
|
|
195
|
+
* single-line message (a thrown Error is read programmatically, so it stays
|
|
196
|
+
* compact) and returns the validated name otherwise.
|
|
197
|
+
* @param {unknown} name
|
|
198
|
+
* @returns {string}
|
|
199
|
+
*/
|
|
200
|
+
export function assertValidAppName(name) {
|
|
201
|
+
const result = checkAppName(name);
|
|
202
|
+
if (!result.ok) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Invalid app name '${describeName(name)}'. ${result.reason[0].toUpperCase()}${result.reason.slice(1)}. Allowed: ${APP_NAME_SHAPE}.`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return /** @type {string} */ (name);
|
|
208
|
+
}
|
package/lib/create.js
CHANGED
|
@@ -18,6 +18,7 @@ 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 { assertValidAppName } from './app-name.js';
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Detect which package manager invoked us. Reads `npm_config_user_agent`,
|
|
@@ -166,8 +167,11 @@ async function writeUiBootstrap(appDir) {
|
|
|
166
167
|
);
|
|
167
168
|
await writeFile(join(appDir, 'lib', 'utils', 'dom.ts'), domContent);
|
|
168
169
|
|
|
169
|
-
// 2) components.json:
|
|
170
|
-
//
|
|
170
|
+
// 2) components.json: byte for byte what `webjsui init` writes (see the
|
|
171
|
+
// DEFAULT_ALIASES / DEFAULT_TAILWIND_CSS constants in
|
|
172
|
+
// packages/ui/src/commands/init.js, which #1129 made the single source of
|
|
173
|
+
// these values). Keep the two in step: an app that scaffolds and one that
|
|
174
|
+
// runs `webjs ui init` must end up with the same config. The utils alias
|
|
171
175
|
// is lib/utils/cn so get-config.js's `+ '.ts'` resolves to lib/utils/cn.ts.
|
|
172
176
|
// The theme CSS lives at styles/globals.css, NOT app/globals.css: app/ is
|
|
173
177
|
// routing-only, so a non-routing stylesheet does not belong there.
|
|
@@ -255,9 +259,15 @@ function assertUiRegistryAvailable() {
|
|
|
255
259
|
* @param {string} cwd Current working directory
|
|
256
260
|
*/
|
|
257
261
|
export async function scaffoldApp(name, cwd, opts = {}) {
|
|
262
|
+
// Defence in depth, same as the template check below. `webjs create` already
|
|
263
|
+
// validates the name, but a programmatic caller can pass anything, and the
|
|
264
|
+
// name is interpolated into generated source as a template-literal value
|
|
265
|
+
// (#1066), so an unvalidated quote / backtick / `${` would emit a file that
|
|
266
|
+
// fails to parse. Throwing here happens before any directory is created.
|
|
267
|
+
assertValidAppName(name);
|
|
258
268
|
const template = opts.template || 'full-stack';
|
|
259
|
-
// A human-friendly display title for the example home page. The
|
|
260
|
-
// stays the raw slug
|
|
269
|
+
// A human-friendly display title for the example home page. The package
|
|
270
|
+
// `name` stays the raw slug as typed, but showing a hyphenated slug as
|
|
261
271
|
// a hero title looks unpolished, so title-case it for display ("my-app" ->
|
|
262
272
|
// "My App"). Replace this with your real brand anyway.
|
|
263
273
|
const displayName = name.replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
@@ -388,10 +398,12 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
388
398
|
'test:browser': 'webjs test --browser',
|
|
389
399
|
check: 'webjs check',
|
|
390
400
|
typecheck: 'webjs typecheck',
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
401
|
+
// Project health: a contributor runs `npm run doctor` after cloning to
|
|
402
|
+
// assert the toolchain (Node floor, tsconfig flag, env drift, vendor
|
|
403
|
+
// pins, @webjsdev versions, git hook), and CI runs the same script. Which
|
|
404
|
+
// findings are FATAL comes from the `webjs.doctor.gate` block below, so
|
|
405
|
+
// the environment-shaped checks (env drift, pin freshness over the
|
|
406
|
+
// network, the git hook) stay warns and cannot make CI flaky.
|
|
395
407
|
doctor: 'webjs doctor',
|
|
396
408
|
'db:generate': 'webjs db generate',
|
|
397
409
|
'db:migrate': 'webjs db migrate',
|
|
@@ -481,6 +493,17 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
481
493
|
}),
|
|
482
494
|
},
|
|
483
495
|
start: { before: isApi ? ['webjs db migrate'] : ['webjs db migrate', cssBuildCmd] },
|
|
496
|
+
// Which doctor findings are FATAL is the app's own call (#1257), declared
|
|
497
|
+
// here rather than in the CI workflow so `npm run doctor` locally and the
|
|
498
|
+
// workflow step agree about what fails. UNMARKED_ASSET_LINKS starts at
|
|
499
|
+
// error because an un-versioned /public url is a real deploy-staleness
|
|
500
|
+
// bug (it shipped a visible regression on webjs.dev) and the generated
|
|
501
|
+
// layout already writes asset(), so a fresh app is green on day one.
|
|
502
|
+
// Two checks are fatal with no entry here at all, NODE_VERSION and
|
|
503
|
+
// TSCONFIG_ERASABLE, because either would 500 the app at runtime;
|
|
504
|
+
// everything else keeps its default warn. Add a code with "off" to
|
|
505
|
+
// silence it, or "error" to make it fatal too.
|
|
506
|
+
doctor: { gate: { UNMARKED_ASSET_LINKS: 'error' } },
|
|
484
507
|
},
|
|
485
508
|
}, null, 2) + '\n');
|
|
486
509
|
|
|
@@ -1149,6 +1172,7 @@ ${uiThemeRaw}
|
|
|
1149
1172
|
await copyGallery(appDir);
|
|
1150
1173
|
|
|
1151
1174
|
await writeFile(join(appDir, 'app', 'layout.ts'), `import { html, cspNonce, asset } from '@webjsdev/core';
|
|
1175
|
+
import type { LayoutProps } from '@webjsdev/core';
|
|
1152
1176
|
import '#components/theme-toggle.ts';
|
|
1153
1177
|
|
|
1154
1178
|
/**
|
|
@@ -1167,7 +1191,11 @@ import '#components/theme-toggle.ts';
|
|
|
1167
1191
|
// lives at public/favicon.svg and serves at /public/favicon.svg.
|
|
1168
1192
|
export const metadata = { icons: '/public/favicon.svg' };
|
|
1169
1193
|
|
|
1170
|
-
|
|
1194
|
+
// LayoutProps types every layout argument (children, params, searchParams,
|
|
1195
|
+
// url) from the framework, so children is a TemplateResult rather than an
|
|
1196
|
+
// untyped value. Derive types like this everywhere instead of widening to
|
|
1197
|
+
// unknown; see .agents/skills/webjs/references/typescript.md.
|
|
1198
|
+
export default function RootLayout({ children }: LayoutProps) {
|
|
1171
1199
|
// Read the in-flight request's CSP nonce so the theme-detection inline script
|
|
1172
1200
|
// passes strict CSP. Returns '' when no CSP nonce is set.
|
|
1173
1201
|
const nonce = cspNonce();
|