@webjsdev/cli 0.10.37 → 0.10.39
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/bin/webjs.js +116 -3
- package/lib/clear-placeholders.js +98 -0
- package/lib/create.js +156 -131
- package/lib/db-hints.js +34 -0
- package/lib/design-bar.js +67 -0
- package/lib/doctor.js +122 -4
- package/lib/runtime-rewrite.js +4 -3
- package/lib/saas-template.js +45 -6
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +33 -15
- package/templates/.claude/hooks/design-review-before-stop.sh +36 -0
- package/templates/.claude/hooks/route-skills.sh +35 -0
- package/templates/.claude/settings.json +14 -0
- package/templates/.claude/skills/webjs-design-review/SKILL.md +84 -0
- package/templates/.cursorrules +33 -15
- package/templates/.github/copilot-instructions.md +33 -15
- package/templates/AGENTS.md +41 -24
- package/templates/CONVENTIONS.md +60 -29
- package/templates/LAYOUT-REFERENCE.md +96 -0
- package/templates/public/tailwind-browser.js +0 -947
package/lib/doctor.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* WebJs has unusually many fragile preconditions, each an independent failure
|
|
5
5
|
* mode a contributor onboarding to an existing repo only hits at runtime: the
|
|
6
6
|
* Node 24+ strip-types floor, the `erasableSyntaxOnly` TS flag, importmap pin
|
|
7
|
-
* freshness, env drift vs `.env.example`, `@webjsdev/*` version coherence,
|
|
8
|
-
* the
|
|
9
|
-
* and
|
|
7
|
+
* freshness, env drift vs `.env.example`, `@webjsdev/*` version coherence,
|
|
8
|
+
* whether the framework even resolves from the app dir (the fresh-git-worktree
|
|
9
|
+
* trap, #954), and the git pre-commit hook activation. `webjs doctor` verifies
|
|
10
|
+
* each one up front and prints pass/warn/fail with an actionable fix line.
|
|
10
11
|
*
|
|
11
12
|
* This module is PURE: `runDoctorChecks(appDir, opts?)` reads files (and, for
|
|
12
13
|
* the pin check, optionally the network), but NEVER calls `process.exit` and
|
|
@@ -25,7 +26,8 @@
|
|
|
25
26
|
* app's own runtime concern, never a doctor hard-fail: a missing tsconfig
|
|
26
27
|
* (a JS-only app legitimately has none), env drift, an outdated or
|
|
27
28
|
* unverifiable vendor pin, a `@webjsdev/*` version drift or missing install,
|
|
28
|
-
*
|
|
29
|
+
* an unresolvable framework (a worktree with no node_modules, #954), and a
|
|
30
|
+
* missing/non-executable git hook.
|
|
29
31
|
* - 'pass' is the green path.
|
|
30
32
|
*
|
|
31
33
|
* Every NETWORK touch (only the vendor-pin freshness check) is BEST-EFFORT: a
|
|
@@ -37,6 +39,7 @@
|
|
|
37
39
|
import { existsSync, statSync } from 'node:fs';
|
|
38
40
|
import { readFile } from 'node:fs/promises';
|
|
39
41
|
import { join, relative } from 'node:path';
|
|
42
|
+
import { createRequire } from 'node:module';
|
|
40
43
|
import { checkNodeInline } from './node-preflight.js';
|
|
41
44
|
|
|
42
45
|
/**
|
|
@@ -842,6 +845,119 @@ async function checkElisionCarriers(appDir) {
|
|
|
842
845
|
};
|
|
843
846
|
}
|
|
844
847
|
|
|
848
|
+
/**
|
|
849
|
+
* ADVISORY: the delivered app still rides the scaffold shell. AGENTS.md /
|
|
850
|
+
* CONVENTIONS.md item 6 asks a UI app to own its design (layout, palette,
|
|
851
|
+
* typography, chrome); the scaffold is a teaching artifact, not a starting
|
|
852
|
+
* design. WARN-level and never a hard fail: a reading column or a theme toggle
|
|
853
|
+
* CAN be a legitimate choice, so this nudges, it does not gate. The signal is
|
|
854
|
+
* objective (distinctive scaffold-authored chrome strings still present in the
|
|
855
|
+
* root layout), not a judgment of taste. Two or more tells is the threshold.
|
|
856
|
+
* @param {string} appDir
|
|
857
|
+
* @returns {Promise<DoctorResult>}
|
|
858
|
+
*/
|
|
859
|
+
async function checkScaffoldDesign(appDir) {
|
|
860
|
+
const name = 'App design (own design, not the scaffold shell)';
|
|
861
|
+
let layoutSrc = '';
|
|
862
|
+
for (const ext of ['ts', 'js', 'mts', 'mjs']) {
|
|
863
|
+
const p = join(appDir, 'app', `layout.${ext}`);
|
|
864
|
+
if (existsSync(p)) { layoutSrc = await readFile(p, 'utf8').catch(() => ''); break; }
|
|
865
|
+
}
|
|
866
|
+
if (!layoutSrc) {
|
|
867
|
+
return { name, status: 'pass', message: 'no app/layout to analyse' };
|
|
868
|
+
}
|
|
869
|
+
const { scaffoldShellTells } = await import('./design-bar.js');
|
|
870
|
+
const tells = scaffoldShellTells(layoutSrc);
|
|
871
|
+
if (tells.length < 2) {
|
|
872
|
+
return { name, status: 'pass', message: 'app/layout does not look like the unmodified scaffold shell' };
|
|
873
|
+
}
|
|
874
|
+
return {
|
|
875
|
+
name,
|
|
876
|
+
status: 'warn',
|
|
877
|
+
message:
|
|
878
|
+
`app/layout still carries ${tells.length} scaffold design signal(s): ${tells.join(', ')}. ` +
|
|
879
|
+
'A delivered UI app should own its design (layout AND palette), not adapt the scaffold.',
|
|
880
|
+
fix: 'Design the app\'s own layout, palette, typography, and chrome from what the app IS (a centered board, a full-bleed dashboard, ...), not the scaffold\'s exact 760px reading column, its "Built with webjs" attribution footer, or the unmodified starter palette values (the theme-toggle and --header-h are keep-infrastructure). Recoloring the scaffold is not a redesign. Render the app and look at it. See AGENTS.md / CONVENTIONS.md item 6.',
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Probe whether `@webjsdev/core` resolves from `appDir`. Node resolution is
|
|
886
|
+
* directory-relative, so this must probe FROM the app (not the CLI's own
|
|
887
|
+
* location, which resolves the framework fine from a global install even when
|
|
888
|
+
* the app cannot). A no-op-cheap resolve, no I/O beyond what Node's resolver
|
|
889
|
+
* does, no network. Returns true when the framework resolves, false otherwise.
|
|
890
|
+
* @param {string} appDir
|
|
891
|
+
* @returns {boolean}
|
|
892
|
+
*/
|
|
893
|
+
export function frameworkResolves(appDir) {
|
|
894
|
+
try {
|
|
895
|
+
// The base file need not exist; createRequire only uses it to anchor the
|
|
896
|
+
// node_modules lookup at appDir.
|
|
897
|
+
const require = createRequire(join(appDir, '__webjs_resolve_probe__.js'));
|
|
898
|
+
require.resolve('@webjsdev/core');
|
|
899
|
+
return true;
|
|
900
|
+
} catch {
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* CHECK 8, framework resolvability (#954). WARN when `@webjsdev/core` cannot be
|
|
907
|
+
* resolved FROM the app directory, which is the fresh-git-worktree trap: a
|
|
908
|
+
* worktree does not copy `node_modules`, so a plain `webjs dev` there dies at
|
|
909
|
+
* SSR with a raw `ERR_MODULE_NOT_FOUND: Cannot find package '@webjsdev/core'`
|
|
910
|
+
* whose remedy is not obvious. Silent PASS when the framework resolves (the
|
|
911
|
+
* common case), so this never slows a healthy app. WARN (not a hard fail): it
|
|
912
|
+
* is a setup/environment concern, the same tier as the version-coherence check.
|
|
913
|
+
* @param {string} appDir
|
|
914
|
+
* @returns {DoctorResult}
|
|
915
|
+
*/
|
|
916
|
+
export function checkFrameworkResolves(appDir) {
|
|
917
|
+
const name = 'framework-resolve';
|
|
918
|
+
if (frameworkResolves(appDir)) {
|
|
919
|
+
return { name, status: 'pass', message: '@webjsdev/core resolves from the app directory.' };
|
|
920
|
+
}
|
|
921
|
+
const hasNodeModules = existsSync(join(appDir, 'node_modules'));
|
|
922
|
+
// A git worktree checks out `.git` as a FILE (a gitdir pointer), not a
|
|
923
|
+
// directory. That, plus a missing node_modules, is the exact #954 cause.
|
|
924
|
+
let isWorktree = false;
|
|
925
|
+
try {
|
|
926
|
+
isWorktree = statSync(join(appDir, '.git')).isFile();
|
|
927
|
+
} catch {
|
|
928
|
+
isWorktree = false;
|
|
929
|
+
}
|
|
930
|
+
if (isWorktree && !hasNodeModules) {
|
|
931
|
+
return {
|
|
932
|
+
name,
|
|
933
|
+
status: 'warn',
|
|
934
|
+
message:
|
|
935
|
+
'@webjsdev/core cannot be resolved from this directory, and this is a git worktree with no ' +
|
|
936
|
+
'node_modules. Git worktrees do not copy node_modules, so the framework is unresolvable here ' +
|
|
937
|
+
'and `webjs dev` / `webjs start` would fail at SSR with a raw ERR_MODULE_NOT_FOUND.',
|
|
938
|
+
fix:
|
|
939
|
+
'Install dependencies in this worktree (`npm install`), or symlink node_modules from the ' +
|
|
940
|
+
'primary checkout (`ln -s ../<primary-checkout>/node_modules node_modules`).',
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
if (!hasNodeModules) {
|
|
944
|
+
return {
|
|
945
|
+
name,
|
|
946
|
+
status: 'warn',
|
|
947
|
+
message: '@webjsdev/core cannot be resolved from this directory (no node_modules present).',
|
|
948
|
+
fix: 'Run `npm install` in the app directory so the framework resolves.',
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
return {
|
|
952
|
+
name,
|
|
953
|
+
status: 'warn',
|
|
954
|
+
message:
|
|
955
|
+
'@webjsdev/core cannot be resolved from this directory even though node_modules exists ' +
|
|
956
|
+
'(a partial or corrupted install).',
|
|
957
|
+
fix: 'Reinstall dependencies (`npm install`, or remove node_modules and reinstall).',
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
845
961
|
export async function runDoctorChecks(appDir, opts = {}) {
|
|
846
962
|
const cliDir = opts.cliDir || new URL('.', import.meta.url).pathname;
|
|
847
963
|
const results = await Promise.all([
|
|
@@ -851,9 +967,11 @@ export async function runDoctorChecks(appDir, opts = {}) {
|
|
|
851
967
|
checkVendorPin(appDir, opts),
|
|
852
968
|
checkVendorGitignore(appDir),
|
|
853
969
|
checkWebjsVersions(appDir),
|
|
970
|
+
Promise.resolve(checkFrameworkResolves(appDir)),
|
|
854
971
|
checkImportmapCoherence(appDir, opts),
|
|
855
972
|
Promise.resolve(checkGitHook(appDir)),
|
|
856
973
|
checkElisionCarriers(appDir),
|
|
974
|
+
checkScaffoldDesign(appDir),
|
|
857
975
|
]);
|
|
858
976
|
return results;
|
|
859
977
|
}
|
package/lib/runtime-rewrite.js
CHANGED
|
@@ -142,9 +142,10 @@ export function bunifyDockerfile(s) {
|
|
|
142
142
|
.replace(
|
|
143
143
|
/# `npm start` is a thin alias[\s\S]*?the migrate no longer depends on an npm `prestart` hook\.\nCMD \["npm", "start"\]/,
|
|
144
144
|
'# `bun --bun run start` runs the `start` script on Bun (the server serves via\n' +
|
|
145
|
-
'# Bun.serve). `webjs start` runs the `webjs.start.before`
|
|
146
|
-
'#
|
|
147
|
-
'#
|
|
145
|
+
'# Bun.serve). `webjs start` runs the `webjs.start.before` steps: `webjs db migrate`\n' +
|
|
146
|
+
'# (resolves drizzle-kit and runs it under Bun, no npx, #570) and, for a UI app,\n' +
|
|
147
|
+
'# the Tailwind compile under `bun --bun` (no Node / npm in the image, #947), both\n' +
|
|
148
|
+
'# idempotent, then serves on $PORT.\n' +
|
|
148
149
|
'CMD ["bun", "--bun", "run", "start"]',
|
|
149
150
|
);
|
|
150
151
|
}
|
package/lib/saas-template.js
CHANGED
|
@@ -118,6 +118,10 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
118
118
|
" ...(process.env.AUTH_GOOGLE_ID ? [Google({ clientId: process.env.AUTH_GOOGLE_ID, clientSecret: process.env.AUTH_GOOGLE_SECRET })] : []),",
|
|
119
119
|
" ],",
|
|
120
120
|
" secret: authSecret,",
|
|
121
|
+
" // A failed credentials sign-in 302s to `${pages.error}?error=CredentialsSignin`.",
|
|
122
|
+
" // Point it at /login so app/login/page.ts reads searchParams.error and shows a",
|
|
123
|
+
" // message, instead of the createAuth default (the home page) swallowing the error.",
|
|
124
|
+
" pages: { error: '/login' },",
|
|
121
125
|
"});",
|
|
122
126
|
"",
|
|
123
127
|
].join('\n'));
|
|
@@ -339,7 +343,17 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
339
343
|
"",
|
|
340
344
|
"export const metadata = { title: 'Login' };",
|
|
341
345
|
"",
|
|
342
|
-
"
|
|
346
|
+
"// A failed sign-in 302s back here with ?error=... (createAuth is configured",
|
|
347
|
+
"// with pages.error: '/login' in lib/auth.server.ts). Map the code to a plain",
|
|
348
|
+
"// message so a bad password gets visible feedback instead of a silent bounce.",
|
|
349
|
+
"function errorMessage(code: string | undefined): string | null {",
|
|
350
|
+
" if (!code) return null;",
|
|
351
|
+
" if (code === 'CredentialsSignin') return 'Invalid email or password.';",
|
|
352
|
+
" return 'Could not sign you in. Please try again.';",
|
|
353
|
+
"}",
|
|
354
|
+
"",
|
|
355
|
+
"export default function LoginPage({ searchParams }: { searchParams: { error?: string } }) {",
|
|
356
|
+
" const error = errorMessage(searchParams.error);",
|
|
343
357
|
" return html`",
|
|
344
358
|
" <div class=\"max-w-sm mx-auto mt-12\">",
|
|
345
359
|
" <div class=${cardClass()}>",
|
|
@@ -348,6 +362,7 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
348
362
|
" <p class=${cardDescriptionClass()}>Welcome back: log in to continue.</p>",
|
|
349
363
|
" </div>",
|
|
350
364
|
" <div class=${cardContentClass()}>",
|
|
365
|
+
" ${error ? html`<p role=\"alert\" class=\"mb-4 text-sm text-destructive\">${error}</p>` : ''}",
|
|
351
366
|
" <form method=\"POST\" action=\"/api/auth/signin/credentials\" class=\"flex flex-col gap-4\">",
|
|
352
367
|
" <!-- createAuth reads redirectTo from the posted form and 302s there after a successful signin. -->",
|
|
353
368
|
" <input type=\"hidden\" name=\"redirectTo\" value=\"/dashboard\">",
|
|
@@ -461,12 +476,39 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
461
476
|
"",
|
|
462
477
|
].join('\n'));
|
|
463
478
|
|
|
479
|
+
// app/dashboard/layout.ts: a thin sub-nav shared by every /dashboard page
|
|
480
|
+
// (page.ts and settings/page.ts). It carries the logout control so a signed-in
|
|
481
|
+
// user can end the session from anywhere under /dashboard.
|
|
482
|
+
await writeFile(join(appDir, 'app', 'dashboard', 'layout.ts'), [
|
|
483
|
+
"import { html } from '@webjsdev/core';",
|
|
484
|
+
"import { buttonClass } from '#components/ui/button.ts';",
|
|
485
|
+
"",
|
|
486
|
+
"// Nested layout for the protected /dashboard subtree. Logout is a plain",
|
|
487
|
+
"// <form method=\"POST\"> posting to the createAuth signout route: it clears the",
|
|
488
|
+
"// session cookie and 302s home, and works with JS off (progressive-enhancement",
|
|
489
|
+
"// default). signOut is server-only (lib/auth.server.ts), so we POST to its route",
|
|
490
|
+
"// rather than import it into a browser-shipping page. After signout the dashboard",
|
|
491
|
+
"// middleware bounces any later /dashboard visit to /login.",
|
|
492
|
+
"export default function DashboardLayout({ children }: { children: unknown }) {",
|
|
493
|
+
" return html`",
|
|
494
|
+
" <nav class=\"flex items-center gap-4 mb-6 pb-4 border-b border-border\">",
|
|
495
|
+
" <a href=\"/dashboard\" class=\"text-sm font-medium hover:underline\">Dashboard</a>",
|
|
496
|
+
" <a href=\"/dashboard/settings\" class=\"text-sm font-medium hover:underline\">Settings</a>",
|
|
497
|
+
" <form method=\"POST\" action=\"/api/auth/signout\" class=\"ml-auto\">",
|
|
498
|
+
" <button class=${buttonClass({ variant: 'outline', size: 'sm' })} type=\"submit\">Log out</button>",
|
|
499
|
+
" </form>",
|
|
500
|
+
" </nav>",
|
|
501
|
+
" ${children}",
|
|
502
|
+
" `;",
|
|
503
|
+
"}",
|
|
504
|
+
"",
|
|
505
|
+
].join('\n'));
|
|
506
|
+
|
|
464
507
|
// app/dashboard/page.ts
|
|
465
508
|
await writeFile(join(appDir, 'app', 'dashboard', 'page.ts'), [
|
|
466
509
|
"import { html } from '@webjsdev/core';",
|
|
467
510
|
"import { currentUser } from '#modules/auth/queries/current-user.server.ts';",
|
|
468
|
-
"import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass
|
|
469
|
-
"import { buttonClass } from '#components/ui/button.ts';",
|
|
511
|
+
"import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass } from '#components/ui/card.ts';",
|
|
470
512
|
"import { badgeClass } from '#components/ui/badge.ts';",
|
|
471
513
|
"",
|
|
472
514
|
"export const metadata = { title: 'Dashboard' };",
|
|
@@ -483,9 +525,6 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
483
525
|
" <h2 class=${cardTitleClass()}>Welcome, ${user?.name || user?.email}!</h2>",
|
|
484
526
|
" <p class=${cardDescriptionClass()}>You're authenticated. Replace this scaffold with your real app.</p>",
|
|
485
527
|
" </div>",
|
|
486
|
-
" <div class=${cardContentClass()}>",
|
|
487
|
-
" <a class=${buttonClass({ variant: 'outline' })} href=\"/dashboard/settings\">Settings</a>",
|
|
488
|
-
" </div>",
|
|
489
528
|
" </div>",
|
|
490
529
|
" `;",
|
|
491
530
|
"}",
|
package/package.json
CHANGED
|
@@ -48,21 +48,39 @@ cover what you need, the full hosted docs are at **https://docs.webjs.dev**.
|
|
|
48
48
|
logic in `modules/`.
|
|
49
49
|
- **Use a unique design, and redesign means more than recolor (UI apps).** Give
|
|
50
50
|
the app a design of its own (palette, typography, LAYOUT, and chrome) chosen
|
|
51
|
-
from what the app IS.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
utilities wherever they reach, and use custom
|
|
64
|
-
cannot express (@theme tokens, @keyframes,
|
|
65
|
-
gradients). The `api` template has no UI, so
|
|
51
|
+
from what the app IS. `app/layout.ts` ships as a MINIMAL shell (theme, design
|
|
52
|
+
tokens, and Tailwind infra, then `${children}` in a bare padded container) with
|
|
53
|
+
NO header, nav, footer, or reading column: design the app's own chrome from
|
|
54
|
+
scratch. Decide whether it needs a header at all, a nav (or none), a footer, a
|
|
55
|
+
sidebar, a centered reading column, or a full-bleed canvas, from what fits the
|
|
56
|
+
app. `LAYOUT-REFERENCE.md` at the project root is a complete worked layout to
|
|
57
|
+
learn the patterns from, then build your own. Two `webjs-scaffold-placeholder`
|
|
58
|
+
markers gate `webjs check`: the minimal shell ("design your layout from
|
|
59
|
+
scratch") and the palette block ("own the colors"), so check fails until each
|
|
60
|
+
is addressed. Keep the design TOKENS and theme wiring in `app/layout.ts`
|
|
61
|
+
(infrastructure the ui kit reads) and set the token VALUES to your own palette;
|
|
62
|
+
run `webjs check --clear-placeholders` to keep the starter palette
|
|
63
|
+
deliberately. Style with Tailwind utilities wherever they reach, and use custom
|
|
64
|
+
CSS only for what utilities cannot express (@theme tokens, @keyframes,
|
|
65
|
+
scrollbar, complex color-mix or gradients). The `api` template has no UI, so
|
|
66
|
+
this does not apply there.
|
|
67
|
+
- **Render the app and LOOK before you call UI work done (every agent, not just one harness).**
|
|
68
|
+
You write CSS blind, so a layout or design defect ships silently: `webjs check`
|
|
69
|
+
and `webjs typecheck` pass even when a component collapses, grid cells are
|
|
70
|
+
uneven, the layout resizes as it fills, or the app just kept the scaffold's
|
|
71
|
+
colors. Static tools give no failure signal for this. The only thing that
|
|
72
|
+
catches it is rendering the app and looking at the pixels. So for ANY page,
|
|
73
|
+
layout, or component work: run it (`webjs dev`), open every route you changed in
|
|
74
|
+
a real browser (drive it with your harness's browser tool or MCP if it has one,
|
|
75
|
+
otherwise open it yourself and screenshot), and PLAY THROUGH every state (empty,
|
|
76
|
+
filled, win, draw, reload, narrow and wide, light and dark). Confirm nothing
|
|
77
|
+
collapses or reflows, that cells stay equal, that the design is the app's OWN,
|
|
78
|
+
and that both themes read. Ship a real-browser test (`webjs test --browser`) for
|
|
79
|
+
the mechanical floor (measure `getBoundingClientRect()` and assert cells stay
|
|
80
|
+
equal across a move). Fix and re-render until it holds, then state in your final
|
|
81
|
+
message what you rendered and confirmed. Claude Code additionally ENFORCES this
|
|
82
|
+
via the `webjs-design-review` skill plus a Stop hook, but the discipline is
|
|
83
|
+
harness-agnostic and this rule is the source of truth for every agent.
|
|
66
84
|
- **Only three templates exist:** `webjs create <name>` (default full-stack),
|
|
67
85
|
`--template api`, `--template saas`. The CLI rejects any other `--template`
|
|
68
86
|
value. Pick:
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Claude Code Stop hook: render-and-look before finishing UI work.
|
|
4
|
+
#
|
|
5
|
+
# An AI agent writes CSS blind (it never renders), and layout / design defects
|
|
6
|
+
# have NO failure signal: `webjs check` and `typecheck` pass, the app runs. So a
|
|
7
|
+
# collapsed board, uneven cells, a layout that resizes as it fills, or an app
|
|
8
|
+
# that just kept the scaffold's design all ship silently. The one thing that
|
|
9
|
+
# catches them is looking at the rendered pixels. This backstop fires at the END
|
|
10
|
+
# of a turn that touched UI files and reminds you to render the app and inspect
|
|
11
|
+
# every state (see the webjs-design-review skill + CONVENTIONS item 6) before you
|
|
12
|
+
# stop. Loop-safe (fires at most once per stop) and skipped when no UI changed.
|
|
13
|
+
#
|
|
14
|
+
# Disable with WEBJS_NO_DESIGN_STOP=1.
|
|
15
|
+
|
|
16
|
+
set -uo pipefail
|
|
17
|
+
payload=$(cat 2>/dev/null || true)
|
|
18
|
+
|
|
19
|
+
if [ "${WEBJS_NO_DESIGN_STOP:-}" = "1" ]; then exit 0; fi
|
|
20
|
+
active=$(printf '%s' "$payload" | jq -r '.stop_hook_active // false' 2>/dev/null || echo false)
|
|
21
|
+
if [ "$active" = "true" ]; then exit 0; fi
|
|
22
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then exit 0; fi
|
|
23
|
+
|
|
24
|
+
# Did this turn touch UI surface? A component, a page/layout, or app styling.
|
|
25
|
+
ui_changed=$(git status --porcelain --untracked-files=all 2>/dev/null \
|
|
26
|
+
| grep -vE '(^|/)(node_modules|\.webjs)(/|$)' \
|
|
27
|
+
| grep -cE '(app/.*(page|layout)\.(t|j)sx?$)|(components/.*\.(t|j)sx?$)|(modules/.*components/.*\.(t|j)sx?$)|(\.css$)' || true)
|
|
28
|
+
|
|
29
|
+
if [ -z "$ui_changed" ] || [ "$ui_changed" -lt 1 ]; then exit 0; fi
|
|
30
|
+
|
|
31
|
+
reason="You changed UI in this turn but a design/layout defect has no failing test: check and typecheck pass even when a component collapses, cells are uneven, the layout shifts as it fills, or the app just resembles the scaffold. Before you stop, RENDER the app and LOOK at it: start it (webjs dev / start), open the routes you changed in a browser, and PLAY THROUGH every state (fill the board, win, draw, reload). Confirm (1) nothing collapses or resizes, cells stay equal; (2) the design is the app's OWN (layout, palette, typography, chrome), not the scaffold shell or its default colors; (3) it looks correct in light AND dark. See the webjs-design-review skill and CONVENTIONS item 6. If you already rendered and verified it this turn, say so in your final message. Disable this backstop with WEBJS_NO_DESIGN_STOP=1."
|
|
32
|
+
|
|
33
|
+
jq -n --arg r "$reason" '{decision: "block", reason: $r}' 2>/dev/null \
|
|
34
|
+
|| printf '{"decision":"block","reason":%s}\n' "$(printf '%s' "$reason" | jq -Rs . 2>/dev/null || echo '""')"
|
|
35
|
+
|
|
36
|
+
exit 0
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# UserPromptSubmit hook: route a UI-building prompt to the design-review
|
|
4
|
+
# skill, so it is never silently skipped.
|
|
5
|
+
#
|
|
6
|
+
# Why this exists: a Skill is model-invoked, so it fires only when the model
|
|
7
|
+
# judges the prompt to match, and that judgement is exactly what fails for
|
|
8
|
+
# design work ("build a tic-tac-toe app" reads as backend/logic work and the
|
|
9
|
+
# render-and-look step gets skipped, shipping a collapsed or scaffold-looking
|
|
10
|
+
# UI). A hook is deterministic: it runs on every prompt, decides from the
|
|
11
|
+
# prompt TEXT, and injects a directive the model reads before acting. It
|
|
12
|
+
# cannot invoke the Skill itself (the harness forbids that); the strongest
|
|
13
|
+
# lever is UserPromptSubmit additionalContext.
|
|
14
|
+
#
|
|
15
|
+
# Output contract: print one JSON object with
|
|
16
|
+
# hookSpecificOutput.additionalContext and exit 0. Never block (exit 2 would
|
|
17
|
+
# erase the prompt); routing informs, it does not gate.
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
payload=$(cat)
|
|
21
|
+
prompt=$(printf '%s' "$payload" | jq -r '.prompt // empty' 2>/dev/null || true)
|
|
22
|
+
[ -z "$prompt" ] && exit 0
|
|
23
|
+
lc=$(printf '%s' "$prompt" | tr '[:upper:]' '[:lower:]')
|
|
24
|
+
has() { printf '%s' "$lc" | grep -Eq "$1"; }
|
|
25
|
+
|
|
26
|
+
# UI / app-building intent: any request to build/create/change something the
|
|
27
|
+
# user will SEE. Broad on purpose (a false positive just reminds you to look).
|
|
28
|
+
if has '(build|create|make|add|design|redesign|style|implement|scaffold).{0,40}(app|page|layout|component|screen|view|board|form|dashboard|ui|site|game|list|table|card|nav|header|footer|modal|button|theme)' \
|
|
29
|
+
|| has '(make|help me|let'\''s).{0,20}(look|prettier|beautiful|nicer|design)' \
|
|
30
|
+
|| has '(tic.?tac.?toe|todo|blog|dashboard|landing|storefront|kanban|chat)'; then
|
|
31
|
+
ctx="ROUTING: this prompt involves UI work. Invoke the webjs-design-review skill (Skill tool) as part of this task: after building/changing any page, layout, or component and BEFORE reporting the work done, render the app in a real browser and LOOK at every state, confirming the app owns its design (layout + palette + type, not the scaffold), nothing collapses or resizes, cells stay even, and light + dark both read. A design/layout defect has NO failing test, so the render-and-look is the only check that catches it."
|
|
32
|
+
jq -n --arg c "$ctx" '{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: $c}}'
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
exit 0
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"hooks": {
|
|
3
|
+
"UserPromptSubmit": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": ".claude/hooks/route-skills.sh"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
],
|
|
3
13
|
"PreToolUse": [
|
|
4
14
|
{
|
|
5
15
|
"matcher": "Write|Edit|MultiEdit",
|
|
@@ -73,6 +83,10 @@
|
|
|
73
83
|
{
|
|
74
84
|
"type": "command",
|
|
75
85
|
"command": ".claude/hooks/commit-before-stop.sh"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"type": "command",
|
|
89
|
+
"command": ".claude/hooks/design-review-before-stop.sh"
|
|
76
90
|
}
|
|
77
91
|
]
|
|
78
92
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webjs-design-review
|
|
3
|
+
description: >-
|
|
4
|
+
Render-and-look review for ANY UI work in a WebJs app. Invoke after building
|
|
5
|
+
or changing a page, layout, or component, and before you report the work done.
|
|
6
|
+
Triggers: "build", "create", "add a page", "component", "layout", "style",
|
|
7
|
+
"design", "UI", "screen", "board", "form", "dashboard", "make it look".
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Render the app and LOOK before you call UI work done
|
|
11
|
+
|
|
12
|
+
You write CSS blind. You never see the pixels, so a whole class of defects ships
|
|
13
|
+
silently: `webjs check` passes, `webjs typecheck` passes, the server boots, and
|
|
14
|
+
the app still looks broken. A collapsed component, cells of unequal size, a
|
|
15
|
+
layout that resizes as it fills with content, text that overflows its box, an
|
|
16
|
+
app that just kept the scaffold's colors and chrome, none of these fail a test.
|
|
17
|
+
The only thing that catches them is rendering the app and looking at it.
|
|
18
|
+
|
|
19
|
+
So for ANY work that touches a page, layout, or component, this is the loop:
|
|
20
|
+
|
|
21
|
+
## 1. Run the app and open what you changed
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
webjs dev # or: webjs start, for the production render
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Open every route you touched in a real browser. Use the browser MCP
|
|
28
|
+
(`mcp__playwright__*` or `mcp__chrome-devtools__*`) if available so you can drive
|
|
29
|
+
and screenshot it; otherwise open it yourself and take screenshots.
|
|
30
|
+
|
|
31
|
+
## 2. Drive EVERY state, not just the first paint
|
|
32
|
+
|
|
33
|
+
The first paint is the easy case. Bugs hide in the states you reach by
|
|
34
|
+
interacting. Play the app the way a user will:
|
|
35
|
+
|
|
36
|
+
- A game board: play a full game. Fill it. Win. Draw. Reset. Watch whether the
|
|
37
|
+
board or its cells change size as marks appear (they must NOT).
|
|
38
|
+
- A list: empty, one item, many items, an item long enough to wrap.
|
|
39
|
+
- A form: empty, invalid, submitted, error returned, success.
|
|
40
|
+
- Anything async: loading, loaded, error, refetch.
|
|
41
|
+
|
|
42
|
+
Reload each state. Resize the window narrow (mobile) and wide.
|
|
43
|
+
|
|
44
|
+
## 3. Confirm the things a test can't
|
|
45
|
+
|
|
46
|
+
Look at each state and confirm, with your eyes:
|
|
47
|
+
|
|
48
|
+
1. **Nothing collapses, overflows, or resizes.** A container is the size it
|
|
49
|
+
should be (not 0-height, not collapsed to its content when it should fill).
|
|
50
|
+
Grid/flex children that should be equal ARE equal, and STAY equal as content
|
|
51
|
+
changes. Text stays inside its box.
|
|
52
|
+
2. **The design is this app's OWN.** Not the scaffold shell, not its default
|
|
53
|
+
color tokens. The palette (real `oklch`/hex values, not just shadcn token
|
|
54
|
+
NAMES), the typography, the layout, and the chrome are chosen for THIS app.
|
|
55
|
+
"It still looks like the starter" is a defect to fix, not ship.
|
|
56
|
+
3. **Light AND dark both look right.** Toggle the theme. Check contrast, that
|
|
57
|
+
nothing disappears against its background, that borders and shadows read.
|
|
58
|
+
4. **It still renders with JavaScript OFF.** WebJs is SSR + progressive
|
|
59
|
+
enhancement, so the page must read and look right with no JS (disable it in
|
|
60
|
+
devtools, or load in a JS-off context). Content shows, `<a>` navigates,
|
|
61
|
+
forms submit, and CRUCIALLY the CSS is fully applied (the app links a static
|
|
62
|
+
compiled `public/tailwind.css`, so utilities resolve with no JS). An app that
|
|
63
|
+
goes unstyled or blank with JS off is a broken first paint, not a design to
|
|
64
|
+
ship.
|
|
65
|
+
|
|
66
|
+
## 4. Iterate until it holds, then say what you saw
|
|
67
|
+
|
|
68
|
+
If any of the above is wrong, fix it and re-render. Do not stop on the first
|
|
69
|
+
render. When it holds, state in your final message WHAT you rendered and WHAT
|
|
70
|
+
you confirmed (which states, light + dark), so the review is on the record.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
**Why this is a skill and not just a test:** a real-browser test
|
|
75
|
+
(`webjs test --browser`) catches the mechanical failures (collapse, uneven
|
|
76
|
+
cells, reflow) and you SHOULD ship one. There is no framework helper for this; a
|
|
77
|
+
layout-stability check is a few lines you write against your own component (in a
|
|
78
|
+
`*/test/**/browser/*.test.js`): measure `getBoundingClientRect()` on the grid
|
|
79
|
+
children and assert they stay equal-sized before AND after a move, so a collapse
|
|
80
|
+
or reflow FAILS the test. But "looks like the scaffold", "the
|
|
81
|
+
palette is bland", "the spacing is off", "it's ugly in dark mode" are judgment
|
|
82
|
+
calls no assertion makes for you. That is what this human-in-the-loop look is
|
|
83
|
+
for. Do both: the test for the mechanical floor, the look for everything above
|
|
84
|
+
it. See CONVENTIONS item 6 and `agent-docs/styling.md`.
|
package/templates/.cursorrules
CHANGED
|
@@ -48,21 +48,39 @@ cover what you need, the full hosted docs are at **https://docs.webjs.dev**.
|
|
|
48
48
|
`lib/utils/`, feature logic in `modules/`.
|
|
49
49
|
- **Use a unique design, and redesign means more than recolor (UI apps).** Give
|
|
50
50
|
the app a design of its own (palette, typography, LAYOUT, and chrome) chosen
|
|
51
|
-
from what the app IS.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
utilities wherever they reach, and use custom
|
|
64
|
-
cannot express (@theme tokens, @keyframes,
|
|
65
|
-
gradients). The `api` template has no UI, so
|
|
51
|
+
from what the app IS. `app/layout.ts` ships as a MINIMAL shell (theme, design
|
|
52
|
+
tokens, and Tailwind infra, then `${children}` in a bare padded container) with
|
|
53
|
+
NO header, nav, footer, or reading column: design the app's own chrome from
|
|
54
|
+
scratch. Decide whether it needs a header at all, a nav (or none), a footer, a
|
|
55
|
+
sidebar, a centered reading column, or a full-bleed canvas, from what fits the
|
|
56
|
+
app. `LAYOUT-REFERENCE.md` at the project root is a complete worked layout to
|
|
57
|
+
learn the patterns from, then build your own. Two `webjs-scaffold-placeholder`
|
|
58
|
+
markers gate `webjs check`: the minimal shell ("design your layout from
|
|
59
|
+
scratch") and the palette block ("own the colors"), so check fails until each
|
|
60
|
+
is addressed. Keep the design TOKENS and theme wiring in `app/layout.ts`
|
|
61
|
+
(infrastructure the ui kit reads) and set the token VALUES to your own palette;
|
|
62
|
+
run `webjs check --clear-placeholders` to keep the starter palette
|
|
63
|
+
deliberately. Style with Tailwind utilities wherever they reach, and use custom
|
|
64
|
+
CSS only for what utilities cannot express (@theme tokens, @keyframes,
|
|
65
|
+
scrollbar, complex color-mix or gradients). The `api` template has no UI, so
|
|
66
|
+
this does not apply there.
|
|
67
|
+
- **Render the app and LOOK before you call UI work done (every agent, not just one harness).**
|
|
68
|
+
You write CSS blind, so a layout or design defect ships silently: `webjs check`
|
|
69
|
+
and `webjs typecheck` pass even when a component collapses, grid cells are
|
|
70
|
+
uneven, the layout resizes as it fills, or the app just kept the scaffold's
|
|
71
|
+
colors. Static tools give no failure signal for this. The only thing that
|
|
72
|
+
catches it is rendering the app and looking at the pixels. So for ANY page,
|
|
73
|
+
layout, or component work: run it (`webjs dev`), open every route you changed in
|
|
74
|
+
a real browser (drive it with your harness's browser tool or MCP if it has one,
|
|
75
|
+
otherwise open it yourself and screenshot), and PLAY THROUGH every state (empty,
|
|
76
|
+
filled, win, draw, reload, narrow and wide, light and dark). Confirm nothing
|
|
77
|
+
collapses or reflows, that cells stay equal, that the design is the app's OWN,
|
|
78
|
+
and that both themes read. Ship a real-browser test (`webjs test --browser`) for
|
|
79
|
+
the mechanical floor (measure `getBoundingClientRect()` and assert cells stay
|
|
80
|
+
equal across a move). Fix and re-render until it holds, then state in your final
|
|
81
|
+
message what you rendered and confirmed. Claude Code additionally ENFORCES this
|
|
82
|
+
via the `webjs-design-review` skill plus a Stop hook, but the discipline is
|
|
83
|
+
harness-agnostic and this rule is the source of truth for every agent.
|
|
66
84
|
- **Only three templates exist:** `webjs create <name>` (default
|
|
67
85
|
full-stack), `--template api`, `--template saas`. The CLI rejects any
|
|
68
86
|
other `--template` value. Pick:
|