@webjsdev/cli 0.10.67 → 0.10.69

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 CHANGED
@@ -591,7 +591,7 @@ async function main() {
591
591
  superviseDevServer({
592
592
  cwd: process.cwd(),
593
593
  plan,
594
- env: { ...process.env, __WEBJS_DEV_CHILD: '1' },
594
+ env: { ...process.env, ...plan.env, __WEBJS_DEV_CHILD: '1' },
595
595
  onExit: (code) => { killTasks(); process.exit(code); },
596
596
  });
597
597
  break;
@@ -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
  /**
@@ -382,10 +383,15 @@ export async function scaffoldApp(name, cwd, opts = {}) {
382
383
  // would exec WebJs under Node, silently running the "bun" app on Node).
383
384
  // Baking it into the script body means a plain `bun run dev` (or even
384
385
  // `npm run dev`) starts on Bun, so a user never has to remember the flag.
385
- // The runtime-neutral tooling scripts below (test / db / check / typecheck
386
- // / doctor) stay plain `webjs ...`: they spawn node tooling (`node --test`,
387
- // drizzle-kit, tsc) and forcing `--bun` there buys nothing (and `webjs
388
- // test` shells `node --test`, which a `bun --test` would not be).
386
+ // The db scripts force it too (#1598): `webjs db` runs drizzle-kit and the
387
+ // seed with the CLI's own runtime, and through the node shebang that was
388
+ // Node, at 2.5 to 4 times the CPU of the same command on Bun (generate
389
+ // about 3 CPU-s against 1, migrate 1.2 against 0.5, seed 1 against 0.2,
390
+ // measured on the Postgres scaffold); on Bun the seed also sees `.env`.
391
+ // It is the path a Node-less oven/bun image already takes (#570). The
392
+ // other tooling scripts (test / check / typecheck / doctor / ci) stay
393
+ // plain `webjs ...`: `webjs test` shells `node --test`, which a
394
+ // `bun --test` would not be, and `check` costs the same on both.
389
395
  // Compile Tailwind from public/input.css to a STATIC public/tailwind.css
390
396
  // that app/layout.ts links, so the app is fully styled with JavaScript
391
397
  // DISABLED (a real stylesheet, not an in-browser compile). Runs inside the
@@ -418,11 +424,11 @@ export async function scaffoldApp(name, cwd, opts = {}) {
418
424
  // from the step list in the `webjs.ci` block below. Runtime-neutral like
419
425
  // the other tooling scripts (it spawns `webjs ...` children).
420
426
  ci: 'webjs ci',
421
- 'db:generate': 'webjs db generate',
422
- 'db:migrate': 'webjs db migrate',
423
- 'db:push': 'webjs db push',
424
- 'db:studio': 'webjs db studio',
425
- 'db:seed': 'webjs db seed',
427
+ 'db:generate': isBun ? 'bun --bun webjs db generate' : 'webjs db generate',
428
+ 'db:migrate': isBun ? 'bun --bun webjs db migrate' : 'webjs db migrate',
429
+ 'db:push': isBun ? 'bun --bun webjs db push' : 'webjs db push',
430
+ 'db:studio': isBun ? 'bun --bun webjs db studio' : 'webjs db studio',
431
+ 'db:seed': isBun ? 'bun --bun webjs db seed' : 'webjs db seed',
426
432
  },
427
433
  dependencies: {
428
434
  // Drizzle ORM (no codegen, no engine binary). Pinned to the 1.0 line
@@ -1246,10 +1252,15 @@ export type ActionResult<T> =
1246
1252
  const swSrc = join(TEMPLATES, 'public', swFile);
1247
1253
  if (existsSync(swSrc)) await cp(swSrc, join(publicDir, swFile));
1248
1254
  }
1249
- // A base SVG favicon (the root layout links it). It ships with the app, not
1250
- // the gallery, so it survives `npm run gallery:clear`.
1251
- const faviconSrc = join(TEMPLATES, 'public', 'favicon.svg');
1252
- if (existsSync(faviconSrc)) await cp(faviconSrc, join(publicDir, 'favicon.svg'));
1255
+ // The app icon and web app manifest: `app/icon.svg` (a neutral PLACEHOLDER
1256
+ // the agent replaces with the app's own icon; `webjs doctor` warns while it
1257
+ // is still there) and `app/manifest.webmanifest` (the app's name). The
1258
+ // framework links both into <head> and answers /favicon.ico with the icon,
1259
+ // so the layout declares nothing. They ship with the app, not the gallery,
1260
+ // so they survive `gallery:clear`.
1261
+ await mkdir(join(appDir, 'app'), { recursive: true });
1262
+ await writeFile(join(appDir, 'app', 'icon.svg'), PLACEHOLDER_ICON_SVG);
1263
+ await writeFile(join(appDir, 'app', 'manifest.webmanifest'), appManifest(displayName));
1253
1264
 
1254
1265
  // The gallery-reset script (wired as `gallery:clear`). Only UI templates have
1255
1266
  // a gallery, so it ships here (NOT in the flat templateFiles list, which would
@@ -1355,11 +1366,10 @@ import '#components/theme-toggle.ts';
1355
1366
  * text-foreground, bg-card, bg-primary, and border-border all work.
1356
1367
  */
1357
1368
 
1358
- // Declare the favicon via metadata.icons (NOT a hand-written <link> in the
1359
- // template): the framework emits metadata links into <head>, whereas a <link>
1360
- // written in the layout body stays in <body>, where browsers ignore it. The SVG
1361
- // lives at public/favicon.svg and serves at /public/favicon.svg.
1362
- export const metadata = { icons: '/public/favicon.svg' };
1369
+ // The favicon and manifest are app/icon.svg and app/manifest.webmanifest: the
1370
+ // framework links them into <head> itself, so nothing is declared here. Replace
1371
+ // the placeholder icon with this app's own (see the skill's routing-and-pages
1372
+ // reference, "App icon").
1363
1373
 
1364
1374
  // LayoutProps types every layout argument (children, params, searchParams,
1365
1375
  // url) from the framework, so children is a TemplateResult rather than an
@@ -1711,8 +1721,8 @@ ThemeToggle.register('theme-toggle');
1711
1721
  `);
1712
1722
  }
1713
1723
  console.log(`For AI agents, read this before editing:
1714
- • Read AGENTS.md, then .agents/skills/webjs/SKILL.md. The skill is the guide
1715
- to building a WebJs app and routes to focused references on demand.
1724
+ • Read AGENTS.md first: it carries the build steps and a worked example of
1725
+ every common pattern. .agents/skills/webjs/ is the deeper reference.
1716
1726
  • This scaffold is a minimal starting point, not a demo to prune. Grow the app
1717
1727
  in place: add routes under app/, components under components/, and features
1718
1728
  under modules/<feature>/, and keep server-only code behind .server.ts.
@@ -78,7 +78,7 @@ export function isBootFile(path) {
78
78
  * @param {string[]} opts.argv `process.argv.slice(1)` (the script path followed by its args), forwarded to the child verbatim.
79
79
  * @param {boolean} opts.noHot Whether `--no-hot` was passed (opt out of the supervisor entirely).
80
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.
81
- * @returns {{ mode: 'inline' } | { mode: 'supervise', args: string[], restartOnChange: boolean, restartFor?: (path: string) => boolean, inPlaceRestartFor?: (path: string) => boolean, watchDirs: string[], watchFiles: string[] }}
81
+ * @returns {{ mode: 'inline' } | { mode: 'supervise', args: string[], env?: Record<string, string>, restartOnChange: boolean, restartFor?: (path: string) => boolean, inPlaceRestartFor?: (path: string) => boolean, watchDirs: string[], watchFiles: string[] }}
82
82
  * `inline` runs the server in this process (no reload watcher); `supervise`
83
83
  * spawns `process.execPath` with `args` and `__WEBJS_DEV_CHILD=1` under the
84
84
  * supervisor, which watches `watchDirs` (recursively) and `watchFiles` (at
@@ -101,6 +101,7 @@ export function planDevSupervisor({ isBun, argv, noHot, sourceLocations = false
101
101
  return {
102
102
  mode: 'supervise',
103
103
  args: ['--hot', ...argv],
104
+ env: BUN_CHILD_ENV,
104
105
  restartOnChange: true,
105
106
  restartFor: (p) => plugin(p) || isBootFile(p),
106
107
  inPlaceRestartFor: isBootFile,
@@ -110,6 +111,20 @@ export function planDevSupervisor({ isBun, argv, noHot, sourceLocations = false
110
111
  return { mode: 'supervise', args: [...argv], restartOnChange: true, ...watch };
111
112
  }
112
113
 
114
+ /**
115
+ * Extra env for the Bun dev child: turn off Bun's runtime transpiler cache.
116
+ *
117
+ * Bun caches the transpile of every source over 50KB on disk, keyed by the
118
+ * file's CONTENT, with the import paths a `Bun.plugin` `onResolve` returned
119
+ * baked in. The dev alias resolver (#1575) returns absolute paths, so the
120
+ * cached output of a large app module pins its `#` imports to the checkout
121
+ * that first ran `webjs dev`. Any other copy of the same file (a git worktree,
122
+ * a moved or copied app, a later `webjs start`) then imports from that old
123
+ * directory: a 500 when it is gone, the other copy's code when it is not. The
124
+ * variable is read at process start, so it has to be set on the child.
125
+ */
126
+ const BUN_CHILD_ENV = Object.freeze({ BUN_RUNTIME_TRANSPILER_CACHE_PATH: '0' });
127
+
113
128
  const SERVER_MODULE = /\.server\.m?[jt]s$/;
114
129
  const APP_MODULE = /\.m?[jt]s$/;
115
130
 
@@ -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
+ }
@@ -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, the `metadata.icons` favicon, and
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
  *
@@ -13,6 +13,14 @@
13
13
  * 'drizzle-kit/bin.cjs')` throws `ERR_PACKAGE_PATH_NOT_EXPORTED`. The `.` main
14
14
  * entry DOES resolve, so resolve that, walk up to the package root (the nearest
15
15
  * dir with a package.json), and read the `bin` field, which is version-robust.
16
+ *
17
+ * Installed means reachable through a `node_modules/<pkg>` entry in `cwd` or an
18
+ * ancestor, the lookup Node's resolver does. That is checked BEFORE resolving
19
+ * because Bun auto-installs: when no `node_modules` sits above `cwd`, Bun's
20
+ * `require.resolve` fetches an undeclared package into its global cache and
21
+ * returns that path. Without the check, an app that never installed
22
+ * @web/test-runner would launch whatever version Bun downloaded instead of
23
+ * getting the "not installed" remedy.
16
24
  */
17
25
  import { createRequire } from 'node:module';
18
26
  import { readFileSync, existsSync } from 'node:fs';
@@ -27,6 +35,9 @@ import { join, dirname, resolve } from 'node:path';
27
35
  * @throws if the package is not installed or has no matching bin
28
36
  */
29
37
  export function resolveBin(cwd, pkgName, binName) {
38
+ if (!hasNodeModulesEntry(cwd, pkgName)) {
39
+ throw new Error(`Cannot find package '${pkgName}' in a node_modules above ${cwd}`);
40
+ }
30
41
  const req = createRequire(join(cwd, 'package.json'));
31
42
  // `.` (the main entry) is exported even when subpaths are not.
32
43
  let pkgDir = dirname(req.resolve(pkgName));
@@ -40,3 +51,18 @@ export function resolveBin(cwd, pkgName, binName) {
40
51
  if (!binRel) throw new Error(`bin '${binName}' not found in ${pkgName}`);
41
52
  return resolve(pkgDir, binRel);
42
53
  }
54
+
55
+ /**
56
+ * @param {string} cwd
57
+ * @param {string} pkgName
58
+ * @returns {boolean} whether `node_modules/<pkgName>` exists in cwd or an ancestor
59
+ */
60
+ function hasNodeModulesEntry(cwd, pkgName) {
61
+ let dir = resolve(cwd);
62
+ for (;;) {
63
+ if (existsSync(join(dir, 'node_modules', pkgName))) return true;
64
+ const parent = dirname(dir);
65
+ if (parent === dir) return false;
66
+ dir = parent;
67
+ }
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/cli",
3
- "version": "0.10.67",
3
+ "version": "0.10.69",
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.84",
21
+ "@webjsdev/server": "^0.8.87",
22
22
  "@webjsdev/ui": "^0.3.15"
23
23
  },
24
24
  "publishConfig": {
@@ -2,23 +2,21 @@
2
2
 
3
3
  You are working on a WebJs app (AI-first, no-build, web-components-first). This
4
4
  file is the WORKFLOW contract (git, tests, review). For HOW to build (routing,
5
- components, actions, styling, the framework API), read
6
- `.agents/skills/webjs/SKILL.md`, which routes to focused references on demand.
7
- Read `AGENTS.md` first. Full hosted docs are at https://webjs.dev/docs.
5
+ components, actions, styling, the framework API), read `AGENTS.md`; the
6
+ deeper reference set is `.agents/skills/webjs/SKILL.md`. Full hosted docs are
7
+ at https://webjs.dev/docs.
8
8
 
9
9
  ## Grow the app in place (non-negotiable)
10
10
 
11
- - **Study the shipped examples, then clear them and build.** The scaffold is a
12
- starting point with a browsable showcase to learn the real idioms from, plus a
13
- database wired up. A full-stack app ships a UI feature gallery (`app/features/`,
14
- `app/examples/todo`); the api template ships a backend-features showcase
15
- (`app/api/features/`), with logic in `modules/`. Building a real app: study the
16
- parts that match your task (the skill teaches the same and SURVIVES the clear),
17
- run `npm run gallery:clear` to shed the showcase (it keeps the agent skill and
18
- the database wiring, and resets to a clean base), then regenerate the database
19
- and grow the app in place under `app/`, `components/`, and `modules/<feature>/`.
20
- `AGENTS.md` carries the full template-specific build playbook and the order to
21
- follow.
11
+ - **Clear the showcase, then build.** The scaffold is a starting point with a
12
+ browsable demo showcase plus a database wired up. A full-stack app ships a UI
13
+ feature gallery (`app/features/`, `app/examples/todo`); the api template ships
14
+ a backend-features showcase (`app/api/features/`), with logic in `modules/`.
15
+ Building a real app: run `npm run gallery:clear` to shed the showcase (it
16
+ keeps the agent docs and the database wiring, and resets to a clean base),
17
+ then regenerate the database and grow the app in place under `app/`,
18
+ `components/`, and `modules/<feature>/`. `AGENTS.md` carries the
19
+ template-specific build steps and the order to follow.
22
20
  - **Use the wired-up database (Drizzle), never JSON files.** For any data the app
23
21
  stores, define a Drizzle table in `db/schema.server.ts`, then
24
22
  `npm run db:generate` and `npm run db:migrate`. Never use a JSON file, a
@@ -26,9 +24,8 @@ Read `AGENTS.md` first. Full hosted docs are at https://webjs.dev/docs.
26
24
  - **`app/` is routing-only.** Only routing files live in `app/` (page, layout,
27
25
  route, middleware, metadata routes). Browser-safe helpers go in `lib/utils/`,
28
26
  feature logic in `modules/`, server-only code behind `.server.ts`.
29
- - **For a UI app, render and LOOK before calling it done.** Define design tokens
30
- in `app/layout.ts` with a palette that fits the app
31
- (`.agents/skills/webjs/references/styling.md` is the guide), then open every
27
+ - **For a UI app, render and LOOK before calling it done.** Give the design
28
+ tokens in `public/input.css` a palette that fits the app, then open every
32
29
  route you changed in a real browser and play through its states.
33
30
  `npm run check` and `npm run typecheck` pass even when a layout collapses, so
34
31
  the browser is the real check.
@@ -9,6 +9,8 @@ Use this skill for end-to-end WebJs app work. It helps you choose the right laye
9
9
 
10
10
  ## Full Documentation
11
11
 
12
+ In a scaffolded app, `AGENTS.md` carries the build steps and a worked example of every common pattern (pages, layouts, form-bound actions with validation, queries, owner-scoped CRUD, `createAuth`, a component with signals, a test). Build from it first, and come here for a surface it does not show.
13
+
12
14
  This skill is the quick guide. When you need the full API reference for a surface, load the matching file in `references/` (listed below). For even deeper framework detail, WebJs ships buildless, so the source you run IS the source you read: look in `node_modules/@webjsdev/{core,server,cli}/` (each package ships its own `AGENTS.md`). The complete hosted docs live at https://webjs.dev/docs.
13
15
 
14
16
  ## What WebJs Is
@@ -174,7 +176,7 @@ Find the right export fast. Load the linked reference for full examples.
174
176
 
175
177
  ### File conventions
176
178
 
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`).
179
+ `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
180
 
179
181
  ## Canonical Patterns
180
182
 
@@ -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.
@@ -278,12 +278,26 @@ export default function robots({ siteUrl }: MetadataRouteContext) {
278
278
 
279
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).
280
280
 
281
- **`icon` and `apple-icon` are LINKED for you.** An app that declares no `metadata.icons` gets `<link rel="icon" href="/icon">` and `<link rel="apple-touch-icon" href="/apple-icon">` in the head automatically, for whichever of the two routes it defines (base-path prefixed, since that is where the route answers). No `type` or `sizes` is asserted, because the route picks its content type at request time and the browser sniffs the served one.
281
+ ### App icon and manifest (replace the placeholder)
282
282
 
283
- Declaring `metadata.icons` **suppresses** the routes rather than merging with them, which is what Next does with its static icon files. So an app that outgrows a placeholder `app/icon.ts` names its real icons and the route stops being linked without having to be deleted:
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.
296
+
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/`):
284
298
 
285
299
  ```ts
286
- // 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
287
301
  export const metadata = {
288
302
  icons: {
289
303
  icon: [
@@ -295,7 +309,7 @@ export const metadata = {
295
309
  };
296
310
  ```
297
311
 
298
- Declare a favicon through `metadata.icons` (or a metadata route), never 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. A `public/favicon.ico` needs no declaration either way, since the framework serves it at the origin root for crawlers that read no markup.
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.
299
313
 
300
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.
301
315
 
@@ -44,6 +44,8 @@ Three seams pick a runtime-specific implementation, all inside the framework, no
44
44
 
45
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
+ After a reset, app code that imports `@webjsdev/server` gets a fresh copy of the package while the first run's server keeps handling requests. Everything the two copies must agree on (the request `auth()`, `cookies()` and `headers()` read, the action signal, the seed collector and action identity, the default cache store, sessions, broadcast clients) lives in process-wide state keyed by `Symbol.for`, so a signed-in page and the `'use server'` queries it calls still see the request after any number of edits (#1590). Before that fix, `auth()` without an explicit request returned `null` in app code after the first edit.
48
+
47
49
  **`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
50
 
49
51
  **`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.
@@ -30,6 +30,13 @@ fi
30
30
  # Read stdin so we don't break Claude Code's hook contract.
31
31
  cat /dev/stdin >/dev/null 2>&1 || true
32
32
 
33
+ # A repository with no commit yet is a first build from the scaffold: the whole
34
+ # build is one logical unit (CLAUDE.md), committed once at the end, so nudging
35
+ # mid-build would only split it. The Stop hook still asks for that commit.
36
+ if ! git rev-parse --verify -q HEAD >/dev/null 2>&1; then
37
+ exit 0
38
+ fi
39
+
33
40
  CHANGED=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
34
41
 
35
42
  if [ -z "$CHANGED" ] || [ "$CHANGED" -lt "$THRESHOLD" ]; then