@webjsdev/cli 0.10.57 → 0.10.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/bin/webjs.js +316 -9
- package/lib/app-tasks.js +70 -10
- package/lib/audit.js +218 -0
- package/lib/browser-test-files.js +125 -0
- package/lib/check-target.js +1 -1
- package/lib/ci-config.js +250 -0
- package/lib/ci-runner.js +499 -0
- package/lib/create.js +104 -21
- package/lib/db-rewrite.js +137 -0
- package/lib/doctor/codes.js +1 -0
- package/lib/doctor/probes/workspace-overrides.js +79 -0
- package/lib/doctor/runner.js +2 -0
- package/lib/package-manager.js +93 -0
- package/lib/run-tasks.js +23 -3
- package/package.json +3 -3
- package/templates/.agents/rules/workflow.md +15 -9
- package/templates/.agents/skills/webjs/SKILL.md +1 -0
- package/templates/.agents/skills/webjs/references/auth-and-sessions.md +11 -0
- package/templates/.agents/skills/webjs/references/built-ins.md +79 -0
- package/templates/.agents/skills/webjs/references/styling.md +1 -1
- package/templates/.agents/skills/webjs/references/testing.md +13 -0
- package/templates/.agents/skills/webjs/references/ui-kit.md +25 -0
- package/templates/.dockerignore +1 -1
- package/templates/.github/pull_request_template.md +3 -4
- package/templates/.github/workflows/ci.yml +38 -88
- package/templates/.hooks/pre-commit +5 -4
- package/templates/Dockerfile +3 -3
- package/templates/compose.yaml +3 -3
- package/templates/partials/agents-playbook-api.md +14 -8
- package/templates/partials/agents-playbook-fullstack.md +16 -10
|
@@ -264,6 +264,7 @@ Success is a 303 (PRG); failure re-renders the page at 422 with the result on `a
|
|
|
264
264
|
|
|
265
265
|
## Testing Defaults
|
|
266
266
|
|
|
267
|
+
- `npm run ci` before every push: it runs the `webjs.ci` step list in `package.json` (correctness, project health, types, a dependency audit, then the server, browser, and e2e test layers) with a result line per step, and CI runs the same list, so a green local run predicts the pipeline. `npm run ci -- --only Tests` runs one layer while iterating. See `references/testing.md` and `references/built-ins.md`.
|
|
267
268
|
- Prefer server/handler tests first: drive the app with `handle()` from `@webjsdev/server/testing` and assert on the `Response`.
|
|
268
269
|
- Add a browser test (`npm run test:browser`) for anything touching hydration, the client router, slots, or custom-element upgrade. A unit test is necessary but NOT sufficient for a browser-facing change.
|
|
269
270
|
- Render the app and LOOK for any UI change: `npm run check` and `npm run typecheck` pass even when a layout collapses. Static tools give no signal for a visual defect.
|
|
@@ -113,6 +113,17 @@ export const POST = handlers.POST;
|
|
|
113
113
|
<form method="POST" action="/api/auth/signout"><button>Log out</button></form>
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
+
**OAuth sign-in returns the user where they started, with the same one form.** POST to `/api/auth/signin/github` (or `google`) with a hidden `redirectTo`, or link to `GET /api/auth/signin/github?redirectTo=/dashboard/x`; `signIn('github', undefined, { redirectTo })` does the same from an action. The target rides through the provider round trip in a short-lived signed cookie and the callback lands on it, so no wrapper around the auth route is needed:
|
|
117
|
+
|
|
118
|
+
```html
|
|
119
|
+
<form method="POST" action="/api/auth/signin/github">
|
|
120
|
+
<input type="hidden" name="redirectTo" value="/dashboard/x">
|
|
121
|
+
<button>Sign in with GitHub</button>
|
|
122
|
+
</form>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
A `redirectTo` that arrives from a request (a form field or a query param, for OAuth or credentials) must be a same-origin local path: one leading `/`, not followed by `/` or `\`. An absolute URL, a protocol-relative `//host`, or a backslash variant is dropped (not repaired) and the sign-in lands on `/`, so the field is never an open redirect. A denied sign-in still goes to `pages.error`.
|
|
126
|
+
|
|
116
127
|
For a programmatic sign-in (the auto-login-after-signup pattern), `signIn('credentials', creds, { redirectTo })` returns a `302` `Response` that a form-bound action can return directly (the framework honors a returned `Response` verbatim).
|
|
117
128
|
|
|
118
129
|
Sessions are JWT by default (stateless, scales horizontally). OAuth
|
|
@@ -22,6 +22,24 @@ Read this when wiring caching or rate limiting, storing uploads, hardening heade
|
|
|
22
22
|
| `REDIS_URL` | When set, sessions, rate limit, and cache use Redis instead of memory |
|
|
23
23
|
| `SESSION_SECRET` / `AUTH_SECRET` | Session and auth signing (see `auth-and-sessions.md`) |
|
|
24
24
|
| `PORT` | Listen port. Precedence `--port` flag, then `PORT` (real env or `.env`), then `8080` |
|
|
25
|
+
| `WEBJS_SOURCE_LOCATIONS` | `webjs dev` only. `1` stamps `data-webjs-src="<app-relative-file>:<line>"` on the elements of the app's `html` templates (see below). Ignored by `webjs start` |
|
|
26
|
+
| `WEBJS_EMBED_ORIGINS` | `webjs dev` only. Comma-separated parent origins (`https://builder.dev,http://localhost:8080`) allowed to frame the dev server and receive the embed bridge's messages (see below). Ignored by `webjs start` |
|
|
27
|
+
|
|
28
|
+
**Source locations for tooling (`WEBJS_SOURCE_LOCATIONS=1`, dev only).** A tool that hosts the app (an inspector, click-to-edit in an embedding builder) can map a clicked element back to the line that wrote it. With the variable set, `webjs dev` adds `data-webjs-src="components/todo-list.ts:12"` to every element opening tag written in an `html` template inside the app, both in the SSR markup and in client renders (one source transform applied to the served module and to the module the server imports, so the two agree and hydration is unaffected). Read it with `el.closest('[data-webjs-src]')`. Not annotated: `*.server.*` modules, `node_modules`, `css` / `svg` tagged templates, `html` / `head` / `body` / head-only and raw-text elements, and the descendants of `svg` / `math`. Lines are exact; nothing reaches production. The importmap `<script>` in `<head>` carries an unrelated `data-webjs-src` (the app-source deploy id), so match the `file:line` value shape when querying the whole document.
|
|
29
|
+
|
|
30
|
+
**Embed bridge for iframe previews (`WEBJS_EMBED_ORIGINS`, dev only).** A tool that previews the app inside an iframe (an app builder, a docs playground) sets `WEBJS_EMBED_ORIGINS` to its own origin(s). `webjs dev` then (1) drops `X-Frame-Options` and adds those origins to a CSP `frame-ancestors`, so the frame loads without the app stripping headers in `webjs.headers`, and (2) inlines a small nonce-signed script into every document that, when framed by a listed origin, posts to `window.parent` (with that exact target origin, never `*`):
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
{ source: 'webjs-embed', type: 'ready', path, title } // document parsed
|
|
34
|
+
{ source: 'webjs-embed', type: 'navigate', path, title } // every client-router navigation + popstate
|
|
35
|
+
{ source: 'webjs-embed', type: 'console', level: 'error' | 'warn', message, dropped? } // max 20/s
|
|
36
|
+
{ source: 'webjs-embed', type: 'error', message, stack, file, line, column } // window error + unhandledrejection
|
|
37
|
+
{ source: 'webjs-embed', type: 'network', method, url, status, error? } // fetch/XHR status >= 500, or status 0 on failure
|
|
38
|
+
{ source: 'webjs-embed', type: 'server-error', kind, message, file, line, path } // the dev error overlay went up
|
|
39
|
+
{ source: 'webjs-embed', type: 'select', src, tag, text, rect: { x, y, width, height } } // a click in inspect mode
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
and accepts, only from `window.parent` on a listed origin: `{ source: 'webjs-embed-host', type: 'navigate', path }` (a local path; a soft navigation when the client router is on), `{ source: 'webjs-embed-host', type: 'reload' }`, and `{ source: 'webjs-embed-host', type: 'inspect', enabled: true | false }` (hover highlight plus click capture that posts `select`; `src` is the nearest `data-webjs-src`, so pair it with `WEBJS_SOURCE_LOCATIONS=1` for click-to-edit). Paths are app paths (`/api/x`, `/components/x.ts`). Unset adds zero bytes; `webjs start` never injects it nor relaxes a header.
|
|
25
43
|
|
|
26
44
|
Defaults are single-instance memory stores. To scale horizontally, switch the store once at startup: `setStore(redisStore({ url: process.env.REDIS_URL }))`.
|
|
27
45
|
|
|
@@ -216,6 +234,48 @@ An over-limit body responds `413` without buffering the whole payload.
|
|
|
216
234
|
|
|
217
235
|
`before` runs to completion first (a non-zero exit aborts the boot). `parallel` (dev only) runs long-lived watchers alongside the server and tears them down on exit. `watch` (dev only) adds extra live-reload directories outside the app tree.
|
|
218
236
|
|
|
237
|
+
### Local CI (`webjs.ci`)
|
|
238
|
+
|
|
239
|
+
`webjs ci` runs the step list the block declares, the Rails 8.1 `bin/ci` posture: your machine is the first CI runner, and a cloud pipeline runs the SAME list by calling `npm run ci`, so the two cannot drift. Each step prints a heading, then `✅ <title> passed in 2.11s` or `❌ <title> failed in 0.01s`; the run ends with every failure listed and one total line, and exits 1 on any failure.
|
|
240
|
+
|
|
241
|
+
```jsonc
|
|
242
|
+
{ "webjs": { "ci": { "steps": [
|
|
243
|
+
{ "title": "Setup", "run": "webjs db migrate" },
|
|
244
|
+
{ "title": "Checks", "parallel": 2, "steps": [ // two at a time
|
|
245
|
+
"webjs check", // a string is a command titled by itself
|
|
246
|
+
{ "title": "Types", "run": "webjs typecheck" },
|
|
247
|
+
{ "title": "Tests", "steps": [ // a nested group takes ONE slot, runs in order
|
|
248
|
+
{ "title": "Tests: server", "run": "webjs test --server" },
|
|
249
|
+
{ "title": "Tests: e2e", "run": "webjs test --server", "env": { "WEBJS_E2E": "1" } }
|
|
250
|
+
] }
|
|
251
|
+
] }
|
|
252
|
+
] } } }
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
A step is a string, a `{ title, run, env? }` command, or a `{ title, steps, parallel? }` group. `parallel` is a slot count (default 1); a parallel group captures each step's output and replays it whole when the step finishes, so two steps never interleave, and a group nested inside it takes one slot and runs sequentially (it cannot declare `parallel`, which the reader reports rather than honours). `env` is per-step, so the e2e opt-in does not depend on a shell prefix. Every child runs with `CI=true`, `node_modules/.bin` on PATH, and `.env` loaded first (a real env var wins), so a `webjs db migrate` step sees `DATABASE_URL`.
|
|
256
|
+
|
|
257
|
+
Flags: `-f` / `--fail-fast` stops after the first failure (the default runs everything and lists every failure), `--only <title>` runs one step or group by title (repeatable, an unknown title is an error rather than an empty green run), `--json` emits one document on stdout (`{ ok, seconds, steps: [{ title, run, group, ok, code, seconds, output? }] }`, failed steps carrying their captured output, the human report on stderr) for an agent loop, and `--signoff` runs `gh signoff` after a green run. To hold a merge until a LOCAL run is green, install `basecamp/gh-signoff`, run `gh signoff install` once (a branch-protection rule requiring the `signoff` status), and run `npm run ci -- --signoff`; a red run posts nothing.
|
|
258
|
+
|
|
259
|
+
Under GitHub Actions each step is a `::group::` in the log, a failed step is an `::error::` annotation, and a step table is appended to the job summary, so a single job running the whole list still names the layer that broke. The scaffold's workflow is exactly that one job; `webjs create --skip-ci` omits it and the local list always ships. A malformed block (a group with no title, a nested `parallel`, an unknown key) refuses to run and names every problem by JSON path, because a silently dropped step is a check that never ran. Nothing declared is exit 1 too, naming any workspace member that declares one, since "ran zero steps" would read as green.
|
|
260
|
+
|
|
261
|
+
### Bring your own ORM (`webjs.db`)
|
|
262
|
+
|
|
263
|
+
Drizzle is the scaffold DEFAULT, not lock-in. The runtime never imports it, `db/connection.server.ts` is the app's own file, and `webjs db` is adapter-driven: a `db` block maps each verb to the shell command `webjs db <verb>` runs instead of the drizzle-kit default (node_modules/.bin on PATH like a `before` step, extra CLI args appended).
|
|
264
|
+
|
|
265
|
+
```jsonc
|
|
266
|
+
{ "webjs": {
|
|
267
|
+
"db": {
|
|
268
|
+
"generate": "prisma migrate dev --create-only",
|
|
269
|
+
"migrate": "prisma migrate deploy",
|
|
270
|
+
"push": "prisma db push",
|
|
271
|
+
"studio": "prisma studio",
|
|
272
|
+
"reset": "prisma migrate reset --force"
|
|
273
|
+
}
|
|
274
|
+
} }
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Any key is a verb (`reset` above adds `webjs db reset`). A verb the block does not name keeps its default (drizzle-kit for `generate` / `migrate` / `push` / `studio`, `db/seed.server.ts` for `seed`), so an app with no block is unchanged and the scaffold emits none. The payoff is that `webjs db migrate` stays one spelling across ORMs, so the scaffolded `dev.before` / `start.before`, the Dockerfile, CI, and the deploy docs all keep working after a swap. Write the bare binary (`prisma migrate deploy`), not `npx prisma ...`, since a pure Bun image has no `npx`. The swap itself is the app's own files: replace `db/connection.server.ts` with the new client, drop `drizzle.config.ts` / `db/columns.server.ts`, and keep server-only imports behind `.server.ts` as before.
|
|
278
|
+
|
|
219
279
|
### Doctor severity gate
|
|
220
280
|
|
|
221
281
|
`webjs doctor` reports project health, and by default only a broken toolchain fails the exit. `--strict` makes EVERY warning fatal, which is unusable in CI, because four checks are environment-shaped: `GIT_HOOK` wants a local pre-commit hook a runner has no reason to have, `ENV_DRIFT` compares against a `.env` CI does not carry, `VENDOR_PIN` fetches the network, and `FRAMEWORK_RESOLVE` plus `FRAMEWORK_LINKS` depend on the environment. So per-check severity is CONFIG, keyed by the stable code every result carries.
|
|
@@ -233,6 +293,25 @@ Three levels, the same scale ESLint uses: `error` fails the exit, `warn` reports
|
|
|
233
293
|
|
|
234
294
|
Two guarantees worth knowing. A result that could not check (a network or toolchain outage) is capped at `warn` and can never be escalated, so a jspm or npm outage cannot red your CI. And a malformed gate exits 1 naming the offender rather than being ignored, so a typo cannot silently un-gate the build. That covers an unknown code, a bad severity, a wrong shape (a non-object `doctor` or `gate`), and a misspelled sibling of `gate` such as `gates`, since every one of those would otherwise leave the build un-gated while the `package.json` looks gated. Under `--json` the offenders come back as a `configErrors` array alongside an empty `results`, each entry a `{ kind }` of `malformed` / `unknown-key` / `unknown-code` / `bad-severity`. Wire it up with one workflow step, `npm run doctor`, and change what is fatal in `package.json` rather than in the workflow.
|
|
235
295
|
|
|
296
|
+
### Dependency audit allowlist
|
|
297
|
+
|
|
298
|
+
`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.
|
|
299
|
+
|
|
300
|
+
```jsonc
|
|
301
|
+
{ "webjs": {
|
|
302
|
+
"audit": {
|
|
303
|
+
"level": "high",
|
|
304
|
+
"ignore": [
|
|
305
|
+
{ "id": "GHSA-vfj7-8cjw-p6xm", "reason": "braces has no patched release; reached only through dev tooling" }
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
} }
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The allowlist is the ONE place an accepted advisory lives, each with its reason. Accept only an advisory with no patched release that the app's users cannot reach; upgrade anything that has a fix. Never "fix" a red audit with `npm audit fix --force`, which proposes breaking majors that often keep the same vulnerable chain. A malformed block (unknown key, bad level, an entry without an id or a reason) exits 1, and an id the audit stops reporting prints as stale, so remove it then.
|
|
312
|
+
|
|
313
|
+
Overrides apply only at a WORKSPACE ROOT. The scaffold's `overrides` block (the `puppeteer-core` and `basic-ftp` security floors) is ignored once the app is a member of an npm or bun workspace, so move it into the root `package.json`; `webjs doctor` warns with `WORKSPACE_OVERRIDES` until you do.
|
|
314
|
+
|
|
236
315
|
## Observability
|
|
237
316
|
|
|
238
317
|
Wired at the single response funnel, covering pages, routes, actions, and assets uniformly.
|
|
@@ -115,7 +115,7 @@ So near an island the fragment is the smaller and more predictable choice, not a
|
|
|
115
115
|
|
|
116
116
|
### A design system for repeated PRIMITIVES: class helpers built on `@webjsdev/ui`
|
|
117
117
|
|
|
118
|
-
An `html`-fragment helper is right for a repeated CHUNK of markup (the rubric above). For a repeated UI PRIMITIVE (button, input, card, badge) that needs variants and sizes, use a class helper instead: a function that returns a Tailwind class STRING you spread onto a native element. That is exactly what `@webjsdev/ui` ships (`buttonClass({ variant, size })`, `cardClass()`, `inputClass()`, `badgeClass({ variant })`), and it is what the scaffold gallery uses in `components/ui/`. To style a ONE-OFF that a variant does not cover (a circular icon button, a pill), compose the helper and override the bespoke bits with `cn()`: `cn(buttonClass({ variant: 'secondary', size: 'none' }), 'w-9 h-9 rounded-full')`. `cn` resolves Tailwind conflicts so a later class wins, including a shorthand over the axis it subsumes (`p-0` beats an earlier `px-4 py-2`), so an override just works. Conflicts are keyed on the CSS PROPERTY wherever `cn` can tell the properties apart, rather than on the shared class prefix, so the common prefix collisions do NOT evict: `cn('border-2', 'border-primary')` keeps both (a width and a colour), `cn('flex', 'flex-1')` keeps both (a `display` and a `flex-grow`, the shape an element that is both a flex container and a flex child needs), `cn('shadow-lg', 'shadow-red-500')` keeps both (a box-shadow and its colour), `cn('bg-clip-text', 'bg-primary')` keeps both (a clip and a colour, so the gradient-text idiom survives a later background), and an arbitrary value carrying a type hint is read as the property the hint names (`cn('shadow-lg', 'shadow-[color:red]')` keeps both). It is a small hand-rolled merger, not `tailwind-merge`, so it is still coarse in two ways. A prefix outside the families it knows is not grouped at all, so both classes are emitted and the winner is left to compiled stylesheet order (`inset-shadow-sm` against `inset-shadow-red-500`, `ring-2` against `ring-red-500`). And where one prefix carries two properties it reads the value against Tailwind's DEFAULT scales, so a `@theme`-extended name it cannot know about can still be misread and evict the wrong class: a custom `--shadow-card` makes `shadow-card` a box-shadow, but `cn` sees an unfamiliar name under a prefix whose bare names are usually colours and treats it as one. When an override has to win and you are unsure, pass the one class rather than layering, or install `clsx` + `tailwind-merge` and replace the helper (its header comment shows the swap). For an icon button prefer `size: 'none'` (it states "I supply my own box" by dropping the helper's padding + radius) over layering a `p-0` on top of the default size.
|
|
118
|
+
An `html`-fragment helper is right for a repeated CHUNK of markup (the rubric above). For a repeated UI PRIMITIVE (button, input, card, badge) that needs variants and sizes, use a class helper instead: a function that returns a Tailwind class STRING you spread onto a native element. That is exactly what `@webjsdev/ui` ships (`buttonClass({ variant, size })`, `cardClass()`, `inputClass()`, `badgeClass({ variant })`), and it is what the scaffold gallery uses in `components/ui/`. To style a ONE-OFF that a variant does not cover (a circular icon button, a pill), compose the helper and override the bespoke bits with `cn()`: `cn(buttonClass({ variant: 'secondary', size: 'none' }), 'w-9 h-9 rounded-full')`. `cn` resolves Tailwind conflicts so a later class wins, including a shorthand over the axis it subsumes (`p-0` beats an earlier `px-4 py-2`), so an override just works. Conflicts are keyed on the CSS PROPERTY wherever `cn` can tell the properties apart, rather than on the shared class prefix, so the common prefix collisions do NOT evict: `cn('border-2', 'border-primary')` keeps both (a width and a colour), `cn('flex', 'flex-1')` keeps both (a `display` and a `flex-grow`, the shape an element that is both a flex container and a flex child needs), `cn('shadow-lg', 'shadow-red-500')` keeps both (a box-shadow and its colour), `cn('bg-clip-text', 'bg-primary')` keeps both (a clip and a colour, so the gradient-text idiom survives a later background), and an arbitrary value carrying a type hint is read as the property the hint names (`cn('shadow-lg', 'shadow-[color:red]')` keeps both). It is a small hand-rolled merger, not `tailwind-merge`, so it is still coarse in two ways. A prefix outside the families it knows is not grouped at all, so both classes are emitted and the winner is left to compiled stylesheet order (`inset-shadow-sm` against `inset-shadow-red-500`, `ring-2` against `ring-red-500`). And where one prefix carries two properties it reads the value against Tailwind's DEFAULT scales, so a `@theme`-extended name it cannot know about can still be misread and evict the wrong class: a custom `--shadow-card` makes `shadow-card` a box-shadow, but `cn` sees an unfamiliar name under a prefix whose bare names are usually colours and treats it as one. When an override has to win and you are unsure, pass the one class rather than layering, or install `clsx` + `tailwind-merge` and replace the helper (its header comment shows the swap). For an icon button prefer `size: 'none'` (it states "I supply my own box" by dropping the helper's padding + radius) over layering a `p-0` on top of the default size. Under the opt-in `webjsui lint` (configured by a `lint` block in `components.json`, see `references/ui-kit.md`), `w-9 h-9` is layout and `rounded-full` is shape in the shadcn category taxonomy, so an app running it sets `no-restyle` to `allow: ["layout", "rounded"]` for this idiom to pass: the plain radius group is granted by name without opening the whole `shape` category, and `border-2` beside a helper still fires.
|
|
119
119
|
|
|
120
120
|
```ts
|
|
121
121
|
// components/ui/button.ts (npx webjsdev ui add button, themed to your app)
|
|
@@ -63,6 +63,8 @@ WEBJS_E2E=1 npm run test # adds the e2e layer
|
|
|
63
63
|
|
|
64
64
|
`npm run test` dispatches on the runtime (`node --test` on Node, `bun test` on Bun). The scaffold's `web-test-runner.config.js` globs `test/**/browser/**/*.test.js` and is already wired, so you do not set it up.
|
|
65
65
|
|
|
66
|
+
An app with no browser tests yet (for example right after `npm run gallery:clear`) is not a failure: `webjs test --browser` sees that no file matches the config's `files` globs, prints `no browser tests yet`, and exits 0, the same way the server layer passes with zero files. So `npm run ci` stays green until you write the first browser test, and from then on the browser layer runs as normal.
|
|
67
|
+
|
|
66
68
|
A scaffolded app has one root `test/` directory shaped the same way (feature first, kind second):
|
|
67
69
|
|
|
68
70
|
```
|
|
@@ -189,6 +191,17 @@ WEBJS_ELIDE=0 npm run test:e2e
|
|
|
189
191
|
|
|
190
192
|
A test that passes under one and fails under the other is a wrong verdict, and `webjs elision` tells you which module and on what evidence. If the component's interactivity is genuinely invisible to static analysis, the fix is `static interactive = true` on it; see `components.md` for what that override does and does not rescue.
|
|
191
193
|
|
|
194
|
+
## One command for every layer (`webjs ci`)
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
npm run ci # the webjs.ci list: check, doctor, typecheck, audit, then every test layer
|
|
198
|
+
npm run ci -- --only Tests # one step or group by title
|
|
199
|
+
npm run ci -- --fail-fast # stop at the first failure
|
|
200
|
+
npm run ci -- --json # one JSON document for an agent loop
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The scaffold declares its gate once, in `package.json` under `webjs.ci`, and `npm run ci` runs it with a timed result line per step (the Rails `bin/ci` model). The generated GitHub workflow runs the same list, so a green local run predicts CI. Run it before every push; the pre-commit hook deliberately runs none of it so a commit stays fast. Browser and e2e steps need a Chromium on the machine (`npx playwright install chromium`, plus `puppeteer-core` for the e2e layer), which the workflow installs for itself. The list, the flags, the `--signoff` merge gate, and the JSON shape are in `references/built-ins.md` under "Local CI".
|
|
204
|
+
|
|
192
205
|
## Type-checking your tests (`webjs typecheck`)
|
|
193
206
|
|
|
194
207
|
Your tests are inside the tsconfig `include`, so `npm run typecheck` reads them (#1299). Treat a type error in a test as a failed gate, not a review catch: the checker sees a wrong argument shape or an unannotated parameter in a test the same way it sees one in `app/`.
|
|
@@ -52,6 +52,31 @@ So the loop is: `add` the component, then query `ui <name>` (MCP) or
|
|
|
52
52
|
that ships inside the installed `@webjsdev/ui`, with no network. This pins you
|
|
53
53
|
to the installed version; run `npx webjsdev ui diff` to see where your local copies
|
|
54
54
|
drift from the upstream (that command alone compares against the live registry).
|
|
55
|
+
- `npx webjsdev ui lint` is an OPT-IN design-system linter over the app's own
|
|
56
|
+
source. It reads the Tailwind classes in `html` templates, `cn()` calls and
|
|
57
|
+
`class=${...}` holes and reports, at the line, a raw palette colour where the
|
|
58
|
+
theme declares a role token (`no-raw-colors`, with a message naming only the
|
|
59
|
+
`--color-*` tokens the configured `tailwind.css` actually declares), an
|
|
60
|
+
arbitrary value such as `p-[13px]` (`no-arbitrary-values`; an arbitrary
|
|
61
|
+
VARIANT like `[&_svg]:size-4` never fires), and a class composed over a kit
|
|
62
|
+
helper (`no-restyle`, naming the helper's real variants and sizes read from
|
|
63
|
+
the app's copied `components/ui/*.ts`). It is off until `components.json`
|
|
64
|
+
carries a `lint` block, and with no block it reports nothing and exits 0.
|
|
65
|
+
`components/ui/**` is skipped by default (a copied primitive owns structural
|
|
66
|
+
values no variant expresses), and `allow` uses shadcn's category taxonomy
|
|
67
|
+
(`layout`, `color`, `typography`, `spacing`, `shape`, `effects`, `motion`) or
|
|
68
|
+
a class-group id such as `rounded`. `--json` emits `{ violations, summary }`
|
|
69
|
+
for an agent loop; `--max-warnings <n>` pins a count.
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
"lint": {
|
|
73
|
+
"rules": {
|
|
74
|
+
"no-raw-colors": "warn",
|
|
75
|
+
"no-arbitrary-values": { "severity": "warn", "allow": ["layout"] },
|
|
76
|
+
"no-restyle": { "severity": "error", "allow": ["layout", "rounded"] }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
55
80
|
|
|
56
81
|
## Inventory (run `npx webjsdev ui list` or the MCP `ui` tool for the authoritative, current set)
|
|
57
82
|
|
package/templates/.dockerignore
CHANGED
|
@@ -18,7 +18,7 @@ build
|
|
|
18
18
|
out
|
|
19
19
|
.cache
|
|
20
20
|
|
|
21
|
-
# Local env files. The container gets its env from compose
|
|
21
|
+
# Local env files. The container gets its env from compose or the host, not a
|
|
22
22
|
# committed file. Keep the example for reference.
|
|
23
23
|
.env
|
|
24
24
|
!.env.example
|
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
## Test plan
|
|
6
6
|
|
|
7
|
-
- [ ]
|
|
8
|
-
- [ ]
|
|
9
|
-
- [ ]
|
|
10
|
-
- [ ] `webjs doctor` passes (project health; it fails on whatever `webjs.doctor.gate` marks `error`, plus the hard `NODE_VERSION` / `TSCONFIG_ERASABLE` checks)
|
|
7
|
+
- [ ] `webjs ci` passes locally (the `webjs.ci` step list in package.json: `webjs check`, `webjs doctor`, `webjs typecheck`, the dependency audit, and the server / browser / e2e test layers; CI runs the same list)
|
|
8
|
+
- [ ] Unit tests added/updated
|
|
9
|
+
- [ ] E2E tests added/updated for user-facing changes (`WEBJS_E2E=1 webjs test`)
|
|
11
10
|
|
|
12
11
|
## Definition of done
|
|
13
12
|
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
name: CI
|
|
2
2
|
|
|
3
|
-
# The
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
3
|
+
# The cloud half of CI for {{APP_NAME}}. The step list itself lives in
|
|
4
|
+
# package.json under "webjs": { "ci": { "steps": [...] } }, and `npm run ci`
|
|
5
|
+
# runs it: on a developer machine before a push, and here on every PR into
|
|
6
|
+
# main and every push to main, so the two can never drift. This job only
|
|
7
|
+
# prepares the runner (Node, dependencies, a browser, the database) and then
|
|
8
|
+
# runs that one command.
|
|
8
9
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
10
|
+
# One job on purpose. Each step is still a collapsible log group with its own
|
|
11
|
+
# result line, a failing step is annotated, and the run's step table lands in
|
|
12
|
+
# the job summary, so a failure names the layer that broke. Mark this job as a
|
|
13
|
+
# required status check in the branch-protection rule for main. A team that
|
|
14
|
+
# wants one required check PER LAYER can add a matrix job that runs
|
|
15
|
+
# `npx webjs ci --only "<group title>"` per entry.
|
|
16
|
+
#
|
|
17
|
+
# The local pre-commit hook deliberately runs none of this, so a commit stays
|
|
18
|
+
# fast; `npm run ci` before pushing is the local gate. To hold a merge until a
|
|
19
|
+
# LOCAL run is green, `npm run ci -- --signoff` posts a green commit status
|
|
20
|
+
# through basecamp/gh-signoff, which branch protection can require (`gh signoff
|
|
21
|
+
# install`), the Rails posture. Free on public repos (ubuntu-latest has
|
|
22
|
+
# unlimited Actions minutes).
|
|
13
23
|
|
|
14
24
|
on:
|
|
15
25
|
pull_request:
|
|
@@ -17,40 +27,21 @@ on:
|
|
|
17
27
|
push:
|
|
18
28
|
branches: [main]
|
|
19
29
|
|
|
30
|
+
permissions:
|
|
31
|
+
contents: read
|
|
32
|
+
|
|
20
33
|
# A newer push to the same branch cancels the older in-flight run.
|
|
21
34
|
concurrency:
|
|
22
35
|
group: ci-${{ github.ref }}
|
|
23
36
|
cancel-in-progress: true
|
|
24
37
|
|
|
25
38
|
jobs:
|
|
26
|
-
|
|
27
|
-
name:
|
|
28
|
-
runs-on: ubuntu-latest
|
|
29
|
-
steps:
|
|
30
|
-
- uses: actions/checkout@v6
|
|
31
|
-
- uses: actions/setup-node@v6
|
|
32
|
-
with:
|
|
33
|
-
node-version: '24'
|
|
34
|
-
cache: npm
|
|
35
|
-
- run: npm ci
|
|
36
|
-
- run: npm run check
|
|
37
|
-
# Project health, on top of the correctness checks. WHICH findings are
|
|
38
|
-
# fatal is your call, declared in package.json under
|
|
39
|
-
# "webjs": { "doctor": { "gate": { "<CODE>": "off" | "warn" | "error" } } },
|
|
40
|
-
# so this step and a local `npm run doctor` always agree. The scaffold
|
|
41
|
-
# starts with UNMARKED_ASSET_LINKS at error (an un-versioned /public url
|
|
42
|
-
# is a real deploy-staleness bug). Two checks fail with no gate entry at
|
|
43
|
-
# all, NODE_VERSION and TSCONFIG_ERASABLE, because either would 500 the
|
|
44
|
-
# app at runtime; everything else stays a warn and cannot fail this job.
|
|
45
|
-
# Widen or narrow the gate in package.json, not
|
|
46
|
-
# here. Deliberately not --strict: the git-hook, env-drift, vendor-pin,
|
|
47
|
-
# and framework-resolve checks are environment-shaped and would fail a
|
|
48
|
-
# perfectly healthy runner.
|
|
49
|
-
- run: npm run doctor
|
|
50
|
-
|
|
51
|
-
unit:
|
|
52
|
-
name: Unit + integration (node --test)
|
|
39
|
+
ci:
|
|
40
|
+
name: CI (npm run ci)
|
|
53
41
|
runs-on: ubuntu-latest
|
|
42
|
+
timeout-minutes: 20
|
|
43
|
+
env:
|
|
44
|
+
DATABASE_URL: file:./ci.db
|
|
54
45
|
steps:
|
|
55
46
|
- uses: actions/checkout@v6
|
|
56
47
|
- uses: actions/setup-node@v6
|
|
@@ -58,58 +49,17 @@ jobs:
|
|
|
58
49
|
node-version: '24'
|
|
59
50
|
cache: npm
|
|
60
51
|
- run: npm ci
|
|
61
|
-
- name:
|
|
62
|
-
run: npm run db:generate && npm run db:migrate
|
|
63
|
-
env:
|
|
64
|
-
DATABASE_URL: file:./ci.db
|
|
65
|
-
# --server keeps this job to node:test (the browser layer is its own
|
|
66
|
-
# job below). Without WEBJS_E2E the e2e folders are skipped too.
|
|
67
|
-
- run: npm run test:server
|
|
68
|
-
env:
|
|
69
|
-
DATABASE_URL: file:./ci.db
|
|
70
|
-
|
|
71
|
-
browser:
|
|
72
|
-
name: Browser (web-test-runner / Playwright)
|
|
73
|
-
runs-on: ubuntu-latest
|
|
74
|
-
steps:
|
|
75
|
-
- uses: actions/checkout@v6
|
|
76
|
-
- uses: actions/setup-node@v6
|
|
77
|
-
with:
|
|
78
|
-
node-version: '24'
|
|
79
|
-
cache: npm
|
|
80
|
-
- run: npm ci
|
|
81
|
-
- name: Install Playwright Chromium
|
|
52
|
+
- name: Install Playwright Chromium (browser + e2e layers)
|
|
82
53
|
run: npx playwright install --with-deps chromium
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
- uses: actions/checkout@v6
|
|
90
|
-
- uses: actions/setup-node@v6
|
|
91
|
-
with:
|
|
92
|
-
node-version: '24'
|
|
93
|
-
cache: npm
|
|
94
|
-
- run: npm ci
|
|
95
|
-
- name: Set up the database (generate + apply migrations)
|
|
96
|
-
run: npm run db:generate && npm run db:migrate
|
|
97
|
-
env:
|
|
98
|
-
DATABASE_URL: file:./ci.db
|
|
99
|
-
# The scaffold's e2e test (test/hello/e2e/) drives a real browser
|
|
100
|
-
# via puppeteer-core, which is not a default dependency (the test
|
|
101
|
-
# skips when it is absent). Install it and Chromium so the e2e
|
|
102
|
-
# layer actually runs in CI rather than skipping silently.
|
|
103
|
-
- name: Install puppeteer-core + Chromium
|
|
104
|
-
run: |
|
|
105
|
-
npm install --no-save puppeteer-core
|
|
106
|
-
npx playwright install --with-deps chromium
|
|
54
|
+
# The scaffold's e2e test (test/hello/e2e/) drives a real browser via
|
|
55
|
+
# puppeteer-core, which is not a default dependency (the test skips when
|
|
56
|
+
# it is absent). Install it so the e2e layer runs here rather than
|
|
57
|
+
# skipping silently.
|
|
58
|
+
- name: Install puppeteer-core
|
|
59
|
+
run: npm install --no-save puppeteer-core
|
|
107
60
|
- name: Resolve the Chromium binary path
|
|
108
61
|
run: echo "CHROMIUM_PATH=$(node -e "console.log(require('playwright-core').chromium.executablePath())")" >> "$GITHUB_ENV"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
WEBJS_E2E: '1'
|
|
114
|
-
DATABASE_URL: file:./ci.db
|
|
115
|
-
run: npm run test:server
|
|
62
|
+
- name: Set up the database (generate + apply migrations)
|
|
63
|
+
run: npm run db:generate && npm run db:migrate
|
|
64
|
+
# Everything above prepares the runner. This is the whole gate.
|
|
65
|
+
- run: npm run ci
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
#
|
|
8
8
|
# To bypass in emergencies: git commit --no-verify
|
|
9
9
|
#
|
|
10
|
-
# Tests and convention checks run
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# on every push and pull
|
|
10
|
+
# Tests and convention checks do not run here, so a commit stays fast. The
|
|
11
|
+
# step list lives in package.json under "webjs": { "ci" }. Run it locally
|
|
12
|
+
# with `npm run ci` before pushing (the local gate), and the CI workflow
|
|
13
|
+
# (.github/workflows/ci.yml) runs the same list on every push and pull
|
|
14
|
+
# request, where a local --no-verify cannot skip it.
|
|
14
15
|
#
|
|
15
16
|
# Running more than one AI agent on this repo at once? Give each task its own
|
|
16
17
|
# git worktree, not a shared checkout. Two agents in one working directory
|
package/templates/Dockerfile
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Production image for the {{APP_NAME}} webjs app.
|
|
2
2
|
#
|
|
3
|
-
# Works with a plain `docker build` / `docker compose up`, and
|
|
4
|
-
#
|
|
3
|
+
# Works with a plain `docker build` / `docker compose up`, and with any host
|
|
4
|
+
# that builds from a Dockerfile.
|
|
5
5
|
#
|
|
6
6
|
# webjs serves .ts directly by stripping types at the runtime layer, so there is
|
|
7
7
|
# NO JavaScript build step (webjs is buildless end to end; there is no bundler or
|
|
@@ -41,7 +41,7 @@ COPY . .
|
|
|
41
41
|
# step runs `webjs db migrate`). See the CMD note below.
|
|
42
42
|
|
|
43
43
|
ENV NODE_ENV=production
|
|
44
|
-
# webjs start reads $PORT (default 8080). compose
|
|
44
|
+
# webjs start reads $PORT (default 8080). compose and most hosts set it.
|
|
45
45
|
ENV PORT=8080
|
|
46
46
|
EXPOSE 8080
|
|
47
47
|
|
package/templates/compose.yaml
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
#
|
|
3
3
|
# docker compose up --build → http://localhost:8080
|
|
4
4
|
#
|
|
5
|
-
# In production
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# In production your host provides DATABASE_URL + AUTH_SECRET. Locally this
|
|
6
|
+
# uses the scaffold's SQLite file on a named volume so data survives
|
|
7
|
+
# `compose down`.
|
|
8
8
|
services:
|
|
9
9
|
app:
|
|
10
10
|
build: .
|
|
@@ -44,15 +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
|
|
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:
|
|
48
50
|
|
|
49
|
-
- `
|
|
50
|
-
- `
|
|
51
|
-
`
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
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.
|
|
56
61
|
|
|
57
62
|
Then boot `npm run dev` and probe each endpoint for the expected status and JSON
|
|
58
63
|
shape.
|
|
@@ -66,6 +71,7 @@ npm run dev # dev server at http://localhost:8080
|
|
|
66
71
|
npm run start # production server
|
|
67
72
|
npm test # unit + browser tests
|
|
68
73
|
npm run typecheck
|
|
74
|
+
npm run ci # every gate, one command (the webjs.ci steps in package.json)
|
|
69
75
|
npm run check # correctness checks
|
|
70
76
|
npm run doctor # project health (severity per check: webjs.doctor.gate)
|
|
71
77
|
npm run db:generate && npm run db:migrate
|
|
@@ -95,16 +95,21 @@ accessor). Use the shorthand for primitives
|
|
|
95
95
|
|
|
96
96
|
### 7. Verify before you call it done
|
|
97
97
|
|
|
98
|
-
Run
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
`
|
|
105
|
-
|
|
106
|
-
- `
|
|
107
|
-
-
|
|
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
|
|
112
|
+
`npm run css:build` (compile Tailwind).
|
|
108
113
|
|
|
109
114
|
Then boot `npm run dev`, confirm every page route returns HTTP 200, and open
|
|
110
115
|
every route you changed in a real browser and play through its states: `check`
|
|
@@ -121,6 +126,7 @@ npm run start # production server
|
|
|
121
126
|
npm test # unit + browser tests
|
|
122
127
|
npm run typecheck
|
|
123
128
|
npm run css:build # compile Tailwind
|
|
129
|
+
npm run ci # every gate, one command (the webjs.ci steps in package.json)
|
|
124
130
|
npm run check # correctness checks
|
|
125
131
|
npm run doctor # project health (severity per check: webjs.doctor.gate)
|
|
126
132
|
npx webjsdev ui add <name> # copy a ui primitive into components/ui/
|