@webjsdev/cli 0.9.1 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/webjs.js +241 -1
- package/lib/create.js +15 -4
- package/package.json +2 -2
- package/templates/.agents/rules/workflow.md +149 -0
- package/templates/.claude/hooks/require-tests-with-src.sh +83 -0
- package/templates/.claude/settings.json +9 -0
- package/templates/.cursorrules +36 -22
- package/templates/.github/copilot-instructions.md +53 -26
- package/templates/.github/pull_request_template.md +27 -3
- package/templates/.hooks/pre-commit +26 -1
- package/templates/AGENTS.md +121 -8
- package/templates/CONVENTIONS.md +150 -12
- package/templates/test/hello/hello.test.ts +2 -1
- package/templates/.windsurfrules +0 -91
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ the CLI gives you `webjs ui` automatically. See
|
|
|
66
66
|
The scaffold seeds opinionated defaults so AI agents produce consistent code:
|
|
67
67
|
|
|
68
68
|
- `AGENTS.md` + `CONVENTIONS.md` (the machine-readable contract)
|
|
69
|
-
- `.claude/`, `.cursorrules`, `.
|
|
69
|
+
- `.claude/`, `.cursorrules`, `.agents/rules/workflow.md` (Antigravity), `.github/copilot-instructions.md`
|
|
70
70
|
- `test/<feature>/` (with optional `browser/` / `e2e/` subfolders per kind) with example tests
|
|
71
71
|
- Tailwind CSS via CLI (no browser runtime at build time)
|
|
72
72
|
- TypeScript, `.editorconfig`, `.gitignore`
|
package/bin/webjs.js
CHANGED
|
@@ -8,7 +8,7 @@ const [cmd, ...rest] = process.argv.slice(2);
|
|
|
8
8
|
|
|
9
9
|
// Exactly three scaffolds exist. Keep this list as the single source of
|
|
10
10
|
// truth. AI-agent docs in README.md / AGENTS.md / .cursorrules /
|
|
11
|
-
// .
|
|
11
|
+
// .agents/rules/workflow.md / .github/copilot-instructions.md mirror it.
|
|
12
12
|
const TEMPLATES = ['full-stack', 'api', 'saas'];
|
|
13
13
|
|
|
14
14
|
const USAGE = `webjs commands:
|
|
@@ -26,6 +26,11 @@ const USAGE = `webjs commands:
|
|
|
26
26
|
webjs ui <subcmd> AI-first component library CLI
|
|
27
27
|
(init / add / list / view / diff / info)
|
|
28
28
|
Requires @webjsdev/ui installed in the project
|
|
29
|
+
webjs vendor pin [--download] Pin client-side npm packages to .webjs/vendor/importmap.json
|
|
30
|
+
Default: writes jspm.io URLs (browser fetches from CDN)
|
|
31
|
+
--download: also downloads bundles for offline production
|
|
32
|
+
webjs vendor unpin <pkg> Remove a specific package from the pin file
|
|
33
|
+
webjs vendor list Show pinned packages with versions and URLs
|
|
29
34
|
webjs help Show this help`;
|
|
30
35
|
|
|
31
36
|
/** @param {string[]} args */
|
|
@@ -266,6 +271,241 @@ Full docs: https://docs.webjs.com`);
|
|
|
266
271
|
await scaffoldApp(name, process.cwd(), { template, install: !noInstall });
|
|
267
272
|
break;
|
|
268
273
|
}
|
|
274
|
+
case 'vendor': {
|
|
275
|
+
const sub = rest[0];
|
|
276
|
+
const args = rest.slice(1);
|
|
277
|
+
const appDir = process.cwd();
|
|
278
|
+
const { pinAll, unpinPackage, listPinned, auditPinned, findOutdated, updatePinned, readPinFile, SUPPORTED_PROVIDERS } = await import('@webjsdev/server');
|
|
279
|
+
|
|
280
|
+
// Parse `--from <provider>` once at the top so subcommands share it.
|
|
281
|
+
// Mirrors importmap-rails's `bin/importmap pin foo --from jsdelivr`.
|
|
282
|
+
let from = 'jspm';
|
|
283
|
+
const fromIdx = args.indexOf('--from');
|
|
284
|
+
if (fromIdx !== -1) {
|
|
285
|
+
from = args[fromIdx + 1];
|
|
286
|
+
if (!from || !SUPPORTED_PROVIDERS.has(from)) {
|
|
287
|
+
console.error(
|
|
288
|
+
`Unknown --from provider '${from || ''}'. Supported: ${[...SUPPORTED_PROVIDERS].join(', ')}.`,
|
|
289
|
+
);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
// Strip --from + its argument so downstream flag checks like
|
|
293
|
+
// `args.includes('--download')` aren't confused.
|
|
294
|
+
args.splice(fromIdx, 2);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (sub === 'pin') {
|
|
298
|
+
const download = args.includes('--download');
|
|
299
|
+
// Same precedence rule as `vendor update`: explicit --from
|
|
300
|
+
// wins; otherwise pinAll reads the pin file's persisted
|
|
301
|
+
// provider so a user who pinned via jsdelivr stays on it.
|
|
302
|
+
// Pass undefined (not the parsed 'jspm' default) when no
|
|
303
|
+
// --from to engage the pin-file fallback. Peek at the pin
|
|
304
|
+
// file here to compute the log line before pinAll runs.
|
|
305
|
+
const explicitFrom = fromIdx !== -1 ? from : undefined;
|
|
306
|
+
const existing = await readPinFile(appDir);
|
|
307
|
+
const usedFrom = explicitFrom || existing?.provider || 'jspm';
|
|
308
|
+
console.log(
|
|
309
|
+
`Pinning vendor packages from ${appDir}` +
|
|
310
|
+
(usedFrom !== 'jspm' ? ` via ${usedFrom}` : '') +
|
|
311
|
+
(download ? ' (downloading bundles)' : '') + '...',
|
|
312
|
+
);
|
|
313
|
+
const result = await pinAll(appDir, { download, from: explicitFrom });
|
|
314
|
+
if (result.noBareImports) {
|
|
315
|
+
// Scanner found zero bare-specifier imports in client-
|
|
316
|
+
// reachable source. Without this branch pinAll would write
|
|
317
|
+
// `{ imports: {} }`, which readPinFile then rejects as empty,
|
|
318
|
+
// leaving a useless file behind in whatever cwd.
|
|
319
|
+
console.error(
|
|
320
|
+
`Pin: no bare-specifier npm imports found in client code under ${appDir}. ` +
|
|
321
|
+
`Nothing to pin (no pin file written). Add a bare import like ` +
|
|
322
|
+
`\`import x from 'pkg-name'\` to a page or component, then rerun.`,
|
|
323
|
+
);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
if (result.failed) {
|
|
327
|
+
// pinAll refused to write the pin file because every install
|
|
328
|
+
// failed to resolve via the chosen resolver (jspm.io's
|
|
329
|
+
// Generator API powers all providers; the failure mode is
|
|
330
|
+
// typically a brand-new published version not yet on the
|
|
331
|
+
// CDN, a network outage, or a provider-side 5xx). Surface
|
|
332
|
+
// the failure with the actual provider in the message so
|
|
333
|
+
// the user can fix the cause before shipping.
|
|
334
|
+
const provider = result.provider || 'jspm.io';
|
|
335
|
+
console.error(
|
|
336
|
+
`Pin FAILED: every package failed to resolve via ${provider}. No pin file written ` +
|
|
337
|
+
`(would shadow the live-API fallback with an empty importmap and break the browser).`,
|
|
338
|
+
);
|
|
339
|
+
console.error(`Attempted installs:`);
|
|
340
|
+
for (const i of result.attemptedInstalls) console.error(` ${i}`);
|
|
341
|
+
console.error(
|
|
342
|
+
`Possible causes: the package version is too new for ${provider}'s CDN to have indexed yet; ` +
|
|
343
|
+
`network outage; ${provider} is down. Try again in a few minutes, or pin an older version.`,
|
|
344
|
+
);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
const { pins, pruned, downloaded } = result;
|
|
348
|
+
for (const p of pins) {
|
|
349
|
+
const sizeStr = p.bytes != null ? ` ${(p.bytes / 1024).toFixed(1)} KB` : '';
|
|
350
|
+
console.log(` ${(p.pkg + '@' + p.version).padEnd(40)}${sizeStr}`);
|
|
351
|
+
}
|
|
352
|
+
for (const f of pruned) {
|
|
353
|
+
console.log(` ${f.padEnd(40)} REMOVED (orphan)`);
|
|
354
|
+
}
|
|
355
|
+
const pinMsg = `Pinned ${pins.length} package${pins.length === 1 ? '' : 's'}, wrote .webjs/vendor/importmap.json` +
|
|
356
|
+
(downloaded ? ` + ${downloaded} bundle${downloaded === 1 ? '' : 's'}` : '') + '.';
|
|
357
|
+
const pruneMsg = pruned.length ? ` Pruned ${pruned.length} orphan${pruned.length === 1 ? '' : 's'}.` : '';
|
|
358
|
+
console.log(pinMsg + pruneMsg);
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (sub === 'unpin') {
|
|
363
|
+
if (args.length === 0) {
|
|
364
|
+
console.error('Usage: webjs vendor unpin <pkg>');
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
let unpinFailed = false;
|
|
368
|
+
for (const pkg of args) {
|
|
369
|
+
const r = await unpinPackage(appDir, pkg);
|
|
370
|
+
if (!r.removed) {
|
|
371
|
+
console.error(` ${pkg.padEnd(40)} not in pin file`);
|
|
372
|
+
unpinFailed = true;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const extra = r.deletedFile ? ` (also deleted ${r.deletedFile})` : '';
|
|
376
|
+
console.log(` ${pkg.padEnd(40)} unpinned${extra}`);
|
|
377
|
+
}
|
|
378
|
+
// Exit non-zero if ANY of the requested packages weren't in
|
|
379
|
+
// the pin file. Scripts wrapping the CLI rely on the exit
|
|
380
|
+
// code to detect "nothing was removed"; printing the message
|
|
381
|
+
// alone wasn't enough.
|
|
382
|
+
if (unpinFailed) process.exit(1);
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (sub === 'list') {
|
|
387
|
+
const entries = await listPinned(appDir);
|
|
388
|
+
if (entries.length === 0) {
|
|
389
|
+
console.log('No pin file. Run "webjs vendor pin" to create .webjs/vendor/importmap.json.');
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
console.log(`Pinned packages from ${appDir}/.webjs/vendor/importmap.json:`);
|
|
393
|
+
for (const e of entries) {
|
|
394
|
+
const sizeStr = e.bytes != null ? ` ${(e.bytes / 1024).toFixed(1)} KB` : '';
|
|
395
|
+
console.log(` ${(e.pkg + '@' + e.version).padEnd(40)}${sizeStr}`);
|
|
396
|
+
console.log(` ${e.url}`);
|
|
397
|
+
}
|
|
398
|
+
console.log(`${entries.length} package${entries.length === 1 ? '' : 's'} pinned.`);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (sub === 'audit') {
|
|
403
|
+
// npm bulk-advisories check against pinned versions. Mirrors
|
|
404
|
+
// bin/importmap audit. Exits non-zero when any vulnerability
|
|
405
|
+
// is found so CI can gate on it.
|
|
406
|
+
const { vulnerable, totalChecked, errored } = await auditPinned(appDir);
|
|
407
|
+
if (totalChecked === 0) {
|
|
408
|
+
console.log('No pinned packages to audit. Run "webjs vendor pin" first.');
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
if (errored) {
|
|
412
|
+
console.error(
|
|
413
|
+
`Could not reach registry.npmjs.org for security advisories ` +
|
|
414
|
+
`(network failure, timeout, or 5xx). Retry when connectivity is back.`,
|
|
415
|
+
);
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
if (vulnerable.length === 0) {
|
|
419
|
+
console.log(`No vulnerable packages found (${totalChecked} checked).`);
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
console.log(`Package Severity Vulnerable versions Title`);
|
|
423
|
+
for (const v of vulnerable) {
|
|
424
|
+
console.log(
|
|
425
|
+
` ${v.name.padEnd(38)} ${v.severity.padEnd(10)} ${v.vulnerableVersions.padEnd(25)} ${v.title}`,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
const bySeverity = vulnerable.reduce((acc, v) => {
|
|
429
|
+
acc[v.severity] = (acc[v.severity] || 0) + 1;
|
|
430
|
+
return acc;
|
|
431
|
+
}, /** @type {Record<string,number>} */ ({}));
|
|
432
|
+
const summary = Object.entries(bySeverity)
|
|
433
|
+
.sort((a, b) => b[1] - a[1])
|
|
434
|
+
.map(([sev, n]) => `${n} ${sev}`).join(', ');
|
|
435
|
+
console.error(
|
|
436
|
+
` ${vulnerable.length} vulnerabilit${vulnerable.length === 1 ? 'y' : 'ies'} found: ${summary}`,
|
|
437
|
+
);
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (sub === 'outdated') {
|
|
442
|
+
// npm registry latest-version check against pinned versions.
|
|
443
|
+
// Mirrors bin/importmap outdated. Exits non-zero when any
|
|
444
|
+
// package is outdated so CI / Renovate-style automation can
|
|
445
|
+
// detect it.
|
|
446
|
+
const outdated = await findOutdated(appDir);
|
|
447
|
+
if (outdated.length === 0) {
|
|
448
|
+
console.log('No outdated packages found.');
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
console.log(`Package Current Latest`);
|
|
452
|
+
for (const o of outdated) {
|
|
453
|
+
console.log(` ${o.pkg.padEnd(38)} ${o.current.padEnd(21)} ${o.latest}`);
|
|
454
|
+
}
|
|
455
|
+
console.error(
|
|
456
|
+
` ${outdated.length} outdated package${outdated.length === 1 ? '' : 's'} found.`,
|
|
457
|
+
);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (sub === 'update') {
|
|
462
|
+
// Re-pin outdated packages to latest. Mirrors bin/importmap
|
|
463
|
+
// update. Does NOT modify package.json or node_modules; the
|
|
464
|
+
// user should run `npm install <pkg>@<latest>` afterward to
|
|
465
|
+
// keep the local install in sync.
|
|
466
|
+
//
|
|
467
|
+
// Provider precedence: explicit --from CLI flag wins. Without
|
|
468
|
+
// it, updatePinned reads the pin file's persisted provider so
|
|
469
|
+
// a user who pinned via jsdelivr stays on jsdelivr after
|
|
470
|
+
// update. Pass `undefined` (not the parsed `from = 'jspm'`
|
|
471
|
+
// default) when no --from was given so updatePinned's
|
|
472
|
+
// pin-file fallback engages.
|
|
473
|
+
const explicitFrom = fromIdx !== -1 ? from : undefined;
|
|
474
|
+
const existing = await readPinFile(appDir);
|
|
475
|
+
const usedFrom = explicitFrom || existing?.provider || 'jspm';
|
|
476
|
+
console.log(`Updating outdated vendor pins in ${appDir}${usedFrom !== 'jspm' ? ` via ${usedFrom}` : ''}...`);
|
|
477
|
+
const result = await updatePinned(appDir, { from: explicitFrom });
|
|
478
|
+
if (result.noOutdated) {
|
|
479
|
+
console.log('No outdated packages found.');
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
if (result.updated.length === 0) {
|
|
483
|
+
console.error('No packages were updated (jspm.io may have failed to resolve any of the new versions).');
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
for (const u of result.updated) {
|
|
487
|
+
console.log(` ${u.pkg.padEnd(38)} ${u.from} → ${u.to}`);
|
|
488
|
+
}
|
|
489
|
+
console.log(
|
|
490
|
+
`Updated ${result.updated.length} package${result.updated.length === 1 ? '' : 's'}. ` +
|
|
491
|
+
`Run \`npm install ${result.updated.map(u => `${u.pkg}@${u.to}`).join(' ')}\` to ` +
|
|
492
|
+
`sync your node_modules.`,
|
|
493
|
+
);
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
console.error(`Unknown vendor subcommand: ${sub || '(none)'}\n` +
|
|
498
|
+
`Usage:\n` +
|
|
499
|
+
` webjs vendor pin [--from PROVIDER] [--download] Pin packages to .webjs/vendor/importmap.json\n` +
|
|
500
|
+
` webjs vendor unpin <pkg> Remove a package from the pin file\n` +
|
|
501
|
+
` webjs vendor list Show pinned packages with versions and URLs\n` +
|
|
502
|
+
` webjs vendor audit Run a security audit against pinned versions\n` +
|
|
503
|
+
` webjs vendor outdated Check pinned packages for newer versions\n` +
|
|
504
|
+
` webjs vendor update [--from PROVIDER] Re-pin outdated packages to latest\n` +
|
|
505
|
+
`\n` +
|
|
506
|
+
` --from PROVIDER CDN to resolve through. One of: ${[...SUPPORTED_PROVIDERS].join(', ')}. Default: jspm.`);
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
269
509
|
case 'help':
|
|
270
510
|
case undefined:
|
|
271
511
|
console.log(USAGE);
|
package/lib/create.js
CHANGED
|
@@ -373,6 +373,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
373
373
|
'.claude/hooks/block-prose-punctuation.sh',
|
|
374
374
|
'.claude/hooks/guard-branch-context.sh',
|
|
375
375
|
'.claude/hooks/nudge-uncommitted.sh',
|
|
376
|
+
'.claude/hooks/require-tests-with-src.sh',
|
|
376
377
|
// Gemini CLI config + hooks
|
|
377
378
|
'.gemini/settings.json',
|
|
378
379
|
'.gemini/hooks/nudge-uncommitted.sh',
|
|
@@ -381,9 +382,14 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
381
382
|
'.cursor/hooks/nudge-uncommitted.sh',
|
|
382
383
|
// OpenCode plugins (loaded as TS by Bun at runtime)
|
|
383
384
|
'.opencode/plugins/nudge-uncommitted.ts',
|
|
385
|
+
// Antigravity workspace rules (Google's documented convention is
|
|
386
|
+
// `.agents/rules/*.md`, lowercase, per the Codelab
|
|
387
|
+
// "Build Autonomous Developer Pipelines using agents.md and skills.md
|
|
388
|
+
// in Antigravity"). Replaced the legacy `.windsurfrules` ship when
|
|
389
|
+
// Windsurf was acquired by Google.
|
|
390
|
+
'.agents/rules/workflow.md',
|
|
384
391
|
// Cross-agent config files
|
|
385
392
|
'.cursorrules',
|
|
386
|
-
'.windsurfrules',
|
|
387
393
|
'.github/copilot-instructions.md',
|
|
388
394
|
'.github/pull_request_template.md',
|
|
389
395
|
'.editorconfig',
|
|
@@ -400,7 +406,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
400
406
|
|
|
401
407
|
// Make hook scripts executable
|
|
402
408
|
const { chmod } = await import('node:fs/promises');
|
|
403
|
-
for (const hook of ['block-prose-punctuation.sh', 'guard-branch-context.sh', 'nudge-uncommitted.sh']) {
|
|
409
|
+
for (const hook of ['block-prose-punctuation.sh', 'guard-branch-context.sh', 'nudge-uncommitted.sh', 'require-tests-with-src.sh']) {
|
|
404
410
|
const hookPath = join(appDir, '.claude', 'hooks', hook);
|
|
405
411
|
if (existsSync(hookPath)) await chmod(hookPath, 0o755);
|
|
406
412
|
}
|
|
@@ -606,7 +612,7 @@ export type ActionResult<T> =
|
|
|
606
612
|
.replace(/`/g, '\\`')
|
|
607
613
|
.replace(/\$\{/g, '\\${');
|
|
608
614
|
|
|
609
|
-
await writeFile(join(appDir, 'app', 'layout.ts'), `import { html } from '@webjsdev/core';
|
|
615
|
+
await writeFile(join(appDir, 'app', 'layout.ts'), `import { html, cspNonce } from '@webjsdev/core';
|
|
610
616
|
import '@webjsdev/core/client-router';
|
|
611
617
|
import '../components/theme-toggle.ts';
|
|
612
618
|
// Webjs UI components are tiered:
|
|
@@ -638,8 +644,13 @@ const navLink = (href: string, label: string) => html\`
|
|
|
638
644
|
\`;
|
|
639
645
|
|
|
640
646
|
export default function RootLayout({ children }: { children: unknown }) {
|
|
647
|
+
// Read the in-flight request's CSP nonce so the theme-detection
|
|
648
|
+
// inline script below passes strict CSP (script-src 'nonce-...').
|
|
649
|
+
// Returns '' when no CSP nonce is set, in which case the attribute
|
|
650
|
+
// is empty and the browser ignores it.
|
|
651
|
+
const nonce = cspNonce();
|
|
641
652
|
return html\`
|
|
642
|
-
<script>
|
|
653
|
+
<script nonce="\${nonce}">
|
|
643
654
|
(function(){
|
|
644
655
|
try {
|
|
645
656
|
var mq = window.matchMedia('(prefers-color-scheme: light)');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "webjs CLI - dev, start, create, db",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@webjsdev/server": "^0.
|
|
16
|
+
"@webjsdev/server": "^0.8.0",
|
|
17
17
|
"@webjsdev/ui": "^0.3.1"
|
|
18
18
|
},
|
|
19
19
|
"publishConfig": {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Antigravity Workspace Rules: webjs app
|
|
2
|
+
|
|
3
|
+
You are working on a webjs app, an AI-first, no-build, web-components-first
|
|
4
|
+
framework. Read AGENTS.md for the full API reference and CONVENTIONS.md for
|
|
5
|
+
project-specific conventions before writing any code. When AGENTS.md does not
|
|
6
|
+
cover what you need, the full hosted docs are at **https://docs.webjs.com**.
|
|
7
|
+
|
|
8
|
+
## Persistence + scaffold rules (non-negotiable)
|
|
9
|
+
|
|
10
|
+
- **Use Prisma + SQLite for data, never JSON files.** It is already wired up
|
|
11
|
+
(`prisma/schema.prisma`, `lib/prisma.server.ts`, `npm run db:migrate`). For
|
|
12
|
+
ANY data the app stores (todos, posts, messages, products, comments), define
|
|
13
|
+
a Prisma model. NEVER create `data/*.json`, `db.json`, or any JSON file as a
|
|
14
|
+
fake database. NEVER use module-scope arrays / Maps as a substitute. NEVER
|
|
15
|
+
use localStorage for app data. `webjs check`'s `no-json-data-files` rule
|
|
16
|
+
will fail the build if you do.
|
|
17
|
+
- **The scaffold is reference, not the final product.** Replace `app/page.ts`,
|
|
18
|
+
the example `User` model, the example users module, etc. with the app the
|
|
19
|
+
user actually asked for. Do not ship "Hello from <app-name>" as the
|
|
20
|
+
deliverable.
|
|
21
|
+
- **Only three templates exist:** `webjs create <name>` (default full-stack),
|
|
22
|
+
`--template api`, `--template saas`. The CLI rejects any other `--template`
|
|
23
|
+
value. Pick:
|
|
24
|
+
- Any product UI (todo, blog, dashboard, marketplace, social) goes through
|
|
25
|
+
the default template.
|
|
26
|
+
- HTTP/JSON API only, no UI, uses `--template api`.
|
|
27
|
+
- Auth / login / signup / SaaS uses `--template saas`.
|
|
28
|
+
|
|
29
|
+
## Before starting ANY work
|
|
30
|
+
|
|
31
|
+
FIRST, before writing any code:
|
|
32
|
+
1. Check `git branch --show-current`.
|
|
33
|
+
- If on main/master: create a feature branch before editing.
|
|
34
|
+
- If on a feature branch: verify it matches the current task.
|
|
35
|
+
2. Sync: `git fetch origin && git rebase origin/main` if behind.
|
|
36
|
+
|
|
37
|
+
## Autonomous mode (sandbox / no-prompt)
|
|
38
|
+
|
|
39
|
+
If running without interactive approval, auto-decide:
|
|
40
|
+
- On main? Auto-create feature/<task-slug> branch.
|
|
41
|
+
- Parent behind? Auto-rebase. Merge? Auto-merge + delete feature branches.
|
|
42
|
+
- Auto-generate commit messages. Fix failing tests and violations.
|
|
43
|
+
Quality bar stays the same, no blocking on questions.
|
|
44
|
+
|
|
45
|
+
## Mandatory workflow (never skip)
|
|
46
|
+
|
|
47
|
+
Every code change must include:
|
|
48
|
+
1. Server tests in `test/<feature>/*.test.ts` (node:test).
|
|
49
|
+
2. Browser tests in `test/<feature>/browser/*.test.js` (WTR + Playwright, real Chromium).
|
|
50
|
+
3. Documentation updates. Walk every surface in the **Definition of done**
|
|
51
|
+
section of CONVENTIONS.md (AGENTS.md, CONVENTIONS.md, README.md, docs/,
|
|
52
|
+
website/, scaffold scripts) and either update it or write
|
|
53
|
+
"N/A because <reason>" in the PR body. Docs land on the same PR as the
|
|
54
|
+
code, never as a follow-up.
|
|
55
|
+
4. Convention check: `webjs check` must pass.
|
|
56
|
+
5. Pre-merge self-review loop. Before saying the PR is ready for merge, run
|
|
57
|
+
fresh-context review rounds until one round finds zero issues. Antigravity
|
|
58
|
+
primitive: open a new Cascade thread or a fresh side-panel session for
|
|
59
|
+
each round so the reviewer has no prior context on the implementation
|
|
60
|
+
decisions. Minimum two rounds; rotate focus each round. Skip the loop
|
|
61
|
+
only for one-line trivial changes; skipping on a change that touches
|
|
62
|
+
logic, public surface, build, security, or multiple files is the exact
|
|
63
|
+
failure mode the loop exists to prevent. The full rule, prompt template,
|
|
64
|
+
and reporting contract live in the **Pre-merge self-review loop** section
|
|
65
|
+
of CONVENTIONS.md.
|
|
66
|
+
|
|
67
|
+
The user should never have to ask for tests, documentation, or the
|
|
68
|
+
self-review loop.
|
|
69
|
+
|
|
70
|
+
## Git rules
|
|
71
|
+
|
|
72
|
+
- COMMIT AND PUSH PER LOGICAL UNIT, NOT AT THE END. One feature, one fix, one
|
|
73
|
+
rename, one doc rewrite per commit. Always `git push` after committing.
|
|
74
|
+
The user should never have to ask for a commit.
|
|
75
|
+
- HARD LIMIT: if you have 5+ unstaged files spanning different concerns,
|
|
76
|
+
commit before continuing. The Claude Code hook at
|
|
77
|
+
`.claude/hooks/nudge-uncommitted.sh` fires at threshold 4. Antigravity
|
|
78
|
+
users should self-enforce the same rule. Batching multiple logical units
|
|
79
|
+
into one commit is the failure mode this rule exists to prevent.
|
|
80
|
+
- Meaningful commit messages: what changed and why.
|
|
81
|
+
- NEVER add Co-Authored-By or AI attribution trailers to commits.
|
|
82
|
+
- NEVER use em-dashes (U+2014), a hyphen-as-pause (` - `), or a
|
|
83
|
+
semicolon-as-pause (` ; `) in commit messages or anywhere else. Rewrite the
|
|
84
|
+
sentence so no pause-punctuation crutch is needed. Use a period, comma,
|
|
85
|
+
colon, parentheses, or a restructured phrasing. Plain hyphens stay fine in
|
|
86
|
+
compound words, CLI flags, filenames, and ranges. Semicolons stay fine
|
|
87
|
+
inside code.
|
|
88
|
+
- Work on feature branches, never push directly to main.
|
|
89
|
+
- Create pull requests for review.
|
|
90
|
+
- NEVER merge any branch without explicit user permission. Always ask:
|
|
91
|
+
"Ready to merge <branch> into <target>? Delete or keep <branch> after?"
|
|
92
|
+
Wait for approval AND the delete/keep preference. Applies to ALL merges.
|
|
93
|
+
- Run `webjs test` before every commit.
|
|
94
|
+
|
|
95
|
+
## Framework rules
|
|
96
|
+
|
|
97
|
+
- No build step: ES modules served directly.
|
|
98
|
+
- **Erasable TypeScript only.** Node 24+ strips types via
|
|
99
|
+
`module.stripTypeScriptTypes` (whitespace replacement, byte-exact position
|
|
100
|
+
preservation, no sourcemap). The scaffold's `tsconfig.json` sets
|
|
101
|
+
`erasableSyntaxOnly: true`, so the TS compiler rejects `enum`, `namespace`
|
|
102
|
+
with values, constructor parameter properties, legacy decorators with
|
|
103
|
+
`emitDecoratorMetadata`, and `import = require`. Use erasable equivalents:
|
|
104
|
+
`const X = { ... } as const` plus a derived union type instead of `enum`;
|
|
105
|
+
explicit fields plus constructor body assignments instead of parameter
|
|
106
|
+
properties. If `erasableSyntaxOnly` is disabled and non-erasable syntax is
|
|
107
|
+
used, the dev server fails at strip time and returns a 500 pointing at the
|
|
108
|
+
`no-non-erasable-typescript` lint rule. webjs is buildless end-to-end and
|
|
109
|
+
has no bundler fallback.
|
|
110
|
+
- Web components render into light DOM by default (so Tailwind / global CSS
|
|
111
|
+
apply directly). Opt in to shadow DOM per component with
|
|
112
|
+
`static shadow = true` when you need scoped styles (via
|
|
113
|
+
`static styles = css\`...\``) or third-party-embed isolation. `<slot>`
|
|
114
|
+
projection works identically in both modes (named slots, fallback content,
|
|
115
|
+
`assignedNodes` / `slotchange`, first-wins resolution).
|
|
116
|
+
- Custom-element tag names are passed to `.register('tag-name')`. They are NOT
|
|
117
|
+
a static field on the class.
|
|
118
|
+
- One function per server action file (`*.server.ts`).
|
|
119
|
+
- Server-only code (`@prisma/client`, `node:*`, anything that needs Node APIs)
|
|
120
|
+
goes only in `.server.{js,ts}` files, `route.ts` handlers, or
|
|
121
|
+
`middleware.ts`. Never in pages, layouts, or components. Wrap the access in
|
|
122
|
+
a `.server.{js,ts}` file; the framework rewrites that import into an RPC
|
|
123
|
+
stub for the browser. `lib/` holds both server-only infra
|
|
124
|
+
(`lib/prisma.server.ts`) and browser-safe utilities (`lib/utils/cn.ts` with
|
|
125
|
+
`cn`); follow the same rule per file.
|
|
126
|
+
- Directives are deliberately minimal: only `unsafeHTML`, `live`, and `repeat`
|
|
127
|
+
ship. Use plain template-literal expressions
|
|
128
|
+
(`class=${active ? 'btn active' : 'btn'}`, `style=${'color:' + color}`,
|
|
129
|
+
`${cond ? a : b}`) and lifecycle hooks (`this.query('#el')` in
|
|
130
|
+
`firstUpdated`) instead of Lit's `classMap` / `styleMap` / `ref` / `when` /
|
|
131
|
+
`choose` / `guard`.
|
|
132
|
+
- Use Context for cross-component data, Task for async data in components.
|
|
133
|
+
- **Progressive enhancement is the default.** Pages AND every web component
|
|
134
|
+
are SSR'd to real HTML. Write components so the first paint is the right
|
|
135
|
+
content. Read SSR-meaningful defaults in `constructor()`. `connectedCallback`
|
|
136
|
+
is never called on the server, so anything there only runs after
|
|
137
|
+
hydration. Initial data for components comes from the page function
|
|
138
|
+
(server-side fetch plus pass as attribute/property), NOT from `fetch` calls
|
|
139
|
+
in `connectedCallback`. For write-paths, prefer `<form action=...>` plus
|
|
140
|
+
server action over `fetch` plus click handler. The framework upgrades plain
|
|
141
|
+
forms to partial-swap submissions automatically.
|
|
142
|
+
- **Client navigation is auto-magic.** Real `<a href>` and `<form action>`
|
|
143
|
+
get partial-swap behavior with no opt-in. Because layouts persist across
|
|
144
|
+
navigation, put shared chrome (sidenav, header) in `layout.ts` and
|
|
145
|
+
page-specific content in `page.ts`. For validation errors, return 4xx HTML
|
|
146
|
+
from a `route.ts` POST handler; the router renders it in place preserving
|
|
147
|
+
the user's input. For non-layout swap regions, wrap in `<webjs-frame id="...">`. See "Client
|
|
148
|
+
navigation patterns" in AGENTS.md.
|
|
149
|
+
- Full API reference in AGENTS.md.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# PreToolUse hook (scaffolded by `webjs create`): block a `git commit`
|
|
4
|
+
# that adds or changes application code without any accompanying test.
|
|
5
|
+
#
|
|
6
|
+
# webjs is AI-first: most apps are built with an AI agent, and the
|
|
7
|
+
# easiest corner to cut is shipping a feature with no test. This gate
|
|
8
|
+
# makes "every change ships with a test" a hard floor, not a suggestion.
|
|
9
|
+
#
|
|
10
|
+
# What a hook CANNOT do: judge WHICH test layer a change needs (a unit
|
|
11
|
+
# test vs a browser/e2e test is a judgement call). So it enforces the
|
|
12
|
+
# floor (some real test must accompany app code) and reminds you to add
|
|
13
|
+
# browser/e2e coverage for interactive surfaces. `webjs test` runs the
|
|
14
|
+
# actual suite in the commit hook.
|
|
15
|
+
#
|
|
16
|
+
# Scope: fires only on `git commit`. Inspects the STAGED diff.
|
|
17
|
+
#
|
|
18
|
+
# Blocks (exit 2) when the staged diff changes app code (app/, modules/,
|
|
19
|
+
# components/, lib/) but stages no test (test/** or *.test.* / *.spec.*).
|
|
20
|
+
# Allowed: commits with no app-code change, commits that stage a test
|
|
21
|
+
# alongside, and WEBJS_NO_TEST_GATE=1 for a genuine non-code commit.
|
|
22
|
+
#
|
|
23
|
+
# Bypass (humans, emergencies): git commit --no-verify.
|
|
24
|
+
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
|
|
27
|
+
if [ "${WEBJS_NO_TEST_GATE:-}" = "1" ]; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
payload=$(cat)
|
|
32
|
+
cmd=$(printf '%s' "$payload" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
|
|
33
|
+
if [ -z "$cmd" ]; then exit 0; fi
|
|
34
|
+
# Match `git commit` as a whole word so sibling subcommands
|
|
35
|
+
# (git commit-graph, git commit-tree) and string mentions do not trip it.
|
|
36
|
+
if ! printf '%s' "$cmd" | grep -Eq '(^|[^[:alnum:]-])git commit([^[:alnum:]-]|$)'; then
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then exit 0; fi
|
|
41
|
+
|
|
42
|
+
staged=$(git diff --cached --name-only 2>/dev/null || true)
|
|
43
|
+
if [ -z "$staged" ]; then exit 0; fi
|
|
44
|
+
|
|
45
|
+
# App code lives under app/, modules/, components/, lib/. A `.server.*`
|
|
46
|
+
# file is still app code. Match source extensions only (skip .css, .md).
|
|
47
|
+
app_code=$(printf '%s\n' "$staged" \
|
|
48
|
+
| grep -E '^(app|modules|components|lib)/.*\.([mc]?[jt]sx?)$' || true)
|
|
49
|
+
if [ -z "$app_code" ]; then exit 0; fi
|
|
50
|
+
|
|
51
|
+
test_staged=$(printf '%s\n' "$staged" \
|
|
52
|
+
| grep -E '(^|/)test/|\.test\.[mc]?[jt]sx?$|\.spec\.[mc]?[jt]sx?$' || true)
|
|
53
|
+
|
|
54
|
+
if [ -z "$test_staged" ]; then
|
|
55
|
+
cat >&2 <<'EOF'
|
|
56
|
+
BLOCKED: this commit changes app code but stages no test.
|
|
57
|
+
|
|
58
|
+
You staged application code (app/, modules/, components/, lib/) with no
|
|
59
|
+
accompanying test. Every change ships with a test. Add or update the test
|
|
60
|
+
that proves the new behaviour, then `git add` it.
|
|
61
|
+
|
|
62
|
+
Pick the layer the change needs (a unit test is not always enough):
|
|
63
|
+
- logic / actions / queries / utils -> a unit test
|
|
64
|
+
- a component, hydration, a server action called from the client, the
|
|
65
|
+
router, anything interactive -> a browser or e2e test that asserts the
|
|
66
|
+
real behaviour in a browser, not just the function in isolation.
|
|
67
|
+
|
|
68
|
+
See `webjs test` and the testing guide. Genuine non-code commit (docs,
|
|
69
|
+
config) that needs no test? Re-run with WEBJS_NO_TEST_GATE=1.
|
|
70
|
+
|
|
71
|
+
Hook: .claude/hooks/require-tests-with-src.sh
|
|
72
|
+
EOF
|
|
73
|
+
exit 2
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# Reminder for interactive surfaces: a unit test alone rarely covers them.
|
|
77
|
+
interactive=$(printf '%s\n' "$app_code" | grep -E '^components/|/components/' || true)
|
|
78
|
+
if [ -n "$interactive" ]; then
|
|
79
|
+
jq -n --arg ctx "Reminder: this commit changes component code. A unit test alone usually is not enough for an interactive component; add a browser test (webjs test --browser) that asserts the rendered/hydrated behaviour." '{
|
|
80
|
+
hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext: $ctx }
|
|
81
|
+
}'
|
|
82
|
+
fi
|
|
83
|
+
exit 0
|