@webjsdev/cli 0.10.31 → 0.10.33

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/lib/create.js CHANGED
@@ -49,6 +49,27 @@ function runInstall(appDir, pm) {
49
49
  return r.status === 0;
50
50
  }
51
51
 
52
+ /**
53
+ * Author the INITIAL Drizzle migration for the shipped schema, so the app boots
54
+ * with its tables and the very first `run dev` works with no manual step. The
55
+ * scaffold's schema is TypeScript (`db/schema.server.ts`); `db migrate` (run in
56
+ * `webjs.dev.before` / `webjs.start.before`) only applies migration SQL FILES, so
57
+ * with no file the shipped example hits "no such table". `db generate` turns the
58
+ * schema into that first `db/migrations/*.sql`. It runs OFFLINE (a schema-to-SQL
59
+ * diff, no database connection), so it is safe for sqlite AND postgres here, well
60
+ * before any `DATABASE_URL` exists. Needs `drizzle-kit`, so it only runs after a
61
+ * successful install; on `--no-install` the printed next-steps still show
62
+ * `db:generate`. Returns whether a migration was authored.
63
+ *
64
+ * @param {string} appDir
65
+ * @param {string} pm
66
+ * @returns {boolean}
67
+ */
68
+ function runDbGenerate(appDir, pm) {
69
+ const r = spawnSync(pm, ['run', 'db:generate'], { cwd: appDir, stdio: 'inherit' });
70
+ return r.status === 0;
71
+ }
72
+
52
73
  const __dirname = dirname(fileURLToPath(import.meta.url));
53
74
  const TEMPLATES = resolve(__dirname, '..', 'templates');
54
75
 
@@ -401,14 +422,18 @@ export async function scaffoldApp(name, cwd, opts = {}) {
401
422
  // for relations v2. SQLite needs NO driver dependency: the connection
402
423
  // uses the built-in node:sqlite (Node) / bun:sqlite (Bun) via Drizzle's
403
424
  // node-sqlite / bun-sqlite adapters. Postgres still needs the pg driver.
404
- 'drizzle-orm': '^1.0.0-rc.3',
425
+ // Pinned EXACTLY (no caret): a caret on a prerelease still admits later
426
+ // rc.N of 1.0.0, and the relations-v2 query API the scaffold is written and
427
+ // tested against is rc.3 (#562). An exact pin keeps generated apps
428
+ // deterministic instead of silently drifting to a newer rc.
429
+ 'drizzle-orm': '1.0.0-rc.3',
405
430
  ...(dialect === 'postgres' ? { pg: '^8.13.0' } : {}),
406
431
  '@webjsdev/cli': 'latest',
407
432
  '@webjsdev/core': 'latest',
408
433
  '@webjsdev/server': 'latest',
409
434
  },
