@webjsdev/cli 0.9.1 → 0.10.0
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 +13 -3
- package/package.json +2 -2
- package/templates/.agents/rules/workflow.md +149 -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 +1 -1
- package/templates/AGENTS.md +112 -6
- package/templates/CONVENTIONS.md +141 -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
|
@@ -381,9 +381,14 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
381
381
|
'.cursor/hooks/nudge-uncommitted.sh',
|
|
382
382
|
// OpenCode plugins (loaded as TS by Bun at runtime)
|
|
383
383
|
'.opencode/plugins/nudge-uncommitted.ts',
|
|
384
|
+
// Antigravity workspace rules (Google's documented convention is
|
|
385
|
+
// `.agents/rules/*.md`, lowercase, per the Codelab
|
|
386
|
+
// "Build Autonomous Developer Pipelines using agents.md and skills.md
|
|
387
|
+
// in Antigravity"). Replaced the legacy `.windsurfrules` ship when
|
|
388
|
+
// Windsurf was acquired by Google.
|
|
389
|
+
'.agents/rules/workflow.md',
|
|
384
390
|
// Cross-agent config files
|
|
385
391
|
'.cursorrules',
|
|
386
|
-
'.windsurfrules',
|
|
387
392
|
'.github/copilot-instructions.md',
|
|
388
393
|
'.github/pull_request_template.md',
|
|
389
394
|
'.editorconfig',
|
|
@@ -606,7 +611,7 @@ export type ActionResult<T> =
|
|
|
606
611
|
.replace(/`/g, '\\`')
|
|
607
612
|
.replace(/\$\{/g, '\\${');
|
|
608
613
|
|
|
609
|
-
await writeFile(join(appDir, 'app', 'layout.ts'), `import { html } from '@webjsdev/core';
|
|
614
|
+
await writeFile(join(appDir, 'app', 'layout.ts'), `import { html, cspNonce } from '@webjsdev/core';
|
|
610
615
|
import '@webjsdev/core/client-router';
|
|
611
616
|
import '../components/theme-toggle.ts';
|
|
612
617
|
// Webjs UI components are tiered:
|
|
@@ -638,8 +643,13 @@ const navLink = (href: string, label: string) => html\`
|
|
|
638
643
|
\`;
|
|
639
644
|
|
|
640
645
|
export default function RootLayout({ children }: { children: unknown }) {
|
|
646
|
+
// Read the in-flight request's CSP nonce so the theme-detection
|
|
647
|
+
// inline script below passes strict CSP (script-src 'nonce-...').
|
|
648
|
+
// Returns '' when no CSP nonce is set, in which case the attribute
|
|
649
|
+
// is empty and the browser ignores it.
|
|
650
|
+
const nonce = cspNonce();
|
|
641
651
|
return html\`
|
|
642
|
-
<script>
|
|
652
|
+
<script nonce="\${nonce}">
|
|
643
653
|
(function(){
|
|
644
654
|
try {
|
|
645
655
|
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.0",
|
|
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.
|
package/templates/.cursorrules
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Cursor Rules
|
|
1
|
+
# Cursor Rules: webjs app
|
|
2
2
|
|
|
3
|
-
You are working on a webjs app
|
|
3
|
+
You are working on a webjs app, an AI-first, no-build, web-components-first
|
|
4
4
|
framework. Read AGENTS.md for the full API reference and CONVENTIONS.md for
|
|
5
5
|
project-specific conventions before writing any code. When AGENTS.md doesn't
|
|
6
6
|
cover what you need, the full hosted docs are at **https://docs.webjs.com**.
|
|
@@ -36,7 +36,7 @@ FIRST, before writing any code:
|
|
|
36
36
|
- If upstream has new commits: `git rebase origin/main` before starting.
|
|
37
37
|
- Resolve any conflicts before proceeding with the task.
|
|
38
38
|
|
|
39
|
-
## Autonomous mode (sandbox / no-prompt
|
|
39
|
+
## Autonomous mode (sandbox / no-prompt)
|
|
40
40
|
|
|
41
41
|
If running without interactive approval, auto-decide:
|
|
42
42
|
- On main? Auto-create feature/<task-slug> branch
|
|
@@ -44,29 +44,44 @@ If running without interactive approval, auto-decide:
|
|
|
44
44
|
- Merge? Auto-merge in autonomous mode, delete feature branches after
|
|
45
45
|
- Commit message? Auto-generate (meaningful, no AI attribution)
|
|
46
46
|
- Tests failing? Fix them. Convention violations? Fix them.
|
|
47
|
-
Quality bar stays the same
|
|
47
|
+
Quality bar stays the same, no blocking on questions.
|
|
48
48
|
|
|
49
49
|
## Mandatory workflow (never skip)
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
Every code change must include:
|
|
52
|
+
1. Server tests in `test/<feature>/*.test.ts` (node:test).
|
|
53
|
+
2. Browser tests in `test/<feature>/browser/*.test.js` (WTR + Playwright, real Chromium).
|
|
54
|
+
3. Documentation updates. Walk every surface in the **Definition of done**
|
|
55
|
+
section of CONVENTIONS.md (AGENTS.md, CONVENTIONS.md, README.md, docs/,
|
|
56
|
+
website/, scaffold scripts) and either update it or write
|
|
57
|
+
"N/A because <reason>" in the PR body. Docs land on the same PR as the
|
|
58
|
+
code, never as a follow-up.
|
|
59
|
+
4. Convention check: `webjs check` must pass.
|
|
60
|
+
5. Pre-merge self-review loop. Before saying the PR is ready for merge, run
|
|
61
|
+
fresh-context review rounds until one round finds zero issues. Cursor
|
|
62
|
+
primitive: open a NEW composer tab and prompt the review there so the
|
|
63
|
+
reviewer has no prior context on your decisions. Minimum two rounds;
|
|
64
|
+
rotate focus each round. Skip the loop only for one-line trivial
|
|
65
|
+
changes; skipping on a change that touches logic, public surface, build,
|
|
66
|
+
security, or multiple files is the exact failure mode the loop exists
|
|
67
|
+
to prevent. The full rule, prompt template, and reporting contract live
|
|
68
|
+
in the **Pre-merge self-review loop** section of CONVENTIONS.md.
|
|
54
69
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
3. CONVENTIONS: Run `webjs check` and fix violations before committing.
|
|
70
|
+
The user should never have to ask for tests, documentation, or the
|
|
71
|
+
self-review loop.
|
|
59
72
|
|
|
60
73
|
## Git rules
|
|
61
74
|
|
|
62
75
|
- COMMIT AND PUSH PER LOGICAL UNIT, NOT AT THE END. One feature, one fix,
|
|
63
76
|
one rename, one doc rewrite per commit. Always `git push` after
|
|
64
|
-
committing.
|
|
77
|
+
committing. The user should never have to ask for a commit.
|
|
65
78
|
- HARD LIMIT: if you have 5+ unstaged files spanning different concerns,
|
|
66
|
-
commit before continuing.
|
|
67
|
-
`.
|
|
68
|
-
|
|
69
|
-
|
|
79
|
+
commit before continuing. Cursor 1.7+ has its own
|
|
80
|
+
`.cursor/hooks/nudge-uncommitted.sh` (afterFileEdit) firing at threshold
|
|
81
|
+
4; the same enforcement runs for Claude users via
|
|
82
|
+
`.claude/hooks/nudge-uncommitted.sh`. On older Cursor versions without
|
|
83
|
+
the hook, self-enforce the same rule. Batching multiple logical units
|
|
84
|
+
into one commit is the failure mode this rule exists to prevent.
|
|
70
85
|
- Write meaningful commit messages: what changed and why, not "update files"
|
|
71
86
|
- NEVER add "Co-Authored-By", "Generated by", "AI-assisted" or similar
|
|
72
87
|
attribution trailers to commits
|
|
@@ -77,23 +92,22 @@ Quality bar stays the same - just no blocking on questions.
|
|
|
77
92
|
Plain hyphens stay fine in compound words, CLI flags, filenames,
|
|
78
93
|
and ranges. Semicolons stay fine inside code
|
|
79
94
|
- Work on feature branches, not main
|
|
80
|
-
- NEVER push directly to main
|
|
95
|
+
- NEVER push directly to main. Create a pull request instead.
|
|
81
96
|
- NEVER merge any branch without explicit user permission. Always ask:
|
|
82
97
|
"Ready to merge <branch> into <target>? Delete or keep <branch> after?"
|
|
83
98
|
Wait for approval AND the delete/keep preference before proceeding.
|
|
84
99
|
This applies to ALL merges, not just merges into main.
|
|
85
100
|
- Run tests before every commit
|
|
86
|
-
- Keep commits small and focused
|
|
87
101
|
|
|
88
102
|
## Framework rules
|
|
89
103
|
|
|
90
104
|
- No build step: source files are served as ES modules
|
|
91
|
-
- **Erasable TypeScript only.** Node 24+ strips types via `module.stripTypeScriptTypes` (whitespace replacement, byte-exact position preservation, no sourcemap). The scaffold's tsconfig.json sets `erasableSyntaxOnly: true`, so the TS compiler rejects `enum`, `namespace` with values, constructor parameter properties, legacy decorators with `emitDecoratorMetadata`, and `import = require`. Use erasable equivalents: `const X = { ... } as const` plus a derived union type instead of `enum`; explicit fields plus constructor body assignments instead of parameter properties. If `erasableSyntaxOnly` is disabled and non-erasable syntax is used, the dev server
|
|
105
|
+
- **Erasable TypeScript only.** Node 24+ strips types via `module.stripTypeScriptTypes` (whitespace replacement, byte-exact position preservation, no sourcemap). The scaffold's tsconfig.json sets `erasableSyntaxOnly: true`, so the TS compiler rejects `enum`, `namespace` with values, constructor parameter properties, legacy decorators with `emitDecoratorMetadata`, and `import = require`. Use erasable equivalents: `const X = { ... } as const` plus a derived union type instead of `enum`; explicit fields plus constructor body assignments instead of parameter properties. If `erasableSyntaxOnly` is disabled and non-erasable syntax is used, the dev server fails at strip time and returns a 500 pointing at the `no-non-erasable-typescript` lint rule. webjs is buildless end-to-end and has no bundler fallback.
|
|
92
106
|
- Web components with shadow DOM: use `static styles = css` not inline styles
|
|
93
107
|
- One function per server action file (*.server.ts)
|
|
94
108
|
- Components must call customElements.define('tag', Class)
|
|
95
109
|
- Server-only code (@prisma/client, node:*, anything that needs Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap the access in a .server.{js,ts} file; the framework rewrites that import into an RPC stub for the browser. lib/ holds both server-only infra (lib/prisma.server.ts) and browser-safe utilities (lib/utils/cn.ts with cn); follow the same rule per file: if a lib/ file needs Node APIs, only import it from server-only files.
|
|
96
|
-
- Directives are deliberately minimal: only `unsafeHTML`, `live`, and `repeat` ship. Lit's `classMap` / `styleMap` / `ref` / `when` / `choose` / `guard` are NOT exported
|
|
97
|
-
- **Progressive enhancement is the default.** Pages AND every web component are SSR'd to real HTML. Write components so the first paint is the right content
|
|
98
|
-
- **Client navigation is auto-magic.** Real `<a href>` and `<form action>` get partial-swap behavior with no opt-in.
|
|
110
|
+
- Directives are deliberately minimal: only `unsafeHTML`, `live`, and `repeat` ship. Lit's `classMap` / `styleMap` / `ref` / `when` / `choose` / `guard` are NOT exported. Use plain template-literal expressions (`class=${cond ? 'a' : 'b'}`, `${cond ? a : b}`) and lifecycle hooks (`this.query('#el')` in `firstUpdated`) instead.
|
|
111
|
+
- **Progressive enhancement is the default.** Pages AND every web component are SSR'd to real HTML. Write components so the first paint is the right content. Read SSR-meaningful defaults in `constructor()`. `connectedCallback` is never called on the server, so anything there only runs after hydration. Initial data for components comes from the page function (server-side fetch plus pass as attribute/property), NOT from `fetch` calls in `connectedCallback`. For write-paths, prefer `<form action=...>` plus server action over `fetch` plus click handler. The framework upgrades plain forms to partial-swap submissions automatically.
|
|
112
|
+
- **Client navigation is auto-magic.** Real `<a href>` and `<form action>` get partial-swap behavior with no opt-in. Because layouts persist across navigation, put shared chrome (sidenav, header) in `layout.ts` and page-specific content in `page.ts`. For validation errors, return 4xx HTML from a `route.ts` POST handler; the router renders it in place preserving the user's input. For non-layout swap regions, wrap in `<webjs-frame id="...">`. See "Client navigation patterns" in AGENTS.md.
|
|
99
113
|
- See AGENTS.md for the complete directive decision guide
|
|
@@ -33,53 +33,80 @@ FIRST, before writing any code:
|
|
|
33
33
|
- If on a feature branch: verify it matches the task at hand.
|
|
34
34
|
2. Sync: `git fetch origin && git rebase origin/main` if behind.
|
|
35
35
|
|
|
36
|
-
## Autonomous mode
|
|
36
|
+
## Autonomous mode (sandbox / no-prompt)
|
|
37
37
|
|
|
38
38
|
If running without interactive approval (sandbox, auto-approve, etc.):
|
|
39
39
|
- On main? Auto-create feature/<task-slug> branch
|
|
40
40
|
- Parent behind? Auto-rebase. Merge? Auto-merge + delete feature branches.
|
|
41
41
|
- Auto-generate meaningful commit messages. Fix tests and violations.
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Quality bar stays the same, no blocking on questions.
|
|
44
|
+
|
|
45
|
+
## Mandatory workflow (never skip)
|
|
44
46
|
|
|
45
47
|
Every code change must include:
|
|
46
|
-
1.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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 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,
|
|
57
|
+
run fresh-context review rounds until one round finds zero issues.
|
|
58
|
+
Copilot primitive: open a NEW chat session (reset the side panel) 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
|
|
64
|
+
template, and reporting contract live in the **Pre-merge self-review
|
|
65
|
+
loop** section of CONVENTIONS.md.
|
|
66
|
+
|
|
67
|
+
The user should never have to ask for tests, documentation, or the
|
|
68
|
+
self-review loop. The commit-per-logical-unit rule lives under "Git rules"
|
|
69
|
+
below, not here, since it governs how work is grouped rather than what
|
|
70
|
+
each change must include.
|
|
56
71
|
|
|
57
72
|
## Git rules
|
|
58
73
|
|
|
59
|
-
-
|
|
74
|
+
- COMMIT AND PUSH PER LOGICAL UNIT, NOT AT THE END. One feature, one fix,
|
|
75
|
+
one rename, one doc rewrite per commit. Always `git push` after
|
|
76
|
+
committing. The user should never have to ask for a commit.
|
|
77
|
+
- HARD LIMIT: if you have 5+ unstaged files spanning different concerns,
|
|
78
|
+
commit before continuing. The Claude Code hook at
|
|
79
|
+
`.claude/hooks/nudge-uncommitted.sh` enforces threshold 4 for Claude
|
|
80
|
+
users. Copilot has no equivalent hook surface today; self-enforce the
|
|
81
|
+
same rule. Batching multiple logical units into one commit is the
|
|
82
|
+
failure mode this rule exists to prevent.
|
|
60
83
|
- Meaningful commit messages: what changed and why
|
|
61
84
|
- NEVER add Co-Authored-By or AI attribution trailers to commits
|
|
85
|
+
- NEVER use em-dashes (U+2014), a hyphen-as-pause (` - `), or a
|
|
86
|
+
semicolon-as-pause (` ; `) in commit messages or anywhere else.
|
|
87
|
+
Rewrite the sentence so no pause-punctuation crutch is needed. Use a
|
|
88
|
+
period, comma, colon, parentheses, or a restructured phrasing. Plain
|
|
89
|
+
hyphens stay fine in compound words, CLI flags, filenames, and ranges.
|
|
90
|
+
Semicolons stay fine inside code.
|
|
62
91
|
- Work on feature branches, create PRs, never push directly to main
|
|
63
92
|
- NEVER merge any branch without explicit user permission. Always ask:
|
|
64
93
|
"Ready to merge <branch> into <target>? Delete or keep <branch> after?"
|
|
65
94
|
Wait for approval AND the delete/keep preference. Applies to ALL merges.
|
|
66
95
|
- Run `webjs test` before every commit
|
|
67
96
|
|
|
68
|
-
##
|
|
97
|
+
## Framework rules
|
|
69
98
|
|
|
70
|
-
-
|
|
71
|
-
|
|
99
|
+
- No build step: source files are served as ES modules. Don't introduce
|
|
100
|
+
build tools or bundlers in the critical path.
|
|
101
|
+
- **Erasable TypeScript only.** Node 24+ strips types via `module.stripTypeScriptTypes` (whitespace replacement, byte-exact position preservation, no sourcemap). The scaffold's tsconfig.json sets `erasableSyntaxOnly: true`, so the TS compiler rejects `enum`, `namespace` with values, constructor parameter properties, legacy decorators with `emitDecoratorMetadata`, and `import = require`. Use erasable equivalents: `const X = { ... } as const` plus a derived union type instead of `enum`; explicit fields plus constructor body assignments instead of parameter properties. If `erasableSyntaxOnly` is disabled and non-erasable syntax is used, the dev server fails at strip time and returns a 500 pointing at the `no-non-erasable-typescript` lint rule. webjs is buildless end-to-end and has no bundler fallback.
|
|
102
|
+
- Tagged template: html`<div>${value}</div>` with css`...` for styles.
|
|
103
|
+
Don't use inline `style="..."` on components (use `static styles = css\`...\``).
|
|
72
104
|
- Components: extend WebComponent, declare `static properties` (and `static styles` for shadow-DOM components), call `Class.register('tag-name')` at the bottom of the file. The tag name is the argument to `.register()`, not a static field.
|
|
73
|
-
- Server actions: *.server.ts files with one exported async function each
|
|
74
|
-
-
|
|
105
|
+
- Server actions: *.server.ts files with one exported async function each.
|
|
106
|
+
- Server-only code (@prisma/client, node:*, anything needing Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap in a .server.{js,ts} file; the framework rewrites that import to an RPC stub for the browser. lib/ holds both server-only infra (lib/prisma.server.ts) and browser-safe utilities (lib/utils/cn.ts with cn); apply the same rule per file.
|
|
107
|
+
- Directives: webjs ships only `unsafeHTML`, `live`, and `repeat`. Lit's `classMap` / `styleMap` / `ref` / `when` / `choose` / `guard` are NOT exported. Use plain template-literal expressions and lifecycle hooks instead.
|
|
75
108
|
- Context: import { createContext, ContextProvider, ContextConsumer } from '@webjsdev/core/context'
|
|
76
109
|
- Task: import { Task, TaskStatus } from '@webjsdev/core/task'
|
|
77
|
-
- Routing: file-based under app/ (page.ts, layout.ts, route.ts, middleware.ts)
|
|
78
|
-
|
|
79
|
-
## What NOT to do
|
|
80
|
-
|
|
81
|
-
- Don't introduce build tools or bundlers in the critical path
|
|
82
|
-
- Server-only code (@prisma/client, node:*, anything needing Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap in a .server.{js,ts} file; the framework rewrites that import to an RPC stub for the browser. lib/ holds both server-only infra (lib/prisma.server.ts) and browser-safe utilities (lib/utils/cn.ts with cn); apply the same rule per file.
|
|
83
|
-
- Don't use inline style="..." on components (use static styles = css`...`)
|
|
110
|
+
- Routing: file-based under app/ (page.ts, layout.ts, route.ts, middleware.ts).
|
|
84
111
|
- Component state lives in signals from @webjsdev/core. Module-scope signals share state across components; instance signals (created in the constructor) carry component-local state. Reactive properties (static properties + declare) are for HTML attributes and .prop=${...} hydration.
|
|
85
|
-
- Don't skip tests or documentation updates
|
|
112
|
+
- Don't skip tests or documentation updates.
|
|
@@ -8,7 +8,31 @@
|
|
|
8
8
|
- [ ] E2E tests added/updated for user-facing changes (`webjs test --e2e` passes)
|
|
9
9
|
- [ ] `webjs check` passes (no convention violations)
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Definition of done
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
Documentation MUST land on the same PR as the code change. Drift is how
|
|
14
|
+
a codebase rots. Walk every markdown file in the project (`git ls-files
|
|
15
|
+
'*.md'`) and ask whether this PR changed behaviour, surface, or
|
|
16
|
+
invariants it describes. For each row below, write `Updated <path>` or
|
|
17
|
+
`N/A because <reason>`. Reviewers should reject the PR if this section
|
|
18
|
+
is left as the template default. See the **Definition of done** section
|
|
19
|
+
in [`CONVENTIONS.md`](../CONVENTIONS.md) for the full guidance.
|
|
20
|
+
|
|
21
|
+
- [ ] **Tests.** Unit coverage for logic. Real-browser coverage for
|
|
22
|
+
user-facing behaviour.
|
|
23
|
+
- [ ] **Every markdown file in the project** that describes the
|
|
24
|
+
changed surface. Common cases (non-exhaustive): `AGENTS.md` (root
|
|
25
|
+
+ nested), `CONVENTIONS.md`, `README.md` (root + nested),
|
|
26
|
+
`CHANGELOG.md`, `docs/**/*.md`, `agent-docs/**/*.md`,
|
|
27
|
+
`.github/*.md`. The rule is generative: if a markdown file in
|
|
28
|
+
this project mentions a thing this PR changed, it gets touched
|
|
29
|
+
on this PR.
|
|
30
|
+
- [ ] **`website/`** (if the project has one). Marketing copy on the
|
|
31
|
+
landing or pricing page when the change touches a claim made
|
|
32
|
+
there.
|
|
33
|
+
- [ ] **Scaffold scripts / codegen** (if the project has any). Updated
|
|
34
|
+
when the change affects what new instances generate.
|
|
35
|
+
- [ ] **Pre-merge self-review loop.** Ran N rounds; last round clean.
|
|
36
|
+
Skip only for one-line trivial changes. See the **Pre-merge
|
|
37
|
+
self-review loop** section in [`CONVENTIONS.md`](../CONVENTIONS.md)
|
|
38
|
+
for the prompt template and reporting contract.
|
|
@@ -22,7 +22,7 @@ if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
|
|
|
22
22
|
fi
|
|
23
23
|
|
|
24
24
|
# webjs test + webjs check on every commit. Tool-agnostic enforcement:
|
|
25
|
-
# fires regardless of which agent (Claude, Cursor,
|
|
25
|
+
# fires regardless of which agent (Claude, Cursor, Antigravity, Copilot,
|
|
26
26
|
# human) is making the commit. Skipped if the CLI is not yet installed
|
|
27
27
|
# (fresh clone before npm install).
|
|
28
28
|
if command -v webjs >/dev/null 2>&1 || [ -x "node_modules/.bin/webjs" ]; then
|
package/templates/AGENTS.md
CHANGED
|
@@ -301,6 +301,20 @@ In Docker / Railway, prefer `npm start` (or `node node_modules/.bin/npm
|
|
|
301
301
|
start`) as the CMD over `node ... webjs.js start ...`. The npm form
|
|
302
302
|
fires `prestart`; the direct binary form skips it.
|
|
303
303
|
|
|
304
|
+
**Health and readiness probes.** Every webjs server answers two endpoints:
|
|
305
|
+
`/__webjs/health` (liveness, 200 once the process is listening) and
|
|
306
|
+
`/__webjs/ready` (readiness, 503 until the instance is fully warm, then 200).
|
|
307
|
+
Fully warm means the deterministic analysis AND the first vendor attempt have
|
|
308
|
+
both completed, so the importmap and its build id are settled. Point your
|
|
309
|
+
platform's readiness check at `/__webjs/ready` so it holds traffic off a
|
|
310
|
+
not-yet-warmed instance instead of routing the first user request into the cold
|
|
311
|
+
analysis or the brief window where the importmap is still resolving. On
|
|
312
|
+
Railway, set `"healthcheckPath": "/__webjs/ready"` under `deploy` in
|
|
313
|
+
`railway.json`. For dependency-aware
|
|
314
|
+
readiness (gate on a live DB ping), add an optional `readiness.{js,ts}` at the
|
|
315
|
+
app root that default-exports an async check; `/__webjs/ready` runs it once warm
|
|
316
|
+
and reports 503 if it returns `false` or throws.
|
|
317
|
+
|
|
304
318
|
Scripts:
|
|
305
319
|
|
|
306
320
|
- `npm run db:migrate`: `prisma migrate dev` (dev-time schema changes + migration + generate)
|
|
@@ -320,6 +334,87 @@ const users = await prisma.user.findMany();
|
|
|
320
334
|
To switch to Postgres or MySQL: change `provider` in `prisma/schema.prisma`
|
|
321
335
|
and the `DATABASE_URL` in `.env`.
|
|
322
336
|
|
|
337
|
+
## NPM packages (vendor pipeline)
|
|
338
|
+
|
|
339
|
+
Adding a third-party npm package follows the same `npm install` flow
|
|
340
|
+
as any Node project, with one webjs-specific concern: how the BROWSER
|
|
341
|
+
fetches that package.
|
|
342
|
+
|
|
343
|
+
```sh
|
|
344
|
+
npm install dayjs # standard npm install
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Now write `import dayjs from 'dayjs'` in any component or page. The
|
|
348
|
+
import works in dev immediately. webjs's scanner discovers bare
|
|
349
|
+
imports on the first request (memoized for the process) and asks
|
|
350
|
+
`api.jspm.io` to resolve them to CDN URLs (jspm.io serves pre-bundled
|
|
351
|
+
ESM for every npm package). The browser fetches the bundle directly
|
|
352
|
+
from `https://ga.jspm.io`.
|
|
353
|
+
|
|
354
|
+
**For production deploys**, run `webjs vendor pin` once and commit
|
|
355
|
+
the result:
|
|
356
|
+
|
|
357
|
+
```sh
|
|
358
|
+
webjs vendor pin # writes .webjs/vendor/importmap.json
|
|
359
|
+
git add .webjs/vendor/
|
|
360
|
+
git commit -m "vendor dayjs"
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
The pin file holds the resolved jspm.io URLs. Server reads it from
|
|
364
|
+
disk on the first request (memoized); no `api.jspm.io` call needed in
|
|
365
|
+
production. Deterministic across deploys.
|
|
366
|
+
|
|
367
|
+
**For offline-capable / strict-CSP production**, use `--download`:
|
|
368
|
+
|
|
369
|
+
```sh
|
|
370
|
+
webjs vendor pin --download # also vendors bundle bytes locally
|
|
371
|
+
git add .webjs/vendor/
|
|
372
|
+
git commit -m "vendor + download dayjs"
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Bundle files land in `.webjs/vendor/<pkg>@<version>.js`. importmap
|
|
376
|
+
points at local `/__webjs/vendor/` paths. Browser fetches from your
|
|
377
|
+
own origin. Suitable for `script-src 'self'` CSP, air-gapped deploys,
|
|
378
|
+
or compliance environments. See [docs.webjs.com Deployment → CSP](https://docs.webjs.com/docs/deployment#csp).
|
|
379
|
+
|
|
380
|
+
**Other CLI commands:**
|
|
381
|
+
|
|
382
|
+
```sh
|
|
383
|
+
webjs vendor list # show pinned packages with versions
|
|
384
|
+
webjs vendor unpin <pkg> # remove one entry from pin file
|
|
385
|
+
webjs vendor audit # npm security advisories against pinned versions
|
|
386
|
+
webjs vendor outdated # list pinned packages with newer versions on npm
|
|
387
|
+
webjs vendor update # re-pin every outdated package to its latest
|
|
388
|
+
|
|
389
|
+
# Switch CDN at pin time (default: jspm.io). Resolver options:
|
|
390
|
+
# jspm, jsdelivr, unpkg, skypack. Useful for jspm.io incident response.
|
|
391
|
+
webjs vendor pin --from jsdelivr
|
|
392
|
+
webjs vendor update --from jsdelivr
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Same posture as Rails 7 + importmap-rails: explicit pin command,
|
|
396
|
+
committed manifest, optional `--download` for full offline capability,
|
|
397
|
+
and a `--from` knob to swap the resolver CDN if jspm.io has an
|
|
398
|
+
incident.
|
|
399
|
+
|
|
400
|
+
**Don't auto-run `webjs vendor pin` in `predev` / `prestart`.** Auto-pin
|
|
401
|
+
would silently churn the committed importmap.json as jspm.io resolves
|
|
402
|
+
URLs or transitive deps drift. Pin is a deliberate developer action,
|
|
403
|
+
like `npm install` itself.
|
|
404
|
+
|
|
405
|
+
**Do NOT modify the `.webjs/` lines in `.gitignore` / `.dockerignore`.**
|
|
406
|
+
The scaffolded pattern is three lines (`.webjs/*` + `!.webjs/vendor/`
|
|
407
|
+
+ `!.webjs/vendor/**`) and is structurally load-bearing. Collapsing it
|
|
408
|
+
to a single `.webjs/` excludes the parent directory; once the parent
|
|
409
|
+
is excluded, git cannot re-include `.webjs/vendor/` via a child
|
|
410
|
+
negation (gitignore semantics: parent exclusion blocks child
|
|
411
|
+
negations). The breakage is invisible: `webjs vendor pin` runs, writes
|
|
412
|
+
files, and git silently ignores them. Production then has no
|
|
413
|
+
importmap.json and the server falls back to calling api.jspm.io on
|
|
414
|
+
every cold start. The `gitignore-vendor-not-ignored` lint rule
|
|
415
|
+
(`webjs check`) verifies the pattern with `git check-ignore` and will
|
|
416
|
+
fail CI if it regresses.
|
|
417
|
+
|
|
323
418
|
## Imports
|
|
324
419
|
|
|
325
420
|
```ts
|
|
@@ -770,9 +865,9 @@ composition, so a nested shell ends up dropped by the HTML parser.
|
|
|
770
865
|
```
|
|
771
866
|
|
|
772
867
|
If you turn `erasableSyntaxOnly` off and use non-erasable syntax,
|
|
773
|
-
the dev server
|
|
774
|
-
|
|
775
|
-
|
|
868
|
+
the dev server fails at strip time and returns a 500 naming the
|
|
869
|
+
file and pointing at the `no-non-erasable-typescript` lint rule.
|
|
870
|
+
webjs is buildless end-to-end and has no bundler fallback. The
|
|
776
871
|
`erasable-typescript-only` convention check warns when the flag
|
|
777
872
|
is missing or set to false.
|
|
778
873
|
9. **No em-dashes (U+2014) anywhere, and no hyphen or semicolon used
|
|
@@ -802,14 +897,25 @@ composition, so a nested shell ends up dropped by the HTML parser.
|
|
|
802
897
|
| Gemini CLI | `.gemini/hooks/nudge-uncommitted.sh` (`AfterTool`) | `.gemini/settings.json` |
|
|
803
898
|
| Cursor 1.7+ | `.cursor/hooks/nudge-uncommitted.sh` (`afterFileEdit`) | `.cursor/hooks.json` |
|
|
804
899
|
| OpenCode | `.opencode/plugins/nudge-uncommitted.ts` (`tool.execute.after`) | `.opencode/plugins/` |
|
|
805
|
-
|
|
|
900
|
+
| Antigravity (Google) | text rule only (post-write hooks not yet exposed) | `.agents/rules/workflow.md` |
|
|
806
901
|
| GitHub Copilot | text rule only (no hooks API) | `.github/copilot-instructions.md` |
|
|
807
|
-
| Google Antigravity | text rule only (no hooks API) | `AGENTS.md` |
|
|
808
902
|
|
|
809
903
|
Tool-agnostic fallback: `.hooks/pre-commit` runs `webjs test` + `webjs check`
|
|
810
904
|
on every commit, regardless of which agent (or human) made it. No AI
|
|
811
905
|
attribution trailers in commit messages.
|
|
812
|
-
4.
|
|
906
|
+
4. Run the **pre-merge self-review loop** before signaling the PR is
|
|
907
|
+
ready. After committing the work, trigger a fresh-context review
|
|
908
|
+
pass (a new chat / composer tab / subagent / Cascade thread
|
|
909
|
+
depending on your tool) and iterate fix-then-review rounds until
|
|
910
|
+
one round finds zero issues. Minimum two rounds; rotate focus each
|
|
911
|
+
round so the reviewer does not rediscover the same surface twice.
|
|
912
|
+
Skip the loop only for one-line trivial changes; skipping on a
|
|
913
|
+
change that touches logic, public surface, build, security, or
|
|
914
|
+
multiple files is the exact failure mode the loop exists to
|
|
915
|
+
prevent. The full rule, prompt template, and reporting contract
|
|
916
|
+
live in the **Pre-merge self-review loop** section of
|
|
917
|
+
`CONVENTIONS.md`.
|
|
918
|
+
5. When unsure how a framework feature works, `grep` or `cat` the
|
|
813
919
|
relevant `node_modules/@webjsdev/*/src/` file before asking the user.
|
|
814
920
|
|
|
815
921
|
Project-specific conventions and overrides live in
|
package/templates/CONVENTIONS.md
CHANGED
|
@@ -95,16 +95,135 @@ even if the user doesn't explicitly ask.**
|
|
|
95
95
|
Run `webjs test` after every change. Never mark work as done with
|
|
96
96
|
failing tests.
|
|
97
97
|
|
|
98
|
-
3. **Documentation updates.**
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
3. **Convention check.** Run `webjs check` after changes and fix
|
|
98
|
+
3. **Documentation updates.** See the **Definition of done** section
|
|
99
|
+
below for the per-surface checklist. The short version: docs land on
|
|
100
|
+
the same PR as the code, never as a follow-up. Drift is how a
|
|
101
|
+
codebase rots; the user should never have to ask "did you update the
|
|
102
|
+
docs?"
|
|
103
|
+
|
|
104
|
+
4. **Convention check.** Run `webjs check` after changes and fix
|
|
106
105
|
any violations before reporting the task as done.
|
|
107
106
|
|
|
107
|
+
### Definition of done (MUST be addressed BEFORE opening the PR)
|
|
108
|
+
|
|
109
|
+
This is the per-PR contract. Before running `gh pr create`, walk through
|
|
110
|
+
every surface below and either update it OR write `N/A because <reason>`
|
|
111
|
+
in the PR body so the omission is visible. The
|
|
112
|
+
[`.github/pull_request_template.md`](./.github/pull_request_template.md)
|
|
113
|
+
checklist mirrors this list.
|
|
114
|
+
|
|
115
|
+
**Surfaces to consider on EVERY PR:**
|
|
116
|
+
|
|
117
|
+
1. **Tests.** Unit coverage for new logic. Real-browser coverage for
|
|
118
|
+
user-facing behaviour. `webjs test` must pass; `webjs test --browser`
|
|
119
|
+
for any DOM-touching change. See the "Testing" section below for the
|
|
120
|
+
per-change matrix.
|
|
121
|
+
2. **Every markdown file in the project.** Walk the whole tree, not a
|
|
122
|
+
closed list. Run `git ls-files '*.md'` (or `git ls-files '*.md'
|
|
123
|
+
'*.mdx'` if the project ships MDX) and for each path ask: does this
|
|
124
|
+
file describe behaviour, surface, or invariants that this PR changed?
|
|
125
|
+
If yes, update it on this PR. Common surfaces (non-exhaustive):
|
|
126
|
+
- `AGENTS.md` (root and every nested one) for API surface, invariants,
|
|
127
|
+
file-routing rules, project-wide agent workflow.
|
|
128
|
+
- `CONVENTIONS.md` (this file) for architectural conventions. Do NOT
|
|
129
|
+
enumerate lint rules in prose; those live in `package.json` under
|
|
130
|
+
`"webjs": { "conventions": { … } }`.
|
|
131
|
+
- `README.md` (root and any nested ones) for install / use / public
|
|
132
|
+
surface descriptions.
|
|
133
|
+
- `CHANGELOG.md` for any user-visible change, including the SHA / PR
|
|
134
|
+
reference. Keep it in chronological order; don't backdate.
|
|
135
|
+
- `docs/` (if the project has one). Every user-visible change. Add a
|
|
136
|
+
new page if the surface is new and there's no obvious home.
|
|
137
|
+
- Any `*.md` under `agent-docs/`, `docs-internal/`, `decisions/`, or
|
|
138
|
+
similar reference trees.
|
|
139
|
+
- `.github/*.md` (issue templates, PR templates, contributing) when
|
|
140
|
+
a workflow rule shifts.
|
|
141
|
+
3. **`website/`** (if the project has one). Marketing copy on the
|
|
142
|
+
landing page or pricing page when the change touches a claim made
|
|
143
|
+
there.
|
|
144
|
+
4. **Scaffold or codegen scripts** (if the project has any). Update
|
|
145
|
+
when the change affects what new instances generate.
|
|
146
|
+
5. **PR body.** Summary, test plan checklist, and a per-row answer to
|
|
147
|
+
the Definition-of-done checklist (`Updated <path>` or `N/A because
|
|
148
|
+
<reason>`).
|
|
149
|
+
|
|
150
|
+
**How to use the checklist.** For each surface above, explicitly answer
|
|
151
|
+
one of:
|
|
152
|
+
|
|
153
|
+
- **Updated**, with the file path in the commit and PR body.
|
|
154
|
+
- **N/A because**, with a one-sentence reason.
|
|
155
|
+
|
|
156
|
+
The "every markdown file" rule is generative, not enumerative. New
|
|
157
|
+
markdown files appear over a project's lifetime, and this checklist
|
|
158
|
+
must not silently exclude them. The git query above is the source of
|
|
159
|
+
truth; the named files are just common cases.
|
|
160
|
+
|
|
161
|
+
If you find yourself writing `N/A` for every surface except tests, that
|
|
162
|
+
is a smell. Most user-visible code changes touch at least one markdown
|
|
163
|
+
file and either `AGENTS.md` or `CONVENTIONS.md`.
|
|
164
|
+
|
|
165
|
+
**Worked examples:**
|
|
166
|
+
|
|
167
|
+
- Add a new server action `modules/posts/actions/create-post.server.ts`.
|
|
168
|
+
Updated: test (`test/posts/posts.test.ts`), `AGENTS.md` (action listed
|
|
169
|
+
in the module map if the project keeps one), `CHANGELOG.md` (one-line
|
|
170
|
+
entry), `docs/` (the page listing shipped actions if one exists). N/A
|
|
171
|
+
on website / scaffold scripts.
|
|
172
|
+
- Rename a directory convention (e.g. `modules/` to `features/`).
|
|
173
|
+
Updated: existing tests still pass after renames, every markdown file
|
|
174
|
+
that mentions the old name (run `git grep -l 'modules/' '*.md'`),
|
|
175
|
+
scaffold scripts, `CHANGELOG.md`. N/A on website unless the layout
|
|
176
|
+
appears in a landing-page screenshot.
|
|
177
|
+
- Fix a bug in `rateLimit()` that doesn't change the surface. Updated:
|
|
178
|
+
test (regression), `CHANGELOG.md` (one-line entry under fixes). N/A
|
|
179
|
+
on every other markdown file because the public contract did not
|
|
180
|
+
change.
|
|
181
|
+
|
|
182
|
+
### Pre-merge self-review loop (MUST run before signaling the PR is ready)
|
|
183
|
+
|
|
184
|
+
Saying "ready for merge" before a self-review loop converges is a recurring source of low-quality PRs. The pattern to avoid: agent claims ready-for-merge, user requests a code review, agent finds issues, fixes them, claims ready-for-merge again, repeat 4-5 cycles before a review comes back clean. The cure is to run that loop internally before the first "ready" signal, so the user only hears "ready to merge" after the loop has converged on a clean round.
|
|
185
|
+
|
|
186
|
+
**How the loop works:**
|
|
187
|
+
|
|
188
|
+
1. After committing the work and (if remote pushes are in use) pushing the branch, do NOT report "ready for merge" yet. Trigger a **fresh-context review pass**: an AI review with NO prior knowledge of the decisions you made during the implementation. Each AI tool exposes its own primitive for this:
|
|
189
|
+
|
|
190
|
+
- **Cursor**: open a new composer tab.
|
|
191
|
+
- **Claude Code**: spawn a `general-purpose` subagent via the Agent tool.
|
|
192
|
+
- **GitHub Copilot**: open a new chat (reset the side panel).
|
|
193
|
+
- **Antigravity** (Google, formerly Windsurf): open a new Cascade thread or a fresh side-panel session.
|
|
194
|
+
- **Aider**: invoke a separately-started `aider` session (do NOT use `/ask` inside the same session; `/ask` only flips the mode for the next message and still sees the existing context).
|
|
195
|
+
- **Gemini CLI**: invoke a separately-started `gemini` session.
|
|
196
|
+
- **OpenCode**: open a new agent session (the `tool.execute.after` hook is a different surface and not a fresh-context primitive).
|
|
197
|
+
|
|
198
|
+
The shared property is that the reviewer does not see your decision log. That independence is what makes the review catch blind spots. If your tool does not expose a true fresh-context primitive, the canonical fallback is a separately-invoked CLI process; what matters is the reviewer starts with an empty context, not the specific UI affordance.
|
|
199
|
+
|
|
200
|
+
2. Prompt the review for problems only. A working prompt template:
|
|
201
|
+
|
|
202
|
+
> Review the changes on this branch against the project's `AGENTS.md` and `CONVENTIONS.md`. Look for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations. Read every file the diff touches in its current state, not just the diff hunks. Specifically check: \<focus rotates per round\>. Report findings as a numbered list with file:line references. Problems only, no suggestions. If you find nothing genuinely wrong, reply exactly `CLEAN` on its own line and stop.
|
|
203
|
+
|
|
204
|
+
3. For each finding the review reports, either:
|
|
205
|
+
|
|
206
|
+
- Fix it on the branch (commit + push), OR
|
|
207
|
+
- Reject it explicitly with a one-sentence reason. False positives are real, but rejection has to be defensible (e.g. "the reviewer flagged X as a security issue but X runs server-side only and never reaches user input"). Hand-waving doesn't count.
|
|
208
|
+
|
|
209
|
+
4. If the round found any findings, run another round. The new round picks a slightly different focus: if round 1 was broad, round 2 zooms in on the file you most edited; if round 2 zoomed in, round 3 zooms out to cross-file consistency. Rotate focus to avoid the reviewer rediscovering the same surface twice.
|
|
210
|
+
|
|
211
|
+
5. If the round reports `CLEAN`, the loop is done.
|
|
212
|
+
|
|
213
|
+
The minimum is TWO rounds. A clean first round is rare and usually means the review was too shallow; if round 1 is clean, run a second one with a sharper focus before believing the result.
|
|
214
|
+
|
|
215
|
+
**When to skip the loop:**
|
|
216
|
+
|
|
217
|
+
Skip only for changes that touch a single line of trivially-correct content (a doc typo, a renamed local variable, a one-token config bump). Anything that touches logic, public surface, build, security, or multiple files goes through the loop without exception. A bias toward running the loop is correct; a bias toward skipping it is the exact failure mode this rule exists to prevent.
|
|
218
|
+
|
|
219
|
+
**Reporting after the loop:**
|
|
220
|
+
|
|
221
|
+
When the user is notified the PR is ready, the message should carry:
|
|
222
|
+
|
|
223
|
+
> Ready for merge. Self-review loop ran \<K\> rounds; last round clean. Issues found and fixed during the loop: \<one-line list, or "none" if rounds 2+ kept finding nothing\>.
|
|
224
|
+
|
|
225
|
+
If you cannot honestly say "last round clean", you cannot say "ready for merge". If a finding was rejected as a false positive, mention it so the user can second-guess the rejection.
|
|
226
|
+
|
|
108
227
|
### Autonomous mode (sandbox / bypass permissions)
|
|
109
228
|
|
|
110
229
|
When running without interactive approval, agents must NOT ask questions.
|
|
@@ -653,7 +772,15 @@ Use `rateLimit()` as per-segment middleware to protect routes:
|
|
|
653
772
|
```ts
|
|
654
773
|
// app/api/auth/middleware.ts: protect auth endpoints
|
|
655
774
|
import { rateLimit } from '@webjsdev/server';
|
|
775
|
+
|
|
776
|
+
// Direct deploy (default). Keys on the socket-stamped IP, ignoring
|
|
777
|
+
// forwarded-IP headers.
|
|
656
778
|
export default rateLimit({ window: '10s', max: 5 });
|
|
779
|
+
|
|
780
|
+
// Behind a reverse proxy or CDN (Cloudflare, Railway, Fly, Vercel,
|
|
781
|
+
// nginx, Caddy). Set trustProxy to honour X-Forwarded-For. The proxy
|
|
782
|
+
// MUST strip inbound X-Forwarded-For before adding its own.
|
|
783
|
+
export default rateLimit({ window: '10s', max: 5, trustProxy: true });
|
|
657
784
|
```
|
|
658
785
|
|
|
659
786
|
Place `middleware.ts` at any route level. It applies to that subtree only.
|
|
@@ -681,9 +808,11 @@ SSR content is visible immediately. Only the JS download is deferred.
|
|
|
681
808
|
## expose(): REST endpoints from server actions
|
|
682
809
|
|
|
683
810
|
<!-- OVERRIDE -->
|
|
684
|
-
Tag a server action to also be reachable over HTTP:
|
|
811
|
+
Tag a server action to also be reachable over HTTP. The file MUST be a `.server.{js,ts}` file: `expose()` is server-only and the bare `@webjsdev/core` specifier resolves to the browser entry which excludes it, so importing from a client-bound file silently reads `undefined`.
|
|
685
812
|
|
|
686
813
|
```ts
|
|
814
|
+
// modules/posts/actions/create-post.server.ts
|
|
815
|
+
'use server';
|
|
687
816
|
import { expose } from '@webjsdev/core';
|
|
688
817
|
export const createPost = expose('POST /api/posts', async ({ title, body }) => {
|
|
689
818
|
return prisma.post.create({ data: { title, body } });
|
|
@@ -823,7 +952,7 @@ export async function createPost(input: {
|
|
|
823
952
|
constructor(x: number) { this.x = x; }
|
|
824
953
|
}
|
|
825
954
|
```
|
|
826
|
-
If you turn `erasableSyntaxOnly` off and use non-erasable syntax, the dev server
|
|
955
|
+
If you turn `erasableSyntaxOnly` off and use non-erasable syntax, the dev server fails at strip time and returns a 500 pointing at the `no-non-erasable-typescript` lint rule. webjs is buildless end-to-end and has no bundler fallback. The `erasable-typescript-only` convention check warns when the flag is off.
|
|
827
956
|
- No semicolons (or with semicolons, pick one and stay consistent)
|
|
828
957
|
- `const` by default, `let` when needed, never `var`
|
|
829
958
|
- Prefer `async/await` over `.then()` chains
|
|
@@ -837,7 +966,7 @@ export async function createPost(input: {
|
|
|
837
966
|
<!-- OVERRIDE -->
|
|
838
967
|
|
|
839
968
|
This project enforces a git workflow via agent-specific config files
|
|
840
|
-
(`.claude/settings.json`, `.cursorrules`, `.
|
|
969
|
+
(`.claude/settings.json`, `.cursorrules`, `.agents/rules/workflow.md`,
|
|
841
970
|
`.github/copilot-instructions.md`). These rules apply to ALL AI agents:
|
|
842
971
|
|
|
843
972
|
**Commit rules:**
|
|
@@ -862,7 +991,7 @@ This project enforces a git workflow via agent-specific config files
|
|
|
862
991
|
Delete or keep `<branch>` after?" Wait for approval AND the preference.
|
|
863
992
|
- **Claude Code hook** (`.claude/hooks/guard-main-merge.sh`) enforces
|
|
864
993
|
merge/push-to-main approval programmatically for Claude agents.
|
|
865
|
-
Other agents enforce this via `.cursorrules`, `.
|
|
994
|
+
Other agents enforce this via `.cursorrules`, `.agents/rules/workflow.md`,
|
|
866
995
|
`.github/copilot-instructions.md`.
|
|
867
996
|
|
|
868
997
|
**Pre-commit checks:**
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { test } from 'node:test';
|
|
8
8
|
import assert from 'node:assert/strict';
|
|
9
|
-
import { html
|
|
9
|
+
import { html } from '@webjsdev/core';
|
|
10
|
+
import { renderToString } from '@webjsdev/core/server';
|
|
10
11
|
|
|
11
12
|
test('html template renders correctly', async () => {
|
|
12
13
|
const result = await renderToString(html`<p>Hello, ${'world'}!</p>`);
|
package/templates/.windsurfrules
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
# Windsurf 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 doesn't
|
|
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's already wired up
|
|
11
|
-
(`prisma/schema.prisma`, `lib/prisma.server.ts`, `npm run db:migrate`). For ANY
|
|
12
|
-
data the app stores (todos, posts, messages, products, comments…),
|
|
13
|
-
define a Prisma model. NEVER create `data/*.json`, `db.json`, or any
|
|
14
|
-
JSON file as a fake database. NEVER use module-scope arrays / Maps as
|
|
15
|
-
a substitute. NEVER use localStorage for app data. `webjs check`'s
|
|
16
|
-
`no-json-data-files` rule will fail the build if you do.
|
|
17
|
-
- **The scaffold is reference, not the final product.** Replace
|
|
18
|
-
`app/page.ts`, the example `User` model, the example users module, etc.
|
|
19
|
-
with the app the user actually asked for. Don't ship "Hello from
|
|
20
|
-
<app-name>" as the deliverable.
|
|
21
|
-
- **Only three templates exist:** `webjs create <name>` (default
|
|
22
|
-
full-stack), `--template api`, `--template saas`. The CLI rejects any
|
|
23
|
-
other `--template` value. Pick:
|
|
24
|
-
- Any product UI (todo, blog, dashboard, marketplace, social…) → default
|
|
25
|
-
- HTTP/JSON API only, no UI → `--template api`
|
|
26
|
-
- Auth / login / signup / SaaS → `--template saas`
|
|
27
|
-
|
|
28
|
-
## Before starting ANY work
|
|
29
|
-
|
|
30
|
-
FIRST, before writing any code:
|
|
31
|
-
1. Check `git branch --show-current`.
|
|
32
|
-
- If on main/master: create a feature branch before editing.
|
|
33
|
-
- If on a feature branch: verify it matches the current task.
|
|
34
|
-
2. Sync: `git fetch origin && git rebase origin/main` if behind.
|
|
35
|
-
|
|
36
|
-
## Autonomous mode (sandbox / no-prompt)
|
|
37
|
-
|
|
38
|
-
If running without interactive approval, auto-decide:
|
|
39
|
-
- On main? Auto-create feature/<task-slug> branch
|
|
40
|
-
- Parent behind? Auto-rebase. Merge? Auto-merge + delete feature branches.
|
|
41
|
-
- Auto-generate commit messages. Fix failing tests and violations.
|
|
42
|
-
Quality bar stays the same - no blocking on questions.
|
|
43
|
-
|
|
44
|
-
## Mandatory workflow (never skip)
|
|
45
|
-
|
|
46
|
-
Every code change must include:
|
|
47
|
-
1. Server tests in test/<feature>/*.test.ts (node:test)
|
|
48
|
-
2. Browser tests in test/<feature>/browser/*.test.js (WTR + Playwright, real Chromium)
|
|
49
|
-
3. Documentation updates (AGENTS.md, docs/, website/ if they exist)
|
|
50
|
-
4. Convention check: `webjs check` must pass
|
|
51
|
-
|
|
52
|
-
The user should never have to ask for tests or documentation.
|
|
53
|
-
|
|
54
|
-
## Git rules
|
|
55
|
-
|
|
56
|
-
- COMMIT AND PUSH PER LOGICAL UNIT, NOT AT THE END. One feature, one fix,
|
|
57
|
-
one rename, one doc rewrite per commit. Always `git push` after
|
|
58
|
-
committing. This is automatic.
|
|
59
|
-
- HARD LIMIT: if you have 5+ unstaged files spanning different concerns,
|
|
60
|
-
commit before continuing. The Claude Code hook at
|
|
61
|
-
`.claude/hooks/nudge-uncommitted.sh` fires at threshold 4. Windsurf
|
|
62
|
-
users should self-enforce the same rule. Batching multiple logical units
|
|
63
|
-
into one commit is the failure mode this rule exists to prevent.
|
|
64
|
-
- Meaningful commit messages: what changed and why
|
|
65
|
-
- NEVER add Co-Authored-By or AI attribution trailers to commits
|
|
66
|
-
- NEVER use em-dashes (U+2014), a hyphen-as-pause (` - `), or a
|
|
67
|
-
semicolon-as-pause (` ; `) in commit messages or anywhere else.
|
|
68
|
-
Rewrite the sentence so no pause-punctuation crutch is needed.
|
|
69
|
-
Use a period, comma, colon, parentheses, or a restructured phrasing.
|
|
70
|
-
Plain hyphens stay fine in compound words, CLI flags, filenames,
|
|
71
|
-
and ranges. Semicolons stay fine inside code
|
|
72
|
-
- Work on feature branches, never push directly to main
|
|
73
|
-
- Create pull requests for review
|
|
74
|
-
- NEVER merge any branch without explicit user permission. Always ask:
|
|
75
|
-
"Ready to merge <branch> into <target>? Delete or keep <branch> after?"
|
|
76
|
-
Wait for approval AND the delete/keep preference. Applies to ALL merges.
|
|
77
|
-
- Run `webjs test` before every commit
|
|
78
|
-
|
|
79
|
-
## Framework specifics
|
|
80
|
-
|
|
81
|
-
- No build step: ES modules served directly
|
|
82
|
-
- **Erasable TypeScript only.** Node 24+ strips types via `module.stripTypeScriptTypes` (whitespace replacement, byte-exact position preservation, no sourcemap). The scaffold's tsconfig.json sets `erasableSyntaxOnly: true`, so the TS compiler rejects `enum`, `namespace` with values, constructor parameter properties, legacy decorators with `emitDecoratorMetadata`, and `import = require`. Use erasable equivalents: `const X = { ... } as const` plus a derived union type instead of `enum`; explicit fields plus constructor body assignments instead of parameter properties. If `erasableSyntaxOnly` is disabled and non-erasable syntax is used, the dev server falls back to esbuild + inline sourcemap for those files (~3x wire bytes per request).
|
|
83
|
-
- Web components render into light DOM by default (so Tailwind / global CSS apply directly). Opt in to shadow DOM per component with `static shadow = true` when you need scoped styles (via `static styles = css\`...\``) or third-party-embed isolation. `<slot>` projection works identically in both modes (named slots, fallback content, `assignedNodes` / `slotchange`, first-wins resolution).
|
|
84
|
-
- Custom-element tag names are passed to `.register('tag-name')` - they are NOT a static field on the class.
|
|
85
|
-
- One function per server action file (*.server.ts)
|
|
86
|
-
- Server-only code (@prisma/client, node:*, anything that needs Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap the access in a .server.{js,ts} file; the framework rewrites that import into an RPC stub for the browser. lib/ holds both server-only infra (lib/prisma.server.ts) and browser-safe utilities (lib/utils/cn.ts with cn); follow the same rule per file.
|
|
87
|
-
- Directives are deliberately minimal: only `unsafeHTML`, `live`, and `repeat` ship. Use plain template-literal expressions (`class=${active ? 'btn active' : 'btn'}`, `style=${'color:' + color}`, `${cond ? a : b}`) and lifecycle hooks (`this.query('#el')` in `firstUpdated`) instead of Lit's `classMap` / `styleMap` / `ref` / `when` / `choose` / `guard`.
|
|
88
|
-
- Use Context for cross-component data, Task for async data in components
|
|
89
|
-
- **Progressive enhancement is the default.** Pages AND every web component are SSR'd to real HTML. Write components so the first paint is the right content (read SSR-meaningful defaults in `constructor()`, not `connectedCallback` - the server doesn't call lifecycle hooks). Initial data for components comes from the page function (server-side fetch + pass as attribute/property), NOT from `fetch` calls in `connectedCallback`. For write-paths, prefer `<form action=...>` + server action over `fetch` + click handler - the framework upgrades plain forms to partial-swap submissions automatically.
|
|
90
|
-
- **Client navigation is auto-magic.** Real `<a href>` and `<form action>` get partial-swap behavior with no opt-in. Layouts persist across navigation - put shared chrome (sidenav, header) in `layout.ts`, page-specific content in `page.ts`. For validation errors, return 4xx HTML from a `route.ts` POST handler; the router renders it in place preserving the user's input. For non-layout swap regions, wrap in `<webjs-frame id="...">`. See "Client navigation patterns" in AGENTS.md.
|
|
91
|
-
- Full API reference in AGENTS.md
|