@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 +1 -1
- package/lib/app-icon.js +55 -0
- package/lib/create.js +30 -20
- package/lib/dev-supervisor.js +16 -1
- 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/lib/resolve-bin.js +26 -0
- package/package.json +2 -2
- package/templates/.agents/rules/workflow.md +14 -17
- package/templates/.agents/skills/webjs/SKILL.md +3 -1
- package/templates/.agents/skills/webjs/references/built-ins.md +2 -0
- package/templates/.agents/skills/webjs/references/routing-and-pages.md +18 -4
- package/templates/.agents/skills/webjs/references/runtime.md +2 -0
- package/templates/.claude/hooks/nudge-uncommitted.sh +7 -0
- package/templates/AGENTS.md +47 -77
- package/templates/CLAUDE.md +14 -16
- package/templates/CONVENTIONS.md +10 -11
- package/templates/gallery/app/icon.ts +8 -6
- package/templates/gallery/app/manifest.ts +4 -1
- package/templates/partials/agents-playbook-fullstack.md +566 -130
- package/templates/scripts/clear-gallery.mjs +6 -6
- package/templates/public/favicon.svg +0 -12
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;
|
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
|
/**
|
|
@@ -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
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
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
|
-
//
|
|
1250
|
-
// the
|
|
1251
|
-
|
|
1252
|
-
|
|
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
|
-
//
|
|
1359
|
-
//
|
|
1360
|
-
//
|
|
1361
|
-
//
|
|
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
|
|
1715
|
-
|
|
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.
|
package/lib/dev-supervisor.js
CHANGED
|
@@ -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
|
|
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/lib/resolve-bin.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
7
|
-
|
|
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
|
-
- **
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
`app/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.**
|
|
30
|
-
in `
|
|
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
|
-
|
|
281
|
+
### App icon and manifest (replace the placeholder)
|
|
282
282
|
|
|
283
|
-
|
|
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
|
-
|
|
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
|