410
435
  devDependencies: {
411
- 'drizzle-kit': '^1.0.0-rc.3',
436
+ 'drizzle-kit': '1.0.0-rc.3',
412
437
  ...(dialect === 'postgres' ? { '@types/pg': '^8.11.0' } : {}),
413
438
  // The TypeScript compiler, for `npm run typecheck` (webjs typecheck runs
414
439
  // tsc --noEmit). Not needed at runtime (Node strips types in place), only
@@ -528,6 +553,8 @@ export async function scaffoldApp(name, cwd, opts = {}) {
528
553
  '.claude/hooks/block-prose-punctuation.sh',
529
554
  '.claude/hooks/guard-branch-context.sh',
530
555
  '.claude/hooks/nudge-uncommitted.sh',
556
+ '.claude/hooks/commit-before-stop.sh',
557
+ '.claude/hooks/cleanup-merged-worktree.sh',
531
558
  '.claude/hooks/require-tests-with-src.sh',
532
559
  '.claude/hooks/check-server-imports.sh',
533
560
  '.claude/hooks/check-server-imports.mjs',
@@ -603,7 +630,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
603
630
 
604
631
  // Make hook scripts executable
605
632
  const { chmod } = await import('node:fs/promises');
606
- for (const hook of ['block-prose-punctuation.sh', 'guard-branch-context.sh', 'nudge-uncommitted.sh', 'require-tests-with-src.sh']) {
633
+ for (const hook of ['block-prose-punctuation.sh', 'guard-branch-context.sh', 'nudge-uncommitted.sh', 'commit-before-stop.sh', 'cleanup-merged-worktree.sh', 'require-tests-with-src.sh']) {
607
634
  const hookPath = join(appDir, '.claude', 'hooks', hook);
608
635
  if (existsSync(hookPath)) await chmod(hookPath, 0o755);
609
636
  }
@@ -1274,8 +1301,10 @@ ${UI_THEME}
1274
1301
  <main class="flex-1 w-full max-w-[760px] mx-auto px-4 sm:px-6 pt-[72px] pb-12">
1275
1302
  \${children}
1276
1303
  </main>
1277
- <!-- "Built with webjs" attribution. Keep it or replace it with your own
1278
- footer; the gradient mark uses the --logo-from/--logo-to tokens. -->
1304
+ <!-- webjs-scaffold-placeholder. This "Built with webjs" footer is SCAFFOLD
1305
+ branding, not your app's. REMOVE it, or replace it with your own
1306
+ footer, before shipping a delivered app. Delete this line once done.
1307
+ webjs check fails while the marker remains. -->
1279
1308
  <footer class="border-t border-border">
1280
1309
  <div class="max-w-[760px] mx-auto px-4 sm:px-6 py-6 flex items-center justify-center">
1281
1310
  <a href="https://webjs.dev" class="inline-flex items-center gap-2 no-underline text-sm text-muted-foreground hover:text-foreground transition-colors">
@@ -1668,11 +1697,21 @@ For AI agents, read this before editing scaffolded files:
1668
1697
  // (#541). Otherwise honour the invoking PM (npm / pnpm / yarn / bun).
1669
1698
  const pm = isBun ? 'bun' : detectPackageManager();
1670
1699
  let installed = false;
1700
+ let generatedMigration = false;
1671
1701
  if (shouldInstall) {
1672
1702
  console.log(`Running '${pm} install' in ${name}/ ...\n`);
1673
1703
  installed = runInstall(appDir, pm);
1674
1704
  if (!installed) {
1675
1705
  console.log(`\n[warn] ${pm} install failed. Run '${pm} install' manually in ${name}/ to finish setup.\n`);
1706
+ } else {
1707
+ // Author the initial migration NOW (drizzle-kit is installed), so the
1708
+ // shipped schema's tables exist and the very first `run dev` works with no
1709
+ // manual step (webjs.*.before applies the migration on boot). See runDbGenerate.
1710
+ console.log(`Authoring the initial database migration ('${pm} run db:generate') ...\n`);
1711
+ generatedMigration = runDbGenerate(appDir, pm);
1712
+ if (!generatedMigration) {
1713
+ console.log(`\n[warn] '${pm} run db:generate' failed. Run it manually in ${name}/ before '${pm} run dev'.\n`);
1714
+ }
1676
1715
  }
1677
1716
  }
1678
1717
 
@@ -1683,14 +1722,14 @@ For AI agents, read this before editing scaffolded files:
1683
1722
  // templates ship with @webjsdev/ui already initialised; the api
1684
1723
  // template has no UI but may add one later.
1685
1724
  const installSegment = installed ? '' : `${pm} install && `;
1686
- // Some examples query the db on their first request, so a migration must be
1687
- // authored first: `db:generate` writes it and the `webjs.dev.before` migrate
1688
- // applies it on `run dev` (Drizzle splits Prisma's `migrate dev` into
1689
- // generate-then-migrate). The saas example queries users (auth); the
1690
- // full-stack scaffold ships the gallery's /examples/todo route (queries todos).
1691
- // The api template has no such first-request query, so it boots with just
1692
- // `run dev`; once you add a db route, `db:generate` then `run dev` is the loop.
1693
- const dbSegment = isApi ? '' : `${pm} run db:generate && `;
1725
+ // The shipped schema is applied on the first `run dev` (webjs.*.before runs
1726
+ // `db migrate`), but only if a migration FILE exists. When we installed, we
1727
+ // already authored it above (runDbGenerate), so the run command is just
1728
+ // `run dev`. Otherwise (--no-install, or generate failed) the user authors it
1729
+ // first: `db:generate` writes the migration from db/schema.server.ts, then
1730
+ // `run dev` applies it (Drizzle splits Prisma's `migrate dev` into
1731
+ // generate-then-migrate).
1732
+ const dbSegment = generatedMigration ? '' : `${pm} run db:generate && `;
1694
1733
  const runCommand = `cd ${name} && ${installSegment}${dbSegment}${pm} run dev`;
1695
1734
  // Postgres needs a reachable DATABASE_URL before any migrate (sqlite uses a
1696
1735
  // local file with no .env). Point it at a running database; `dev` / `start`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/cli",
3
- "version": "0.10.31",
3
+ "version": "0.10.33",
4
4
  "type": "module",
5
5
  "description": "webjs CLI - dev, start, create, db",
6
6
  "bin": {
@@ -46,12 +46,23 @@ cover what you need, the full hosted docs are at **https://docs.webjs.dev**.
46
46
  route, middleware, metadata routes). CSS, helpers, and constants do NOT:
47
47
  `globals.css` is at `styles/`, browser-safe helpers at `lib/utils/`, feature
48
48
  logic in `modules/`.
49
- - **Use a unique design (UI apps).** When the app has a UI, give it a design of
50
- your own (palette, layout, typography, chrome) that fits what the user asked
51
- for. Do NOT mimic the scaffold's example look (its warm colors, the 760px
52
- reading column, the example header/nav). The `api` template has no UI, so this
53
- does not apply there. Keep the design tokens and theme setup in
54
- `app/layout.ts`, those are infrastructure, and restyle on top of them.
49
+ - **Use a unique design, and redesign means more than recolor (UI apps).** Give
50
+ the app a design of its own (palette, typography, LAYOUT, and chrome) chosen
51
+ from what the app IS. Recoloring the scaffold and swapping the logo while
52
+ keeping its skeleton (a fixed top header with a Home link and a theme toggle,
53
+ the centered ~760px reading column, the "Built with webjs" footer) is NOT a
54
+ unique design. Decide from scratch whether this app even needs a header or
55
+ footer, what nav (if any), and what layout fits (a centered board, a full-bleed
56
+ dashboard, a split, a single card). The scaffold ships a
57
+ `webjs-scaffold-placeholder` marker on its footer, so `webjs check` fails until
58
+ you remove or replace the "Built with webjs" branding. Self-audit before
59
+ finishing: nothing should read as the scaffold example (no "Built with webjs"
60
+ footer, no leftover example nav, no default reading column unless it truly
61
+ fits). Keep only the design TOKENS and theme wiring in `app/layout.ts`
62
+ (infrastructure the ui kit reads) and restyle on top. Style with Tailwind
63
+ utilities wherever they reach, and use custom CSS only for what utilities
64
+ cannot express (@theme tokens, @keyframes, scrollbar, complex color-mix or
65
+ gradients). The `api` template has no UI, so this does not apply there.
55
66
  - **Only three templates exist:** `webjs create <name>` (default full-stack),
56
67
  `--template api`, `--template saas`. The CLI rejects any other `--template`
57
68
  value. Pick:
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Claude Code PostToolUse hook (matcher: Bash).
4
+ #
5
+ # After a `gh pr merge`, sweep the repo's git worktrees and REMOVE the ones
6
+ # whose work has already landed, so a merged branch's worktree does not leak.
7
+ # Accumulated stale worktrees (a session that merged but never cleaned up, or
8
+ # crashed mid-task) are exactly what this closes: the webjs-start-work skill
9
+ # already says "after the PR merges, git worktree remove", but as guidance it
10
+ # gets skipped, so this makes the cleanup deterministic.
11
+ #
12
+ # CONSERVATIVE BY DESIGN. A worktree is removed ONLY when ALL hold:
13
+ # * it is a LINKED worktree, not the primary checkout;
14
+ # * it is NOT the current directory (you cannot remove the one you are in);
15
+ # * its branch is not main/master;
16
+ # * its branch is MERGED (an ancestor of the base ref, OR a merged GitHub PR
17
+ # for that head branch, which is how squash-merges are detected);
18
+ # * its working tree is CLEAN apart from untracked node_modules / .webjs.
19
+ # Anything with uncommitted or unpushed-looking work is KEPT and reported, so
20
+ # the hook can never destroy in-flight work.
21
+ #
22
+ # It never blocks the tool (always exits 0) and reports what it did back to the
23
+ # model via hookSpecificOutput.additionalContext. Disable with
24
+ # WEBJS_NO_WORKTREE_CLEANUP=1.
25
+ #
26
+ # Rule: AGENTS.md "One task per git worktree" + the webjs-start-work skill.
27
+
28
+ set -uo pipefail
29
+
30
+ # Read the whole payload first so we always honour the hook contract.
31
+ payload=$(cat 2>/dev/null || true)
32
+
33
+ if [ "${WEBJS_NO_WORKTREE_CLEANUP:-}" = "1" ]; then exit 0; fi
34
+
35
+ cmd=$(printf '%s' "$payload" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
36
+ if [ -z "$cmd" ]; then exit 0; fi
37
+
38
+ # Only act after a `gh pr merge` (whole word, not `gh pr merge-queue` typos etc.).
39
+ if ! printf '%s' "$cmd" | grep -Eq '(^|[^[:alnum:]-])gh pr merge([^[:alnum:]-]|$)'; then
40
+ exit 0
41
+ fi
42
+
43
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then exit 0; fi
44
+
45
+ # The base ref merged branches land on. Prefer origin/main; fall back to a
46
+ # local main/master (the test harness has no remote).
47
+ base=""
48
+ for ref in origin/main origin/master main master; do
49
+ if git rev-parse --verify --quiet "$ref" >/dev/null 2>&1; then base="$ref"; break; fi
50
+ done
51
+ [ -z "$base" ] && exit 0
52
+
53
+ here=$(git rev-parse --show-toplevel 2>/dev/null || printf '%s' "$PWD")
54
+ # The primary worktree is the first entry of `git worktree list`.
55
+ primary=$(git worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}')
56
+
57
+ is_merged() {
58
+ local br="$1"
59
+ # Ancestor of the base ref (fast-forward / rebase merges, and the real
60
+ # merges the test harness makes).
61
+ if git merge-base --is-ancestor "refs/heads/$br" "$base" 2>/dev/null; then return 0; fi
62
+ # A merged GitHub PR for this head branch (squash merges, which are NOT an
63
+ # ancestor of base). Network; skipped when gh is absent or unauthenticated.
64
+ if command -v gh >/dev/null 2>&1; then
65
+ local n
66
+ n=$(gh pr list --state merged --head "$br" --json number --jq '.[0].number' 2>/dev/null || true)
67
+ [ -n "$n" ] && return 0
68
+ fi
69
+ return 1
70
+ }
71
+
72
+ # Clean = nothing in `git status` except untracked node_modules / .webjs caches.
73
+ is_clean() {
74
+ local wt="$1" dirty
75
+ dirty=$(git -C "$wt" status --porcelain 2>/dev/null \
76
+ | grep -vE '(^|/)(node_modules|\.webjs)(/|$)' || true)
77
+ [ -z "$dirty" ]
78
+ }
79
+
80
+ removed=()
81
+ kept=()
82
+
83
+ # Parse worktree path + branch pairs.
84
+ wt=""
85
+ while IFS= read -r line; do
86
+ case "$line" in
87
+ worktree\ *) wt="${line#worktree }" ;;
88
+ branch\ *)
89
+ br="${line#branch refs/heads/}"
90
+ # Skip the primary checkout and main/master lines.
91
+ if [ "$wt" = "$primary" ] || [ "$br" = "main" ] || [ "$br" = "master" ]; then wt=""; continue; fi
92
+ # Never remove the worktree we are currently in.
93
+ if [ "$wt" = "$here" ]; then
94
+ kept+=("$wt (current directory; cd out then \`git worktree remove\`)")
95
+ wt=""; continue
96
+ fi
97
+ if ! is_clean "$wt"; then
98
+ kept+=("$wt (uncommitted changes)"); wt=""; continue
99
+ fi
100
+ if ! is_merged "$br"; then
101
+ kept+=("$wt (branch $br not merged yet)"); wt=""; continue
102
+ fi
103
+ if git worktree remove --force "$wt" >/dev/null 2>&1; then
104
+ removed+=("$wt ($br)")
105
+ else
106
+ kept+=("$wt (git worktree remove failed)")
107
+ fi
108
+ wt="" ;;
109
+ "") wt="" ;;
110
+ esac
111
+ done < <(git worktree list --porcelain 2>/dev/null)
112
+
113
+ git worktree prune >/dev/null 2>&1 || true
114
+
115
+ # Report nothing if there was nothing to do.
116
+ if [ "${#removed[@]}" -eq 0 ] && [ "${#kept[@]}" -eq 0 ]; then exit 0; fi
117
+
118
+ msg="Worktree cleanup after \`gh pr merge\`:"
119
+ for r in "${removed[@]:-}"; do [ -n "$r" ] && msg="$msg"$'\n'" removed $r (merged, clean)"; done
120
+ for k in "${kept[@]:-}"; do [ -n "$k" ] && msg="$msg"$'\n'" kept $k"; done
121
+
122
+ jq -n --arg ctx "$msg" '{
123
+ hookSpecificOutput: {
124
+ hookEventName: "PostToolUse",
125
+ additionalContext: $ctx
126
+ }
127
+ }' 2>/dev/null || true
128
+
129
+ exit 0
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Claude Code Stop hook.
4
+ #
5
+ # The commit-per-logical-unit rule (CLAUDE.md + AGENTS.md "Git workflow") is
6
+ # easy for an agent to defer to "the end", and then the end arrives with the
7
+ # whole feature done and ZERO commits, which is the worst outcome: git history,
8
+ # the user's revert and cherry-pick safety net, is empty. The PostToolUse
9
+ # `nudge-uncommitted.sh` reminds DURING work but is only a soft context nudge an
10
+ # agent can ignore. This Stop hook is the backstop at the END of a turn: if you
11
+ # try to finish with a pile of uncommitted work on a feature branch, it blocks
12
+ # the stop once and tells you to commit the completed unit first.
13
+ #
14
+ # Loop-safe: when `stop_hook_active` is already true (this hook fired and the
15
+ # agent is continuing because of it), it does NOT block again, so it nags at
16
+ # most once per stop and can never trap the agent in a loop.
17
+ #
18
+ # Skipped on main/master (you must not commit there anyway) and outside a git
19
+ # work tree. Threshold via WEBJS_COMMIT_STOP_THRESHOLD (default 2). Disable
20
+ # entirely with WEBJS_NO_COMMIT_STOP=1.
21
+
22
+ set -uo pipefail
23
+
24
+ payload=$(cat 2>/dev/null || true)
25
+
26
+ if [ "${WEBJS_NO_COMMIT_STOP:-}" = "1" ]; then exit 0; fi
27
+
28
+ # Loop guard: if we already blocked once this stop-cycle, let the agent stop.
29
+ active=$(printf '%s' "$payload" | jq -r '.stop_hook_active // false' 2>/dev/null || echo false)
30
+ if [ "$active" = "true" ]; then exit 0; fi
31
+
32
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then exit 0; fi
33
+
34
+ branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
35
+ if [ -z "$branch" ] || [ "$branch" = "main" ] || [ "$branch" = "master" ]; then exit 0; fi
36
+
37
+ threshold="${WEBJS_COMMIT_STOP_THRESHOLD:-2}"
38
+
39
+ # Count real changes: tracked modifications + staged + untracked, minus the
40
+ # noise the agent should never commit (node_modules, the sqlite db, caches).
41
+ changed=$(git status --porcelain 2>/dev/null \
42
+ | grep -vE '(^|/)(node_modules|\.webjs)(/|$)|dev\.db($|-journal)' \
43
+ | grep -c . || true)
44
+
45
+ if [ -z "$changed" ] || [ "$changed" -lt "$threshold" ]; then exit 0; fi
46
+
47
+ reason="You are ending the turn with ${changed} uncommitted changes on '${branch}'. This project OVERRIDES Claude Code's never-commit default: commit per logical unit (see CLAUDE.md and AGENTS.md \"Git workflow\"). Before you stop, group the completed work into a meaningful commit ('git add' the related files, 'git commit' with an imperative subject under 72 chars) and push. If the work is genuinely mid-change and not yet a coherent unit, commit what IS complete, or explain in your final message why it cannot be committed yet. To relax this backstop set WEBJS_COMMIT_STOP_THRESHOLD, or disable it with WEBJS_NO_COMMIT_STOP=1."
48
+
49
+ jq -n --arg r "$reason" '{decision: "block", reason: $r}' 2>/dev/null \
50
+ || printf '{"decision":"block","reason":%s}\n' "$(printf '%s' "$reason" | jq -Rs . 2>/dev/null || echo '""')"
51
+
52
+ exit 0
@@ -56,6 +56,25 @@
56
56
  "command": ".claude/hooks/nudge-uncommitted.sh"
