Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 7 additions & 13 deletions .agents/skills/webjs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@ Common bundles:
1. **Classify the change.** Route contract, data model, server mutation, auth, or only UI?
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.
3. **Put code in the narrowest owner.** Route-local first (`modules/<feature>/`), promote to `lib/` or `components/` only when reuse is real.
4. **Keep server-only code behind `.server.ts`.** The DB driver, secrets, and `node:*` never belong in a page, layout, or component.
4. **Keep server-only code behind `.server.ts`** (Core WebJs Rules 1 and 2).
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.
6. **Validate input at the boundary.** Declare `export const validate` on an action; the RPC and `route()` boundaries run it.
7. **Default mutations to optimistic UI** where the client can predict the result (`optimistic()` from `@webjsdev/core`).
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`.
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).
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. Carry a row type into a shipping component with `import type` (erased before the browser sees it). Nothing enforces this (both spellings are valid TypeScript, so `webjs check` and `tsc` pass either way), which is why it is written down. See `references/typescript.md`.
9. **Test the narrowest meaningful layer**, and look at any UI change in a real browser (see Testing Defaults).

## Project Layout

Expand Down Expand Up @@ -137,7 +137,7 @@ App-internal imports use the `#` root alias (`import { db } from '#db/connection
## Core WebJs Rules (invariants)

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).
2. `'use server'` exports are `async` functions returning serializer-safe values. Files without `'use server'` are server-only utilities.
2. `'use server'` exports are `async` functions returning serializer-safe values, callable from browser code as RPC. Files without `'use server'` are server-only utilities: reach them only from `'use server'` actions, `route.ts`, or middleware. Never add `'use server'` to a file only other server code imports (the DB connection, the schema).
3. Custom element tag names contain a hyphen. Pass the tag to `Class.register('tag-name')`.
4. Event (`@`), property (`.`), and boolean (`?`) holes in `html` are UNQUOTED: `@click=${fn}`, never `@click="${fn}"`.
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.
Expand All @@ -147,7 +147,7 @@ App-internal imports use the `#` root alias (`import { db } from '#db/connection
9. No backtick characters inside an `html\`...\`` body, even in comments (it closes the literal and 500s).
10. TypeScript must be erasable (`erasableSyntaxOnly: true`): no `enum`, no value `namespace`, no constructor parameter properties, no legacy decorators.
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).
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.
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 (the identity IS the button's name/value pair, so both halves are spoken for), a `.prop` spelling of any of those, `action=${fn}` off a `<form>`, a bound form with `method="get"` (a GET sends no body for the action to read), 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.

## Export Map

Expand Down Expand Up @@ -272,18 +272,12 @@ Success is a 303 (PRG); failure re-renders the page at 422 with the result on `a

## Common Mistakes To Avoid

- Treating a page or layout like a React component and expecting its markup to hydrate. It runs server-only; put interactivity in a component.
The invariants above are not repeated here; this list is the mistakes they do not already name.

- 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.
- 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.
- Using a `static properties` block or a class-field initializer for reactive props instead of the `WebComponent({ ... })` factory.
- Quoting an event / property / boolean hole (`@click="${fn}"`).
- Writing `fetch()` to call your own server instead of importing the action.
- 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.
- 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.
- 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.
- 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`).
- 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.
- 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.
- Throwing `redirect()` / `notFound()` inside a `route.ts` handler (uncaught 500). Return a `Response` there.
- 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()`.
- A browser global (`window`, `document`, `localStorage`) in the constructor or `render()`. It throws at SSR; do browser-only work in `connectedCallback`.
Expand Down
30 changes: 6 additions & 24 deletions packages/cli/templates/.agents/rules/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,12 @@ Read `AGENTS.md` first. Full hosted docs are at https://webjs.dev/docs.

## Grow the app in place (non-negotiable)

- **Study the shipped examples, then clear them and build.** The scaffold is a
starting point with a browsable showcase to learn the real idioms from, plus a
database wired up. A full-stack app ships a UI feature gallery (`app/features/`,
`app/examples/todo`); the api template ships a backend-features showcase
(`app/api/features/`), with logic in `modules/`. Building a real app: study the
parts that match your task (the skill teaches the same and SURVIVES the clear),
run `npm run gallery:clear` to shed the showcase (it keeps the agent skill and
the database wiring, and resets to a clean base), then regenerate the database
and grow the app in place under `app/`, `components/`, and `modules/<feature>/`.
`AGENTS.md` carries the full template-specific build playbook and the order to
follow.
- **Use the wired-up database (Drizzle), never JSON files.** For any data the app
stores, define a Drizzle table in `db/schema.server.ts`, then
`npm run db:generate` and `npm run db:migrate`. Never use a JSON file, a
module-scope array or Map, or localStorage as a database.
- **`app/` is routing-only.** Only routing files live in `app/` (page, layout,
route, middleware, metadata routes). Browser-safe helpers go in `lib/utils/`,
feature logic in `modules/`, server-only code behind `.server.ts`.
- **For a UI app, render and LOOK before calling it done.** Define design tokens
in `app/layout.ts` with a palette that fits the app
(`.agents/skills/webjs/references/styling.md` is the guide), then open every
route you changed in a real browser and play through its states.
`npm run check` and `npm run typecheck` pass even when a layout collapses, so
the browser is the real check.
Study the shipped showcase (a full-stack app's UI gallery under
`app/features/` and `app/examples/todo`, the api template's
`app/api/features/`), run `npm run gallery:clear`, then grow the app in place
under `app/`, `components/`, and `modules/<feature>/`. `AGENTS.md` carries the
build order, the data rule (the wired-up Drizzle database, never JSON files),
and the browser check for a UI app.

## Before starting ANY work

Expand Down
45 changes: 5 additions & 40 deletions packages/cli/templates/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,49 +34,14 @@ This is what separates a working app from a broken one.

## Type everything (all templates)

Full-stack type safety is what the `.server.ts` boundary buys you: a client
component importing a server action resolves to that action's real signature at
type-check time, with no build step and no code generation in between. So
DERIVE the type at every boundary instead of widening it:

- A database row: `export type Todo = typeof todos.$inferSelect` in
`db/schema.server.ts` (`$inferInsert` for a write), carried into a
browser-shipped component with `import type` (erased before it reaches the
browser, so it does not trip the server-import boundary).
- An action's input: a named `interface`. Its result: `ActionResult<T>`.
Narrow with `if (result.success && result.data)`.
- Routing files: `PageProps<'/blog/[slug]'>`, `LayoutProps`,
`RouteHandlerContext`, all from `@webjsdev/core`. Run `npx webjsdev types`
for the typed `Route` union and per-route `params`.
- A reactive property: `prop<Student>(Object)`, `prop<Tag[]>(Array)`.

Never reach for `any` or a loose `as any` cast, and do not reach for `unknown`
either just because it looks safer. `unknown` is right for a payload nothing
has vouched for yet, narrowed on the very next line (a `route.ts` `await
req.json()`, an action's `export const validate` or a validator it delegates
to, a `catch` binding), and for a parameter of YOUR OWN helper that forwards
into an `html` template hole (a hole renders a string, a number, a
`TemplateResult`, or an array of those, so `TemplateResult` alone is too
narrow). That second case is about a value you accept, never one the framework
already types. Everywhere else it is a missing type, not a safe one: `unknown`
that survives into a return type, a component prop, a layout's `children`, or
an action signature is the shape to fix.
Nothing enforces this (both are valid TypeScript, so `webjs check` and `tsc`
pass either way), which is exactly why it is written down. The full ladder,
with an end-to-end example, is in
Derive the type at every boundary from its source. Never reach for `any`, and
never `unknown` where a real type exists. The rule is step 8 of the skill's "Default
Workflow"; the full ladder, with an end-to-end example, is
`.agents/skills/webjs/references/typescript.md`.

Keep server-only code (database drivers, secrets, `node:*` builtins) in
`.server.ts` modules. There are exactly two kinds:

- A `.server.ts` file WITH `'use server';` as its first line is a server
action: WebJs exposes its exported async functions to browser code as RPC
calls, so browser modules may import it directly.
- A `.server.ts` file WITHOUT `'use server'` is a server-only utility:
importing it from a page, layout, or component CRASHES in the browser at
module load. Reach it only from `'use server'` actions, `route.ts` handlers,
or middleware. Never add `'use server'` to a file only other server code
imports (the DB connection, the schema).
`.server.ts` modules. The two kinds (with and without `'use server'`) are the
skill's "Core WebJs Rules" 1 and 2.

## Data (all templates)

Expand Down
11 changes: 3 additions & 8 deletions packages/cli/templates/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,9 @@ is complete, WITHOUT being asked. Do not save all the work for one commit at the
end. A finished implementation with zero commits is a mistake here, because git
history is the user's revert and cherry-pick safety net.

- After each completed unit whose tests pass, `git add` the related files and
`git commit` with an imperative subject under 72 chars, then push. If 5+ files
span more than one concern, you already waited too long.
- Never commit to `main`. Work on a feature branch (the
`.claude/hooks/guard-branch-context.sh` hook enforces this).
- No AI-attribution trailers (`Co-Authored-By`, `Generated by`).

See AGENTS.md "Git workflow" for the full contract. Two hooks back this up: the
The full git contract (branches, commit messages, attribution) is
`.agents/rules/workflow.md` "Git rules"; the
`.claude/hooks/guard-branch-context.sh` hook refuses a commit on `main`. Two hooks back this up: the
`.claude/hooks/nudge-uncommitted.sh` PostToolUse hook reminds you while
uncommitted changes pile up during work, and the
`.claude/hooks/commit-before-stop.sh` Stop hook stops you from ending a turn
Expand Down
42 changes: 12 additions & 30 deletions packages/cli/templates/CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,16 @@
The conventions for building a WebJs app live in the agent skill. **Read
`AGENTS.md` first, then `.agents/skills/webjs/SKILL.md`** (it routes to focused
references under `.agents/skills/webjs/references/`, loaded on demand). This file
is the short version.
only says where each rule lives, so no rule is stated twice.

## The essentials

- **`app/` is routing only.** Only routing files live there (page, layout, route,
middleware, metadata routes). Feature logic goes in `modules/<feature>/`
(`actions/`, `queries/`, `components/`, `utils/`); shared UI primitives go in
top-level `components/`; browser-safe helpers in `lib/utils/`.
- **Server-only code goes behind `.server.ts`.** Reach it from a page or component
through a `'use server'` action, never by importing a server-only utility
directly into browser-bound code.
- **Use the wired-up database (Drizzle).** Define real models in
`db/schema.server.ts`, then `npm run db:generate` and `npm run db:migrate`.
Never persist to a JSON file, an in-memory array or Map, or localStorage.
- **The scaffold ships a showcase to learn from.** A full-stack app ships a UI
feature gallery (`app/features/`, `app/examples/todo`); the api template ships
a backend-features showcase (`app/api/features/`), with logic in `modules/`.
When you build a real app, study the parts that match your task (the skill
teaches the same and survives the clear), run `npm run gallery:clear` to shed
the showcase, then grow the app in place. `AGENTS.md` has the full
template-specific playbook.
- **Derive types at every boundary.** Rows from `$inferSelect`, action inputs
from an `interface`, routing files from `PageProps` / `LayoutProps`. Never
`any`, and never `unknown` where a real type exists.
- **Progressive enhancement is the default.** Pages render as HTML, `<a>`
navigates, a `<form action=${importedAction}>` submits, all with JavaScript off; opt into
interactivity per behaviour inside a component.
- **Commit per logical unit** as soon as it is complete, and never push to `main`.

Everything else (the module architecture, the `ActionResult` envelope, styling,
testing, the client router, optimistic UI) is in the skill's references.
- **Build order** (study the showcase: a full-stack app's gallery under
`app/features/` and `app/examples/todo`, the api template's
`app/api/features/`; then `npm run gallery:clear`, data, UI, verify):
the playbook in `AGENTS.md`.
- **Data** (the wired-up Drizzle database, never a JSON file, an in-memory
array or Map, or localStorage): `AGENTS.md`, "Data".
- **Layout** (`app/` is routing only, logic in `modules/<feature>/`): the
skill's "Project Layout".
- **Server boundary, progressive enhancement, typing**: the skill's "What WebJs
Is", "Core WebJs Rules", and "Default Workflow".
- **Git, tests, and `npm run ci`**: `.agents/rules/workflow.md`.
Loading
Loading