@webjsdev/cli 0.10.31 → 0.10.32
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 +13 -5
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +17 -6
- package/templates/.claude/hooks/cleanup-merged-worktree.sh +129 -0
- package/templates/.claude/hooks/commit-before-stop.sh +52 -0
- package/templates/.claude/settings.json +19 -0
- package/templates/.cursorrules +17 -7
- package/templates/.github/copilot-instructions.md +17 -7
- package/templates/AGENTS.md +31 -10
- package/templates/CLAUDE.md +22 -0
- package/templates/CONVENTIONS.md +24 -10
- package/templates/gallery/app/features/forms/page.ts +6 -0
- package/templates/gallery/app/features/service-worker/page.ts +2 -1
- package/templates/gallery/modules/components/components/browser/counter-card.test.js +36 -0
- package/templates/gallery/modules/todo/queries/list-todos.server.ts +6 -0
- package/templates/test/hello/browser/hello.test.js +6 -0
- package/templates/web-test-runner.config.js +9 -1
package/lib/create.js
CHANGED
|
@@ -401,14 +401,18 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
401
401
|
// for relations v2. SQLite needs NO driver dependency: the connection
|
|
402
402
|
// uses the built-in node:sqlite (Node) / bun:sqlite (Bun) via Drizzle's
|
|
403
403
|
// node-sqlite / bun-sqlite adapters. Postgres still needs the pg driver.
|
|
404
|
-
|
|
404
|
+
// Pinned EXACTLY (no caret): a caret on a prerelease still admits later
|
|
405
|
+
// rc.N of 1.0.0, and the relations-v2 query API the scaffold is written and
|
|
406
|
+
// tested against is rc.3 (#562). An exact pin keeps generated apps
|
|
407
|
+
// deterministic instead of silently drifting to a newer rc.
|
|
408
|
+
'drizzle-orm': '1.0.0-rc.3',
|
|
405
409
|
...(dialect === 'postgres' ? { pg: '^8.13.0' } : {}),
|
|
406
410
|
'@webjsdev/cli': 'latest',
|
|
407
411
|
'@webjsdev/core': 'latest',
|
|
408
412
|
'@webjsdev/server': 'latest',
|
|
409
413
|
},
|
|
410
414
|
devDependencies: {
|
|
411
|
-
'drizzle-kit': '
|
|
415
|
+
'drizzle-kit': '1.0.0-rc.3',
|
|
412
416
|
...(dialect === 'postgres' ? { '@types/pg': '^8.11.0' } : {}),
|
|
413
417
|
// The TypeScript compiler, for `npm run typecheck` (webjs typecheck runs
|
|
414
418
|
// tsc --noEmit). Not needed at runtime (Node strips types in place), only
|
|
@@ -528,6 +532,8 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
528
532
|
'.claude/hooks/block-prose-punctuation.sh',
|
|
529
533
|
'.claude/hooks/guard-branch-context.sh',
|
|
530
534
|
'.claude/hooks/nudge-uncommitted.sh',
|
|
535
|
+
'.claude/hooks/commit-before-stop.sh',
|
|
536
|
+
'.claude/hooks/cleanup-merged-worktree.sh',
|
|
531
537
|
'.claude/hooks/require-tests-with-src.sh',
|
|
532
538
|
'.claude/hooks/check-server-imports.sh',
|
|
533
539
|
'.claude/hooks/check-server-imports.mjs',
|
|
@@ -603,7 +609,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
603
609
|
|
|
604
610
|
// Make hook scripts executable
|
|
605
611
|
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']) {
|
|
612
|
+
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
613
|
const hookPath = join(appDir, '.claude', 'hooks', hook);
|
|
608
614
|
if (existsSync(hookPath)) await chmod(hookPath, 0o755);
|
|
609
615
|
}
|
|
@@ -1274,8 +1280,10 @@ ${UI_THEME}
|
|
|
1274
1280
|
<main class="flex-1 w-full max-w-[760px] mx-auto px-4 sm:px-6 pt-[72px] pb-12">
|
|
1275
1281
|
\${children}
|
|
1276
1282
|
</main>
|
|
1277
|
-
<!-- "Built with webjs"
|
|
1278
|
-
|
|
1283
|
+
<!-- webjs-scaffold-placeholder. This "Built with webjs" footer is SCAFFOLD
|
|
1284
|
+
branding, not your app's. REMOVE it, or replace it with your own
|
|
1285
|
+
footer, before shipping a delivered app. Delete this line once done.
|
|
1286
|
+
webjs check fails while the marker remains. -->
|
|
1279
1287
|
<footer class="border-t border-border">
|
|
1280
1288
|
<div class="max-w-[760px] mx-auto px-4 sm:px-6 py-6 flex items-center justify-center">
|
|
1281
1289
|
<a href="https://webjs.dev" class="inline-flex items-center gap-2 no-underline text-sm text-muted-foreground hover:text-foreground transition-colors">
|
package/package.json
CHANGED
|
@@ -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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
}
|
package/templates/.cursorrules
CHANGED
|
@@ -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
|
|
50
|
-
design of
|
|
51
|
-
the
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
50
|
-
design of
|
|
51
|
-
the
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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:
|
package/templates/AGENTS.md
CHANGED
|
@@ -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,
|
|
25
|
-
|
|
26
|
-
scaffold
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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)
|
|
@@ -1404,8 +1417,10 @@ composition, so a nested shell ends up dropped by the HTML parser.
|
|
|
1404
1417
|
3. Commit and push **per logical unit**, not at the end. A logical unit is one
|
|
1405
1418
|
feature, one fix, one rename, one doc rewrite. If you have 5+ unstaged files
|
|
1406
1419
|
spanning different concerns, commit the current group before continuing.
|
|
1407
|
-
|
|
1408
|
-
|
|
1420
|
+
For Claude Code, its `CLAUDE.md` explicitly OVERRIDES Claude Code's built-in
|
|
1421
|
+
never-commit default, so it commits per unit without waiting to be asked. The
|
|
1422
|
+
framework also ships a `nudge-uncommitted` hook for several agents that fires
|
|
1423
|
+
at threshold 4:
|
|
1409
1424
|
|
|
1410
1425
|
| Agent | Hook path | Doc |
|
|
1411
1426
|
|---|---|---|
|
|
@@ -1416,6 +1431,12 @@ composition, so a nested shell ends up dropped by the HTML parser.
|
|
|
1416
1431
|
| Antigravity (Google) | text rule only (post-write hooks not yet exposed) | `.agents/rules/workflow.md` |
|
|
1417
1432
|
| GitHub Copilot | text rule only (no hooks API) | `.github/copilot-instructions.md` |
|
|
1418
1433
|
|
|
1434
|
+
Claude Code adds two more backstops of its own. A `commit-before-stop.sh`
|
|
1435
|
+
Stop hook refuses to end a turn with a pile of uncommitted work on a feature
|
|
1436
|
+
branch (loop-safe, disable with `WEBJS_NO_COMMIT_STOP=1`), and a
|
|
1437
|
+
`cleanup-merged-worktree.sh` PostToolUse hook removes a merged branch's
|
|
1438
|
+
worktree after a `gh pr merge`.
|
|
1439
|
+
|
|
1419
1440
|
The `.hooks/pre-commit` hook blocks commits to main and nothing else;
|
|
1420
1441
|
`webjs test` + `webjs check` run in CI (`.github/workflows/ci.yml`) on
|
|
1421
1442
|
every PR and push to main, regardless of which agent (or human) made
|
package/templates/CLAUDE.md
CHANGED
|
@@ -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.
|
package/templates/CONVENTIONS.md
CHANGED
|
@@ -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
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
|
1259
|
-
"please commit". Commit after completing each
|
|
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
|
-
//
|
|
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
|
-
|
|
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.
|