57
57
  }
58
58
  ]
59
+ },
60
+ {
61
+ "matcher": "Bash",
62
+ "hooks": [
63
+ {
64
+ "type": "command",
65
+ "command": ".claude/hooks/cleanup-merged-worktree.sh"
66
+ }
67
+ ]
68
+ }
69
+ ],
70
+ "Stop": [
71
+ {
72
+ "hooks": [
73
+ {
74
+ "type": "command",
75
+ "command": ".claude/hooks/commit-before-stop.sh"
76
+ }
77
+ ]
59
78
  }
60
79
  ]
61
80
  }
@@ -46,13 +46,23 @@ cover what you need, the full hosted docs are at **https://docs.webjs.dev**.
46
46
  layout, route, middleware, metadata routes). CSS, helpers, and constants
47
47
  do NOT: `globals.css` is at `styles/`, browser-safe helpers at
48
48
  `lib/utils/`, feature logic in `modules/`.
49
- - **Use a unique design (UI apps).** When the app has a UI, give it a
50
- design of your own (palette, layout, typography, chrome) that fits what
51
- the user asked for. Do NOT mimic the scaffold's example look (its warm
52
- colors, the 760px reading column, the example header/nav). The `api`
53
- template has no UI, so this does not apply there. Keep the design tokens
54
- and theme setup in `app/layout.ts`, those are infrastructure, and
55
- restyle on top of them.
49
+ - **Use a unique design, and redesign means more than recolor (UI apps).** Give
50
+ the app a design of its own (palette, typography, LAYOUT, and chrome) chosen
51
+ from what the app IS. Recoloring the scaffold and swapping the logo while
52
+ keeping its skeleton (a fixed top header with a Home link and a theme toggle,
53
+ the centered ~760px reading column, the "Built with webjs" footer) is NOT a
54
+ unique design. Decide from scratch whether this app even needs a header or
55
+ footer, what nav (if any), and what layout fits (a centered board, a
56
+ full-bleed dashboard, a split, a single card). The scaffold ships a
57
+ `webjs-scaffold-placeholder` marker on its footer, so `webjs check` fails until
58
+ you remove or replace the "Built with webjs" branding. Self-audit before
59
+ finishing: nothing should read as the scaffold example (no "Built with webjs"
60
+ footer, no leftover example nav, no default reading column unless it truly
61
+ fits). Keep only the design TOKENS and theme wiring in `app/layout.ts`
62
+ (infrastructure the ui kit reads) and restyle on top. Style with Tailwind
63
+ utilities wherever they reach, and use custom CSS only for what utilities
64
+ cannot express (@theme tokens, @keyframes, scrollbar, complex color-mix or
65
+ gradients). The `api` template has no UI, so this does not apply there.
56
66
  - **Only three templates exist:** `webjs create <name>` (default
57
67
  full-stack), `--template api`, `--template saas`. The CLI rejects any
