@webjsdev/cli 0.10.71 → 0.10.72
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 +2 -6
- package/lib/doctor/codes.js +0 -1
- package/lib/doctor/runner.js +0 -2
- package/package.json +2 -2
- package/templates/.agents/rules/workflow.md +24 -6
- package/templates/.agents/skills/webjs/SKILL.md +13 -7
- package/templates/.agents/skills/webjs/references/built-ins.md +0 -2
- package/templates/.agents/skills/webjs/references/styling.md +1 -1
- package/templates/AGENTS.md +40 -5
- package/templates/CLAUDE.md +8 -3
- package/templates/CONVENTIONS.md +30 -12
- package/templates/partials/agents-playbook-api.md +14 -6
- package/templates/partials/agents-playbook-fullstack.md +19 -9
- package/lib/doctor/probes/dark-theme.js +0 -91
package/lib/create.js
CHANGED
|
@@ -585,13 +585,9 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
585
585
|
// layout already writes asset(), so a fresh app is green on day one.
|
|
586
586
|
// Two checks are fatal with no entry here at all, NODE_VERSION and
|
|
587
587
|
// TSCONFIG_ERASABLE, because either would 500 the app at runtime;
|
|
588
|
-
//
|
|
589
|
-
// tokens, the generated layout writes them, and a layout that drops them
|
|
590
|
-
// leaves the stylesheet's .dark block dead and the OS setting ignored
|
|
591
|
-
// (#1628); elsewhere it is a design convention and stays a warning.
|
|
592
|
-
// Everything else keeps its default warn. Add a code with "off" to
|
|
588
|
+
// everything else keeps its default warn. Add a code with "off" to
|
|
593
589
|
// silence it, or "error" to make it fatal too.
|
|
594
|
-
doctor: { gate: { UNMARKED_ASSET_LINKS: 'error'
|
|
590
|
+
doctor: { gate: { UNMARKED_ASSET_LINKS: 'error' } },
|
|
595
591
|
// The dependency audit's allowlist (#1492), the ONE place an accepted
|
|
596
592
|
// advisory is listed, each with the reason it is safe. `webjs audit`
|
|
597
593
|
// (the CI step below) fails on every other advisory at `level` or above,
|
package/lib/doctor/codes.js
CHANGED
package/lib/doctor/runner.js
CHANGED
|
@@ -13,7 +13,6 @@ import { checkUnmarkedAssetLinks } from './probes/unmarked-asset-links.js';
|
|
|
13
13
|
import { checkFrameworkResolves, checkFrameworkLinks } from './probes/framework-resolves.js';
|
|
14
14
|
import { checkWorkspaceOverrides } from './probes/workspace-overrides.js';
|
|
15
15
|
import { checkAppIcon } from './probes/app-icon.js';
|
|
16
|
-
import { checkDarkTheme } from './probes/dark-theme.js';
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* @typedef {import('./codes.js').DoctorResult} DoctorResult
|
|
@@ -69,7 +68,6 @@ export async function runDoctorChecks(appDir, opts = {}) {
|
|
|
69
68
|
checkUnmarkedAssetLinks(appDir),
|
|
70
69
|
Promise.resolve(checkWorkspaceOverrides(appDir)),
|
|
71
70
|
Promise.resolve(checkAppIcon(appDir)),
|
|
72
|
-
checkDarkTheme(appDir),
|
|
73
71
|
]);
|
|
74
72
|
// Attach the stable machine code to every result (#975). Centralized here so
|
|
75
73
|
// each check function stays free of the code-contract concern.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.72",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The CLI for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Runs the dev and production servers, scaffolds apps, validates conventions, and drives the database. Node 24+ or Bun.",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@webjsdev/mcp": "^0.1.
|
|
20
|
+
"@webjsdev/mcp": "^0.1.16",
|
|
21
21
|
"@webjsdev/server": "^0.8.89",
|
|
22
22
|
"@webjsdev/ui": "^0.3.15"
|
|
23
23
|
},
|
|
@@ -8,12 +8,30 @@ Read `AGENTS.md` first. Full hosted docs are at https://webjs.dev/docs.
|
|
|
8
8
|
|
|
9
9
|
## Grow the app in place (non-negotiable)
|
|
10
10
|
|
|
11
|
-
Study the shipped
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
- **Study the shipped examples, then clear them and build.** The scaffold is a
|
|
12
|
+
starting point with a browsable showcase to learn the real idioms from, plus a
|
|
13
|
+
database wired up. A full-stack app ships a UI feature gallery (`app/features/`,
|
|
14
|
+
`app/examples/todo`); the api template ships a backend-features showcase
|
|
15
|
+
(`app/api/features/`), with logic in `modules/`. Building a real app: study the
|
|
16
|
+
parts that match your task (the skill teaches the same and SURVIVES the clear),
|
|
17
|
+
run `npm run gallery:clear` to shed the showcase (it keeps the agent skill and
|
|
18
|
+
the database wiring, and resets to a clean base), then regenerate the database
|
|
19
|
+
and grow the app in place under `app/`, `components/`, and `modules/<feature>/`.
|
|
20
|
+
`AGENTS.md` carries the full template-specific build playbook and the order to
|
|
21
|
+
follow.
|
|
22
|
+
- **Use the wired-up database (Drizzle), never JSON files.** For any data the app
|
|
23
|
+
stores, define a Drizzle table in `db/schema.server.ts`, then
|
|
24
|
+
`npm run db:generate` and `npm run db:migrate`. Never use a JSON file, a
|
|
25
|
+
module-scope array or Map, or localStorage as a database.
|
|
26
|
+
- **`app/` is routing-only.** Only routing files live in `app/` (page, layout,
|
|
27
|
+
route, middleware, metadata routes). Browser-safe helpers go in `lib/utils/`,
|
|
28
|
+
feature logic in `modules/`, server-only code behind `.server.ts`.
|
|
29
|
+
- **For a UI app, render and LOOK before calling it done.** Define design tokens
|
|
30
|
+
in `app/layout.ts` with a palette that fits the app
|
|
31
|
+
(`.agents/skills/webjs/references/styling.md` is the guide), then open every
|
|
32
|
+
route you changed in a real browser and play through its states.
|
|
33
|
+
`npm run check` and `npm run typecheck` pass even when a layout collapses, so
|
|
34
|
+
the browser is the real check.
|
|
17
35
|
|
|
18
36
|
## Before starting ANY work
|
|
19
37
|
|
|
@@ -104,12 +104,12 @@ Common bundles:
|
|
|
104
104
|
1. **Classify the change.** Route contract, data model, server mutation, auth, or only UI?
|
|
105
105
|
2. **Start from the server.** Add the page/route and its server action or query before wiring interactive UI. A page render or a `<form>` POST should already return correct HTML before any component hydrates.
|
|
106
106
|
3. **Put code in the narrowest owner.** Route-local first (`modules/<feature>/`), promote to `lib/` or `components/` only when reuse is real.
|
|
107
|
-
4. **Keep server-only code behind `.server.ts
|
|
107
|
+
4. **Keep server-only code behind `.server.ts`.** The DB driver, secrets, and `node:*` never belong in a page, layout, or component.
|
|
108
108
|
5. **Add interactivity per behaviour.** Reach for a component (and a signal or `@event`) only where the UI is genuinely interactive. A display-only component is elided from the browser. Then wrap the interactive part and STOP: the static markup around it stays in the page, where it costs nothing.
|
|
109
109
|
6. **Validate input at the boundary.** Declare `export const validate` on an action; the RPC and `route()` boundaries run it.
|
|
110
110
|
7. **Default mutations to optimistic UI** where the client can predict the result (`optimistic()` from `@webjsdev/core`).
|
|
111
|
-
8. **Type every boundary from its source, never `unknown` or `any`.** The row type comes from the schema (`typeof todos.$inferSelect`), the action's input from a named `interface` and its result from `ActionResult<T>`, the routing files from `PageProps` / `LayoutProps` / `RouteHandlerContext`. `unknown` belongs on a payload nothing has vouched for yet that the next line narrows, and on a parameter of your own helper that forwards into an `html` template hole. Everywhere else, including a layout's `children`, it is a missing type.
|
|
112
|
-
9. **Test the narrowest meaningful layer**, and
|
|
111
|
+
8. **Type every boundary from its source, never `unknown` or `any`.** The row type comes from the schema (`typeof todos.$inferSelect`), the action's input from a named `interface` and its result from `ActionResult<T>`, the routing files from `PageProps` / `LayoutProps` / `RouteHandlerContext`. `unknown` belongs on a payload nothing has vouched for yet that the next line narrows, and on a parameter of your own helper that forwards into an `html` template hole. Everywhere else, including a layout's `children`, it is a missing type. See `references/typescript.md`.
|
|
112
|
+
9. **Test the narrowest meaningful layer**, and render the app in a real browser for any UI change (static checks do not catch a collapsed layout).
|
|
113
113
|
|
|
114
114
|
## Project Layout
|
|
115
115
|
|
|
@@ -137,7 +137,7 @@ App-internal imports use the `#` root alias (`import { db } from '#db/connection
|
|
|
137
137
|
## Core WebJs Rules (invariants)
|
|
138
138
|
|
|
139
139
|
1. Server-only code lives in `.server.ts`, `route.ts`, or `middleware.ts`. Never in a page, layout, or component (it crashes the browser at module load).
|
|
140
|
-
2. `'use server'` exports are `async` functions returning serializer-safe values
|
|
140
|
+
2. `'use server'` exports are `async` functions returning serializer-safe values. Files without `'use server'` are server-only utilities.
|
|
141
141
|
3. Custom element tag names contain a hyphen. Pass the tag to `Class.register('tag-name')`.
|
|
142
142
|
4. Event (`@`), property (`.`), and boolean (`?`) holes in `html` are UNQUOTED: `@click=${fn}`, never `@click="${fn}"`.
|
|
143
143
|
5. Signals are the default state primitive. Import `signal` / `computed` from `@webjsdev/core`, read via `signal.get()` inside `render()`. The base-class factory `WebComponent({ ... })` is only for values riding an HTML attribute or arriving via SSR hydration.
|
|
@@ -147,7 +147,7 @@ App-internal imports use the `#` root alias (`import { db } from '#db/connection
|
|
|
147
147
|
9. No backtick characters inside an `html\`...\`` body, even in comments (it closes the literal and 500s).
|
|
148
148
|
10. TypeScript must be erasable (`erasableSyntaxOnly: true`): no `enum`, no value `namespace`, no constructor parameter properties, no legacy decorators.
|
|
149
149
|
11. Reactive properties are declared ONLY through the base-class factory `extends WebComponent({ count: Number })`. Never a `static properties` block, never a class-field initializer (it clobbers the reactive accessor).
|
|
150
|
-
12. A form that writes binds its action: `<form action=${importedAction}>`, or a per-button `<button formaction=${importedAction}>`. A bound submitter is SELF-SUFFICIENT (#1307): the renderer puts `formmethod="post"` and `formenctype` on the button itself, so it needs no bound form around it and works inside any form or none. Quoted bindings, non-submit controls, `<input type="submit">` (the identity needs its `value`, which is also its label, so use a `<button>`), submitter `name` / `value` / `form` / static `formaction` attributes
|
|
150
|
+
12. A form that writes binds its action: `<form action=${importedAction}>`, or a per-button `<button formaction=${importedAction}>`. A bound submitter is SELF-SUFFICIENT (#1307): the renderer puts `formmethod="post"` and `formenctype` on the button itself, so it needs no bound form around it and works inside any form or none. Quoted bindings, non-submit controls, `<input type="submit">` (the identity needs its `value`, which is also its label, so use a `<button>`), submitter `name` / `value` / `form` / static `formaction` attributes, a `.prop` spelling of any of those, `action=${fn}` off a `<form>`, a bound form with `method="get"`, a BOUND submitter's own non-post `formmethod` or unparseable `formenctype`, and a non-action function all throw. A PLAIN button's own `formmethod` / `formenctype` is a legal native override and is left alone. A page has no `action` export, so a bare `<form method="post">` is a 405.
|
|
151
151
|
|
|
152
152
|
## Export Map
|
|
153
153
|
|
|
@@ -272,12 +272,18 @@ Success is a 303 (PRG); failure re-renders the page at 422 with the result on `a
|
|
|
272
272
|
|
|
273
273
|
## Common Mistakes To Avoid
|
|
274
274
|
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
- Treating a page or layout like a React component and expecting its markup to hydrate. It runs server-only; put interactivity in a component.
|
|
277
276
|
- Promoting a whole page section to a component so that one control inside it can be interactive. The island should wrap the control and the state it reads. An oversized island ships its own JS AND un-elides every display-only component inside it, so the cost is not linear in what you moved.
|
|
277
|
+
- Importing a `.server.ts` utility (no `'use server'`) directly into a shipping component. Its browser stub throws at load; reach it through a `'use server'` action.
|
|
278
|
+
- Using a `static properties` block or a class-field initializer for reactive props instead of the `WebComponent({ ... })` factory.
|
|
279
|
+
- Quoting an event / property / boolean hole (`@click="${fn}"`).
|
|
278
280
|
- Writing `fetch()` to call your own server instead of importing the action.
|
|
281
|
+
- Writing a bare `<form method="post">` and expecting a page `action` export to catch it. There is no such export; bind the action with `action=${fn}` or the submission is a 405.
|
|
282
|
+
- Putting a submitter's `formaction=${fn}` on anything that is not a submit control, or on a button carrying its own `name` / `value`. The identity IS the button's name/value pair, so both halves are spoken for.
|
|
283
|
+
- Writing `formmethod="get"` or `formenctype="text/plain"` on a button that BINDS an action. Neither can carry that action's body, so the pair contradicts itself and throws. On a button that binds nothing it is a legal native override and is honoured.
|
|
279
284
|
- Binding an action whose file declares `export const method = 'GET'`. Form-bound actions strictly require POST (default). Binding a GET action to a form is a 405 at runtime and a `webjs check` error (`form-action-not-a-get-action`).
|
|
280
285
|
- Leaving read-only RPC server query actions as default `POST`. Always export `export const method = 'GET'` for RPC data queries so arguments ride URL params, ETags/304 caching work, and CSRF is safely bypassed.
|
|
286
|
+
- Writing `method="get"` on a bound `<form action=${fn}>`. WebJs supplies `method="post"` and `formenctype` automatically, and a bound form declaring `method="get"` is REFUSED at render (a thrown error, not a warning), because a GET sends no body for the action to read.
|
|
281
287
|
- Throwing `redirect()` / `notFound()` inside a `route.ts` handler (uncaught 500). Return a `Response` there.
|
|
282
288
|
- A placeholder first paint that fetches in `connectedCallback`. SSR does not call `connectedCallback`; put first-paint data in the constructor (server-known inputs) or use `async render()`.
|
|
283
289
|
- A browser global (`window`, `document`, `localStorage`) in the constructor or `render()`. It throws at SSR; do browser-only work in `connectedCallback`.
|
|
@@ -298,8 +298,6 @@ Two guarantees worth knowing. A result that could not check (a network or toolch
|
|
|
298
298
|
|
|
299
299
|
`APP_ICON` warns while the favicon is still the scaffold's placeholder `app/icon.svg` (marked `data-webjs-placeholder`). Replace it with the app's own icon (`references/routing-and-pages.md`, "App icon and manifest").
|
|
300
300
|
|
|
301
|
-
`DARK_THEME_UNREACHABLE` warns when the app defines colour tokens with a light value only and nothing applies a dark half: no `light-dark()`, no `@media (prefers-color-scheme: dark)` rule, no theme script in the root layout. The scaffold gates it `error` in its `package.json` because its AGENTS.md mandates `light-dark()` tokens; the fix is the token block in `references/styling.md`.
|
|
302
|
-
|
|
303
301
|
### Dependency audit allowlist
|
|
304
302
|
|
|
305
303
|
`webjs audit` runs `npm audit` or `bun audit` (by the nearest lockfile, so a workspace member uses the root's) and fails on any advisory at or above `webjs.audit.level` (default `high`) that `webjs.audit.ignore` does not list. The scaffold's `Security: dependency audit` CI step runs it.
|
|
@@ -193,7 +193,7 @@ Whichever form you use, a token nothing references is dropped in both, so an unu
|
|
|
193
193
|
</style>
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
-
`light-dark()` is a native CSS function (CSS Color 5, Baseline 2024), not a library, so nothing to import. A single-theme app drops the `[data-theme]` rules and gives each token one colour.
|
|
196
|
+
`light-dark()` is a native CSS function (CSS Color 5, Baseline 2024), not a library, so nothing to import. A single-theme app drops the `[data-theme]` rules and gives each token one colour.
|
|
197
197
|
|
|
198
198
|
**A manual theme toggle** writes `data-theme` on `<html>` (`light` / `dark`, or removes it for "follow the OS"). If you use `@webjsdev/ui` components, ALSO keep the `.dark` class in sync (the ui kit keys its own tokens off `.dark`), and apply the saved choice in a tiny inline `<script>` in the layout head so there is no first-paint flash. Verify dark mode in a real browser. Light mode passing proves nothing about dark.
|
|
199
199
|
|
package/templates/AGENTS.md
CHANGED
|
@@ -34,14 +34,49 @@ This is what separates a working app from a broken one.
|
|
|
34
34
|
|
|
35
35
|
## Type everything (all templates)
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
Full-stack type safety is what the `.server.ts` boundary buys you: a client
|
|
38
|
+
component importing a server action resolves to that action's real signature at
|
|
39
|
+
type-check time, with no build step and no code generation in between. So
|
|
40
|
+
DERIVE the type at every boundary instead of widening it:
|
|
41
|
+
|
|
42
|
+
- A database row: `export type Todo = typeof todos.$inferSelect` in
|
|
43
|
+
`db/schema.server.ts` (`$inferInsert` for a write), carried into a
|
|
44
|
+
browser-shipped component with `import type` (erased before it reaches the
|
|
45
|
+
browser, so it does not trip the server-import boundary).
|
|
46
|
+
- An action's input: a named `interface`. Its result: `ActionResult<T>`.
|
|
47
|
+
Narrow with `if (result.success && result.data)`.
|
|
48
|
+
- Routing files: `PageProps<'/blog/[slug]'>`, `LayoutProps`,
|
|
49
|
+
`RouteHandlerContext`, all from `@webjsdev/core`. Run `npx webjsdev types`
|
|
50
|
+
for the typed `Route` union and per-route `params`.
|
|
51
|
+
- A reactive property: `prop<Student>(Object)`, `prop<Tag[]>(Array)`.
|
|
52
|
+
|
|
53
|
+
Never reach for `any` or a loose `as any` cast, and do not reach for `unknown`
|
|
54
|
+
either just because it looks safer. `unknown` is right for a payload nothing
|
|
55
|
+
has vouched for yet, narrowed on the very next line (a `route.ts` `await
|
|
56
|
+
req.json()`, an action's `export const validate` or a validator it delegates
|
|
57
|
+
to, a `catch` binding), and for a parameter of YOUR OWN helper that forwards
|
|
58
|
+
into an `html` template hole (a hole renders a string, a number, a
|
|
59
|
+
`TemplateResult`, or an array of those, so `TemplateResult` alone is too
|
|
60
|
+
narrow). That second case is about a value you accept, never one the framework
|
|
61
|
+
already types. Everywhere else it is a missing type, not a safe one: `unknown`
|
|
62
|
+
that survives into a return type, a component prop, a layout's `children`, or
|
|
63
|
+
an action signature is the shape to fix.
|
|
64
|
+
Nothing enforces this (both are valid TypeScript, so `webjs check` and `tsc`
|
|
65
|
+
pass either way), which is exactly why it is written down. The full ladder,
|
|
66
|
+
with an end-to-end example, is in
|
|
40
67
|
`.agents/skills/webjs/references/typescript.md`.
|
|
41
68
|
|
|
42
69
|
Keep server-only code (database drivers, secrets, `node:*` builtins) in
|
|
43
|
-
`.server.ts` modules.
|
|
44
|
-
|
|
70
|
+
`.server.ts` modules. There are exactly two kinds:
|
|
71
|
+
|
|
72
|
+
- A `.server.ts` file WITH `'use server';` as its first line is a server
|
|
73
|
+
action: WebJs exposes its exported async functions to browser code as RPC
|
|
74
|
+
calls, so browser modules may import it directly.
|
|
75
|
+
- A `.server.ts` file WITHOUT `'use server'` is a server-only utility:
|
|
76
|
+
importing it from a page, layout, or component CRASHES in the browser at
|
|
77
|
+
module load. Reach it only from `'use server'` actions, `route.ts` handlers,
|
|
78
|
+
or middleware. Never add `'use server'` to a file only other server code
|
|
79
|
+
imports (the DB connection, the schema).
|
|
45
80
|
|
|
46
81
|
## Data (all templates)
|
|
47
82
|
|
package/templates/CLAUDE.md
CHANGED
|
@@ -9,9 +9,14 @@ is complete, WITHOUT being asked. Do not save all the work for one commit at the
|
|
|
9
9
|
end. A finished implementation with zero commits is a mistake here, because git
|
|
10
10
|
history is the user's revert and cherry-pick safety net.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
- After each completed unit whose tests pass, `git add` the related files and
|
|
13
|
+
`git commit` with an imperative subject under 72 chars, then push. If 5+ files
|
|
14
|
+
span more than one concern, you already waited too long.
|
|
15
|
+
- Never commit to `main`. Work on a feature branch (the
|
|
16
|
+
`.claude/hooks/guard-branch-context.sh` hook enforces this).
|
|
17
|
+
- No AI-attribution trailers (`Co-Authored-By`, `Generated by`).
|
|
18
|
+
|
|
19
|
+
See AGENTS.md "Git workflow" for the full contract. Two hooks back this up: the
|
|
15
20
|
`.claude/hooks/nudge-uncommitted.sh` PostToolUse hook reminds you while
|
|
16
21
|
uncommitted changes pile up during work, and the
|
|
17
22
|
`.claude/hooks/commit-before-stop.sh` Stop hook stops you from ending a turn
|
package/templates/CONVENTIONS.md
CHANGED
|
@@ -3,16 +3,34 @@
|
|
|
3
3
|
The conventions for building a WebJs app live in the agent skill. **Read
|
|
4
4
|
`AGENTS.md` first, then `.agents/skills/webjs/SKILL.md`** (it routes to focused
|
|
5
5
|
references under `.agents/skills/webjs/references/`, loaded on demand). This file
|
|
6
|
-
|
|
6
|
+
is the short version.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- **
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
## The essentials
|
|
9
|
+
|
|
10
|
+
- **`app/` is routing only.** Only routing files live there (page, layout, route,
|
|
11
|
+
middleware, metadata routes). Feature logic goes in `modules/<feature>/`
|
|
12
|
+
(`actions/`, `queries/`, `components/`, `utils/`); shared UI primitives go in
|
|
13
|
+
top-level `components/`; browser-safe helpers in `lib/utils/`.
|
|
14
|
+
- **Server-only code goes behind `.server.ts`.** Reach it from a page or component
|
|
15
|
+
through a `'use server'` action, never by importing a server-only utility
|
|
16
|
+
directly into browser-bound code.
|
|
17
|
+
- **Use the wired-up database (Drizzle).** Define real models in
|
|
18
|
+
`db/schema.server.ts`, then `npm run db:generate` and `npm run db:migrate`.
|
|
19
|
+
Never persist to a JSON file, an in-memory array or Map, or localStorage.
|
|
20
|
+
- **The scaffold ships a showcase to learn from.** A full-stack app ships a UI
|
|
21
|
+
feature gallery (`app/features/`, `app/examples/todo`); the api template ships
|
|
22
|
+
a backend-features showcase (`app/api/features/`), with logic in `modules/`.
|
|
23
|
+
When you build a real app, study the parts that match your task (the skill
|
|
24
|
+
teaches the same and survives the clear), run `npm run gallery:clear` to shed
|
|
25
|
+
the showcase, then grow the app in place. `AGENTS.md` has the full
|
|
26
|
+
template-specific playbook.
|
|
27
|
+
- **Derive types at every boundary.** Rows from `$inferSelect`, action inputs
|
|
28
|
+
from an `interface`, routing files from `PageProps` / `LayoutProps`. Never
|
|
29
|
+
`any`, and never `unknown` where a real type exists.
|
|
30
|
+
- **Progressive enhancement is the default.** Pages render as HTML, `<a>`
|
|
31
|
+
navigates, a `<form action=${importedAction}>` submits, all with JavaScript off; opt into
|
|
32
|
+
interactivity per behaviour inside a component.
|
|
33
|
+
- **Commit per logical unit** as soon as it is complete, and never push to `main`.
|
|
34
|
+
|
|
35
|
+
Everything else (the module architecture, the `ActionResult` envelope, styling,
|
|
36
|
+
testing, the client router, optimistic UI) is in the skill's references.
|
|
@@ -44,12 +44,20 @@ cross-origin access use the `cors()` middleware from `@webjsdev/server`; with
|
|
|
44
44
|
|
|
45
45
|
### 5. Verify before you call it done
|
|
46
46
|
|
|
47
|
-
Run `npm run ci` and fix what it reports. It
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
Run `npm run ci` and fix what it reports. It is one command for every gate,
|
|
48
|
+
the step list declared in `package.json` under `webjs.ci`, with a result line
|
|
49
|
+
per step:
|
|
50
|
+
|
|
51
|
+
- `webjs check` (correctness: no browser-import or boundary violation).
|
|
52
|
+
- `webjs doctor` (project health). It fails on whatever `package.json`
|
|
53
|
+
`webjs.doctor.gate` marks `error`, plus the two hard toolchain checks that
|
|
54
|
+
are fatal with no gate entry, `NODE_VERSION` and `TSCONFIG_ERASABLE`.
|
|
55
|
+
- `webjs typecheck` (zero type errors).
|
|
56
|
+
- A dependency audit.
|
|
57
|
+
- The test layers for the endpoints and modules you built.
|
|
58
|
+
|
|
59
|
+
The GitHub workflow runs the same list, so a green local run predicts CI.
|
|
60
|
+
While iterating, `npm run ci -- --only Tests` runs one layer.
|
|
53
61
|
|
|
54
62
|
Then boot `npm run dev` and probe each endpoint for the expected status and JSON
|
|
55
63
|
shape.
|
|
@@ -84,21 +84,31 @@ action without also triggering the row navigation.
|
|
|
84
84
|
|
|
85
85
|
### 6. Build components for interactivity
|
|
86
86
|
|
|
87
|
-
Pages and layouts
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
Pages and layouts (`app/**/page.ts`, `app/**/layout.ts`) are server-only HTML
|
|
88
|
+
generators, so put every interactive behavior inside a `WebComponent` custom
|
|
89
|
+
element. Declare a component's reactive properties in the base-class factory,
|
|
90
|
+
never as a class-field initializer (`items = []` clobbers the reactive
|
|
91
|
+
accessor). Use the shorthand for primitives
|
|
90
92
|
(`extends WebComponent({ name: String, count: Number, open: Boolean })`) and the
|
|
91
93
|
`prop<T>()` helper for typed objects and arrays
|
|
92
94
|
(`extends WebComponent({ items: prop<Item[]>(Array), user: prop<User>(Object) })`).
|
|
93
95
|
|
|
94
96
|
### 7. Verify before you call it done
|
|
95
97
|
|
|
96
|
-
Run `npm run ci` and fix what it reports. It
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
Run `npm run ci` and fix what it reports. It is one command for every gate,
|
|
99
|
+
the step list declared in `package.json` under `webjs.ci`, with a result line
|
|
100
|
+
per step:
|
|
101
|
+
|
|
102
|
+
- `webjs check` (correctness: no browser-import or boundary violation).
|
|
103
|
+
- `webjs doctor` (project health). It fails on whatever `package.json`
|
|
104
|
+
`webjs.doctor.gate` marks `error`, plus the two hard toolchain checks that
|
|
105
|
+
are fatal with no gate entry, `NODE_VERSION` and `TSCONFIG_ERASABLE`.
|
|
106
|
+
- `webjs typecheck` (zero type errors).
|
|
107
|
+
- A dependency audit.
|
|
108
|
+
- The server, browser, and e2e test layers for the features you built.
|
|
109
|
+
|
|
110
|
+
The GitHub workflow runs the same list, so a green local run predicts CI.
|
|
111
|
+
While iterating, `npm run ci -- --only Tests` runs one layer. Then
|
|
102
112
|
`npm run css:build` (compile Tailwind).
|
|
103
113
|
|
|
104
114
|
Then boot `npm run dev`, confirm every page route returns HTTP 200, and open
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { join, relative } from 'node:path';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @typedef {import('../codes.js').DoctorResult} DoctorResult
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const ROOT_LAYOUT = /^app\/layout\.(?:js|mjs|ts|mts)$/;
|
|
10
|
-
/** Stylesheet sources: the Tailwind input and any hand-written sheet; never the compiled output. */
|
|
11
|
-
const STYLE_SOURCE = /^(?:public|styles)\/(?:.+\/)?[^/]+\.css$/;
|
|
12
|
-
const COMPILED_OR_VENDORED = /(?:^|\/)(?:tailwind\.css|node_modules\/|\.webjs\/|dist\/)/;
|
|
13
|
-
/** A colour token definition: the two every palette starts from. */
|
|
14
|
-
const TOKEN_DEF = /--(?:background|foreground)\s*:/;
|
|
15
|
-
/** Any of these makes the dark half reachable. */
|
|
16
|
-
const DUAL_TOKENS = /light-dark\s*\(/;
|
|
17
|
-
const SCHEME_QUERY = /prefers-color-scheme/;
|
|
18
|
-
/** A head script that applies a saved or detected theme. */
|
|
19
|
-
const THEME_SCRIPT = /<script\b[^>]*>[\s\S]*?(?:prefers-color-scheme|localStorage|data-theme|classList\.(?:add|toggle)\(\s*['"`]dark['"`]|dataset\.theme)[\s\S]*?<\/script>/;
|
|
20
|
-
|
|
21
|
-
/** CSS and JS block comments and line comments, so a commented-out token or hint does not count. */
|
|
22
|
-
function stripComments(text) {
|
|
23
|
-
return text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* The app's colour-token sources: the root layout (where the generated app
|
|
28
|
-
* writes its palette) and the stylesheet sources under `public/` and `styles/`.
|
|
29
|
-
* @param {string} appDir
|
|
30
|
-
* @returns {Promise<Array<{ rel: string, content: string }>>}
|
|
31
|
-
*/
|
|
32
|
-
async function tokenSources(appDir) {
|
|
33
|
-
/** @type {Array<{ rel: string, content: string }>} */
|
|
34
|
-
const out = [];
|
|
35
|
-
const read = async (rel) => {
|
|
36
|
-
try { out.push({ rel, content: await readFile(join(appDir, rel), 'utf8') }); } catch { /* unreadable: not a source */ }
|
|
37
|
-
};
|
|
38
|
-
for (const ext of ['ts', 'js', 'mts', 'mjs']) {
|
|
39
|
-
const rel = `app/layout.${ext}`;
|
|
40
|
-
if (existsSync(join(appDir, rel))) { await read(rel); break; }
|
|
41
|
-
}
|
|
42
|
-
for (const dir of ['public', 'styles']) {
|
|
43
|
-
const abs = join(appDir, dir);
|
|
44
|
-
if (!existsSync(abs)) continue;
|
|
45
|
-
for (const e of readdirSync(abs, { recursive: true, withFileTypes: true })) {
|
|
46
|
-
if (!e.isFile() || !e.name.endsWith('.css')) continue;
|
|
47
|
-
const rel = relative(appDir, join(e.parentPath || abs, e.name)).split('\\').join('/');
|
|
48
|
-
if (STYLE_SOURCE.test(rel) && !COMPILED_OR_VENDORED.test(rel)) await read(rel);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return out;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* `DARK_THEME_UNREACHABLE` (#1628): the app defines colour tokens but nothing
|
|
56
|
-
* applies a dark half. The scaffold's stylesheet keeps the ui kit's `.dark`
|
|
57
|
-
* token block, and `AGENTS.md` asks for `light-dark()` tokens under
|
|
58
|
-
* `color-scheme: light dark`; a layout that uses neither, has no
|
|
59
|
-
* `prefers-color-scheme` rule and runs no theme script ships dead CSS and
|
|
60
|
-
* ignores the OS setting, which no light-mode screenshot reveals. A design
|
|
61
|
-
* convention rather than a runtime break, so it lives in doctor (WARN by
|
|
62
|
-
* default) and the scaffold gates it `error` because its AGENTS.md mandates
|
|
63
|
-
* the tokens. Silent when the app defines no colour tokens at all.
|
|
64
|
-
* @param {string} appDir
|
|
65
|
-
* @returns {Promise<DoctorResult>}
|
|
66
|
-
*/
|
|
67
|
-
export async function checkDarkTheme(appDir) {
|
|
68
|
-
const name = 'dark-theme';
|
|
69
|
-
if (!existsSync(join(appDir, 'app'))) {
|
|
70
|
-
return { name, status: 'pass', message: 'no app/ directory to analyse' };
|
|
71
|
-
}
|
|
72
|
-
const sources = await tokenSources(appDir);
|
|
73
|
-
const defining = sources.filter((s) => TOKEN_DEF.test(stripComments(s.content)));
|
|
74
|
-
if (!defining.length) {
|
|
75
|
-
return { name, status: 'pass', message: 'no colour tokens defined (nothing to reach)' };
|
|
76
|
-
}
|
|
77
|
-
const all = sources.map((s) => stripComments(s.content)).join('\n');
|
|
78
|
-
const layout = sources.find((s) => ROOT_LAYOUT.test(s.rel));
|
|
79
|
-
if (DUAL_TOKENS.test(all) || SCHEME_QUERY.test(all) || (layout && THEME_SCRIPT.test(layout.content))) {
|
|
80
|
-
return { name, status: 'pass', message: 'the dark half of the colour tokens is reachable' };
|
|
81
|
-
}
|
|
82
|
-
const where = defining.map((s) => s.rel).join(' and ');
|
|
83
|
-
return {
|
|
84
|
-
name,
|
|
85
|
-
status: 'warn',
|
|
86
|
-
message:
|
|
87
|
-
`colour tokens are defined in ${where} with a light value only: nothing applies a dark half (no light-dark(), no prefers-color-scheme rule, no theme script in the root layout), so the OS dark setting is ignored and any .dark block is dead CSS`,
|
|
88
|
-
fix:
|
|
89
|
-
'Write each colour token ONCE as light-dark(LIGHT, DARK) under `color-scheme: light dark` in the root layout (the block in .agents/skills/webjs/references/styling.md), or give the tokens an `@media (prefers-color-scheme: dark)` half, or add a head <script> that applies the saved theme (data-theme / the .dark class). Then check the app in a real browser with the OS in dark mode.',
|
|
90
|
-
};
|
|
91
|
-
}
|