@webjsdev/cli 0.10.66 → 0.10.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/webjs.js +11 -1
- package/lib/app-icon.js +55 -0
- package/lib/create.js +14 -9
- package/lib/dev-hot-rerun.js +54 -0
- package/lib/dev-reload.js +35 -2
- package/lib/dev-supervisor.js +38 -3
- package/lib/doctor/codes.js +1 -0
- package/lib/doctor/probes/app-icon.js +36 -0
- package/lib/doctor/runner.js +2 -0
- package/lib/gallery-shell-files.js +1 -1
- package/package.json +2 -2
- package/templates/.agents/skills/webjs/SKILL.md +2 -1
- package/templates/.agents/skills/webjs/references/built-ins.md +2 -0
- package/templates/.agents/skills/webjs/references/components.md +1 -1
- package/templates/.agents/skills/webjs/references/data-and-actions.md +2 -0
- package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +2 -0
- package/templates/.agents/skills/webjs/references/routing-and-pages.md +20 -4
- package/templates/.agents/skills/webjs/references/runtime.md +4 -4
- package/templates/gallery/app/icon.ts +8 -6
- package/templates/gallery/app/manifest.ts +4 -1
- package/templates/scripts/clear-gallery.mjs +3 -3
- package/templates/public/favicon.svg +0 -12
package/bin/webjs.js
CHANGED
|
@@ -456,6 +456,12 @@ async function refuseOutsideApp(command) {
|
|
|
456
456
|
}
|
|
457
457
|
|
|
458
458
|
async function main() {
|
|
459
|
+
// A `bun --hot` re-run of the dev server child (#1575): the first run's
|
|
460
|
+
// server owns the process, so hand it the reload and stop here.
|
|
461
|
+
if (cmd === 'dev' && process.env.__WEBJS_DEV_CHILD === '1' && process.versions.bun) {
|
|
462
|
+
const { rerunHotDevServer } = await import('../lib/dev-hot-rerun.js');
|
|
463
|
+
if (await rerunHotDevServer()) return;
|
|
464
|
+
}
|
|
459
465
|
// `--version` / `-v` (top level): print the installed CLI version and exit.
|
|
460
466
|
if (cmd === '--version' || cmd === '-v') {
|
|
461
467
|
console.log(readCliVersion());
|
|
@@ -526,7 +532,11 @@ async function main() {
|
|
|
526
532
|
// parent is gone (killed outright, or crashed), and a child left
|
|
527
533
|
// running would hold the port against the next `webjs dev`. Unref'd
|
|
528
534
|
// so the channel itself never keeps this process alive.
|
|
529
|
-
|
|
535
|
+
// Once per process: `bun --hot` re-runs this file on every edit
|
|
536
|
+
// (#1575), and each run used to add another listener.
|
|
537
|
+
const g = /** @type {any} */ (globalThis);
|
|
538
|
+
if (process.connected && !g.__webjsDevDisconnectHooked) {
|
|
539
|
+
g.__webjsDevDisconnectHooked = true;
|
|
530
540
|
process.channel?.unref?.();
|
|
531
541
|
process.on('disconnect', () => process.exit(0));
|
|
532
542
|
}
|
package/lib/app-icon.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app icon a new scaffold starts with, and how `webjs doctor` recognises
|
|
3
|
+
* an icon that was never replaced.
|
|
4
|
+
*
|
|
5
|
+
* A scaffold ships `app/icon.svg` (auto-linked as the favicon, served at
|
|
6
|
+
* /icon.svg and as the /favicon.ico fallback) and `app/manifest.webmanifest`
|
|
7
|
+
* (auto-linked as the web app manifest). The icon is a deliberately NEUTRAL
|
|
8
|
+
* placeholder: a grey tile with a dashed frame, so a tab strip shows at a
|
|
9
|
+
* glance that the app has no icon of its own yet, and no app ever ships
|
|
10
|
+
* looking like a WebJs demo. It carries `data-webjs-placeholder` so a check
|
|
11
|
+
* (webjs doctor, or any agent's own tester) can tell it apart from a real
|
|
12
|
+
* icon without comparing bytes.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The attribute that marks the scaffold's placeholder icon. */
|
|
16
|
+
export const PLACEHOLDER_MARKER = 'data-webjs-placeholder';
|
|
17
|
+
|
|
18
|
+
/** The placeholder `app/icon.svg`. Replace it with the app's own mark. */
|
|
19
|
+
export const PLACEHOLDER_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" ${PLACEHOLDER_MARKER}="icon">
|
|
20
|
+
<!-- PLACEHOLDER app icon from \`webjs create\`. Replace this file with the
|
|
21
|
+
app's own icon: a simple symbol for what the app is, in its colours,
|
|
22
|
+
legible at 16px. Keep the file name (app/icon.svg) and the framework
|
|
23
|
+
links it, serves it and answers /favicon.ico with it. -->
|
|
24
|
+
<rect width="32" height="32" rx="8" fill="#d4d4d8"/>
|
|
25
|
+
<rect x="7" y="7" width="18" height="18" rx="3" fill="none" stroke="#71717a" stroke-width="2" stroke-dasharray="3 2.4"/>
|
|
26
|
+
</svg>
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The `app/manifest.webmanifest` a scaffold starts with: the app's name, the
|
|
31
|
+
* neutral colours of the scaffold palette, and the icon. Grow it with the app
|
|
32
|
+
* (its real theme colour, a 192 and 512 PNG for installability).
|
|
33
|
+
* @param {string} name the app's display name
|
|
34
|
+
*/
|
|
35
|
+
export function appManifest(name) {
|
|
36
|
+
return JSON.stringify({
|
|
37
|
+
name,
|
|
38
|
+
short_name: name,
|
|
39
|
+
start_url: '/',
|
|
40
|
+
display: 'standalone',
|
|
41
|
+
background_color: '#ffffff',
|
|
42
|
+
theme_color: '#ffffff',
|
|
43
|
+
icons: [{ src: '/icon.svg', sizes: 'any', type: 'image/svg+xml' }],
|
|
44
|
+
}, null, 2) + '\n';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether an SVG is the WebJs brand mark earlier scaffolds shipped as
|
|
49
|
+
* `public/favicon.svg` (a rounded square with the gallery's grey gradient).
|
|
50
|
+
* Apps made before the placeholder still serve it as their favicon.
|
|
51
|
+
* @param {string} svg
|
|
52
|
+
*/
|
|
53
|
+
export function isLegacyBrandFavicon(svg) {
|
|
54
|
+
return /aria-label="WebJs"/.test(svg) && /<linearGradient id="wj"/.test(svg);
|
|
55
|
+
}
|
package/lib/create.js
CHANGED
|
@@ -21,6 +21,7 @@ import { bunifyProse, bunifyDockerfile, bunifyCompose, bunifyCi, bunifyEnvExampl
|
|
|
21
21
|
import { postgresCompose, postgresCi } from './db-rewrite.js';
|
|
22
22
|
import { assertValidAppName, toDatabaseName } from './app-name.js';
|
|
23
23
|
import { isGalleryAppShellFile } from './gallery-shell-files.js';
|
|
24
|
+
import { PLACEHOLDER_ICON_SVG, appManifest } from './app-icon.js';
|
|
24
25
|
import { detectPackageManager } from './package-manager.js';
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -1246,10 +1247,15 @@ export type ActionResult<T> =
|
|
|
1246
1247
|
const swSrc = join(TEMPLATES, 'public', swFile);
|
|
1247
1248
|
if (existsSync(swSrc)) await cp(swSrc, join(publicDir, swFile));
|
|
1248
1249
|
}
|
|
1249
|
-
//
|
|
1250
|
-
// the
|
|
1251
|
-
|
|
1252
|
-
|
|
1250
|
+
// The app icon and web app manifest: `app/icon.svg` (a neutral PLACEHOLDER
|
|
1251
|
+
// the agent replaces with the app's own icon; `webjs doctor` warns while it
|
|
1252
|
+
// is still there) and `app/manifest.webmanifest` (the app's name). The
|
|
1253
|
+
// framework links both into <head> and answers /favicon.ico with the icon,
|
|
1254
|
+
// so the layout declares nothing. They ship with the app, not the gallery,
|
|
1255
|
+
// so they survive `gallery:clear`.
|
|
1256
|
+
await mkdir(join(appDir, 'app'), { recursive: true });
|
|
1257
|
+
await writeFile(join(appDir, 'app', 'icon.svg'), PLACEHOLDER_ICON_SVG);
|
|
1258
|
+
await writeFile(join(appDir, 'app', 'manifest.webmanifest'), appManifest(displayName));
|
|
1253
1259
|
|
|
1254
1260
|
// The gallery-reset script (wired as `gallery:clear`). Only UI templates have
|
|
1255
1261
|
// a gallery, so it ships here (NOT in the flat templateFiles list, which would
|
|
@@ -1355,11 +1361,10 @@ import '#components/theme-toggle.ts';
|
|
|
1355
1361
|
* text-foreground, bg-card, bg-primary, and border-border all work.
|
|
1356
1362
|
*/
|
|
1357
1363
|
|
|
1358
|
-
//
|
|
1359
|
-
//
|
|
1360
|
-
//
|
|
1361
|
-
//
|
|
1362
|
-
export const metadata = { icons: '/public/favicon.svg' };
|
|
1364
|
+
// The favicon and manifest are app/icon.svg and app/manifest.webmanifest: the
|
|
1365
|
+
// framework links them into <head> itself, so nothing is declared here. Replace
|
|
1366
|
+
// the placeholder icon with this app's own (see the skill's routing-and-pages
|
|
1367
|
+
// reference, "App icon").
|
|
1363
1368
|
|
|
1364
1369
|
// LayoutProps types every layout argument (children, params, searchParams,
|
|
1365
1370
|
// url) from the framework, so children is a TemplateResult rather than an
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fast path for a `bun --hot` re-run of the `webjs dev` server child
|
|
3
|
+
* (#1575).
|
|
4
|
+
*
|
|
5
|
+
* `bun --hot` re-evaluates this CLI on every reload. The first run's server
|
|
6
|
+
* owns the process (`dev/hot-host.js` in `@webjsdev/server`) and only needs to
|
|
7
|
+
* hear that the module registry was reset, so a re-run calls the host directly
|
|
8
|
+
* instead of importing the whole server again just to reach `startServer`,
|
|
9
|
+
* which re-evaluated every framework module on every edit for nothing.
|
|
10
|
+
*
|
|
11
|
+
* The one thing a re-run must still notice is the framework itself changing
|
|
12
|
+
* under it (an upgrade while `webjs dev` runs): the first run's code cannot
|
|
13
|
+
* load the new copy in place, so the child exits and the supervisor starts a
|
|
14
|
+
* fresh process.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const HOSTS = Symbol.for('webjs.dev.hotHosts');
|
|
21
|
+
|
|
22
|
+
/** The installed `@webjsdev/server` version, or '' when it cannot be read. */
|
|
23
|
+
function installedServerVersion() {
|
|
24
|
+
try {
|
|
25
|
+
const entry = fileURLToPath(import.meta.resolve('@webjsdev/server'));
|
|
26
|
+
return JSON.parse(readFileSync(join(dirname(entry), 'package.json'), 'utf8')).version || '';
|
|
27
|
+
} catch {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Hand a re-run to the live dev server. Returns false when there is none (the
|
|
34
|
+
* first run), so the caller starts the server as usual.
|
|
35
|
+
*
|
|
36
|
+
* @param {{ exit?: (code: number) => void, log?: (line: string) => void }} [io]
|
|
37
|
+
* @returns {Promise<boolean>}
|
|
38
|
+
*/
|
|
39
|
+
export async function rerunHotDevServer(io = {}) {
|
|
40
|
+
const exit = io.exit || ((code) => process.exit(code));
|
|
41
|
+
const log = io.log || ((line) => console.log(line));
|
|
42
|
+
const hosts = /** @type {any} */ (globalThis)[HOSTS];
|
|
43
|
+
if (!(hosts instanceof Map) || hosts.size === 0) return false;
|
|
44
|
+
const version = installedServerVersion();
|
|
45
|
+
for (const host of hosts.values()) {
|
|
46
|
+
if (version && host.version && host.version !== version) {
|
|
47
|
+
log(`[webjs] @webjsdev/server changed (${host.version} -> ${version}), restarting the dev server`);
|
|
48
|
+
exit(0);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const host of hosts.values()) await host.rerun();
|
|
53
|
+
return true;
|
|
54
|
+
}
|
package/lib/dev-reload.js
CHANGED
|
@@ -32,6 +32,13 @@ import { PORT_IN_USE_EXIT_CODE } from './port.js';
|
|
|
32
32
|
*/
|
|
33
33
|
export const RESTART_DEBOUNCE_MS = 50;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The IPC message a dev server child sends when it reloads every module in
|
|
37
|
+
* place under `bun --hot`, plugin-served ones included (#1575). Mirrors
|
|
38
|
+
* `HOT_IN_PLACE_MESSAGE` in `@webjsdev/server`'s `dev/hot-host.js`.
|
|
39
|
+
*/
|
|
40
|
+
export const HOT_IN_PLACE_MESSAGE = 'hot-reload-in-place';
|
|
41
|
+
|
|
35
42
|
/**
|
|
36
43
|
* Delays before restarting a child that exited on its own (a crash), indexed
|
|
37
44
|
* by consecutive crash count and capped at the last entry. A file change
|
|
@@ -338,12 +345,35 @@ export function createSupervisor({
|
|
|
338
345
|
};
|
|
339
346
|
}
|
|
340
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Which changed paths restart the live child. A Bun child that reloads
|
|
350
|
+
* plugin-served modules in place announces it over IPC once it is up (#1575);
|
|
351
|
+
* from then on only `plan.inPlaceRestartFor` (the boot hooks) restarts it.
|
|
352
|
+
* Per child: `reset()` on every spawn, so a child that never announces (an
|
|
353
|
+
* older `@webjsdev/server`) keeps the restarts it needs.
|
|
354
|
+
*
|
|
355
|
+
* @param {{ restartFor?: (path: string) => boolean, inPlaceRestartFor?: (path: string) => boolean }} plan
|
|
356
|
+
*/
|
|
357
|
+
export function inPlaceRestarts(plan) {
|
|
358
|
+
let inPlace = false;
|
|
359
|
+
const base = plan.restartFor || (() => true);
|
|
360
|
+
return {
|
|
361
|
+
/** @param {string} p */
|
|
362
|
+
restartFor: (p) => (inPlace && plan.inPlaceRestartFor ? plan.inPlaceRestartFor(p) : base(p)),
|
|
363
|
+
/** @param {unknown} msg */
|
|
364
|
+
onMessage: (msg) => {
|
|
365
|
+
if (msg && typeof msg === 'object' && /** @type {any} */ (msg).webjs === HOT_IN_PLACE_MESSAGE) inPlace = true;
|
|
366
|
+
},
|
|
367
|
+
reset: () => { inPlace = false; },
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
341
371
|
/**
|
|
342
372
|
* Run the dev server under the supervisor until a signal stops it.
|
|
343
373
|
*
|
|
344
374
|
* @param {{
|
|
345
375
|
* cwd: string,
|
|
346
|
-
* plan: { args: string[], restartOnChange: boolean, restartFor?: (path: string) => boolean, watchDirs: string[], watchFiles: string[] },
|
|
376
|
+
* plan: { args: string[], restartOnChange: boolean, restartFor?: (path: string) => boolean, inPlaceRestartFor?: (path: string) => boolean, watchDirs: string[], watchFiles: string[] },
|
|
347
377
|
* env: NodeJS.ProcessEnv,
|
|
348
378
|
* onExit: (code: number) => void,
|
|
349
379
|
* }} opts
|
|
@@ -366,14 +396,16 @@ export function superviseDevServer({ cwd, plan, env, onExit }) {
|
|
|
366
396
|
sup.stop().then(() => onExit(code));
|
|
367
397
|
};
|
|
368
398
|
|
|
399
|
+
const restarts = inPlaceRestarts(plan);
|
|
369
400
|
const sup = createSupervisor({
|
|
370
401
|
restartOnChange: plan.restartOnChange,
|
|
371
|
-
...(plan.restartFor ? { restartFor:
|
|
402
|
+
...(plan.restartFor ? { restartFor: restarts.restartFor } : {}),
|
|
372
403
|
log,
|
|
373
404
|
// The child already printed why (the port and its holder); stop with its
|
|
374
405
|
// code rather than restarting a server that can never bind.
|
|
375
406
|
onFinal: (code) => shutdown(code),
|
|
376
407
|
spawnChild: () => {
|
|
408
|
+
restarts.reset();
|
|
377
409
|
const c = spawn(process.execPath, plan.args, {
|
|
378
410
|
// The IPC channel lets the child notice this process is gone and exit,
|
|
379
411
|
// so a killed supervisor never leaves an orphan holding the port.
|
|
@@ -381,6 +413,7 @@ export function superviseDevServer({ cwd, plan, env, onExit }) {
|
|
|
381
413
|
cwd,
|
|
382
414
|
env,
|
|
383
415
|
});
|
|
416
|
+
c.on('message', restarts.onMessage);
|
|
384
417
|
// A failed spawn emits 'error' and may never emit 'exit'; report it as
|
|
385
418
|
// an exit so the backoff retries it (a repeated exit is ignored).
|
|
386
419
|
c.on('error', (err) => {
|
package/lib/dev-supervisor.js
CHANGED
|
@@ -46,7 +46,29 @@ export const WATCH_DIRS = ['app', 'components', 'modules', 'lib', 'actions'];
|
|
|
46
46
|
* never restarts the dev server when edited, which is the quiet half of the
|
|
47
47
|
* bug where a `middleware.ts` was loaded by neither.
|
|
48
48
|
*/
|
|
49
|
-
export const WATCH_FILES = ['middleware.ts', 'middleware.js', 'middleware.mts', 'middleware.mjs'];
|
|
49
|
+
export const WATCH_FILES = ['middleware.ts', 'middleware.js', 'middleware.mts', 'middleware.mjs', ...bootFiles()];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The app-root boot hooks the server runs ONCE per process (#1575):
|
|
53
|
+
* `instrumentation.*` (its `register()`) and `env.*` (the env validation). An
|
|
54
|
+
* edit to one restarts the dev server on either runtime, because no in-place
|
|
55
|
+
* reload re-runs them.
|
|
56
|
+
* @returns {string[]}
|
|
57
|
+
*/
|
|
58
|
+
function bootFiles() {
|
|
59
|
+
return ['instrumentation', 'env'].flatMap((n) => ['ts', 'js', 'mts', 'mjs'].map((x) => `${n}.${x}`));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const BOOT_FILE = /^(?:instrumentation|env)\.m?[jt]s$/;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether a changed (app-relative) path is a boot hook (see `bootFiles`).
|
|
66
|
+
* @param {string} path
|
|
67
|
+
* @returns {boolean}
|
|
68
|
+
*/
|
|
69
|
+
export function isBootFile(path) {
|
|
70
|
+
return BOOT_FILE.test(path);
|
|
71
|
+
}
|
|
50
72
|
|
|
51
73
|
/**
|
|
52
74
|
* Plan how `webjs dev` runs its server.
|
|
@@ -56,7 +78,7 @@ export const WATCH_FILES = ['middleware.ts', 'middleware.js', 'middleware.mts',
|
|
|
56
78
|
* @param {string[]} opts.argv `process.argv.slice(1)` (the script path followed by its args), forwarded to the child verbatim.
|
|
57
79
|
* @param {boolean} opts.noHot Whether `--no-hot` was passed (opt out of the supervisor entirely).
|
|
58
80
|
* @param {boolean} [opts.sourceLocations] Whether dev source locations are on (`WEBJS_SOURCE_LOCATIONS` / `webjs.dev.sourceLocations`), which puts every app module behind a Bun plugin.
|
|
59
|
-
* @returns {{ mode: 'inline' } | { mode: 'supervise', args: string[], restartOnChange: boolean, restartFor?: (path: string) => boolean, watchDirs: string[], watchFiles: string[] }}
|
|
81
|
+
* @returns {{ mode: 'inline' } | { mode: 'supervise', args: string[], restartOnChange: boolean, restartFor?: (path: string) => boolean, inPlaceRestartFor?: (path: string) => boolean, watchDirs: string[], watchFiles: string[] }}
|
|
60
82
|
* `inline` runs the server in this process (no reload watcher); `supervise`
|
|
61
83
|
* spawns `process.execPath` with `args` and `__WEBJS_DEV_CHILD=1` under the
|
|
62
84
|
* supervisor, which watches `watchDirs` (recursively) and `watchFiles` (at
|
|
@@ -71,7 +93,20 @@ export function planDevSupervisor({ isBun, argv, noHot, sourceLocations = false
|
|
|
71
93
|
if (noHot) return { mode: 'inline' };
|
|
72
94
|
|
|
73
95
|
const watch = { watchDirs: [...WATCH_DIRS], watchFiles: [...WATCH_FILES] };
|
|
74
|
-
if (isBun)
|
|
96
|
+
if (isBun) {
|
|
97
|
+
// `restartFor` covers a server that cannot reload a plugin-served module in
|
|
98
|
+
// place. A server that can says so over IPC once it is up (#1575), and the
|
|
99
|
+
// supervisor narrows to `inPlaceRestartFor`: only the boot hooks restart.
|
|
100
|
+
const plugin = bunPluginServed(sourceLocations);
|
|
101
|
+
return {
|
|
102
|
+
mode: 'supervise',
|
|
103
|
+
args: ['--hot', ...argv],
|
|
104
|
+
restartOnChange: true,
|
|
105
|
+
restartFor: (p) => plugin(p) || isBootFile(p),
|
|
106
|
+
inPlaceRestartFor: isBootFile,
|
|
107
|
+
...watch,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
75
110
|
return { mode: 'supervise', args: [...argv], restartOnChange: true, ...watch };
|
|
76
111
|
}
|
|
77
112
|
|
package/lib/doctor/codes.js
CHANGED
|
@@ -50,6 +50,7 @@ export const DOCTOR_CODES = {
|
|
|
50
50
|
'Static build outputs (dev.regenerate freshness)': 'STATIC_ASSET_FRESHNESS',
|
|
51
51
|
'Asset urls (unmarked stylesheet links)': 'UNMARKED_ASSET_LINKS',
|
|
52
52
|
'workspace-overrides': 'WORKSPACE_OVERRIDES',
|
|
53
|
+
'app-icon': 'APP_ICON',
|
|
53
54
|
};
|
|
54
55
|
|
|
55
56
|
/**
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { PLACEHOLDER_MARKER, isLegacyBrandFavicon } from '../../app-icon.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {import('../codes.js').DoctorResult} DoctorResult
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** @param {string} file */
|
|
10
|
+
function read(file) {
|
|
11
|
+
try { return readFileSync(file, 'utf8'); } catch { return ''; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Warn when the app's favicon is still the one `webjs create` shipped: the
|
|
16
|
+
* neutral `app/icon.svg` placeholder, or the WebJs brand mark earlier
|
|
17
|
+
* scaffolds put at `public/favicon.svg`. Either way every app made from the
|
|
18
|
+
* scaffold shows the same tab icon, which reads as a demo rather than a
|
|
19
|
+
* product. An app with no `app/` directory (a library, the api template with
|
|
20
|
+
* no pages) passes.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} appDir
|
|
23
|
+
* @returns {DoctorResult}
|
|
24
|
+
*/
|
|
25
|
+
export function checkAppIcon(appDir) {
|
|
26
|
+
const name = 'app-icon';
|
|
27
|
+
if (!existsSync(join(appDir, 'app'))) return { name, status: 'pass', message: 'no app/ directory to analyse' };
|
|
28
|
+
const fix = 'Replace app/icon.svg with an icon for this app (a simple symbol for what it is, in its own colours, legible at 16px), and delete public/favicon.svg plus any metadata.icons that still points at it.';
|
|
29
|
+
if (read(join(appDir, 'app', 'icon.svg')).includes(PLACEHOLDER_MARKER)) {
|
|
30
|
+
return { name, status: 'warn', message: 'app/icon.svg is still the scaffold placeholder icon', fix };
|
|
31
|
+
}
|
|
32
|
+
if (isLegacyBrandFavicon(read(join(appDir, 'public', 'favicon.svg')))) {
|
|
33
|
+
return { name, status: 'warn', message: 'public/favicon.svg is still the WebJs mark an earlier scaffold shipped', fix };
|
|
34
|
+
}
|
|
35
|
+
return { name, status: 'pass', message: 'the app has its own icon (or declares none)' };
|
|
36
|
+
}
|
package/lib/doctor/runner.js
CHANGED
|
@@ -12,6 +12,7 @@ import { checkStaticAssetFreshness } from './probes/static-asset-freshness.js';
|
|
|
12
12
|
import { checkUnmarkedAssetLinks } from './probes/unmarked-asset-links.js';
|
|
13
13
|
import { checkFrameworkResolves, checkFrameworkLinks } from './probes/framework-resolves.js';
|
|
14
14
|
import { checkWorkspaceOverrides } from './probes/workspace-overrides.js';
|
|
15
|
+
import { checkAppIcon } from './probes/app-icon.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* @typedef {import('./codes.js').DoctorResult} DoctorResult
|
|
@@ -66,6 +67,7 @@ export async function runDoctorChecks(appDir, opts = {}) {
|
|
|
66
67
|
checkStaticAssetFreshness(appDir),
|
|
67
68
|
checkUnmarkedAssetLinks(appDir),
|
|
68
69
|
Promise.resolve(checkWorkspaceOverrides(appDir)),
|
|
70
|
+
Promise.resolve(checkAppIcon(appDir)),
|
|
69
71
|
]);
|
|
70
72
|
// Attach the stable machine code to every result (#975). Centralized here so
|
|
71
73
|
// each check function stays free of the code-contract concern.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* live app deployed on its own, so it needs a root layout, a home page, a theme
|
|
7
7
|
* toggle, and the `cn()` helper. The scaffold writes all four itself, with
|
|
8
8
|
* things the gallery's copies cannot carry: the app's `displayName`, the
|
|
9
|
-
* `cspNonce()` wiring, `LayoutProps` typing,
|
|
9
|
+
* `cspNonce()` wiring, `LayoutProps` typing, and
|
|
10
10
|
* a `cn.ts` read verbatim from the `@webjsdev/ui` registry so `webjs ui add`
|
|
11
11
|
* stays in lockstep with the kit.
|
|
12
12
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.68",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The CLI for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Runs the dev and production servers, scaffolds apps, validates conventions, and drives the database. Node 24+ or Bun.",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@webjsdev/mcp": "^0.1.0",
|
|
21
|
-
"@webjsdev/server": "^0.8.
|
|
21
|
+
"@webjsdev/server": "^0.8.86",
|
|
22
22
|
"@webjsdev/ui": "^0.3.15"
|
|
23
23
|
},
|
|
24
24
|
"publishConfig": {
|
|
@@ -44,6 +44,7 @@ Rows point rather than explain. The reference is the authority on the rule, and
|
|
|
44
44
|
| add a URL, static or with a dynamic segment | a file at `app/<path>/page.ts`, `[id]` for a param | registering the route in a table or config | `references/routing-and-pages.md` | `app/features/routing` |
|
|
45
45
|
| abandon a render because something is missing or not allowed | throw `notFound()` / `forbidden()` / `unauthorized()` | returning an error object and branching in the template | `references/routing-and-pages.md` | `app/features/boundaries` |
|
|
46
46
|
| set a page's title, description, or social preview | `export const metadata` or `generateMetadata()` | writing `<head>` tags in the page | `references/routing-and-pages.md` | `app/features/metadata` |
|
|
47
|
+
| give the app its own favicon, home-screen icon and manifest | replace the placeholder `app/icon.svg` with a simple symbol for the app in its colours, add `app/apple-icon.png`, edit `app/manifest.webmanifest` | leaving the scaffold placeholder, or a hand-written `<link rel="icon">` | `references/routing-and-pages.md` (App icon and manifest) | `app/icon.ts` |
|
|
47
48
|
| make part of the page respond to a click or hold state | a `WebComponent` custom element | expecting the page's own markup to hydrate | `references/components.md` | `app/features/components` |
|
|
48
49
|
| render a keyed list, or swap one node when state changes | `repeat()` / `watch()` from `/directives` | re-rendering the component or diffing by hand | `references/components.md` | `app/features/directives` |
|
|
49
50
|
| get server data into a component's first paint | `async render()` awaiting an action | fetching in `connectedCallback`, which SSR never calls | `references/components.md` | `app/features/async-render` |
|
|
@@ -174,7 +175,7 @@ Find the right export fast. Load the linked reference for full examples.
|
|
|
174
175
|
|
|
175
176
|
### File conventions
|
|
176
177
|
|
|
177
|
-
`page.ts` (server-only fn), `layout.ts` (embeds `children`), `route.ts` (HTTP handler), `middleware.ts`, `*.server.ts` (server boundary), `error.ts` / `loading.ts` / `not-found.ts` / `forbidden.ts` / `unauthorized.ts` (boundaries), metadata routes (`sitemap.ts`, `robots.ts`, `manifest.ts`, `icon.ts`, `opengraph-image.ts`).
|
|
178
|
+
`page.ts` (server-only fn), `layout.ts` (embeds `children`), `route.ts` (HTTP handler), `middleware.ts`, `*.server.ts` (server boundary), `error.ts` / `loading.ts` / `not-found.ts` / `forbidden.ts` / `unauthorized.ts` (boundaries), metadata routes (`sitemap.ts`, `robots.ts`, `manifest.ts`, `icon.ts`, `opengraph-image.ts`), and the static app-root icon files (`icon.svg`, `apple-icon.png`, `manifest.webmanifest`, `favicon.ico`), auto-linked into `<head>`. The scaffold's `app/icon.svg` is a placeholder: replace it with the app's own icon.
|
|
178
179
|
|
|
179
180
|
## Canonical Patterns
|
|
180
181
|
|
|
@@ -296,6 +296,8 @@ Three levels, the same scale ESLint uses: `error` fails the exit, `warn` reports
|
|
|
296
296
|
|
|
297
297
|
Two guarantees worth knowing. A result that could not check (a network or toolchain outage) is capped at `warn` and can never be escalated, so a jspm or npm outage cannot red your CI. And a malformed gate exits 1 naming the offender rather than being ignored, so a typo cannot silently un-gate the build. That covers an unknown code, a bad severity, a wrong shape (a non-object `doctor` or `gate`), and a misspelled sibling of `gate` such as `gates`, since every one of those would otherwise leave the build un-gated while the `package.json` looks gated. Under `--json` the offenders come back as a `configErrors` array alongside an empty `results`, each entry a `{ kind }` of `malformed` / `unknown-key` / `unknown-code` / `bad-severity`. Wire it up with one workflow step, `npm run doctor`, and change what is fatal in `package.json` rather than in the workflow.
|
|
298
298
|
|
|
299
|
+
`APP_ICON` warns while the favicon is still the scaffold's: the placeholder `app/icon.svg` (marked `data-webjs-placeholder`) or the WebJs mark older scaffolds shipped at `public/favicon.svg`. Replace it with the app's own icon (`references/routing-and-pages.md`, "App icon and manifest").
|
|
300
|
+
|
|
299
301
|
### Dependency audit allowlist
|
|
300
302
|
|
|
301
303
|
`webjs audit` runs `npm audit` or `bun audit` (by the nearest lockfile, so a workspace member uses the root's) and fails on any advisory at or above `webjs.audit.level` (default `high`) that `webjs.audit.ignore` does not list. The scaffold's `Security: dependency audit` CI step runs it.
|
|
@@ -30,7 +30,7 @@ Most of what follows restates widely held component-model advice, ported. Lit's
|
|
|
30
30
|
|
|
31
31
|
**4. ARIA state is a hole in `render()`, derived from the same state that drives behaviour.** `aria-expanded=${this.open ? 'true' : 'false'}` cannot disagree with `this.open`. A second function that re-finds the button and calls `setAttribute` can, and does, the first time someone adds a close path that forgets to call it. The same holds for `class`, `?disabled`, and any `.prop`. Two caveats ride this rule:
|
|
32
32
|
|
|
33
|
-
- A plain-attribute hole follows one rule on the server and the client (#1573): `null` / `undefined` omit the attribute, `false` omits it except on an `aria-*` name, where it is written as `"false"
|
|
33
|
+
- A plain-attribute hole follows one rule on the server and the client (#1573): `null` / `undefined` omit the attribute, `false` omits it except on an `aria-*` name, where it is written as `"false"`, and `true` on an HTML boolean attribute (`checked`, `selected`, `disabled`, ...) writes the empty value like `?attr` (#1579). So `checked=${isDefault}` and `selected=${i === 0}` behave exactly like `?checked` / `?selected`, though `?attr` stays the clearer spelling for a boolean attribute. So `aria-expanded=${this.open}` serves `"true"` / `"false"` and hydrates unchanged, and `aria-current=${active ? 'page' : null}` omits the attribute on an inactive link. `?attr=${bool}` is not a substitute for a tri-state ARIA attribute, since a boolean binding omits the attribute when false.
|
|
34
34
|
- A hole commits on the next render, one microtask later. The one place a direct write is still correct is a synchronous snapshot read such as `webjs:before-cache`, where the router reads `outerHTML` in the same task. That is a documented exception, not the normal path.
|
|
35
35
|
|
|
36
36
|
**5. Behaviour needs an importable surface, or its test is a copy of it.** An inline `<script>` in a layout has no module identity, so a browser test cannot import it. It can only transcribe the listener into the test file and assert against the transcription, which then needs a SECOND test to grep the original for drift. Two tests, neither running shipping code. A component is importable, so its browser test mounts the real element and drives real events. A page or layout may still carry an inline `<script>`, but only for pre-paint boot work no module can do: reading a stored theme before first paint so the wrong palette never flashes, or measuring the header height into a CSS custom property. It must not be interactivity, and WHERE it sits decides how often it runs. The ROOT layout's markup sits OUTSIDE every swap range, so a soft navigation does not re-run its script, which is what makes it the right home for boot work and the wrong home for anything that has to respond to a later navigation. A page or a NESTED layout sits inside the swap range instead, so its script re-executes on every navigation that swaps that range (#1102), which means it has to be idempotent or guard on a flag it sets the first time. Neither shape gives you a listener that simply works, which is what a custom element is for. Under an opt-in CSP the script also needs the nonce from `cspNonce()`. `client-router-and-streaming.md` carries the full re-execution rule.
|
|
@@ -139,6 +139,8 @@ The renderer omits the `action` attribute so the form posts to the page's own ur
|
|
|
139
139
|
|
|
140
140
|
**A form-bound action always receives the `FormData`**, which is where it differs from the same function called over RPC (rich arguments) or server-to-server. `validate` is the typing seam: it takes the `FormData` and its transform-return becomes the action's typed input.
|
|
141
141
|
|
|
142
|
+
A failing form action does not need to echo the submission back. On the 422 re-render `actionData.values` already carries every submitted text field (any `values` the action returns are layered on top), and with JavaScript on the client router restores what was typed into every control the re-render did not explicitly set (#1581). Return `values` only to normalize or blank a field; see `routing-and-pages.md` for the page side.
|
|
143
|
+
|
|
142
144
|
Everything the action declares applies here too, or an action would be protected over RPC and open over a form:
|
|
143
145
|
|
|
144
146
|
- `validate` runs on the submitted `FormData`.
|
|
@@ -176,6 +176,8 @@ html`<form method="post"><input name="email"></form>`;
|
|
|
176
176
|
|
|
177
177
|
A plain attribute hole that resolves to `null` or `undefined` omits the attribute on both renderers (#1573), so `method=${null}` and `?method=${false}` both emit nothing and WebJs supplies `method="post"`. An EMPTY string is different: `method=${''}` renders `method=""`, which cannot submit and is refused.
|
|
178
178
|
|
|
179
|
+
A boolean in a plain hole on an HTML boolean attribute renders like `?attr` (core 0.7.64+, #1579): `<option selected=${i === 0}>` and `<input type="radio" checked=${v.attending !== 'no'}>` mark only the true one. On an older core the server served `selected="false"` / `checked="false"`, which HTML reads as PRESENT, so the LAST option or radio won on first paint. `?selected=${...}` / `?checked=${...}` is correct on every version.
|
|
180
|
+
|
|
179
181
|
A string stays a string: `action="/search"` and `action=${'/search'}` are unchanged, which is what a search form (`<form method="get" action="/search">`) and a `route.ts` endpoint both want. Other attributes keep their existing stringify behaviour; only a FUNCTION under `action` / `formaction` is claimed.
|
|
180
182
|
|
|
181
183
|
### `params` and `searchParams` are awaitable AND synchronously readable
|
|
@@ -220,6 +220,8 @@ export default function Contact({ actionData }: {
|
|
|
220
220
|
|
|
221
221
|
How the result is read (server side): a success PRG-redirects with `303` (to a same-site `redirect` path if present, else the page's own URL); a failure re-SSRs the SAME page with `status` (default `422`) and the result on `ctx.actionData`. Failure is detected robustly (`success === false`, OR `fieldErrors` present, OR `error` present with `success !== true`), so an error is never swallowed. `result.redirect` must be a same-site local path (a single leading `/`); for a real external redirect, throw `redirect(absoluteUrl)` instead. On a plain GET render `actionData` is `undefined`. Prefer a bound `<form>` over `fetch` in a `@click` for any write a form can express.
|
|
222
222
|
|
|
223
|
+
**Typed values survive a failed submission without extra code (#1581).** On a failure re-render `actionData.values` carries EVERY submitted text field, with any `values` the action returned layered on top (the action's own win, so it can normalize or blank a field). So `value=${values.x}` refills a field the action never echoed, with JavaScript off. With JavaScript on, the client router goes further: it snapshots the submitted form and, when the non-2xx re-render is applied in place, puts back what was typed in every text input, textarea, select, radio and checkbox whose server-rendered default did not change. A control the server rendered differently on purpose (refilled, normalized, reset) keeps the server's value. Passwords, files and hidden inputs are never restored. Opt a form out with `data-preserve-values="false"`. Refilling from `actionData.values` is still the right habit, since it is what the no-JS path shows; a checkbox group or multi-select repeats its name, and `values` keeps only the last one, so read those from the submitted `FormData` in the action.
|
|
224
|
+
|
|
223
225
|
Three responses that are not the happy path:
|
|
224
226
|
|
|
225
227
|
- **A submission carrying no identity is a `405` + `Allow: GET, HEAD`.** A bare `<form method="post">` binds nothing, and the page path exists but only renders, so the method is what is wrong rather than the url.
|
|
@@ -276,12 +278,26 @@ export default function robots({ siteUrl }: MetadataRouteContext) {
|
|
|
276
278
|
|
|
277
279
|
The IMAGE metadata routes (`icon`, `apple-icon`, `opengraph-image`, `twitter-image`) default-export a function returning a `Response` with an explicit `content-type`, so an inline SVG needs no asset file (buildless).
|
|
278
280
|
|
|
279
|
-
|
|
281
|
+
### App icon and manifest (replace the placeholder)
|
|
282
|
+
|
|
283
|
+
**Every app needs its OWN icon.** `webjs create` ships `app/icon.svg` as a neutral PLACEHOLDER (a grey tile with a dashed frame, marked `data-webjs-placeholder="icon"`) and `app/manifest.webmanifest` with the app's name. Replacing the placeholder is part of building the app, not polish: until you do, the tab, the bookmark and the home-screen icon look like every other unfinished app, and `webjs doctor` warns (`APP_ICON`). Draw a simple symbol for what the app IS (a grid for a tic-tac-toe game, a cup for a cafe, a check for a task list), in the app's own colours, on a 32x32 or 24x24 `viewBox`: a filled rounded tile in the primary colour with one bold shape in its foreground colour reads at 16px. Avoid thin strokes (under 2px at 32px), text longer than one letter, and detail that blurs at tab size. Then set `name`, `short_name`, `theme_color` and `background_color` in `app/manifest.webmanifest` to match.
|
|
284
|
+
|
|
285
|
+
The icon conventions, all at the app ROOT and all auto-linked into `<head>` when the app declares no `metadata.icons`:
|
|
286
|
+
|
|
287
|
+
| File | Served at | Linked as |
|
|
288
|
+
|---|---|---|
|
|
289
|
+
| `app/icon.svg` / `icon.png` / `icon.ico` | `/icon.svg` ... | `<link rel="icon">` with `type`, `sizes="any"` (SVG) or the PNG's real pixel size |
|
|
290
|
+
| `app/apple-icon.png` (180x180) | `/apple-icon.png` | `<link rel="apple-touch-icon" sizes="180x180">` (iOS needs PNG, not SVG) |
|
|
291
|
+
| `app/icon.ts` / `apple-icon.ts` | `/icon`, `/apple-icon` | bare link (the route picks its content type per request) |
|
|
292
|
+
| `app/manifest.webmanifest` / `manifest.json` / `manifest.ts` | `/manifest.webmanifest`, `/manifest.json` | `<link rel="manifest">` (`metadata.manifest` wins; `manifest: null` opts out) |
|
|
293
|
+
| `app/favicon.ico` | `/favicon.ico` | not linked; browsers request it themselves |
|
|
294
|
+
|
|
295
|
+
`/favicon.ico` always answers: `public/favicon.ico`, else `app/favicon.ico`, else the app's icon (raster preferred over SVG, then the `icon.ts` route), so a crawler or feed reader that reads no markup gets the same icon. A static icon file wins the link over an icon route when both exist (the route still serves at its URL). Raster icons are linked before SVG, because Google's favicon crawler takes the first usable icon and wants a square raster. Use `icon.ts` only when the mark must be computed per request (per theme, per tenant); a route can render a PNG for `apple-icon.ts` the same way.
|
|
280
296
|
|
|
281
|
-
Declaring `metadata.icons` **suppresses** the
|
|
297
|
+
Declaring `metadata.icons` **suppresses** all of the above rather than merging with them, which is what Next does with its static icon files. So name icons explicitly only when they live elsewhere (a CDN, `public/`):
|
|
282
298
|
|
|
283
299
|
```ts
|
|
284
|
-
// app/layout.ts -> these win; /icon and /apple-icon are no longer linked
|
|
300
|
+
// app/layout.ts -> these win; app/icon.* and app/apple-icon.* are no longer linked
|
|
285
301
|
export const metadata = {
|
|
286
302
|
icons: {
|
|
287
303
|
icon: [
|
|
@@ -293,7 +309,7 @@ export const metadata = {
|
|
|
293
309
|
};
|
|
294
310
|
```
|
|
295
311
|
|
|
296
|
-
|
|
312
|
+
Never write a favicon as a hand-written `<link rel="icon">`: only the root layout may write a shell at all (invariant 8), so a hand-written tag is unavailable to every other layout.
|
|
297
313
|
|
|
298
314
|
`opengraph-image` and `twitter-image` are LINKED too, Next's behaviour: a page that declares no `openGraph.images` gets `og:image` pointing at the nearest `opengraph-image` route above it (absolute against the site URL), and likewise `twitter:image`. A page that declares its own image keeps it.
|
|
299
315
|
|
|
@@ -32,21 +32,21 @@ Three seams pick a runtime-specific implementation, all inside the framework, no
|
|
|
32
32
|
| Listener | `node:http` shell | native `Bun.serve` (faster on the listening path only, not end-to-end, because SSR render dominates a real page) |
|
|
33
33
|
| TS strip | built-in `module.stripTypeScriptTypes` | `amaro` (byte-identical, position-preserving) |
|
|
34
34
|
| SQLite | built-in `node:sqlite` + `drizzle-orm/node-sqlite` | built-in `bun:sqlite` + `drizzle-orm/bun-sqlite` |
|
|
35
|
-
| Hot reload | restart on change (the `webjs dev` supervisor, #1521) | `bun --hot
|
|
35
|
+
| Hot reload | restart on change (the `webjs dev` supervisor, #1521) | in place in one long-lived process (`bun --hot` resets the module registry, #1575); only an `instrumentation.*` / `env.*` edit restarts it |
|
|
36
36
|
| WebSocket | the `ws` library | native `Bun.serve` + a bridge adapter |
|
|
37
37
|
| 103 Early Hints | yes | no (`Bun.serve` has no informational-response API) |
|
|
38
|
-
| Dev edit to a page / layout | full reload (the dev restart replaces the process) | refreshes IN PLACE, no reload (#1398)
|
|
38
|
+
| Dev edit to a page / layout | full reload (the dev restart replaces the process) | refreshes IN PLACE, no reload (#1398) |
|
|
39
39
|
| Reverse-proxy headers | `X-Forwarded-Proto` / `X-Forwarded-Host` honored | same |
|
|
40
40
|
|
|
41
41
|
**`webjs dev` lets an idle host sleep (#1507).** The live-reload stream sends nothing between edits (no keepalive) and is held open only while a tab showing the app is visible, so a sandbox or preview host that suspends on network quiet can suspend with a backgrounded dev tab open. Showing the tab reconnects, and an edit made meanwhile reloads the page on return. A host that counts an OPEN request as activity (a sandbox that suspends on idle) also needs `"webjs": { "dev": { "reloadIdle": 20 } }` (or `WEBJS_DEV_RELOAD_IDLE=20`): after that many seconds with no edit and no interaction the stream closes, and the next interaction, a tab showing, or an embed-bridge host command (`{ source: 'webjs-embed-host', type: 'resume' }`) reopens it. Off by default. Every reconnect also compares the server's state with the state the page on screen was rendered at, so an edit whose reload signal was lost while the stream was being replaced (a host that closes a held stream when it wakes) still reloads the page.
|
|
42
42
|
|
|
43
43
|
**The in-place dev refresh (#1398) needs the server process to SURVIVE the edit,** which is the whole of the Node-versus-Bun difference in that row. A page or layout never hydrates, so a freshly rendered page is the complete truth for it and the client router can swap it in without a reload, keeping scroll and (for a page edit) the hydrated state of components outside the changed region. The server classifies the changed file and puts the verdict on the live-reload event, so this needs a process that is still alive to do the classifying.
|
|
44
44
|
|
|
45
|
-
Bun
|
|
45
|
+
Bun keeps ONE dev server process for the whole session (#1575), so it gets the refresh. `bun --hot` re-runs the CLI on a change; the first run's server owns the process (its listener, live-reload stream, watchers and analysis caches) and a re-run only tells it the module registry was reset, so nothing is started twice and memory stays flat over hundreds of edits (it used to grow about 25 MB an edit until `bun --hot` stopped reloading). The app's modules load through a `Bun.plugin` that reads them fresh, and its `#` imports resolve through the app's own `imports` map, because Bun keeps the old source of a file that was replaced (an atomic save) and a stale directory listing for a new file next to a `*.server.*` module. So `bun --hot` no longer sees app edits itself, and the dev server asks it for a registry reset after an edit to a module some other module imports, a new module, or a `*.server.*` module, all without a process restart. A page, layout or route handler nothing imports needs no reset: the dev re-import is keyed by the file's content, so an unchanged file reuses its loaded module (no new module instance per request) and an edited one is a new import. `instrumentation.*` and `env.*` run once per process, so an edit to one restarts the server on both runtimes. On Node the `webjs dev` supervisor (it replaced `node --watch` in #1521) RESTARTS the server process on a change under `app`, `components`, `modules`, `lib`, or `actions`, or to a root `middleware.{ts,js,mts,mjs}`, and a fresh process holds no record of what changed, so those edits are always a full reload. Two Node cases still refresh in place: an edit OUTSIDE that watched set (`db/schema.server.ts`, a `webjs.dev.watch` content dir), and running `npm run dev -- --no-hot`, which keeps the server in one process on either runtime. An in-place refresh loads the rebuilt stylesheets (a `webjs.dev.regenerate` compile runs on that request) BEFORE it swaps the new markup in, and drops the old sheets only after, so an element that gained a utility class never paints without its rule (#1535; a full reload never had the gap, since a head stylesheet is render-blocking). A component edit is a full reload everywhere by design, because `customElements.define` is once-per-tag and swapping fresh markup onto the old class would be worse than the reload.
|
|
46
46
|
|
|
47
47
|
**`webjs dev` and `webjs start` serve the directory they are started in, and refuse anywhere else (#1526).** Run them in the app directory, the one holding `app/`. In a workspace (`apps/web` under a root `package.json` with `workspaces`) that is the member, even when the CLI is hoisted to the root `node_modules`: the hoisted bin and the Bun `--hot` child both keep the directory they were started in. Started where there is no `app/` (the workspace root, or a subdirectory such as `app/` itself), both exit 1 before any `before` step runs, naming the app to start (`cd apps/web && webjs dev`), instead of booting a server that answers 404 for every route.
|
|
48
48
|
|
|
49
|
-
**`webjs dev` does not stay down (#1521).** The supervisor and the server's own watcher handle every watcher error: a file in a watched dir the dev server cannot read or watch (the 0600 temp file `sed -i` creates when another user runs it, a file removed mid-scan) logs one `file watcher skipped <path> (EACCES)` warning and both keep running, where `node --watch` used to crash and leave the preview dead. Edits that REPLACE a file (`sed -i`, an editor saving through a temp file, an atomic write) are heard every time, however often the same file is replaced (#1529: on Linux under Node the watchers watch each directory, since Node 24's own recursive watcher stopped hearing a file after its first replacement). A server process that crashes is started again on the next file change, or by itself after a backoff of 0.5s growing to 10s for repeated crashes. On Bun the supervisor restarts the server only for an
|
|
49
|
+
**`webjs dev` does not stay down (#1521).** The supervisor and the server's own watcher handle every watcher error: a file in a watched dir the dev server cannot read or watch (the 0600 temp file `sed -i` creates when another user runs it, a file removed mid-scan) logs one `file watcher skipped <path> (EACCES)` warning and both keep running, where `node --watch` used to crash and leave the preview dead. Edits that REPLACE a file (`sed -i`, an editor saving through a temp file, an atomic write) are heard every time, however often the same file is replaced (#1529: on Linux under Node the watchers watch each directory, since Node 24's own recursive watcher stopped hearing a file after its first replacement). A server process that crashes is started again on the next file change, or by itself after a backoff of 0.5s growing to 10s for repeated crashes. On Bun the supervisor restarts the server only for an `instrumentation.*` / `env.*` edit (#1575), and otherwise does only the crash recovery. An agent writing files the way an AI editor does (bursts, partial writes, syntax errors then fixes, renames, deletes, atomic writes) is exercised by `scripts/dev-reload-stress.mjs` (`node scripts/dev-reload-stress.mjs <appDir> <url>` against any running app). Stopping `webjs dev` (Ctrl-C, SIGTERM) stops the server child too, and a child whose supervisor was killed outright exits on its own, so nothing is left holding the port.
|
|
50
50
|
|
|
51
51
|
The 103 Early Hints gap costs only a small first-load latency edge where an edge proxy forwards the 103, never correctness. The `modulepreload` hints still ship in the document head on both runtimes.
|
|
52
52
|
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
// content type, so an inline SVG needs no asset file. Generate it dynamically
|
|
4
4
|
// (per-theme, per-tenant) when the mark must be computed at request time.
|
|
5
5
|
//
|
|
6
|
-
// This is the DEMO of that surface, not the gallery's own favicon.
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
6
|
+
// This is the DEMO of that surface, not the gallery's own favicon. With no
|
|
7
|
+
// metadata.icons declared, the framework auto-links an app-root icon: a STATIC
|
|
8
|
+
// file (app/icon.svg, app/icon.png) wins that link over this route, and a
|
|
9
|
+
// declared metadata.icons wins over both. The gallery declares its WebJs brand
|
|
10
|
+
// mark in app/layout.ts, so this route stays browsable at /icon without being
|
|
11
|
+
// the tab icon. For an icon that never changes, write app/icon.svg instead
|
|
12
|
+
// (what `webjs create` ships); keep a route like this only when the mark must
|
|
13
|
+
// be computed at request time (per theme, per tenant).
|
|
12
14
|
export default function Icon() {
|
|
13
15
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
|
14
16
|
<rect width="32" height="32" rx="7" fill="#1e2226"/>
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
// icons to your app; pair it with the opt-in service worker for an installable
|
|
4
4
|
// PWA. See agent-docs/service-worker.md. (Gallery files are copied verbatim, so
|
|
5
5
|
// set the real app name here by hand rather than expecting substitution.)
|
|
6
|
+
// The framework links an app-root manifest into <head> by itself. A static
|
|
7
|
+
// app/manifest.webmanifest (what `webjs create` ships) wins that link over this
|
|
8
|
+
// route; write a route like this only when a value must be computed.
|
|
6
9
|
export default function Manifest() {
|
|
7
10
|
return {
|
|
8
11
|
name: 'webjs app',
|
|
@@ -12,7 +15,7 @@ export default function Manifest() {
|
|
|
12
15
|
background_color: '#ffffff',
|
|
13
16
|
theme_color: '#1e2226',
|
|
14
17
|
icons: [
|
|
15
|
-
{ src: '/
|
|
18
|
+
{ src: '/icon', sizes: 'any', type: 'image/svg+xml' },
|
|
16
19
|
],
|
|
17
20
|
};
|
|
18
21
|
}
|
|
@@ -191,9 +191,9 @@ import type { LayoutProps } from '@webjsdev/core';
|
|
|
191
191
|
* to pull primitives, then theme them here.
|
|
192
192
|
*/
|
|
193
193
|
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
194
|
+
// The favicon is app/icon.svg (and the manifest app/manifest.webmanifest): the
|
|
195
|
+
// framework links both into <head>, so nothing is declared here. Replace the
|
|
196
|
+
// placeholder icon with this app's own.
|
|
197
197
|
|
|
198
198
|
export default function RootLayout({ children }: LayoutProps) {
|
|
199
199
|
return html\`
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="WebJs">
|
|
2
|
-
<!-- Matches the gallery navbar brand mark (a rounded square with a
|
|
3
|
-
foreground -> muted-foreground gradient), using the DARK-theme token
|
|
4
|
-
values (#dee2e6 -> #94989c) so it reads on a dark browser tab bar. -->
|
|
5
|
-
<defs>
|
|
6
|
-
<linearGradient id="wj" x1="0" y1="0" x2="1" y2="1">
|
|
7
|
-
<stop offset="0" stop-color="#dee2e6"/>
|
|
8
|
-
<stop offset="1" stop-color="#94989c"/>
|
|
9
|
-
</linearGradient>
|
|
10
|
-
</defs>
|
|
11
|
-
<rect width="32" height="32" rx="10" fill="url(#wj)"/>
|
|
12
|
-
</svg>
|