58
68
  other `--template` value. Pick:
@@ -46,13 +46,23 @@ the full hosted docs are at **https://docs.webjs.dev**.
46
46
  layout, route, middleware, metadata routes). CSS, helpers, and constants
47
47
  do NOT: `globals.css` is at `styles/`, browser-safe helpers at
48
48
  `lib/utils/`, feature logic in `modules/`.
49
- - **Use a unique design (UI apps).** When the app has a UI, give it a
50
- design of your own (palette, layout, typography, chrome) that fits what
51
- the user asked for. Do NOT mimic the scaffold's example look (its warm
52
- colors, the 760px reading column, the example header/nav). The `api`
53
- template has no UI, so this does not apply there. Keep the design tokens
54
- and theme setup in `app/layout.ts`, those are infrastructure, and
55
- restyle on top of them.
49
+ - **Use a unique design, and redesign means more than recolor (UI apps).** Give
50
+ the app a design of its own (palette, typography, LAYOUT, and chrome) chosen
51
+ from what the app IS. Recoloring the scaffold and swapping the logo while
52
+ keeping its skeleton (a fixed top header with a Home link and a theme toggle,
53
+ the centered ~760px reading column, the "Built with webjs" footer) is NOT a
54
+ unique design. Decide from scratch whether this app even needs a header or
55
+ footer, what nav (if any), and what layout fits (a centered board, a
56
+ full-bleed dashboard, a split, a single card). The scaffold ships a
57
+ `webjs-scaffold-placeholder` marker on its footer, so `webjs check` fails until
58
+ you remove or replace the "Built with webjs" branding. Self-audit before
59
+ finishing: nothing should read as the scaffold example (no "Built with webjs"
60
+ footer, no leftover example nav, no default reading column unless it truly
61
+ fits). Keep only the design TOKENS and theme wiring in `app/layout.ts`
62
+ (infrastructure the ui kit reads) and restyle on top. Style with Tailwind
63
+ utilities wherever they reach, and use custom CSS only for what utilities
64
+ cannot express (@theme tokens, @keyframes, scrollbar, complex color-mix or
65
+ gradients). The `api` template has no UI, so this does not apply there.
56
66
  - **Only three templates exist:** `webjs create <name>` (default
57
67
  full-stack), `--template api`, `--template saas`. The CLI rejects any
