@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 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`, `.windsurfrules`, `.github/copilot-instructions.md`
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
- // .windsurfrules / .github/copilot-instructions.md mirror it.
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.9.1",
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.7.2",
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.
@@ -1,6 +1,6 @@
1
- # Cursor Rules - webjs app
1
+ # Cursor Rules: webjs app
2
2
 
3
- You are working on a webjs app - an AI-first, no-build, web-components-first
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 mode)
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 - just no blocking on questions.
47
+ Quality bar stays the same, no blocking on questions.
48
48
 
49
49
  ## Mandatory workflow (never skip)
50
50
 
51
- 1. TESTS: Server tests in test/<feature>/ (node:test), browser tests in
52
- test/<feature>/browser/ (WTR + Playwright, real Chromium). Run `webjs test`
53
- after every change. Never deliver code without passing tests.
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
- 2. DOCS: Update AGENTS.md for API changes. Update docs/ and website/ if
56
- they exist. The user should never have to ask for tests or docs.
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. This is automatic.
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. The Claude Code hook at
67
- `.claude/hooks/nudge-uncommitted.sh` fires at threshold 4. Cursor users
68
- should self-enforce the same rule. Batching multiple logical units into
69
- one commit is the failure mode this rule exists to prevent.
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 - create a pull request
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 falls back to esbuild + inline sourcemap for those files (~3x wire bytes per request).
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 - use plain template-literal expressions (`class=${cond ? 'a' : 'b'}`, `${cond ? a : b}`) and lifecycle hooks (`this.query('#el')` in `firstUpdated`) instead.
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 (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.
98
- - **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.
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
- ## Mandatory workflow
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. Commit and push PER LOGICAL UNIT, not at the end. One feature, one fix,
47
- one rename, one doc rewrite per commit. Always `git push` after
48
- committing. Don't accumulate changes. If you have 5+ unstaged files
49
- spanning different concerns, commit before continuing. The Claude Code
50
- hook at `.claude/hooks/nudge-uncommitted.sh` enforces threshold 4 for
51
- Claude users; Copilot users should self-enforce the same rule. Automatic.
52
- 2. Server tests in test/<feature>/*.test.ts (node:test for actions, queries, utilities)
53
- 3. Browser tests in test/<feature>/browser/*.test.js (WTR + Playwright, real Chromium)
54
- 4. Documentation updates (AGENTS.md for API, docs/ for user guides)
55
- 5. Convention validation: `webjs check` must pass
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
- - Commit after each logical unit of work
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
- ## Code patterns
97
+ ## Framework rules
69
98
 
70
- - **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).
71
- - Tagged template: html`<div>${value}</div>` with css`...` for styles
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
- - 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.
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
- ## Documentation
11
+ ## Definition of done
12
12
 
13
- - [ ] AGENTS.md updated (if API surface changed)
14
- - [ ] Docs updated (if docs/ exists and feature is documented)
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, Windsurf, Copilot,
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
@@ -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 falls back to esbuild and emits inline sourcemaps
774
- for those specific files: roughly 3x wire bytes per request, and
775
- stack-trace positions are no longer byte-exact. The
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
- | Windsurf | text rule only (post-write hooks cannot inject context) | `.windsurfrules` |
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. When unsure how a framework feature works, `grep` or `cat` the
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
@@ -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.** When adding or modifying features:
99
- - Update `AGENTS.md` if the change affects the framework API surface.
100
- - Update `CONVENTIONS.md` only if the change introduces a new convention.
101
- - If a `docs/` directory exists, add or update the relevant doc page.
102
- - If a `website/` directory exists, update the landing page for
103
- user-facing features.
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 falls back to esbuild and ships inline sourcemaps for those files (~3x wire bytes per request and stack traces lose strict accuracy). The `erasable-typescript-only` convention check warns when the flag is off.
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`, `.windsurfrules`,
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`, `.windsurfrules`,
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, renderToString } from '@webjsdev/core';
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>`);
@@ -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