58
68
  other `--template` value. Pick:
@@ -21,12 +21,21 @@ default `<main class="max-w-[760px]">` is a reading column for prose and
21
21
  forms, so for a full-bleed app, dashboard, or board, widen the cap or
22
22
  remove it (keep the theme tokens). A wide layout left in the 760px
23
23
  reading column overflows into a horizontal scrollbar. **Give the app a
24
- unique design.** When it has a UI, choose its palette, layout, typography,
25
- and chrome to fit what the user asked for, rather than mimicking the
26
- scaffold's example look (the warm accent, the reading column, the serif
27
- display, the example header/nav) or just recoloring the same layout. The
28
- `api` template has no UI, so this does not apply there. The design tokens
29
- and theme wiring are infrastructure to keep and restyle on top of. This is ENFORCED:
24
+ unique design, and redesign means more than recolor.** When it has a UI,
25
+ choose its palette, typography, LAYOUT, and chrome from what the app IS.
26
+ Recoloring the scaffold and swapping the logo while keeping its skeleton (a
27
+ fixed top header with a Home link and a theme toggle, the centered ~760px
28
+ reading column, the "Built with webjs" footer) is NOT a unique design.
29
+ Decide from scratch whether this app even needs a header or footer, what nav
30
+ (if any), and what layout fits (a centered board, a full-bleed dashboard, a
31
+ split, a single card). Before finishing, self-audit that nothing still reads
32
+ as the scaffold example (no "Built with webjs" footer, no leftover example
33
+ nav, no default reading column unless it truly fits). The `api` template has
34
+ no UI, so this does not apply there. The design tokens and theme wiring are
35
+ infrastructure to keep and restyle on top of. Style with Tailwind utilities
36
+ wherever they reach, and use custom CSS only for what utilities cannot
37
+ express (@theme tokens, @keyframes, scrollbar, complex color-mix or
38
+ gradients). This is ENFORCED:
30
39
  the example `app/page.ts` and `app/layout.ts` carry a
31
40
  `webjs-scaffold-placeholder` marker comment, and `webjs check` fails
32
41
  while any marker remains, so this freshly scaffolded app fails the check
@@ -372,10 +381,14 @@ db/
372
381
  dev.db SQLite file (gitignored); created when migrations apply (\`dev\`/\`start\` run \`webjs db migrate\`)
373
382
  migrations/ generated migration SQL (committed)
374
383
  drizzle.config.ts drizzle-kit config (root; SQLite by default, --db postgres to switch)
375
- public/ static assets, served at /public/*
384
+ public/ static assets at /public/* (favicon, sw.js, offline.html serve at root)
376
385
  test/<feature>/ feature-scoped tests, one folder per concern
377
386
  <name>.test.ts node unit / integration test (node --test)
378
- browser/<name>.test.js real-browser test (web-test-runner)
387
+ browser/<name>.test.js real-browser test (web-test-runner); may ALSO be
388
+ co-located next to a component, e.g.
389
+ modules/<feature>/components/browser/<name>.test.js
390
+ (see the gallery counter-card test for the idioms:
391
+ suite/test, ssrFixture, inline assert, no chai)
379
392
  e2e/<name>.test.ts end-to-end test (full app boot, opt in via WEBJS_E2E=1)
380
393
  smoke/<name>.test.ts fast post-deploy sanity check
381
394
  middleware.ts root middleware (optional, outermost)
@@ -536,6 +549,7 @@ Scripts (all wrap `drizzle-kit`):
536
549
  - `npm run db:studio`: `webjs db studio` (visual DB browser)
537
550
  - `npm run db:seed`: `webjs db seed` (run `db/seed.server.ts`)
538
551
  - `webjs.dev.before` and `webjs.start.before` both run `webjs db migrate` inside `webjs dev` / `webjs start` (idempotent; replaces the old `prestart` hook), so after you `db:generate` a migration it is applied on the next boot with no manual `db:migrate` step.
552
+ - The INITIAL migration for the shipped schema is authored by `webjs create` at setup time (right after install), so `db/migrations/` is populated and the first `run dev` works with no manual database step. You run `db:generate` yourself only when you CHANGE `db/schema.server.ts` (a new table or column), then the next `run dev` applies it.
539
553
 
540
554
  Always import `db` from `db/connection.server.ts` (the globalThis-cached
541
555
  singleton avoids opening a new connection on every dev-server reload), and
@@ -1404,8 +1418,10 @@ composition, so a nested shell ends up dropped by the HTML parser.
1404
1418
  3. Commit and push **per logical unit**, not at the end. A logical unit is one
1405
1419
  feature, one fix, one rename, one doc rewrite. If you have 5+ unstaged files
1406
1420
  spanning different concerns, commit the current group before continuing.
1407
- The framework ships a `nudge-uncommitted` hook for several agents that
1408
- fires at threshold 4:
1421
+ For Claude Code, its `CLAUDE.md` explicitly OVERRIDES Claude Code's built-in
1422
+ never-commit default, so it commits per unit without waiting to be asked. The
1423
+ framework also ships a `nudge-uncommitted` hook for several agents that fires
1424
+ at threshold 4:
1409
1425
 
1410
1426
  | Agent | Hook path | Doc |
1411
1427
  |---|---|---|
@@ -1416,6 +1432,12 @@ composition, so a nested shell ends up dropped by the HTML parser.
1416
1432
  | Antigravity (Google) | text rule only (post-write hooks not yet exposed) | `.agents/rules/workflow.md` |
1417
1433
  | GitHub Copilot | text rule only (no hooks API) | `.github/copilot-instructions.md` |
1418
1434
 
1435
+ Claude Code adds two more backstops of its own. A `commit-before-stop.sh`
1436
+ Stop hook refuses to end a turn with a pile of uncommitted work on a feature
1437
+ branch (loop-safe, disable with `WEBJS_NO_COMMIT_STOP=1`), and a
1438
+ `cleanup-merged-worktree.sh` PostToolUse hook removes a merged branch's
1439
+ worktree after a `gh pr merge`.
1440
+
1419
1441
  The `.hooks/pre-commit` hook blocks commits to main and nothing else;
1420
1442
  `webjs test` + `webjs check` run in CI (`.github/workflows/ci.yml`) on
1421
1443
  every PR and push to main, regardless of which agent (or human) made
@@ -1,2 +1,24 @@
1
1
  @AGENTS.md
2
2
  @CONVENTIONS.md
3
+
4
+ # Committing per logical unit (this OVERRIDES Claude Code's default)
5
+
6
+ Claude Code's built-in default is to NEVER commit unless the user explicitly
7
+ asks. **For this project that default does NOT apply.** Commit and push per
8
+ logical unit (one feature, one fix, one rename, one doc rewrite) as soon as it
9
+ is complete, WITHOUT being asked. Do not save all the work for one commit at the
10
+ end. A finished implementation with zero commits is a mistake here, because git
11
+ history is the user's revert and cherry-pick safety net.
12
+
13
+ - After each completed unit whose tests pass, `git add` the related files and
14
+ `git commit` with an imperative subject under 72 chars, then push. If 5+ files
15
+ span more than one concern, you already waited too long.
16
+ - Never commit to `main`. Work on a feature branch (the
17
+ `.claude/hooks/guard-branch-context.sh` hook enforces this).
18
+ - No AI-attribution trailers (`Co-Authored-By`, `Generated by`).
19
+
20
+ See AGENTS.md "Git workflow" for the full contract. Two hooks back this up: the
21
+ `.claude/hooks/nudge-uncommitted.sh` PostToolUse hook reminds you while
22
+ uncommitted changes pile up during work, and the
23
+ `.claude/hooks/commit-before-stop.sh` Stop hook stops you from ending a turn
24
+ with a pile of uncommitted work still on a feature branch.
@@ -373,14 +373,24 @@ When the user asks the agent to build their actual app:
373
373
  dashboard, or board, or a wide layout overflows into an unnecessary
374
374
  horizontal scrollbar. Keep the design tokens and theme setup, those
375
375
  are infrastructure.
376
- 6. **Use a unique design (UI apps).** Give the app a look of its own that
377
- fits what the user asked for. Choose the palette, layout, typography,
378
- spacing, and chrome deliberately. Do NOT mimic the scaffold's example
379
- look (its warm accent, the 760px reading column, the serif display, the
380
- example header/nav), and do not just recolor the same layout. The
381
- `api` template has no UI, so this does not apply there. The design
382
- tokens and theme wiring in `app/layout.ts` are infrastructure to keep
383
- and restyle on top of, not the example look to preserve.
376
+ 6. **Use a unique design, and redesign means more than recolor (UI apps).**
377
+ Give the app a design of its own (palette, typography, LAYOUT, spacing,
378
+ and chrome) chosen from what the app IS. Recoloring the scaffold and
379
+ swapping the logo while keeping its skeleton (a fixed top header with a
380
+ Home link and a theme toggle, the centered ~760px reading column, the
381
+ "Built with webjs" footer) is NOT a unique design. Decide from scratch
382
+ whether this app even needs a header or footer, what nav (if any), and
383
+ what layout fits (a centered board, a full-bleed dashboard, a split, a
384
+ single card). The scaffold ships a `webjs-scaffold-placeholder` marker on
385
+ its footer, so `webjs check` fails until you remove or replace the
386
+ "Built with webjs" branding. Self-audit before finishing: nothing should
387
+ read as the scaffold example (no "Built with webjs" footer, no leftover
388
+ example nav, no default reading column unless it truly fits). The design
389
+ tokens and theme wiring in `app/layout.ts` are infrastructure to keep and
390
+ restyle on top of, not the example look to preserve. Style with Tailwind
391
+ utilities wherever they reach, and use custom CSS only for what utilities
392
+ cannot express (@theme tokens, @keyframes, scrollbar, complex color-mix
393
+ or gradients). The `api` template has no UI, so this does not apply there.
384
394
  7. **Keep:** the Drizzle setup, the test config, the agent config files
385
395
  (`AGENTS.md`, `CONVENTIONS.md`, `CLAUDE.md`, `.cursorrules`, etc.),
386
396
  `db/connection.server.ts` + `db/columns.server.ts`, the directory
@@ -1255,8 +1265,12 @@ This project enforces a git workflow via agent-specific config files
1255
1265
  (`Add contact form with email validation`, not `update files`).
1256
1266
  - **NEVER add AI attribution.** No `Co-Authored-By: Claude`, no
1257
1267
  `Generated by AI`, no `AI-assisted` trailers or prefixes.
1258
- - **Committing is automatic.** The user should never have to ask
1259
- "please commit". Commit after completing each logical unit.
1268
+ - **Committing is automatic (this overrides Claude Code's default).** The
1269
+ user should never have to ask "please commit". Commit after completing each
1270
+ logical unit. Claude Code's built-in never-commit default is overridden by
1271
+ `CLAUDE.md`, and the `.claude/hooks/commit-before-stop.sh` Stop hook refuses
1272
+ to end a turn with a pile of uncommitted work on a feature branch (disable
1273
+ with `WEBJS_NO_COMMIT_STOP=1`).
1260
1274
 
1261
1275
  **Branch rules:**
1262
1276
  - **Feature branches.** Never commit directly to main
@@ -58,6 +58,12 @@ export default function FormsFeature({ searchParams, actionData }: { searchParam
58
58
  // The page action runs on a non-GET submission to this URL (the no-JS write
59
59
  // path). Validate, then return a failure (re-renders at 422 with fieldErrors +
60
60
  // values) or a success with a same-site `redirect` (a 303 PRG to the confirmation).
61
+ //
62
+ // FOOTGUN: to redirect on success, RETURN `{ success: true, redirect: '/path' }`
63
+ // (a 303 See Other, so the browser follows with a GET). Do NOT THROW `redirect()`
64
+ // from a page action, that is a 307 which PRESERVES the POST method and body, so
65
+ // the browser re-POSTs to the target and re-runs the mutation (a duplicate write).
66
+ // Throw `redirect()` only from a page render / GET context, never a page action.
61
67
  export async function action({ formData }: { formData: FormData }): Promise<Result> {
62
68
  const name = String(formData.get('name') ?? '').trim();
63
69
  const email = String(formData.get('email') ?? '').trim();
@@ -22,7 +22,8 @@ export default function ServiceWorkerExample() {
22
22
  <pre class="bg-card border border-border rounded-xl p-4 overflow-x-auto text-sm font-mono mb-4"><code>connectedCallback() {
23
23
  super.connectedCallback();
24
24
  if ('serviceWorker' in navigator) {
25
- // Register at the site root so the worker's scope is the whole origin.
25
+ // The framework serves your public/sw.js at the site root /sw.js (with a
26
+ // Service-Worker-Allowed: / header), so the worker's scope is the whole origin.
26
27
  navigator.serviceWorker.register('/sw.js');
27
28
  }
28
29
  }</code></pre>
@@ -0,0 +1,36 @@
1
+ // Co-located browser test for the <counter-card> component. This is the webjs
2
+ // component-test SHAPE, so copy it for your own components:
3
+ // - It runs in REAL Chromium (webjs test --browser, or npx wtr), not jsdom. If
4
+ // the browser binary is missing, install it once: npx playwright install chromium.
5
+ // - The runner's mocha UI is `tdd`, so use suite() / test(), NOT describe / it().
6
+ // - There is NO assertion library in the importmap (no chai, no expect). Throw
7
+ // to fail. A tiny inline `assert` is plenty.
8
+ // - `ssrFixture` server-renders AND hydrates the component, so you exercise the
9
+ // REAL SSR output and the client interactivity, not a jsdom approximation.
10
+ // It lives NEXT TO the component (a `browser/` dir inside the module), the
11
+ // co-located default; the runner discovers browser tests under any browser dir.
12
+ import { html } from '@webjsdev/core';
13
+ import { ssrFixture } from '@webjsdev/core/testing';
14
+ import '../counter-card.ts';
15
+
16
+ const assert = (cond, msg) => { if (!cond) throw new Error(msg || 'assertion failed'); };
17
+
18
+ suite('<counter-card>', () => {
19
+ test('SSRs its initial state and the default label', async () => {
20
+ const el = await ssrFixture(html`<counter-card></counter-card>`);
21
+ assert(el.textContent.includes('0'), 'the count starts at 0');
22
+ assert(el.textContent.includes('Clicks'), 'the default label renders');
23
+ });
24
+
25
+ test('reads the label reactive prop', async () => {
26
+ const el = await ssrFixture(html`<counter-card label="Taps"></counter-card>`);
27
+ assert(el.textContent.includes('Taps'), 'the provided label renders');
28
+ });
29
+
30
+ test('increments on click (hydrated interactivity)', async () => {
31
+ const el = await ssrFixture(html`<counter-card></counter-card>`);
32
+ el.querySelector('button').click();
33
+ await el.updateComplete;
34
+ assert(el.textContent.includes('1'), 'the count becomes 1 after one click');
35
+ });
36
+ });
@@ -14,6 +14,12 @@ export async function listTodos(): Promise<Todo[]> {
14
14
  // imported column mis-compiles to a bad SQL alias in rc.3. Do NOT use
15
15
  // `db.select({ col })` either (its projection overload trips TS2554 in rc.3).
16
16
  // See the Database (Drizzle) section in this app's AGENTS.md.
17
+ //
18
+ // Two equivalent read styles, both fine: the relational query API
19
+ // (`db.query.todos.findFirst({ where: { id } })` / `findMany`, used here and in
20
+ // toggle-todo) reads by an object filter; the core builder
21
+ // (`db.select().from(todos).where(eq(todos.id, id))`) reads with the `eq()`
22
+ // helper. Reach for whichever fits; the relational form is terser for by-id reads.
17
23
  const rows = await db.query.todos.findMany({ orderBy: { createdAt: 'desc' } });
18
24
  return rows as Todo[];
19
25
  }
@@ -3,6 +3,12 @@
3
3
  *
4
4
  * Run: webjs test --browser
5
5
  * npx wtr
6
+ * First run only, if Chromium is missing: npx playwright install chromium
7
+ *
8
+ * The runner's mocha UI is `tdd`: use suite() / test() (NOT describe / it), and
9
+ * throw to fail (there is no chai / expect in the importmap; a tiny inline assert
10
+ * like the one below is the idiom). See modules/components/components/browser for
11
+ * a co-located COMPONENT browser test using ssrFixture.
6
12
  *
7
13
  * Tests here have full access to real browser APIs: Shadow DOM,
8
14
  * adoptedStyleSheets, IntersectionObserver, events, etc.
@@ -33,7 +33,15 @@ export default {
33
33
  // Browser tests are `.js` (web-test-runner serves them through its own test
34
34
  // framework); the components + modules they import are `.ts`, served
35
35
  // transformed by the webjs middleware below.
36
- files: ['test/**/browser/**/*.test.js'],
36
+ // Browser tests live under any `browser/` dir: the top-level `test/<feature>/
37
+ // browser/`, OR co-located next to the code they exercise (a component's
38
+ // `modules/<feature>/components/browser/`, the modules-architecture default).
39
+ files: [
40
+ 'test/**/browser/**/*.test.js',
41
+ 'app/**/browser/**/*.test.js',
42
+ 'modules/**/browser/**/*.test.js',
43
+ 'components/**/browser/**/*.test.js',
44
+ ],
37
45
  // webjs's importmap resolves `@webjsdev/core`, the `#` app aliases, and
38
46
  // vendors, so web-test-runner must NOT rewrite bare specifiers to
39
47
  // node_modules paths.