@webjsdev/cli 0.10.40 → 0.10.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/webjs.js +4 -46
- package/lib/create.js +282 -479
- package/lib/doctor.js +1 -38
- package/package.json +5 -1
- package/templates/.agents/rules/workflow.md +61 -271
- package/templates/.agents/skills/webjs/SKILL.md +226 -0
- package/templates/.agents/skills/webjs/references/auth-and-sessions.md +220 -0
- package/templates/.agents/skills/webjs/references/built-ins.md +200 -0
- package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +204 -0
- package/templates/.agents/skills/webjs/references/components.md +167 -0
- package/templates/.agents/skills/webjs/references/data-and-actions.md +187 -0
- package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +170 -0
- package/templates/.agents/skills/webjs/references/optimistic-ui.md +128 -0
- package/templates/.agents/skills/webjs/references/routing-and-pages.md +158 -0
- package/templates/.agents/skills/webjs/references/runtime.md +80 -0
- package/templates/.agents/skills/webjs/references/service-worker.md +78 -0
- package/templates/.agents/skills/webjs/references/styling.md +123 -0
- package/templates/.agents/skills/webjs/references/testing.md +125 -0
- package/templates/.agents/skills/webjs/references/typescript.md +148 -0
- package/templates/.claude/hooks/check-server-imports.mjs +1 -1
- package/templates/.claude/hooks/require-tests-with-src.sh +1 -1
- package/templates/.claude/settings.json +0 -14
- package/templates/.cursorrules +21 -189
- package/templates/.github/copilot-instructions.md +7 -185
- package/templates/.github/pull_request_template.md +1 -1
- package/templates/AGENTS.md +59 -1494
- package/templates/CLAUDE.md +0 -1
- package/templates/CONVENTIONS.md +32 -1383
- package/templates/GEMINI.md +11 -0
- package/templates/gallery/app/apple-icon.ts +0 -1
- package/templates/gallery/app/examples/todo/page.ts +0 -1
- package/templates/gallery/app/features/async-render/page.ts +0 -1
- package/templates/gallery/app/features/boundaries/page.ts +0 -1
- package/templates/gallery/app/features/broadcast/page.ts +0 -1
- package/templates/gallery/app/features/caching/page.ts +0 -1
- package/templates/gallery/app/features/client-router/page.ts +0 -1
- package/templates/gallery/app/features/client-router/second/page.ts +0 -1
- package/templates/gallery/app/features/components/page.ts +0 -1
- package/templates/gallery/app/features/directives/page.ts +0 -1
- package/templates/gallery/app/features/env/page.ts +0 -1
- package/templates/gallery/app/features/file-storage/page.ts +0 -1
- package/templates/gallery/app/features/forms/page.ts +0 -1
- package/templates/gallery/app/features/metadata/page.ts +0 -1
- package/templates/gallery/app/features/optimistic-ui/page.ts +0 -1
- package/templates/gallery/app/features/rate-limit/page.ts +0 -1
- package/templates/gallery/app/features/route-handler/page.ts +0 -1
- package/templates/gallery/app/features/routing/page.ts +0 -1
- package/templates/gallery/app/features/server-actions/page.ts +0 -1
- package/templates/gallery/app/features/service-worker/page.ts +0 -1
- package/templates/gallery/app/features/sessions/page.ts +0 -1
- package/templates/gallery/app/features/websockets/page.ts +0 -1
- package/templates/gallery/app/global-error.ts +0 -1
- package/templates/gallery/app/global-not-found.ts +0 -1
- package/templates/gallery/app/icon.ts +0 -1
- package/templates/gallery/app/manifest.ts +0 -1
- package/templates/gallery/app/opengraph-image.ts +0 -1
- package/templates/gallery/app/robots.ts +0 -1
- package/templates/gallery/app/sitemap.ts +0 -1
- package/templates/gallery/app/twitter-image.ts +0 -1
- package/templates/public/favicon.svg +5 -0
- package/templates/public/sw.js +1 -1
- package/templates/scripts/clear-gallery.mjs +95 -0
- package/lib/clear-placeholders.js +0 -98
- package/lib/design-bar.js +0 -67
- package/templates/.claude/hooks/design-review-before-stop.sh +0 -36
- package/templates/.claude/hooks/route-skills.sh +0 -35
- package/templates/.claude/skills/webjs-design-review/SKILL.md +0 -84
- package/templates/LAYOUT-REFERENCE.md +0 -96
- package/templates/lib/utils/ui.ts +0 -83
package/templates/CONVENTIONS.md
CHANGED
|
@@ -1,1383 +1,32 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- **Server actions and queries live in `modules/<feature>/actions/` and
|
|
34
|
-
`modules/<feature>/queries/`** (`*.server.{js,ts}`), not loose in the
|
|
35
|
-
app root. The DB connection lives in `db/connection.server.ts`; other
|
|
36
|
-
cross-cutting server infrastructure (session helpers, auth config) lives in `lib/`.
|
|
37
|
-
- **One exported function per action/query file.** Name the file after
|
|
38
|
-
the function (`create-post.server.ts` exports `createPost`). It keeps
|
|
39
|
-
the action surface greppable.
|
|
40
|
-
- **Every feature has tests.** A `modules/<feature>/` directory should
|
|
41
|
-
have matching test files under `test/<feature>/`. A unit test for
|
|
42
|
-
logic, a browser/e2e test for user-facing behaviour.
|
|
43
|
-
- **Persist data with Drizzle + SQLite, never JSON files.** The scaffold
|
|
44
|
-
wires up `db/schema.server.ts` and `db/connection.server.ts`. A
|
|
45
|
-
`data/todos.json` or `db.json` used as a database resets on reload and
|
|
46
|
-
cannot scale; define a Drizzle table instead.
|
|
47
|
-
|
|
48
|
-
---
|
|
49
|
-
|
|
50
|
-
## AI agent workflow (non-negotiable)
|
|
51
|
-
|
|
52
|
-
**These rules apply to ALL AI agents (Claude, Cursor, Copilot, etc.)
|
|
53
|
-
working on this codebase. They are not optional and must not be skipped
|
|
54
|
-
even if the user doesn't explicitly ask.**
|
|
55
|
-
|
|
56
|
-
### Before starting ANY work: verify and sync the branch
|
|
57
|
-
|
|
58
|
-
1. Check `git branch --show-current`
|
|
59
|
-
2. If on `main`/`master` → create a feature branch first
|
|
60
|
-
3. If on a feature branch → verify it matches the current task
|
|
61
|
-
4. Sync with parent: `git fetch origin && git rebase origin/main` if behind
|
|
62
|
-
5. Don't mix unrelated work on the wrong branch
|
|
63
|
-
6. **If more than one agent may work this repo at once, use a dedicated git
|
|
64
|
-
worktree per task, never a shared checkout.** Two agents in one working
|
|
65
|
-
directory collide: a `git checkout` in one moves `HEAD` under the other, so
|
|
66
|
-
the next commit lands on the wrong branch. Isolate each task:
|
|
67
|
-
`git worktree add -b <branch> ../<repo>-<slug> origin/main`, `cd` in, work
|
|
68
|
-
there, and `git worktree remove` after the PR merges. Git enforces
|
|
69
|
-
one-branch-per-worktree, so this makes the collision impossible. A lone agent
|
|
70
|
-
in a clean checkout may use a plain branch.
|
|
71
|
-
|
|
72
|
-
### After cloning: verify the toolchain
|
|
73
|
-
|
|
74
|
-
Run `npm run doctor` (which runs `webjs doctor`) once after cloning to assert
|
|
75
|
-
the project is set up correctly: the Node major (the strip-types floor), the
|
|
76
|
-
tsconfig `erasableSyntaxOnly` flag, `.env` drift vs `.env.example`, vendor-pin
|
|
77
|
-
freshness, the `.gitignore` keeping `.webjs/vendor/` committable
|
|
78
|
-
(`vendor-gitignore`), importmap-coherence (the resolved client deps agree on a
|
|
79
|
-
shared transitive version), `@webjsdev/*` version coherence, and the git
|
|
80
|
-
pre-commit hook. It
|
|
81
|
-
prints `[pass]` / `[warn]` / `[fail]` per check with an actionable fix line and
|
|
82
|
-
exits non-zero only on a hard fail (a broken toolchain), so a green run means
|
|
83
|
-
`npm run dev` will boot. It is a local onboarding/setup-verify tool, not a CI
|
|
84
|
-
gate (its env-drift + network pin-freshness checks would make CI flaky).
|
|
85
|
-
|
|
86
|
-
### Every code change must include:
|
|
87
|
-
|
|
88
|
-
1. **Commit and push per logical unit, not at the end.** A logical unit is
|
|
89
|
-
one feature, one fix, one rename, one doc rewrite. Small, focused commits
|
|
90
|
-
with meaningful messages. Always `git push` after committing. Do not
|
|
91
|
-
accumulate uncommitted or unpushed changes. If you have 5+ unstaged files
|
|
92
|
-
spanning different concerns, commit before continuing. The
|
|
93
|
-
`.claude/hooks/nudge-uncommitted.sh` hook fires at threshold 4 to remind
|
|
94
|
-
you, and ignoring it means you are batching. The user should never have
|
|
95
|
-
to ask for a commit.
|
|
96
|
-
|
|
97
|
-
2. **Tests.** Unit test for logic, E2E test for user-facing behavior.
|
|
98
|
-
See the "Testing" section below for what type of test each change needs.
|
|
99
|
-
Run `webjs test` after every change. Never mark work as done with
|
|
100
|
-
failing tests.
|
|
101
|
-
|
|
102
|
-
3. **Documentation updates.** See the **Definition of done** section
|
|
103
|
-
below for the per-surface checklist. The short version: docs land on
|
|
104
|
-
the same PR as the code, never as a follow-up. Drift is how a
|
|
105
|
-
codebase rots; the user should never have to ask "did you update the
|
|
106
|
-
docs?"
|
|
107
|
-
|
|
108
|
-
4. **Convention check.** Run `webjs check` after changes and fix
|
|
109
|
-
any violations before reporting the task as done.
|
|
110
|
-
|
|
111
|
-
5. **Type check.** Run `npm run typecheck` (which runs `webjs typecheck`,
|
|
112
|
-
a `tsc --noEmit` over the app) and fix any type errors. `webjs check` is
|
|
113
|
-
correctness-only and does NOT type-check, so this is the separate
|
|
114
|
-
is-my-TypeScript-valid gate. It exits non-zero on a type error, so add it
|
|
115
|
-
to CI once the app type-checks cleanly.
|
|
116
|
-
|
|
117
|
-
### Definition of done (MUST be addressed BEFORE opening the PR)
|
|
118
|
-
|
|
119
|
-
This is the per-PR contract. Before running `gh pr create`, walk through
|
|
120
|
-
every surface below and either update it OR write `N/A because <reason>`
|
|
121
|
-
in the PR body so the omission is visible. The
|
|
122
|
-
[`.github/pull_request_template.md`](./.github/pull_request_template.md)
|
|
123
|
-
checklist mirrors this list.
|
|
124
|
-
|
|
125
|
-
**Surfaces to consider on EVERY PR:**
|
|
126
|
-
|
|
127
|
-
1. **Tests.** Unit coverage for new logic. Real-browser coverage for
|
|
128
|
-
user-facing behaviour. `webjs test` must pass; `webjs test --browser`
|
|
129
|
-
for any DOM-touching change. See the "Testing" section below for the
|
|
130
|
-
per-change matrix.
|
|
131
|
-
2. **Every markdown file in the project.** Walk the whole tree, not a
|
|
132
|
-
closed list. Run `git ls-files '*.md'` (or `git ls-files '*.md'
|
|
133
|
-
'*.mdx'` if the project ships MDX) and for each path ask: does this
|
|
134
|
-
file describe behaviour, surface, or invariants that this PR changed?
|
|
135
|
-
If yes, update it on this PR. Common surfaces (non-exhaustive):
|
|
136
|
-
- `AGENTS.md` (root and every nested one) for API surface, invariants,
|
|
137
|
-
file-routing rules, project-wide agent workflow.
|
|
138
|
-
- `CONVENTIONS.md` (this file) for project conventions (layout,
|
|
139
|
-
naming, testing). The `webjs check` correctness rules are a separate
|
|
140
|
-
tool surface, not documented here (run `webjs check --rules`).
|
|
141
|
-
- `README.md` (root and any nested ones) for install / use / public
|
|
142
|
-
surface descriptions.
|
|
143
|
-
- `CHANGELOG.md` for any user-visible change, including the SHA / PR
|
|
144
|
-
reference. Keep it in chronological order; don't backdate.
|
|
145
|
-
- `docs/` (if the project has one). Every user-visible change. Add a
|
|
146
|
-
new page if the surface is new and there's no obvious home.
|
|
147
|
-
- Any `*.md` under `agent-docs/`, `docs-internal/`, `decisions/`, or
|
|
148
|
-
similar reference trees.
|
|
149
|
-
- `.github/*.md` (issue templates, PR templates, contributing) when
|
|
150
|
-
a workflow rule shifts.
|
|
151
|
-
3. **`website/`** (if the project has one). Marketing copy on the
|
|
152
|
-
landing page or pricing page when the change touches a claim made
|
|
153
|
-
there.
|
|
154
|
-
4. **Scaffold or codegen scripts** (if the project has any). Update
|
|
155
|
-
when the change affects what new instances generate.
|
|
156
|
-
5. **PR body.** Summary, test plan checklist, and a per-row answer to
|
|
157
|
-
the Definition-of-done checklist (`Updated <path>` or `N/A because
|
|
158
|
-
<reason>`).
|
|
159
|
-
|
|
160
|
-
**How to use the checklist.** For each surface above, explicitly answer
|
|
161
|
-
one of:
|
|
162
|
-
|
|
163
|
-
- **Updated**, with the file path in the commit and PR body.
|
|
164
|
-
- **N/A because**, with a one-sentence reason.
|
|
165
|
-
|
|
166
|
-
The "every markdown file" rule is generative, not enumerative. New
|
|
167
|
-
markdown files appear over a project's lifetime, and this checklist
|
|
168
|
-
must not silently exclude them. The git query above is the source of
|
|
169
|
-
truth; the named files are just common cases.
|
|
170
|
-
|
|
171
|
-
If you find yourself writing `N/A` for every surface except tests, that
|
|
172
|
-
is a smell. Most user-visible code changes touch at least one markdown
|
|
173
|
-
file and either `AGENTS.md` or `CONVENTIONS.md`.
|
|
174
|
-
|
|
175
|
-
**Worked examples:**
|
|
176
|
-
|
|
177
|
-
- Add a new server action `modules/posts/actions/create-post.server.ts`.
|
|
178
|
-
Updated: test (`test/posts/posts.test.ts`), `AGENTS.md` (action listed
|
|
179
|
-
in the module map if the project keeps one), `CHANGELOG.md` (one-line
|
|
180
|
-
entry), `docs/` (the page listing shipped actions if one exists). N/A
|
|
181
|
-
on website / scaffold scripts.
|
|
182
|
-
- Rename a directory convention (e.g. `modules/` to `features/`).
|
|
183
|
-
Updated: existing tests still pass after renames, every markdown file
|
|
184
|
-
that mentions the old name (run `git grep -l 'modules/' '*.md'`),
|
|
185
|
-
scaffold scripts, `CHANGELOG.md`. N/A on website unless the layout
|
|
186
|
-
appears in a landing-page screenshot.
|
|
187
|
-
- Fix a bug in `rateLimit()` that doesn't change the surface. Updated:
|
|
188
|
-
test (regression), `CHANGELOG.md` (one-line entry under fixes). N/A
|
|
189
|
-
on every other markdown file because the public contract did not
|
|
190
|
-
change.
|
|
191
|
-
|
|
192
|
-
### Pre-merge self-review loop (MUST run before signaling the PR is ready)
|
|
193
|
-
|
|
194
|
-
Saying "ready for merge" before a self-review loop converges is a recurring source of low-quality PRs. The pattern to avoid: agent claims ready-for-merge, user requests a code review, agent finds issues, fixes them, claims ready-for-merge again, repeat 4-5 cycles before a review comes back clean. The cure is to run that loop internally before the first "ready" signal, so the user only hears "ready to merge" after the loop has converged on a clean round.
|
|
195
|
-
|
|
196
|
-
**How the loop works:**
|
|
197
|
-
|
|
198
|
-
1. After committing the work and (if remote pushes are in use) pushing the branch, do NOT report "ready for merge" yet. Trigger a **fresh-context review pass**: an AI review with NO prior knowledge of the decisions you made during the implementation. Each AI tool exposes its own primitive for this:
|
|
199
|
-
|
|
200
|
-
- **Cursor**: open a new composer tab.
|
|
201
|
-
- **Claude Code**: spawn a `general-purpose` subagent via the Agent tool.
|
|
202
|
-
- **GitHub Copilot**: open a new chat (reset the side panel).
|
|
203
|
-
- **Antigravity** (Google, formerly Windsurf): open a new Cascade thread or a fresh side-panel session.
|
|
204
|
-
- **Aider**: invoke a separately-started `aider` session (do NOT use `/ask` inside the same session; `/ask` only flips the mode for the next message and still sees the existing context).
|
|
205
|
-
- **Gemini CLI**: invoke a separately-started `gemini` session.
|
|
206
|
-
- **OpenCode**: open a new agent session (the `tool.execute.after` hook is a different surface and not a fresh-context primitive).
|
|
207
|
-
|
|
208
|
-
The shared property is that the reviewer does not see your decision log. That independence is what makes the review catch blind spots. If your tool does not expose a true fresh-context primitive, the canonical fallback is a separately-invoked CLI process; what matters is the reviewer starts with an empty context, not the specific UI affordance.
|
|
209
|
-
|
|
210
|
-
2. Prompt the review for problems only. A working prompt template:
|
|
211
|
-
|
|
212
|
-
> Review the changes on this branch against the project's `AGENTS.md` and `CONVENTIONS.md`. Look for bugs, regressions, security issues, missed edge cases, broken invariants, doc drift, test gaps, and style violations. Read every file the diff touches in its current state, not just the diff hunks. Specifically check: \<focus rotates per round\>. Report findings as a numbered list with file:line references. Problems only, no suggestions. If you find nothing genuinely wrong, reply exactly `CLEAN` on its own line and stop.
|
|
213
|
-
|
|
214
|
-
3. For each finding the review reports, either:
|
|
215
|
-
|
|
216
|
-
- Fix it on the branch (commit + push), OR
|
|
217
|
-
- Reject it explicitly with a one-sentence reason. False positives are real, but rejection has to be defensible (e.g. "the reviewer flagged X as a security issue but X runs server-side only and never reaches user input"). Hand-waving doesn't count.
|
|
218
|
-
|
|
219
|
-
4. If the round found any findings, run another round. The new round picks a slightly different focus: if round 1 was broad, round 2 zooms in on the file you most edited; if round 2 zoomed in, round 3 zooms out to cross-file consistency. Rotate focus to avoid the reviewer rediscovering the same surface twice.
|
|
220
|
-
|
|
221
|
-
5. If the round reports `CLEAN`, the loop is done.
|
|
222
|
-
|
|
223
|
-
The minimum is TWO rounds. A clean first round is rare and usually means the review was too shallow; if round 1 is clean, run a second one with a sharper focus before believing the result.
|
|
224
|
-
|
|
225
|
-
**When to skip the loop:**
|
|
226
|
-
|
|
227
|
-
Skip only for changes that touch a single line of trivially-correct content (a doc typo, a renamed local variable, a one-token config bump). Anything that touches logic, public surface, build, security, or multiple files goes through the loop without exception. A bias toward running the loop is correct; a bias toward skipping it is the exact failure mode this rule exists to prevent.
|
|
228
|
-
|
|
229
|
-
**Reporting after the loop:**
|
|
230
|
-
|
|
231
|
-
When the user is notified the PR is ready, the message should carry:
|
|
232
|
-
|
|
233
|
-
> Ready for merge. Self-review loop ran \<K\> rounds; last round clean. Issues found and fixed during the loop: \<one-line list, or "none" if rounds 2+ kept finding nothing\>.
|
|
234
|
-
|
|
235
|
-
If you cannot honestly say "last round clean", you cannot say "ready for merge". If a finding was rejected as a false positive, mention it so the user can second-guess the rejection.
|
|
236
|
-
|
|
237
|
-
### Autonomous mode (sandbox / bypass permissions)
|
|
238
|
-
|
|
239
|
-
When running without interactive approval, agents must NOT ask questions.
|
|
240
|
-
Instead, auto-decide using best practices:
|
|
241
|
-
- On `main`? → Auto-create `feature/<task-slug>` branch
|
|
242
|
-
- Parent branch has new commits? → Auto-rebase before starting
|
|
243
|
-
- Ready to merge? → Auto-merge, delete feature/fix branches, keep
|
|
244
|
-
long-lived branches (dev, staging, release/*)
|
|
245
|
-
- Commit message? → Auto-generate: what changed and why
|
|
246
|
-
- Tests failing? → Fix them, don't report the failure and stop
|
|
247
|
-
- Convention violations? → Fix them silently
|
|
248
|
-
|
|
249
|
-
The quality bar is the same. Autonomous mode means faster, not sloppier.
|
|
250
|
-
|
|
251
|
-
### What "automatically" means:
|
|
252
|
-
|
|
253
|
-
When a user says "add a contact page" or "add a delete button to posts",
|
|
254
|
-
the AI agent must deliver:
|
|
255
|
-
- The implementation (page, component, action, etc.)
|
|
256
|
-
- Unit tests for any new server actions/queries/components
|
|
257
|
-
- E2E test if the feature involves user interaction
|
|
258
|
-
- Documentation updates if applicable
|
|
259
|
-
|
|
260
|
-
The user should never have to say "also write tests" or "also update the
|
|
261
|
-
docs". That is the agent's default behavior in a webjs project.
|
|
262
|
-
|
|
263
|
-
---
|
|
264
|
-
|
|
265
|
-
## Data persistence: Drizzle + SQLite, never JSON files
|
|
266
|
-
|
|
267
|
-
<!-- OVERRIDE -->
|
|
268
|
-
|
|
269
|
-
Every webjs app uses **Drizzle + SQLite** for persistence by default. The
|
|
270
|
-
scaffold ships the `db/` folder (`schema.server.ts`, `columns.server.ts`,
|
|
271
|
-
`connection.server.ts`), the `webjs.dev.before` + `webjs.start.before` steps
|
|
272
|
-
that run `webjs db migrate` inside `webjs dev` / `webjs start` (#550), and the
|
|
273
|
-
`npm run db:generate` / `db:migrate` / `db:push` / `db:studio` / `db:seed`
|
|
274
|
-
scripts (which route through `webjs db` to drizzle-kit). The loop after a
|
|
275
|
-
schema change is `db:generate` (authors the migration) then `webjs dev` (the
|
|
276
|
-
`dev.before` step applies it); `db:generate` is never auto-run on boot.
|
|
277
|
-
|
|
278
|
-
**AI agents: these rules are absolute.**
|
|
279
|
-
|
|
280
|
-
1. For ANY data the app stores (todos, posts, messages, products,
|
|
281
|
-
comments, users…), define a Drizzle table in `db/schema.server.ts`
|
|
282
|
-
and persist there.
|
|
283
|
-
2. **NEVER** create JSON files under `data/`, `db.json`, `posts.json`,
|
|
284
|
-
`todos.json`, etc. as a fake database. It resets on reload and cannot
|
|
285
|
-
scale; this is a project convention (see the conventions section above).
|
|
286
|
-
3. **NEVER** use module-scope arrays or `Map`s as a "store". They
|
|
287
|
-
reset on every dev-server reload and can't scale beyond one process.
|
|
288
|
-
4. **NEVER** use `localStorage` / `sessionStorage` to persist app data,
|
|
289
|
-
it's per-browser and never reaches the server. Use it only for UI
|
|
290
|
-
preferences (theme, sidebar collapsed, etc.).
|
|
291
|
-
5. To add a model: edit `db/schema.server.ts`, then `npm run db:generate`
|
|
292
|
-
and `npm run db:migrate`. Access via `import { db } from
|
|
293
|
-
'#db/connection.server.ts'` (and the tables from
|
|
294
|
-
`db/schema.server.ts`) **only inside `.server.{js,ts}` files,
|
|
295
|
-
`route.ts` handlers, or `middleware.ts`**. Components, pages, and
|
|
296
|
-
layouts call into the wrapped server query instead; the framework
|
|
297
|
-
rewrites that import to an RPC stub on the browser side, so the DB
|
|
298
|
-
driver never reaches the client.
|
|
299
|
-
|
|
300
|
-
To switch to Postgres: scaffold with `--db postgres`, or swap
|
|
301
|
-
`db/columns.server.ts` + `db/connection.server.ts` for the Postgres
|
|
302
|
-
variants and point `DATABASE_URL` at Postgres. The schema, queries, and
|
|
303
|
-
actions are unchanged. SQLite is the right default for dev and small
|
|
304
|
-
production workloads.
|
|
305
|
-
|
|
306
|
-
---
|
|
307
|
-
|
|
308
|
-
## The scaffold is reference, not the final product
|
|
309
|
-
|
|
310
|
-
<!-- OVERRIDE -->
|
|
311
|
-
|
|
312
|
-
This project was created with `webjs create`. Every file you see right
|
|
313
|
-
now (the `app/page.ts` homepage, the example `User` model, the
|
|
314
|
-
`theme-toggle` component, the gallery under `app/features/` and
|
|
315
|
-
`app/examples/` in the full-stack template, the example users module in
|
|
316
|
-
api / saas templates) is a **starting point**.
|
|
317
|
-
|
|
318
|
-
The full-stack and saas scaffolds ship a **gallery** organized by kind so features
|
|
319
|
-
and whole apps are not mixed. `app/features/<name>/` are single-feature
|
|
320
|
-
demos, one webjs concept each (routing, boundaries, components, server-actions,
|
|
321
|
-
optimistic-ui, async-render, directives, route-handler). `app/examples/<name>/`
|
|
322
|
-
are whole example apps that compose several features (todo: optimistic UI
|
|
323
|
-
+ progressive enhancement + a11y + db + modules). Both keep their logic in
|
|
324
|
-
`modules/`, are small and heavily commented, and are the PRIMARY reference
|
|
325
|
-
for how webjs works.
|
|
326
|
-
|
|
327
|
-
**Study the whole gallery FIRST, prune SECOND.** Before writing or deleting
|
|
328
|
-
anything, read every feature demo and the example app end to end (code AND
|
|
329
|
-
comments) to absorb the idioms you will reuse. Only AFTER you have
|
|
330
|
-
internalised the patterns should you prune. Never delete the examples
|
|
331
|
-
blindly up front (that discards your context before you have read it), and
|
|
332
|
-
never prune the durable knowledge surfaces (`AGENTS.md`, `CONVENTIONS.md`,
|
|
333
|
-
the per-agent rule files), which stay as context for every future
|
|
334
|
-
iteration.
|
|
335
|
-
|
|
336
|
-
Then prune: the examples are REFERENCE, not the app, so keep and adapt the
|
|
337
|
-
ones you need and **delete the rest**. Pruning a route means deleting its
|
|
338
|
-
`app/features/<name>` or `app/examples/<name>` folder AND its
|
|
339
|
-
`modules/<name>` folder (for the todo app, also the `todos` table in
|
|
340
|
-
`db/schema.server.ts` and its link in `app/page.ts`). Each route page
|
|
341
|
-
carries a `webjs-scaffold-placeholder` marker, so `webjs check` fails until
|
|
342
|
-
you consciously keep-and-adapt or prune it. After pruning, delete any
|
|
343
|
-
now-empty directories, an empty `lib/utils/` or `modules/<name>/` is
|
|
344
|
-
leftover scaffolding, not structure.
|
|
345
|
-
|
|
346
|
-
**`app/` is routing-only.** Only routing files belong in `app/` (page,
|
|
347
|
-
layout, route, middleware, and metadata routes). CSS, helpers, and
|
|
348
|
-
constants do NOT: the theme lives at `styles/globals.css` (NOT
|
|
349
|
-
`app/globals.css`), browser-safe helpers at `lib/utils/`, and feature
|
|
350
|
-
logic in `modules/`. If you add a stylesheet or a helper, put it outside
|
|
351
|
-
`app/`.
|
|
352
|
-
|
|
353
|
-
When the user asks the agent to build their actual app:
|
|
354
|
-
|
|
355
|
-
1. **Replace the example `User` model** in `db/schema.server.ts` with
|
|
356
|
-
the real domain models the app needs (e.g. `Todo`, `Post`, `Message`),
|
|
357
|
-
unless the app actually has users.
|
|
358
|
-
2. **Replace `app/page.ts`** with the app's real homepage. Don't ship
|
|
359
|
-
"Hello from …" as the deliverable.
|
|
360
|
-
3. **Delete or replace `components/theme-toggle.ts`** if the app doesn't
|
|
361
|
-
need a theme picker.
|
|
362
|
-
4. **Delete the example users module** (api/saas templates) if the app
|
|
363
|
-
doesn't use it.
|
|
364
|
-
4b. **Prune the gallery** (full-stack template). Keep and adapt the
|
|
365
|
-
`app/features/` demos and the `app/examples/` app the real app uses,
|
|
366
|
-
delete the rest (route + module + any table), and remove their links
|
|
367
|
-
from `app/page.ts`.
|
|
368
|
-
5. **Design the layout in `app/layout.ts` (it ships MINIMAL on purpose).**
|
|
369
|
-
The root layout wires the theme, design tokens, and Tailwind (keep all of
|
|
370
|
-
that), then drops `${children}` into a bare padded `<main>` with NO chrome.
|
|
371
|
-
There is no header, nav, footer, or reading column to inherit: design the
|
|
372
|
-
app's own from what the app IS. Decide from scratch whether it needs a
|
|
373
|
-
header at all, a nav (or none), a footer, a sidebar, a centered reading
|
|
374
|
-
column, or a full-bleed canvas. `LAYOUT-REFERENCE.md` at the project root is
|
|
375
|
-
a complete worked layout (fixed header, brand, nav, theme toggle, reading
|
|
376
|
-
column, footer) to learn the patterns from, then build your own. Keep the
|
|
377
|
-
design tokens and theme apparatus, those are infrastructure.
|
|
378
|
-
6. **Use a unique design, and redesign means more than recolor (UI apps).**
|
|
379
|
-
Give the app a design of its own (palette, typography, LAYOUT, spacing,
|
|
380
|
-
and chrome) chosen from what the app IS. The layout ships MINIMAL (see item
|
|
381
|
-
5), so there is no scaffold skeleton to inherit: build the chrome the app
|
|
382
|
-
actually needs. Two things are gated by a `webjs-scaffold-placeholder`
|
|
383
|
-
marker, so `webjs check` fails until each is addressed: the minimal shell
|
|
384
|
-
carries a "design your layout from scratch" marker (delete it once you have
|
|
385
|
-
built a real layout), and the palette block carries its own marker (the
|
|
386
|
-
starter orange looks finished on purpose). The design token NAMES and theme
|
|
387
|
-
wiring (`--background`, `--primary`, `--card`, ... in `app/layout.ts`) are
|
|
388
|
-
infrastructure to keep, but their COLOR VALUES are yours: set a distinctive
|
|
389
|
-
palette that fits the app, in both the light and dark blocks. Keeping the
|
|
390
|
-
scaffold's token colors (or a light warm recolor of them) is NOT owning the
|
|
391
|
-
palette. To keep the starter palette deliberately, run
|
|
392
|
-
`webjs check --clear-placeholders`. Style with Tailwind utilities wherever
|
|
393
|
-
they reach, and use custom CSS only for what utilities cannot express (@theme
|
|
394
|
-
tokens, @keyframes, scrollbar, complex color-mix or gradients). The `api`
|
|
395
|
-
template has no UI, so this does not apply there.
|
|
396
|
-
**Size the component HOST, not just an inner wrapper.** A component's host
|
|
397
|
-
custom element is the box its parent lays out. Hosts default to
|
|
398
|
-
`display: block`, but a host that is a flex/grid item in a centering parent
|
|
399
|
-
(`flex justify-center`, `grid place-items-center`) is still sized to its
|
|
400
|
-
content unless it carries width itself. Put `w-full max-w-[...]` on the host,
|
|
401
|
-
not only on an inner `<div>` (an inner `w-full` resolves against a collapsed
|
|
402
|
-
host and the whole component renders tiny). If a board or card renders small
|
|
403
|
-
despite `w-full max-w-[400px]` on its inner grid, move that sizing to the host.
|
|
404
|
-
**Definition of done (design gate):** a UI app is NOT finished until you
|
|
405
|
-
have (a) given it a design of its own (layout AND palette) and removed the
|
|
406
|
-
scaffold shell, (b) run it and PLAYED THROUGH every state in a browser
|
|
407
|
-
(fill the board, win, draw, reload), confirming nothing resizes or shifts as
|
|
408
|
-
it fills (even, stable squares) and it does not resemble the scaffold, and
|
|
409
|
-
(c) confirmed it still reads AND looks right with JavaScript OFF (SSR +
|
|
410
|
-
progressive enhancement): content shows, links navigate, forms submit, and
|
|
411
|
-
the CSS is fully applied (the app links a static compiled `public/tailwind.css`,
|
|
412
|
-
so utilities resolve with no JS). A
|
|
413
|
-
glance at the empty first paint is not enough; the layout bugs show up
|
|
414
|
-
mid-interaction.
|
|
415
|
-
`webjs doctor` emits an advisory when `app/layout` still reproduces scaffold
|
|
416
|
-
design (the exact 760px reading column, the "Built with webjs" attribution, or
|
|
417
|
-
the unmodified starter palette values); the kept theme apparatus (theme-toggle,
|
|
418
|
-
`--header-h`) is infrastructure and does NOT trip it. Treat the advisory as a
|
|
419
|
-
to-do, not noise.
|
|
420
|
-
7. **Keep:** the Drizzle setup, the test config, the agent config files
|
|
421
|
-
(`AGENTS.md`, `CONVENTIONS.md`, `CLAUDE.md`, `.cursorrules`, etc.),
|
|
422
|
-
`db/connection.server.ts` + `db/columns.server.ts`, the directory
|
|
423
|
-
conventions, the design tokens in `app/layout.ts`. These are the
|
|
424
|
-
infrastructure, not the example app.
|
|
425
|
-
|
|
426
|
-
This is enforced, not just advised. The example `app/page.ts`,
|
|
427
|
-
`app/layout.ts`, and each `app/features/<name>/page.ts` +
|
|
428
|
-
`app/examples/<name>/page.ts` carry a
|
|
429
|
-
`webjs-scaffold-placeholder` marker comment, and the
|
|
430
|
-
`no-scaffold-placeholder` check fails while any marker remains, so a
|
|
431
|
-
freshly scaffolded app fails `webjs check` until you address each
|
|
432
|
-
placeholder. The marker is acknowledge-and-remove: replace the example
|
|
433
|
-
content, or deliberately keep it, and in either case delete the marker
|
|
434
|
-
line. So the delivered app contains only what the user asked for, never
|
|
435
|
-
leftover scaffold code. To keep the gallery and clear every marker in one
|
|
436
|
-
step (instead of one edit per file), run `webjs check --clear-placeholders`,
|
|
437
|
-
then delete whichever demo routes/modules you do not want.
|
|
438
|
-
|
|
439
|
-
The scaffold exists so the agent doesn't reinvent the directory layout,
|
|
440
|
-
the Drizzle wiring, the test runner config, or the convention files. It
|
|
441
|
-
does NOT exist so the agent ships the example homepage.
|
|
442
|
-
|
|
443
|
-
### Prune what the app does not use
|
|
444
|
-
|
|
445
|
-
The scaffold is reference, so keep the infrastructure the app actually
|
|
446
|
-
USES and delete the rest, both files AND their folders. No persistence
|
|
447
|
-
means delete `db/`, `drizzle.config.ts`, and the `db:*` scripts. No UI
|
|
448
|
-
kit used means delete `components/ui/`, `components.json`, and
|
|
449
|
-
`lib/utils/cn.ts`. No PWA means delete `public/sw.js` and `offline.html`.
|
|
450
|
-
Always KEEP the durable knowledge (`AGENTS.md`, `CONVENTIONS.md`, the
|
|
451
|
-
per-agent rule files, the MCP wiring), and never prune it, so removing
|
|
452
|
-
example code never removes your context. Prune AFTER you have used the
|
|
453
|
-
features and examples as reference, never blindly up front. This is a no-op for the
|
|
454
|
-
`api` template, which ships no UI kit and no PWA files.
|
|
455
|
-
|
|
456
|
-
---
|
|
457
|
-
|
|
458
|
-
## Sensible defaults
|
|
459
|
-
|
|
460
|
-
<!-- OVERRIDE -->
|
|
461
|
-
WebJs uses sensible defaults. Environment
|
|
462
|
-
variables control infrastructure (no config files needed):
|
|
463
|
-
|
|
464
|
-
| Environment variable | Effect |
|
|
465
|
-
|---|---|
|
|
466
|
-
| `REDIS_URL` | Connection string consumed by `redisStore({ url: process.env.REDIS_URL })`. Not auto-wired. Call `setStore(redisStore())` once at app startup to put cache / sessions / rate-limit on Redis. |
|
|
467
|
-
| `AUTH_SECRET` | Required for auth JWT signing (32+ random chars) |
|
|
468
|
-
| `AUTH_GOOGLE_ID` | Google OAuth client ID (optional) |
|
|
469
|
-
| `AUTH_GITHUB_ID` | GitHub OAuth client ID (optional) |
|
|
470
|
-
| `PORT` | Server port. Precedence: `--port` flag > `PORT` (a real exported env var or a `PORT` in `.env`) > 8080. |
|
|
471
|
-
| `WEBJS_PUBLIC_*` | Any env var starting with this prefix is exposed to the browser as `process.env.WEBJS_PUBLIC_X`. Components can read it directly. No build step, no transform. Use for API base URLs, Stripe publishable keys, analytics IDs, anything that is intended to be visible client-side. |
|
|
472
|
-
|
|
473
|
-
**Server-only by default.** Any env var without the `WEBJS_PUBLIC_` prefix never reaches the browser. Reading `process.env.DATABASE_URL` from a component returns `undefined`, the same as a typo. The prefix is fail-closed: secrets cannot accidentally leak.
|
|
474
|
-
|
|
475
|
-
**Development:** zero env vars needed. Everything works with memory/cookie/disk.
|
|
476
|
-
**Production:** set `AUTH_SECRET` + `SESSION_SECRET`. For horizontal scaling, also set `REDIS_URL` and add one line at app startup:
|
|
477
|
-
|
|
478
|
-
```js
|
|
479
|
-
import { setStore, redisStore } from '@webjsdev/server';
|
|
480
|
-
setStore(redisStore({ url: process.env.REDIS_URL }));
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
---
|
|
484
|
-
|
|
485
|
-
## Architecture: Modules
|
|
486
|
-
|
|
487
|
-
<!-- OVERRIDE -->
|
|
488
|
-
This app uses the **modules architecture** for feature-scoped code:
|
|
489
|
-
|
|
490
|
-
```
|
|
491
|
-
modules/
|
|
492
|
-
<feature>/
|
|
493
|
-
actions/ Server mutations (one async function per file, *.server.ts)
|
|
494
|
-
queries/ Server reads (one async function per file, *.server.ts)
|
|
495
|
-
components/ Feature-owned web components
|
|
496
|
-
utils/ Pure helper functions
|
|
497
|
-
types.ts Shared TypeScript types / JSDoc typedefs
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
**Rules:**
|
|
501
|
-
- **Prefer the `#` root alias over deep relatives.** Write `import { db } from '#db/connection.server.ts'`, `import { Button } from '#components/ui/button.ts'`, `#lib/...`, `#modules/...` instead of `../../../`. It is native `package.json "imports"` (the single `"#*": "./*"` key covers every top-level folder, so a new folder needs no config), resolved by Node and Bun with no build step. There is no slash after the `#` (`#lib/...`, not `#/lib/...`). A same-directory import stays relative (`./sibling.ts`).
|
|
502
|
-
- One exported function per server action/query file
|
|
503
|
-
- Server actions need BOTH the `.server.{js,ts}` extension AND a `'use server'` directive at the top. Extension alone marks a server-only utility (source-protected, not RPC-callable). Directive alone is a lint violation (`use-server-needs-extension`).
|
|
504
|
-
- Components must call `Class.register('tag')`
|
|
505
|
-
- **Server-only code goes in `.server.{js,ts}` files, `route.ts` handlers, or `middleware.ts`. Never in pages, layouts, or components.** Direct imports of a DB driver (`pg`) or `node:*` from pages, layouts, or components crash the browser at module load. Wrap in a `.server.{js,ts}` file; the framework rewrites that import to an RPC stub on the browser side. The DB lives in `db/*.server.ts`; `lib/` holds other server-only infra and browser-safe utilities (`lib/utils/cn.ts` with `cn`); the convention is "if a `lib/` file needs Node APIs, only import it from server-only files." A TYPE-ONLY `import type { Todo } from '#db/schema.server.ts'` is the exception, fine in a page or component because the stripper erases it before it reaches the browser.
|
|
506
|
-
- Routes (`app/**/page.ts`, `app/**/route.ts`) must be thin: import logic from modules
|
|
507
|
-
- **Fetch server data in the component that needs it, with an `async render()`, not by prop-drilling.** A leaf component can write `const u = await getUser(this.uid)` directly in `render()`; SSR awaits it so the data is in the first paint, and the client uses stale-while-revalidate on a re-fetch. Reach for `renderFallback()` only to show a re-fetch loading state, and `Task` / signals only for genuinely client-only data (a `Task` shows its pending state at SSR, losing first-paint data). Do not put `await getData()` in a page / layout when a leaf component can own it (page fetches run sequentially, a route-level waterfall).
|
|
508
|
-
- **Keep pages and layouts as pure carriers, so their modules stay out of the network tab.** A page/layout never hydrates; the framework drops its module from the browser as long as its only browser-relevant job is registering the components it imports. It starts shipping its own module (invisible in tests) the moment its closure does any OTHER client work. So do not give a page/layout module-scope client work (a top-level call, a `window` / `document` / `customElements` access, a bare side-effect import, or a `@webjsdev/core/client-router` import: routing is automatic), and do not import a client-global-touching non-component util into it. Put client behaviour in a component, server-only code in `.server.{js,ts}`. Self-check: `page.ts` / `layout.ts` should not appear in the browser's network tab.
|
|
509
|
-
|
|
510
|
-
---
|
|
511
|
-
|
|
512
|
-
## Architecture: Routes
|
|
513
|
-
|
|
514
|
-
<!-- OVERRIDE -->
|
|
515
|
-
Routes live under `app/` and follow NextJs App Router conventions:
|
|
516
|
-
|
|
517
|
-
- `app/page.ts`: Homepage
|
|
518
|
-
- `app/<segment>/page.ts`: Static route
|
|
519
|
-
- `app/[param]/page.ts`: Dynamic route
|
|
520
|
-
- `app/[...rest]/page.ts`: Catch-all
|
|
521
|
-
- `app/(group)/...`: Route group (folder not in URL)
|
|
522
|
-
- `app/**/route.ts`: API endpoint
|
|
523
|
-
- `app/**/layout.ts`: Layout wrapper
|
|
524
|
-
- `app/**/error.ts`: Error boundary
|
|
525
|
-
- `app/**/middleware.ts`: Per-segment middleware
|
|
526
|
-
- `instrumentation.ts` (app root): optional boot hook. `register()` runs once at startup; call `setOnError()` (from `@webjsdev/server`) inside it to wire an APM error sink (composes with `createRequestHandler({ onError })`).
|
|
527
|
-
- `instrumentation-client.ts` (app root): optional client boot hook, imported first in the browser boot so it runs before app modules.
|
|
528
|
-
|
|
529
|
-
**Special route files:**
|
|
530
|
-
- `app/**/error.ts`: Error boundary. Default export receives `{ error }`, returns `TemplateResult`. Nearest boundary catches errors from pages below it.
|
|
531
|
-
- `app/**/loading.ts`: Loading state. Auto-wraps the sibling page in a `Suspense` boundary. Shown while async page functions resolve.
|
|
532
|
-
- `app/**/not-found.ts`: 404 page. Nearest wins when `notFound()` is thrown.
|
|
533
|
-
- `app/**/forbidden.ts`: 403 page. Nearest wins when `forbidden()` is thrown (an authenticated user who lacks permission).
|
|
534
|
-
- `app/**/unauthorized.ts`: 401 page. Nearest wins when `unauthorized()` is thrown (a request that is not authenticated).
|
|
535
|
-
- `app/global-error.ts` (root only): app-wide error boundary, tried after nested `error.ts` boundaries. Renders its OWN `<!doctype><html><body>` document.
|
|
536
|
-
- `app/global-not-found.ts` (root only): 404 for a URL that matches nothing anywhere.
|
|
537
|
-
- `app/sitemap.ts`: Dynamic sitemap at `/sitemap.xml`. Export a function returning an array of `{ url, lastModified }`.
|
|
538
|
-
- `app/robots.ts`: Dynamic robots.txt at `/robots.txt`.
|
|
539
|
-
- `app/manifest.ts`: Web app manifest at `/manifest.json`.
|
|
540
|
-
|
|
541
|
-
**Rules:**
|
|
542
|
-
- A folder cannot have both `page.ts` and `route.ts`
|
|
543
|
-
- Page/layout default exports must be functions (possibly async)
|
|
544
|
-
- Route handlers export named methods: `GET`, `POST`, `PUT`, `DELETE`, `WS`
|
|
545
|
-
|
|
546
|
-
---
|
|
547
|
-
|
|
548
|
-
## Testing
|
|
549
|
-
|
|
550
|
-
<!-- OVERRIDE -->
|
|
551
|
-
Tests are organised by **feature**, mirroring `modules/<feature>/`.
|
|
552
|
-
Each feature gets its own folder under `test/`, even when it starts
|
|
553
|
-
with a single file. Test KIND (browser / e2e / smoke) lives in a
|
|
554
|
-
subfolder inside the feature, and only appears when there is a real
|
|
555
|
-
test of that kind.
|
|
556
|
-
|
|
557
|
-
```
|
|
558
|
-
test/
|
|
559
|
-
<feature>/
|
|
560
|
-
<name>.test.ts ← node test: unit + integration
|
|
561
|
-
browser/<name>.test.js ← real-browser test (web-test-runner)
|
|
562
|
-
e2e/<name>.test.ts ← full-app end-to-end (opt in: WEBJS_E2E=1)
|
|
563
|
-
smoke/<name>.test.ts ← fast post-deploy sanity check
|
|
564
|
-
```
|
|
565
|
-
|
|
566
|
-
Concrete example:
|
|
567
|
-
|
|
568
|
-
```
|
|
569
|
-
test/
|
|
570
|
-
auth/
|
|
571
|
-
auth.test.ts # signup / login / currentUser, node
|
|
572
|
-
password.test.ts # hashing / verify, node
|
|
573
|
-
browser/login-form.test.js # real-browser, only if exercising DOM
|
|
574
|
-
posts/
|
|
575
|
-
posts.test.ts # CRUD via actions
|
|
576
|
-
browser/post-editor.test.js
|
|
577
|
-
hello/
|
|
578
|
-
hello.test.ts # the scaffold's starter test
|
|
579
|
-
browser/hello.test.js
|
|
580
|
-
e2e/hello.test.ts
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
### Test kinds
|
|
584
|
-
|
|
585
|
-
| Kind | Where | What it does |
|
|
586
|
-
|------|-------|--------------|
|
|
587
|
-
| node (unit + integration) | `test/<feature>/*.test.ts` | Fast, no spawned process. Import server actions/queries/utilities and call them directly. Use `renderToString` for SSR HTML assertions. |
|
|
588
|
-
| browser | `test/<feature>/browser/*.test.js` | Real Chromium via web-test-runner + Playwright. Shadow DOM, events, `adoptedStyleSheets`, `IntersectionObserver`. |
|
|
589
|
-
| e2e | `test/<feature>/e2e/*.test.ts` | Boots the app and drives it through HTTP / a real browser. Gated behind `WEBJS_E2E=1` so it doesn't run on every `webjs test`. |
|
|
590
|
-
|
|
591
|
-
For a browser component test, `ssrFixture(html\`<my-el></my-el>\`)` from
|
|
592
|
-
`@webjsdev/core/testing` server-renders THEN hydrates the component, awaiting
|
|
593
|
-
its native `updateComplete`, so the post-hydration DOM is observable and an
|
|
594
|
-
SSR-vs-hydrate mismatch shows up (contrast `fixture()`, which only waits two
|
|
595
|
-
macrotasks). `assertNoA11yViolations(el)` is the OPT-IN axe-core accessibility
|
|
596
|
-
assertion: axe-core is a test-only devDependency, dynamically imported, never
|
|
597
|
-
shipped to the app runtime, and the assertion is never an automatic gate.
|
|
598
|
-
Install it once with `npm install -D axe-core` (the scaffold already lists it).
|
|
599
|
-
The scaffold's `test/hello/browser/hello.test.js` demonstrates both.
|
|
600
|
-
| smoke | `test/<feature>/smoke/*.test.ts` | Fast deploy-time sanity check (single critical path; "does this surface still return 200"). |
|
|
601
|
-
|
|
602
|
-
### Running
|
|
603
|
-
|
|
604
|
-
- `webjs test` (or `node --test`) runs the node tests (unit + integration + smoke).
|
|
605
|
-
- `webjs test --browser` (or `npx wtr`) runs the browser tests.
|
|
606
|
-
- `WEBJS_E2E=1 webjs test` adds the e2e tests.
|
|
607
|
-
|
|
608
|
-
### The handle() test harness (full-pipeline node tests)
|
|
609
|
-
|
|
610
|
-
For a node test that needs the REAL request pipeline (middleware, routing,
|
|
611
|
-
SSR, page actions, server-action RPC, auth + CSRF), drive
|
|
612
|
-
`createRequestHandler({ appDir }).handle(request)` and assert on the
|
|
613
|
-
`Response`. `@webjsdev/server/testing` ships thin builders over it:
|
|
614
|
-
|
|
615
|
-
```ts
|
|
616
|
-
import { createRequestHandler } from '@webjsdev/server';
|
|
617
|
-
import { testRequest, invokeActionForTest, loginAndGetCookies, withSessionCookie }
|
|
618
|
-
from '@webjsdev/server/testing';
|
|
619
|
-
|
|
620
|
-
const app = await createRequestHandler({ appDir: process.cwd(), dev: true });
|
|
621
|
-
|
|
622
|
-
// fire a request, assert the response
|
|
623
|
-
const res = await testRequest(app.handle, '/about');
|
|
624
|
-
|
|
625
|
-
// real login, reuse the captured session cookie on a protected route
|
|
626
|
-
const { cookies } = await loginAndGetCookies(app.handle, { email, password });
|
|
627
|
-
const dash = await testRequest(app.handle, '/dashboard', withSessionCookie({}, cookies));
|
|
628
|
-
|
|
629
|
-
// round-trip a server action through the REAL /__webjs/action/<hash>/<fn> path
|
|
630
|
-
const out = await invokeActionForTest(app, 'modules/posts/actions/create.server.ts', 'createPost', [input]);
|
|
631
|
-
```
|
|
632
|
-
|
|
633
|
-
Prefer `invokeActionForTest` over a direct import of the action when you want
|
|
634
|
-
to verify the production contract: it exercises the wire serializer (a `Date` /
|
|
635
|
-
`Map` arg survives), the Origin / Sec-Fetch-Site CSRF check (it models a
|
|
636
|
-
same-origin POST), and prod error sanitization, which a direct call bypasses.
|
|
637
|
-
The saas template's `test/auth/auth.test.ts` is a worked example.
|
|
638
|
-
|
|
639
|
-
This is also why the auth test lives at `test/auth/auth.test.ts` (the
|
|
640
|
-
feature-folder convention), NOT `test/unit/auth.test.ts`. Test KIND is a
|
|
641
|
-
subfolder inside a feature, never the top level.
|
|
642
|
-
|
|
643
|
-
**Every change ships with a test.** This is a convention, not a hard
|
|
644
|
-
gate, consistent with the convention-vs-check principle this file
|
|
645
|
-
states (a sensible app can legitimately want a test-less commit for a
|
|
646
|
-
spike, a vendored file, or a pure refactor). For Claude Code, a commit
|
|
647
|
-
that stages app code (`app/`, `modules/`, `components/`, `lib/`) without
|
|
648
|
-
staging a test WARNS via `.claude/hooks/require-tests-with-src.sh`, then
|
|
649
|
-
lets the commit through. A project that wants the strict floor opts into
|
|
650
|
-
a hard block by setting `WEBJS_TEST_GATE=block` (in
|
|
651
|
-
`.claude/settings.json` env, your shell, or CI). A unit test alone is
|
|
652
|
-
not enough for interactive or component code: add the browser test that
|
|
653
|
-
asserts the rendered/hydrated behaviour. The real enforcement is CI: the
|
|
654
|
-
test suite runs in `.github/workflows/ci.yml` on every PR and push to
|
|
655
|
-
main, so the gate cannot be skipped with a local `--no-verify`.
|
|
656
|
-
|
|
657
|
-
### Choosing a feature folder
|
|
658
|
-
|
|
659
|
-
Use the same name as the matching module folder when one exists:
|
|
660
|
-
`modules/posts/` ↔ `test/posts/`. If the test spans more than one
|
|
661
|
-
module (a full-stack flow), pick the most prominent one or create a
|
|
662
|
-
new feature folder (`test/checkout/`, `test/onboarding/`).
|
|
663
|
-
|
|
664
|
-
If you find yourself reaching for `test/utils/` or `test/misc/`,
|
|
665
|
-
the feature folder is missing. Name it after the user-facing concern.
|
|
666
|
-
|
|
667
|
-
### Debugging with Playwright MCP
|
|
668
|
-
|
|
669
|
-
This project includes a Playwright MCP server (`.claude.json`). When
|
|
670
|
-
debugging UI issues, AI agents can use the Playwright MCP tools to:
|
|
671
|
-
- Navigate to pages in a real browser
|
|
672
|
-
- Click elements, fill forms, interact with the UI
|
|
673
|
-
- Take screenshots to see what the user sees
|
|
674
|
-
- Inspect the accessibility tree for element discovery
|
|
675
|
-
|
|
676
|
-
Use `Playwright MCP` tools instead of writing one-shot Bash scripts
|
|
677
|
-
with puppeteer or playwright imports.
|
|
678
|
-
|
|
679
|
-
### When to write tests
|
|
680
|
-
|
|
681
|
-
| Change | Server test (node:test) | Browser test (WTR) |
|
|
682
|
-
|--------|------------------------|-------------------|
|
|
683
|
-
| New server action | Required | - |
|
|
684
|
-
| New component | Required (SSR output) | Required (interaction) |
|
|
685
|
-
| New page/route | - | Required |
|
|
686
|
-
| Bug fix | Required (regression) | If user-facing |
|
|
687
|
-
| Refactor | Existing tests must pass | Existing tests must pass |
|
|
688
|
-
|
|
689
|
-
---
|
|
690
|
-
|
|
691
|
-
## UI components: prefer the Webjs UI kit over raw Tailwind
|
|
692
|
-
|
|
693
|
-
<!-- OVERRIDE -->
|
|
694
|
-
|
|
695
|
-
This scaffold ships with the Webjs UI kit preinstalled at `components/ui/`.
|
|
696
|
-
The kit splits into **two tiers**. Picking the wrong tier produces
|
|
697
|
-
broken markup.
|
|
698
|
-
|
|
699
|
-
**Tier 1: class helpers** (button, card, input, label, alert, badge,
|
|
700
|
-
separator, skeleton, table, etc.): pure functions that return Tailwind
|
|
701
|
-
class strings. Call them and spread onto a **raw native element**.
|
|
702
|
-
|
|
703
|
-
**Tier 2: custom elements** (dialog, popover, tooltip, dropdown-menu,
|
|
704
|
-
tabs, accordion, collapsible, progress, etc.): real `<ui-X>` tags. Import
|
|
705
|
-
the module once (typically in `app/layout.ts`) and use the tag.
|
|
706
|
-
|
|
707
|
-
```ts
|
|
708
|
-
// Tier 1: class helpers on native elements (use this for forms,
|
|
709
|
-
// dashboards, cards, layouts, anywhere the value is purely visual)
|
|
710
|
-
import { buttonClass } from '#components/ui/button.ts';
|
|
711
|
-
import { inputClass } from '#components/ui/input.ts';
|
|
712
|
-
return html`
|
|
713
|
-
<button class=${buttonClass({ size: 'lg' })}>Save</button>
|
|
714
|
-
<input class=${inputClass()} placeholder="Email">
|
|
715
|
-
`;
|
|
716
|
-
|
|
717
|
-
// Tier 2: custom elements (modals, dropdowns, tab strips, tooltips,
|
|
718
|
-
// state the browser doesn't give you natively)
|
|
719
|
-
return html`
|
|
720
|
-
<ui-dialog>
|
|
721
|
-
<ui-dialog-trigger>
|
|
722
|
-
<button class=${buttonClass({ variant: 'outline' })}>Edit</button>
|
|
723
|
-
</ui-dialog-trigger>
|
|
724
|
-
<ui-dialog-content>…</ui-dialog-content>
|
|
725
|
-
</ui-dialog>
|
|
726
|
-
`;
|
|
727
|
-
|
|
728
|
-
// Avoid: hand-rolled Tailwind on every <button> loses visual
|
|
729
|
-
// consistency. Tier-1 helpers give you the same control with one import.
|
|
730
|
-
return html`
|
|
731
|
-
<button class="px-4 py-2 rounded-md bg-accent text-accent-fg">Save</button>
|
|
732
|
-
`;
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
Add more components with `webjs ui add <name>` (e.g. `webjs ui add dialog
|
|
736
|
-
tabs popover`). The catalogue lives at
|
|
737
|
-
[https://ui.webjs.dev](https://ui.webjs.dev).
|
|
738
|
-
|
|
739
|
-
**Hand-rolled Tailwind is still appropriate for:**
|
|
740
|
-
- One-off marketing pages, hero sections, landing CTAs.
|
|
741
|
-
- Anywhere the visual design intentionally diverges from the kit baseline.
|
|
742
|
-
- Layout primitives (`<div class="grid grid-cols-3 gap-4">`).
|
|
743
|
-
|
|
744
|
-
The convention: any visual element with a Tier-1 helper uses the helper.
|
|
745
|
-
Any stateful behavior with a Tier-2 element uses the element.
|
|
746
|
-
|
|
747
|
-
---
|
|
748
|
-
|
|
749
|
-
## Components
|
|
750
|
-
|
|
751
|
-
<!-- OVERRIDE -->
|
|
752
|
-
|
|
753
|
-
```ts
|
|
754
|
-
import { WebComponent, html } from '@webjsdev/core';
|
|
755
|
-
|
|
756
|
-
// Recommended declare-free base-class factory style
|
|
757
|
-
export class MyWidget extends WebComponent({
|
|
758
|
-
label: String,
|
|
759
|
-
count: Number
|
|
760
|
-
}) {
|
|
761
|
-
// Light DOM is the default; Tailwind utility classes apply directly.
|
|
762
|
-
|
|
763
|
-
constructor() {
|
|
764
|
-
super();
|
|
765
|
-
// Defaults go here, never as class-field initializers
|
|
766
|
-
// (`label = ''` would clobber the framework's reactive accessor).
|
|
767
|
-
this.label = '';
|
|
768
|
-
this.count = 0;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
render() {
|
|
772
|
-
return html`
|
|
773
|
-
<div class="p-4 border border-border rounded-lg">
|
|
774
|
-
<p class="font-serif text-foreground">${this.label}: ${this.count}</p>
|
|
775
|
-
</div>
|
|
776
|
-
`;
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
MyWidget.register('my-widget');
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
Reactive properties are declared one way: pass the properties shape directly to the base-class factory `WebComponent({ ... })` (e.g. `label: String`). The property types flow to `this.<prop>` with no `declare` lines needed, and the factory installs the reactive accessors so a class-field initializer can never clobber them. For per-property options use the `prop()` helper inside the shape (`count: prop(Number, { reflect: true })`, `mode: prop({ state: true })`); narrow a type with `prop<Student>(Object)`. Set defaults by assigning in the constructor after `super()`. A hand-written `static properties = { ... }` THROWS at construction (`no-static-properties`). The factory gives you full intelligence in any tsserver-backed editor. See the Editor Setup docs for the standalone `@webjsdev/intellisense` (no Lit dependency) that extends this to tag / attribute intelligence inside `html\`…\`` templates (go-to-definition, binding-aware completions, value/binding diagnostics, hover); in VS Code / Cursor / Windsurf the `webjs` extension bundles it automatically.
|
|
783
|
-
|
|
784
|
-
**Rules:**
|
|
785
|
-
- One component per file
|
|
786
|
-
- **Light DOM by default.** Opt in to shadow DOM with `static shadow = true` when you need scoped styles (via `static styles = css\`...\``) or third-party-embed isolation. `<slot>` projection works identically in both modes (named slots, fallback content, `assignedNodes` / `slotchange`, first-wins resolution), so slot usage alone is never a reason to opt into shadow DOM.
|
|
787
|
-
- Prefer Tailwind utility classes for styling. They're unique by construction (`p-4`, `font-semibold`) so they can't collide across components.
|
|
788
|
-
- **If a light-DOM component authors its own custom CSS (a `<style>` block in `render()` or an imported stylesheet), every class selector MUST be prefixed with the component's tag name.** Either pattern works. Pick one and stay consistent:
|
|
789
|
-
- `.my-widget__body`, `.my-widget__title` (BEM-ish)
|
|
790
|
-
- `my-widget .body`, `my-widget .title` (descendant selector)
|
|
791
|
-
- **Never extend raw HTMLElement directly for app components.** Always subclass `WebComponent` (or the factory form `WebComponent({...})`) to hook into SSR, lifecycle, elision, and the reactive property system. Extend raw HTMLElement only for rare native-API edge cases (like form-associated `ElementInternals` or customized built-in elements), and add a `webjs-allow-htmlelement: <reason>` comment to acknowledge the exception.
|
|
792
|
-
- Tag name must contain a hyphen (HTML spec)
|
|
793
|
-
- Always call `Class.register('tag')`. That's the standard DOM API.
|
|
794
|
-
- **Reactive props are declared via the base-class factory `WebComponent({ ... })`.** A hand-written `static properties = { ... }` throws at construction (`webjs check` flags it via `no-static-properties`). Never write `propName = value` or `propName: Type = value` as a class-field initializer on a reactive prop. It compiles to `Object.defineProperty(this, …)` after `super()` and clobbers the framework's reactive accessor, silently breaking re-renders (`webjs check` flags this via `reactive-props-no-class-field`). Set defaults in the constructor. Declare an array-typed prop with the `Array` constructor, not `Object` (`items: prop<Tag[]>(Array)`): the two share one JSON converter so neither crashes, but `Array` states the shape and `webjs check` flags the `Object` form via `array-prop-uses-array-type`.
|
|
795
|
-
- Component state lives in signals. Import `signal` from `@webjsdev/core`, read via `signal.get()` inside `render()`, write via `signal.set(value)`. Module-scope signals share state across components; instance signals (created in the constructor) carry component-local state. Reactive properties (declared via the factory) wrap HTML attributes, attribute reflection, and `.prop=${value}` SSR hydration.
|
|
796
|
-
- Use lifecycle hooks (`firstUpdated`, `updated`) only when needed
|
|
797
|
-
|
|
798
|
-
---
|
|
799
|
-
|
|
800
|
-
## Components: Light DOM (default) vs Shadow DOM (opt-in)
|
|
801
|
-
|
|
802
|
-
<!-- OVERRIDE -->
|
|
803
|
-
|
|
804
|
-
| Use case | Mode | How |
|
|
805
|
-
|---|---|---|
|
|
806
|
-
| Global / Tailwind CSS, simple composition | **Light DOM** (default) | Write `class="..."` in your template. Plain children, global styles apply. |
|
|
807
|
-
| Scoped styles via `static styles = css\`\`` | Shadow DOM | Set `static shadow = true`. `adoptedStyleSheets` scopes bare selectors. |
|
|
808
|
-
| `<slot>` content projection | **Either** | Same `<slot>` / `<slot name="x">` / fallback / `assignedNodes` / `slotchange` API in both modes. Light DOM uses framework projection; shadow DOM uses native browser projection. |
|
|
809
|
-
| Third-party embed isolation | Shadow DOM | CSS can't leak in or out. |
|
|
810
|
-
|
|
811
|
-
**Light DOM** = the component renders as plain HTML. Global CSS and
|
|
812
|
-
Tailwind utility classes apply directly. Use `document.querySelector`
|
|
813
|
-
to find elements. No `:host`, no `::part`, no CSS-variable plumbing.
|
|
814
|
-
|
|
815
|
-
**Shadow DOM** = opt-in style encapsulation. Declare `static shadow = true`
|
|
816
|
-
and author styles via `static styles = css\`...\`` (adopted via
|
|
817
|
-
`adoptedStyleSheets`). The browser enforces the boundary, and nothing
|
|
818
|
-
leaks in or out.
|
|
819
|
-
|
|
820
|
-
Both modes are fully SSR'd. Light DOM emits content as direct children
|
|
821
|
-
with a `<!--webjs-hydrate-->` marker. Shadow DOM emits a
|
|
822
|
-
`<template shadowrootmode="open">` that the browser attaches automatically.
|
|
823
|
-
Both hydrate without flash on the client.
|
|
824
|
-
|
|
825
|
-
---
|
|
826
|
-
|
|
827
|
-
## Styling: Tailwind-first + JS helpers
|
|
828
|
-
|
|
829
|
-
<!-- OVERRIDE -->
|
|
830
|
-
|
|
831
|
-
The scaffold compiles a **static Tailwind stylesheet** (`css:build` builds
|
|
832
|
-
`public/input.css` into the `public/tailwind.css` the layout links, so the
|
|
833
|
-
app is styled with JavaScript off) + `@theme` design tokens. The token
|
|
834
|
-
VALUES live on `:root` in the root layout (plain CSS, JS-off safe); the
|
|
835
|
-
`@theme` maps live in `public/input.css`. Every colour, font family,
|
|
836
|
-
fluid type scale value, and motion duration is declared once and
|
|
837
|
-
available everywhere via utility classes (`text-foreground`,
|
|
838
|
-
`bg-card`, `font-serif`, `duration-fast`, `text-display`).
|
|
839
|
-
|
|
840
|
-
**One theme, canonical tokens.** The app has a SINGLE theme, defined
|
|
841
|
-
once in `app/layout.ts` using the standard `@webjsdev/ui`
|
|
842
|
-
(shadcn-compatible) semantic tokens set to the brand palette. Use the
|
|
843
|
-
canonical utility names everywhere, in the page chrome AND inside
|
|
844
|
-
components: `bg-background`, `text-foreground`, `bg-card`, `bg-muted`,
|
|
845
|
-
`text-muted-foreground`, `bg-primary`, `text-primary-foreground`,
|
|
846
|
-
`bg-accent`, `text-accent-foreground`, `border-border`, `ring-ring`.
|
|
847
|
-
These are exactly the tokens a component copied in by
|
|
848
|
-
`webjs ui add <name>` reads, so a scaffolded page and a later-added ui
|
|
849
|
-
component share one coherent theme with no extra wiring. **Never invent a
|
|
850
|
-
parallel token vocabulary** (`--fg`, `--bg`, `text-fg`, `bg-elev`, a
|
|
851
|
-
separate `--brand`): it collides with the ui tokens (the accent once
|
|
852
|
-
flipped to neutral on navigation for exactly this reason) and diverges
|
|
853
|
-
from the shadcn conventions the kit and AI agents both expect. Reach for
|
|
854
|
-
opacity modifiers (`bg-primary/10`, `hover:bg-primary/90`,
|
|
855
|
-
`text-muted-foreground/70`) before adding a token; to ADD one, do it the
|
|
856
|
-
canonical way (a `--x` variable in the `:root` / `.dark` blocks plus a
|
|
857
|
-
`--color-x: var(--x)` line in `@theme inline`, then `bg-x` / `text-x`).
|
|
858
|
-
|
|
859
|
-
**Tailwind-first is the strong default for pages AND light-DOM
|
|
860
|
-
components (the default DOM mode).** Use utilities for layout, spacing,
|
|
861
|
-
color (via the `@theme` tokens), typography, borders, radius, shadows,
|
|
862
|
-
and interaction states (hover/focus/active/disabled, dark mode). Light
|
|
863
|
-
DOM does not scope styles, so utilities apply directly.
|
|
864
|
-
|
|
865
|
-
**Pin a header with `position: fixed`, never `position: sticky`.** A
|
|
866
|
-
sticky header flickers its background for one frame on iOS WebKit (every
|
|
867
|
-
iOS browser) during a client-router navigation, because the preserved
|
|
868
|
-
header plus the scroll-to-top trips a WebKit sticky-repaint bug that the
|
|
869
|
-
usual GPU-promotion hacks (`translateZ`, `will-change`) do NOT fix. Use
|
|
870
|
-
`position: fixed` and reserve the header height on the content with a
|
|
871
|
-
`--header-height` variable (the scaffolded `app/layout.ts` does exactly
|
|
872
|
-
this, kept exact by a `ResizeObserver`). It is iOS-only, invisible on
|
|
873
|
-
desktop, Android, and in DevTools emulation, so it shows only on a real
|
|
874
|
-
device.
|
|
875
|
-
|
|
876
|
-
**The lit muscle-memory trap.** If you have written lit, the habit is to
|
|
877
|
-
scope CSS in a shadow root (`static styles = css\`\``) or write an inline
|
|
878
|
-
`<style>` with semantic class names (`.hero`, `.feature`, `.card`) for
|
|
879
|
-
every component. In a webjs light-DOM component the scoped block does
|
|
880
|
-
nothing without `static shadow = true`, and the inline class names leak
|
|
881
|
-
into the global namespace. Prefer Tailwind utilities. When the same
|
|
882
|
-
bundle repeats, extract a `lib/utils/ui.ts` helper (below), not a CSS
|
|
883
|
-
class.
|
|
884
|
-
|
|
885
|
-
**Custom-CSS allowlist (the only things raw CSS is for).** Reserve raw
|
|
886
|
-
CSS for what utilities cannot express: design-token `:root` / `@theme`
|
|
887
|
-
definitions, `@property` animated custom properties with `@keyframes`,
|
|
888
|
-
`::-webkit-scrollbar` / `scrollbar-color`, `prefers-reduced-motion`
|
|
889
|
-
blocks, and complex `color-mix()` or gradient effects. When custom CSS is
|
|
890
|
-
unavoidable in a light-DOM component, the class-prefix rule (see the
|
|
891
|
-
Components section above) still applies. Shadow-DOM components
|
|
892
|
-
(`static shadow = true`) legitimately author `static styles = css\`\``;
|
|
893
|
-
that is the right home for scoped CSS.
|
|
894
|
-
|
|
895
|
-
**Dedup repeated Tailwind class bundles with JS helpers, not `@apply`.**
|
|
896
|
-
When the same string of classes appears in 2+ places, extract it into a
|
|
897
|
-
small function in `lib/utils/ui.ts`:
|
|
898
|
-
|
|
899
|
-
```ts
|
|
900
|
-
// lib/utils/ui.ts
|
|
901
|
-
import { html } from '@webjsdev/core';
|
|
902
|
-
|
|
903
|
-
export function rubric(label: string) {
|
|
904
|
-
return html`
|
|
905
|
-
<span class="block font-mono text-[11px] leading-none font-semibold tracking-[0.2em] uppercase text-accent mb-4">● ${label}</span>
|
|
906
|
-
`;
|
|
907
|
-
}
|
|
908
|
-
```
|
|
909
|
-
|
|
910
|
-
Consume:
|
|
911
|
-
|
|
912
|
-
```ts
|
|
913
|
-
// app/page.ts
|
|
914
|
-
import { rubric } from '#lib/utils/ui.ts';
|
|
915
|
-
|
|
916
|
-
export default function Home() {
|
|
917
|
-
return html`
|
|
918
|
-
${rubric('welcome')}
|
|
919
|
-
<h1 class="font-serif text-display">Hello</h1>
|
|
920
|
-
`;
|
|
921
|
-
}
|
|
922
|
-
```
|
|
923
|
-
|
|
924
|
-
Helpers run at SSR time inside `html\`\``, so the output is identical
|
|
925
|
-
to writing the classes inline. No client-side runtime.
|
|
926
|
-
|
|
927
|
-
**Why not `@apply`?** `@apply` hides which utilities back a class and
|
|
928
|
-
creates a second source of truth. JS helpers keep the class bundle
|
|
929
|
-
visible at the definition site and compose naturally with conditional
|
|
930
|
-
classes and active states.
|
|
931
|
-
|
|
932
|
-
**Custom CSS is still supported.** Plain `<style>` blocks, CSS modules,
|
|
933
|
-
or a build-step pipeline. The framework has no hard dependency on Tailwind.
|
|
934
|
-
If you mix custom CSS into a light-DOM component, apply the class-prefix
|
|
935
|
-
rule (see Components section above).
|
|
936
|
-
|
|
937
|
-
**Dark mode uses two signals; a theme switch must set both.** The editorial
|
|
938
|
-
chrome tokens (`--fg`, `--bg`, `--accent`) follow a `data-theme` attribute on
|
|
939
|
-
`<html>`; the Webjs UI kit under `components/ui/` follows a `.dark` class
|
|
940
|
-
(`@custom-variant dark (&:is(.dark *))`). The scaffold's head init script and
|
|
941
|
-
`theme-toggle` set **both** (`data-theme` AND
|
|
942
|
-
`classList.toggle('dark', isDark)`). If you replace the toggle or build your
|
|
943
|
-
own theme switch, set both, or the `components/ui/*` render light tokens on a
|
|
944
|
-
dark page (white buttons and cards, invisible text) while the chrome looks
|
|
945
|
-
correct. Light mode hides this (both systems default to light), so **verify
|
|
946
|
-
dark mode in a browser, not just light.**
|
|
947
|
-
|
|
948
|
-
---
|
|
949
|
-
|
|
950
|
-
## Styling alternative: vanilla CSS end-to-end
|
|
951
|
-
|
|
952
|
-
<!-- OVERRIDE -->
|
|
953
|
-
|
|
954
|
-
If you'd rather skip Tailwind, webjs works with plain CSS as long as you
|
|
955
|
-
wrap pages, layouts, and components so class names don't collide in the
|
|
956
|
-
global light-DOM namespace.
|
|
957
|
-
|
|
958
|
-
**Convention: three scopes**
|
|
959
|
-
|
|
960
|
-
| Scope | Wrapper | Derivation |
|
|
961
|
-
|---|---|---|
|
|
962
|
-
| **Component** | Custom-element tag | Tag is already unique |
|
|
963
|
-
| **Page** | `.page-<route>` | `app/dashboard/page.ts` → `.page-dashboard`; `app/blog/[slug]/page.ts` → `.page-blog-slug`; root `app/page.ts` → `.page-home` |
|
|
964
|
-
| **Layout** | `.layout-<name>` | `app/layout.ts` → `.layout-root`; `app/admin/layout.ts` → `.layout-admin` |
|
|
965
|
-
|
|
966
|
-
Every page wraps its output in `<div class="page-<route>">`. Every
|
|
967
|
-
layout wraps in `<div class="layout-<name>">`. Components scope via
|
|
968
|
-
their tag. In a PAGE or LAYOUT (which render server-only and never
|
|
969
|
-
hydrate) styles colocate as `const STYLES = css\`…\`` + `<style>${'$'}{STYLES.text}</style>`.
|
|
970
|
-
In a COMPONENT, do NOT interpolate into `<style>` (the client drops the
|
|
971
|
-
raw-text hole on hydrate, so the styles vanish); use `static styles =
|
|
972
|
-
css\`…\`` or Tailwind classes instead.
|
|
973
|
-
|
|
974
|
-
```ts
|
|
975
|
-
// app/dashboard/page.ts
|
|
976
|
-
import { html, css } from '@webjsdev/core';
|
|
977
|
-
|
|
978
|
-
const STYLES = css\`
|
|
979
|
-
.page-dashboard {
|
|
980
|
-
.actions { display: flex; gap: 12px; }
|
|
981
|
-
.btn { padding: 12px 24px; border-radius: 999px; }
|
|
982
|
-
.btn-primary { background: var(--accent); color: var(--accent-fg); }
|
|
983
|
-
}
|
|
984
|
-
\`;
|
|
985
|
-
|
|
986
|
-
export default function Dashboard() {
|
|
987
|
-
return html\`
|
|
988
|
-
<style>${'$'}{STYLES.text}</style>
|
|
989
|
-
<div class="page-dashboard">
|
|
990
|
-
<div class="actions">
|
|
991
|
-
<a class="btn btn-primary" href="/new">+ New</a>
|
|
992
|
-
</div>
|
|
993
|
-
</div>
|
|
994
|
-
\`;
|
|
995
|
-
}
|
|
996
|
-
```
|
|
997
|
-
|
|
998
|
-
Inside each scope, `.btn` / `.input` / `.form` / `.item` are free
|
|
999
|
-
names. CSS descendant combinators stop them at the scope boundary.
|
|
1000
|
-
A small curated set of **primitives** (`rubric`, `banner`,
|
|
1001
|
-
`accent-link`, `display-h1`, …) can live global in the root layout
|
|
1002
|
-
as your design system.
|
|
1003
|
-
|
|
1004
|
-
**When you'd pick this over Tailwind:**
|
|
1005
|
-
- You want zero runtime scripts and zero build step.
|
|
1006
|
-
- You prefer idiomatic CSS and plain-cascade debugging.
|
|
1007
|
-
- You already have a design system in CSS custom properties.
|
|
1008
|
-
|
|
1009
|
-
**Costs:**
|
|
1010
|
-
- Write more per-file CSS (no utility ecosystem).
|
|
1011
|
-
- Discipline: every page/layout remembers to wrap.
|
|
1012
|
-
- Renaming a route folder = 2 textual edits in one file (the wrapper class + the matching `class=` attribute).
|
|
1013
|
-
|
|
1014
|
-
Pick one convention per project and stay consistent.
|
|
1015
|
-
|
|
1016
|
-
---
|
|
1017
|
-
|
|
1018
|
-
## Rate limiting & middleware
|
|
1019
|
-
|
|
1020
|
-
<!-- OVERRIDE -->
|
|
1021
|
-
Use `rateLimit()` as per-segment middleware to protect routes:
|
|
1022
|
-
|
|
1023
|
-
```ts
|
|
1024
|
-
// app/api/auth/middleware.ts: protect auth endpoints
|
|
1025
|
-
import { rateLimit } from '@webjsdev/server';
|
|
1026
|
-
|
|
1027
|
-
// Direct deploy (default). Keys on the socket-stamped IP, ignoring
|
|
1028
|
-
// forwarded-IP headers.
|
|
1029
|
-
export default rateLimit({ window: '10s', max: 5 });
|
|
1030
|
-
|
|
1031
|
-
// Behind a reverse proxy or CDN (Cloudflare, Railway, Fly, Vercel,
|
|
1032
|
-
// nginx, Caddy). Set trustProxy to honour X-Forwarded-For. The proxy
|
|
1033
|
-
// MUST strip inbound X-Forwarded-For before adding its own.
|
|
1034
|
-
export default rateLimit({ window: '10s', max: 5, trustProxy: true });
|
|
1035
|
-
```
|
|
1036
|
-
|
|
1037
|
-
Place `middleware.ts` at any route level. It applies to that subtree only.
|
|
1038
|
-
Chain runs outermost → innermost.
|
|
1039
|
-
|
|
1040
|
-
---
|
|
1041
|
-
|
|
1042
|
-
## Lazy loading
|
|
1043
|
-
|
|
1044
|
-
<!-- OVERRIDE -->
|
|
1045
|
-
For below-the-fold components with heavy JS, defer loading until visible:
|
|
1046
|
-
|
|
1047
|
-
```ts
|
|
1048
|
-
class HeavyChart extends WebComponent {
|
|
1049
|
-
static lazy = true; // module loaded on scroll, not on page load
|
|
1050
|
-
// ...
|
|
1051
|
-
}
|
|
1052
|
-
```
|
|
1053
|
-
|
|
1054
|
-
SSR content is visible immediately. Only the JS download is deferred.
|
|
1055
|
-
**Do NOT use** for above-the-fold or critical UI (navigation, forms).
|
|
1056
|
-
|
|
1057
|
-
---
|
|
1058
|
-
|
|
1059
|
-
## REST endpoints from server actions (route.ts)
|
|
1060
|
-
|
|
1061
|
-
<!-- OVERRIDE -->
|
|
1062
|
-
A server action is RPC-callable from components. To ALSO reach the same
|
|
1063
|
-
function over plain HTTP (mobile apps, webhooks, third parties), put it behind
|
|
1064
|
-
a `route.ts` handler. The action stays a normal `'use server'` function; the
|
|
1065
|
-
route imports and calls it.
|
|
1066
|
-
|
|
1067
|
-
```ts
|
|
1068
|
-
// modules/posts/actions/create-post.server.ts
|
|
1069
|
-
'use server';
|
|
1070
|
-
import { db } from '#db/connection.server.ts';
|
|
1071
|
-
import { posts } from '#db/schema.server.ts';
|
|
1072
|
-
export async function createPost({ title, body }) {
|
|
1073
|
-
const [post] = await db.insert(posts).values({ title, body }).returning();
|
|
1074
|
-
return post;
|
|
1075
|
-
}
|
|
1076
|
-
```
|
|
1077
|
-
|
|
1078
|
-
```ts
|
|
1079
|
-
// app/api/posts/route.ts
|
|
1080
|
-
import { route } from '@webjsdev/server';
|
|
1081
|
-
import { createPost } from '#modules/posts/actions/create-post.server.ts';
|
|
1082
|
-
// The route() adapter merges query + route params + JSON body into one input
|
|
1083
|
-
// object and JSON-responds the result. Pass { validate } to guard the input.
|
|
1084
|
-
export const POST = route(createPost);
|
|
1085
|
-
```
|
|
1086
|
-
|
|
1087
|
-
A hand-written `route.ts` (a `POST(req)` that reads the body and calls the
|
|
1088
|
-
action) is always available for full control (custom headers, streaming).
|
|
1089
|
-
|
|
1090
|
-
**Security:** a `route.ts` REST endpoint is NOT CSRF-protected (only the RPC
|
|
1091
|
-
path is). Authenticate every mutating endpoint via bearer tokens, API keys, or
|
|
1092
|
-
auth middleware.
|
|
1093
|
-
|
|
1094
|
-
---
|
|
1095
|
-
|
|
1096
|
-
## Progressive enhancement (write HTML-first)
|
|
1097
|
-
|
|
1098
|
-
<!-- OVERRIDE -->
|
|
1099
|
-
|
|
1100
|
-
WebJs pages work without JavaScript by design. Read-paths render to
|
|
1101
|
-
real HTML on the server. Write-paths run through plain `<form>` plus
|
|
1102
|
-
server actions, and navigation is a real `<a href>`. Every web component
|
|
1103
|
-
is SSR'd too. Its `render()` runs on the server, so the component's
|
|
1104
|
-
initial markup is in the response before any script loads. With JS
|
|
1105
|
-
disabled, a display-only custom element looks correct, and an
|
|
1106
|
-
interactive one (counter, dropdown, tabs) still paints its initial
|
|
1107
|
-
state. Only the *interactivity itself* (the +/- click, the open/close
|
|
1108
|
-
toggle, the tab switch) requires JS.
|
|
1109
|
-
|
|
1110
|
-
**Default rules:**
|
|
1111
|
-
- **Forms must work as plain HTML POSTs.** Use `<form action=…>` bound
|
|
1112
|
-
to a server action. Never `fetch` + a JS click handler for the
|
|
1113
|
-
happy path. The framework upgrades the form to a partial-swap
|
|
1114
|
-
submission automatically when the client router is active, and with
|
|
1115
|
-
JS disabled the same form does a full-page POST and works identically
|
|
1116
|
-
end-to-end.
|
|
1117
|
-
- **Links must be real `<a href="…">`.** Don't roll a JS-only click
|
|
1118
|
-
handler for navigation. The client router intercepts `<a>` clicks and
|
|
1119
|
-
enhances them into SPA transitions. Without JS, the browser navigates
|
|
1120
|
-
the old-fashioned way.
|
|
1121
|
-
- **Custom elements are the only place JS is allowed to be required.**
|
|
1122
|
-
If a feature works without state (a styled card, a layout, a list,
|
|
1123
|
-
a marketing section), it should not be a custom element with
|
|
1124
|
-
lifecycle. Use a plain function returning `html\`…\`` or a Tier-1
|
|
1125
|
-
Webjs-UI class helper (`buttonClass`, `cardClass`).
|
|
1126
|
-
- **Test JS-off explicitly before marking a feature done.** Open the
|
|
1127
|
-
page in a browser with JS disabled (DevTools → Settings → Debugger
|
|
1128
|
-
→ "Disable JavaScript") and exercise the user's read + write paths.
|
|
1129
|
-
If a write fails without JS, you've reached for `fetch` where a
|
|
1130
|
-
server action would have done the job.
|
|
1131
|
-
- **Don't gate read-paths on hydration.** Never write components whose
|
|
1132
|
-
SSR'd HTML is empty or wrong on purpose with the expectation that
|
|
1133
|
-
JS will fill it in. The first paint must be the right content.
|
|
1134
|
-
- **Label every interactive control.** Give each control an accessible
|
|
1135
|
-
name, and make clickable text a `<label for="control-id">` (or the
|
|
1136
|
-
control itself) so a text click activates the control on BOTH the JS
|
|
1137
|
-
path and the no-JS form-submit path. Use `aria-label` and
|
|
1138
|
-
`aria-pressed` on icon-only controls. `assertNoA11yViolations(el)` in a
|
|
1139
|
-
browser test (see the Testing section) catches missing labels.
|
|
1140
|
-
|
|
1141
|
-
**SSR-meaningful component state.** The SSR pipeline constructs the
|
|
1142
|
-
component, applies its attributes, runs `willUpdate` and controllers'
|
|
1143
|
-
`hostUpdate`, and calls `render()`. It does NOT call `connectedCallback`,
|
|
1144
|
-
`firstUpdated`, `updated`, or any other browser-only lifecycle hook.
|
|
1145
|
-
Whatever state should appear on first paint MUST be set in the
|
|
1146
|
-
constructor (after `super()`), derived in `willUpdate`, or derivable
|
|
1147
|
-
from the factory-declared reactive props + attributes on the rendered
|
|
1148
|
-
tag. Reading
|
|
1149
|
-
`this.getAttribute` / `hasAttribute` in `render()` works server-side (a
|
|
1150
|
-
server attribute shim backs the attribute methods), but a `Task`'s
|
|
1151
|
-
fetch still runs only on the client.
|
|
1152
|
-
|
|
1153
|
-
```ts
|
|
1154
|
-
import { WebComponent, html, signal } from '@webjsdev/core';
|
|
1155
|
-
|
|
1156
|
-
class Cart extends WebComponent {
|
|
1157
|
-
items = signal<Item[]>([]); // instance signal, SSR uses this for first paint
|
|
1158
|
-
|
|
1159
|
-
connectedCallback() {
|
|
1160
|
-
super.connectedCallback();
|
|
1161
|
-
// Browser-only refinement, read localStorage and write the
|
|
1162
|
-
// signal. The component re-renders automatically.
|
|
1163
|
-
const stored = readFromLocalStorage();
|
|
1164
|
-
if (stored) this.items.set(stored);
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
render() {
|
|
1168
|
-
return html`<ul>${this.items.get().map(/* … */)}</ul>`;
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
```
|
|
1172
|
-
|
|
1173
|
-
Where the data lives, where to read it:
|
|
1174
|
-
|
|
1175
|
-
| Data source | Where to read it |
|
|
1176
|
-
|---|---|
|
|
1177
|
-
| Database, session, cookies, request headers | Page function (server). Pass to component as attribute / property. |
|
|
1178
|
-
| Component's own initial defaults | Component `constructor()` after `super()`. |
|
|
1179
|
-
| Browser-only: `localStorage`, viewport, `matchMedia`, `navigator.*` | Component `connectedCallback()`, then write a signal (instance-scoped in the constructor, or module-scope if shared) to refine. |
|
|
1180
|
-
| Theme color, RTL direction (flash-sensitive) | Synchronous inline `<script>` in root layout that sets `document.documentElement` attributes before custom elements upgrade. |
|
|
1181
|
-
|
|
1182
|
-
---
|
|
1183
|
-
|
|
1184
|
-
## Server actions
|
|
1185
|
-
|
|
1186
|
-
<!-- OVERRIDE -->
|
|
1187
|
-
|
|
1188
|
-
**The `.server.ts` vs `'use server'` decision, in one question.** Will the
|
|
1189
|
-
client call it? Add `'use server'` and the file becomes an RPC action
|
|
1190
|
-
(the browser import is rewritten to a typed stub). Is it server-only
|
|
1191
|
-
infra instead (a DB driver, secrets, `node:*`)? Use NO directive, and
|
|
1192
|
-
never import it into a page, layout, or component. Reach it from a
|
|
1193
|
-
`'use server'` action, a `route.ts` handler, or `middleware.ts`. A
|
|
1194
|
-
`.server.ts` file WITHOUT the directive is a server-only utility whose
|
|
1195
|
-
browser import throws at module load, so a page/component that imports it
|
|
1196
|
-
directly crashes on the client.
|
|
1197
|
-
|
|
1198
|
-
```ts
|
|
1199
|
-
// modules/posts/actions/create-post.server.ts
|
|
1200
|
-
'use server';
|
|
1201
|
-
import { db } from '#db/connection.server.ts';
|
|
1202
|
-
import { posts } from '#db/schema.server.ts';
|
|
1203
|
-
import type { ActionResult } from '../types.ts';
|
|
1204
|
-
|
|
1205
|
-
export async function createPost(input: {
|
|
1206
|
-
title: string;
|
|
1207
|
-
body: string;
|
|
1208
|
-
}): Promise<ActionResult<Post>> {
|
|
1209
|
-
// validate, create, return
|
|
1210
|
-
}
|
|
1211
|
-
```
|
|
1212
|
-
|
|
1213
|
-
**Rules:**
|
|
1214
|
-
- One function per file (greppable, AI-agent friendly)
|
|
1215
|
-
- File name matches function name: `create-post.server.ts` → `createPost`
|
|
1216
|
-
- Return `ActionResult<T>` envelope for actions that can fail
|
|
1217
|
-
- Never throw for expected errors. Return `{ success: false, error, status }`
|
|
1218
|
-
- Validate input at the top of the function
|
|
1219
|
-
|
|
1220
|
-
---
|
|
1221
|
-
|
|
1222
|
-
## Mutations: default to optimistic UI
|
|
1223
|
-
|
|
1224
|
-
<!-- OVERRIDE -->
|
|
1225
|
-
|
|
1226
|
-
Default to optimistic UI for every feasible mutation. Use `optimistic()`
|
|
1227
|
-
from `@webjsdev/core` (create, toggle, like, follow, reorder, rename,
|
|
1228
|
-
status change) so the UI updates instantly and rolls back automatically
|
|
1229
|
-
on failure. No hand-written try-catch, cache-and-restore, or temp-id
|
|
1230
|
-
reconciliation.
|
|
1231
|
-
|
|
1232
|
-
```ts
|
|
1233
|
-
import { WebComponent, prop, optimistic, html } from '@webjsdev/core';
|
|
1234
|
-
import { createTodo } from '#modules/todos/actions/create-todo.server.ts';
|
|
1235
|
-
|
|
1236
|
-
class TodoList extends WebComponent({ todos: prop<Todo[]>(Array) }) {
|
|
1237
|
-
private optimisticTodos = optimistic(this, {
|
|
1238
|
-
source: () => this.todos,
|
|
1239
|
-
update: (state, title: string) => [...state, { title, pending: true }],
|
|
1240
|
-
});
|
|
1241
|
-
|
|
1242
|
-
async handleSubmit(title: string) {
|
|
1243
|
-
const promise = createTodo({ title });
|
|
1244
|
-
this.optimisticTodos.add(title, promise); // auto-releases on settle
|
|
1245
|
-
await promise;
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
render() {
|
|
1249
|
-
return html`<ul>${this.optimisticTodos.value.map(t => html`
|
|
1250
|
-
<li class=${t.pending ? 'opacity-50' : ''}>${t.title}</li>`)}</ul>`;
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
```
|
|
1254
|
-
|
|
1255
|
-
Do NOT reach for optimistic UI where it hurts: unpredictable or
|
|
1256
|
-
server-computed results (AI output, server-assigned values the client
|
|
1257
|
-
cannot guess), side-effectful mutations the user must wait on (payment,
|
|
1258
|
-
email, OAuth), and destructive irreversible actions (a confirm-first UX
|
|
1259
|
-
is better). See `agent-docs/advanced.md` for the full API.
|
|
1260
|
-
|
|
1261
|
-
---
|
|
1262
|
-
|
|
1263
|
-
## Code style
|
|
1264
|
-
|
|
1265
|
-
<!-- OVERRIDE -->
|
|
1266
|
-
- TypeScript with explicit `.ts` extensions in imports
|
|
1267
|
-
- **Erasable TypeScript only.** The runtime strips types at the runtime layer (Node 24+'s built-in `module.stripTypeScriptTypes`, or `amaro` on Bun, byte-identical) with whitespace replacement, so line + column positions are byte-exact and no sourcemap ships. Your `tsconfig.json` sets `erasableSyntaxOnly: true`, so the compiler rejects: `enum`, `namespace` with values, constructor parameter properties, legacy decorators with `emitDecoratorMetadata`, and `import = require`. Write the erasable equivalents:
|
|
1268
|
-
```ts
|
|
1269
|
-
// Not allowed
|
|
1270
|
-
enum Color { Red, Green, Blue }
|
|
1271
|
-
class Foo { constructor(public x: number) {} }
|
|
1272
|
-
|
|
1273
|
-
// Erasable equivalents
|
|
1274
|
-
const Color = { Red: 'Red', Green: 'Green', Blue: 'Blue' } as const;
|
|
1275
|
-
type Color = typeof Color[keyof typeof Color];
|
|
1276
|
-
|
|
1277
|
-
class Foo {
|
|
1278
|
-
x: number;
|
|
1279
|
-
constructor(x: number) { this.x = x; }
|
|
1280
|
-
}
|
|
1281
|
-
```
|
|
1282
|
-
If you turn `erasableSyntaxOnly` off and use non-erasable syntax, the dev server fails at strip time and returns a 500 pointing at the `no-non-erasable-typescript` lint rule. WebJs is buildless end-to-end and has no bundler fallback. The `erasable-typescript-only` convention check warns when the flag is off.
|
|
1283
|
-
- No semicolons (or with semicolons, pick one and stay consistent)
|
|
1284
|
-
- `const` by default, `let` when needed, never `var`
|
|
1285
|
-
- Prefer `async/await` over `.then()` chains
|
|
1286
|
-
- Minimal comments. Code should be self-documenting
|
|
1287
|
-
- No barrel files (`index.ts` re-exporting everything). Import from the source directly
|
|
1288
|
-
|
|
1289
|
-
---
|
|
1290
|
-
|
|
1291
|
-
## Git workflow
|
|
1292
|
-
|
|
1293
|
-
<!-- OVERRIDE -->
|
|
1294
|
-
|
|
1295
|
-
This project enforces a git workflow via agent-specific config files
|
|
1296
|
-
(`.claude/settings.json`, `.cursorrules`, `.agents/rules/workflow.md`,
|
|
1297
|
-
`.github/copilot-instructions.md`). These rules apply to ALL AI agents:
|
|
1298
|
-
|
|
1299
|
-
**Commit rules:**
|
|
1300
|
-
- **Commit per logical unit, not at the end.** One feature, one fix, one
|
|
1301
|
-
rename, one doc rewrite per commit. Push after each commit.
|
|
1302
|
-
- **Hard limit.** If you have 5+ unstaged files spanning different concerns,
|
|
1303
|
-
commit before continuing. The `.claude/hooks/nudge-uncommitted.sh` hook
|
|
1304
|
-
fires at threshold 4 to enforce this. Do not ignore the reminder.
|
|
1305
|
-
- **Meaningful messages.** Imperative mood, what changed and why
|
|
1306
|
-
(`Add contact form with email validation`, not `update files`).
|
|
1307
|
-
- **NEVER add AI attribution.** No `Co-Authored-By: Claude`, no
|
|
1308
|
-
`Generated by AI`, no `AI-assisted` trailers or prefixes.
|
|
1309
|
-
- **Committing is automatic (this overrides Claude Code's default).** The
|
|
1310
|
-
user should never have to ask "please commit". Commit after completing each
|
|
1311
|
-
logical unit. Claude Code's built-in never-commit default is overridden by
|
|
1312
|
-
`CLAUDE.md`, and the `.claude/hooks/commit-before-stop.sh` Stop hook refuses
|
|
1313
|
-
to end a turn with a pile of uncommitted work on a feature branch (disable
|
|
1314
|
-
with `WEBJS_NO_COMMIT_STOP=1`).
|
|
1315
|
-
|
|
1316
|
-
**Branch rules:**
|
|
1317
|
-
- **Feature branches.** Never commit directly to main
|
|
1318
|
-
- **Branch naming.** `feature/<name>`, `fix/<name>`, `refactor/<name>`
|
|
1319
|
-
- **Pull requests.** Always create a PR, never push to main directly
|
|
1320
|
-
- **NEVER merge without user permission.** Before merging ANY branch
|
|
1321
|
-
into ANY other branch, ask: "Ready to merge `<branch>` into `<target>`?
|
|
1322
|
-
Delete or keep `<branch>` after?" Wait for approval AND the preference.
|
|
1323
|
-
- **Claude Code hook** (`.claude/hooks/guard-main-merge.sh`) enforces
|
|
1324
|
-
merge/push-to-main approval programmatically for Claude agents.
|
|
1325
|
-
Other agents enforce this via `.cursorrules`, `.agents/rules/workflow.md`,
|
|
1326
|
-
`.github/copilot-instructions.md`.
|
|
1327
|
-
|
|
1328
|
-
**Pre-commit hook (`.hooks/pre-commit`):**
|
|
1329
|
-
- Blocks commits to `main` / `master`. Nothing else runs locally, so
|
|
1330
|
-
`git commit` stays fast. Keeping commits to one logical unit (no
|
|
1331
|
-
unrelated files) is your discipline plus the `nudge-uncommitted`
|
|
1332
|
-
hooks, not something this hook enforces.
|
|
1333
|
-
|
|
1334
|
-
**CI gate (`.github/workflows/ci.yml`), on every PR and push to main:**
|
|
1335
|
-
- `webjs check` (conventions) must pass
|
|
1336
|
-
- `webjs test` (unit + integration), the browser layer, and the e2e
|
|
1337
|
-
layer must pass
|
|
1338
|
-
- Mark these as required status checks in the branch-protection rule for
|
|
1339
|
-
main so a PR can only merge when the gate is green. The gate lives in
|
|
1340
|
-
CI, where a local `--no-verify` cannot skip it.
|
|
1341
|
-
|
|
1342
|
-
---
|
|
1343
|
-
|
|
1344
|
-
## Customizing conventions
|
|
1345
|
-
|
|
1346
|
-
The conventions in this file are guidance, so customize them directly:
|
|
1347
|
-
edit the prose under any `<!-- OVERRIDE -->` marker. There is no
|
|
1348
|
-
`package.json` switch and nothing to toggle, because conventions are not
|
|
1349
|
-
enforced by a tool.
|
|
1350
|
-
|
|
1351
|
-
`webjs check` is separate: it runs only correctness checks (a crash, a
|
|
1352
|
-
security leak, a build/type-strip failure), always, with no per-project
|
|
1353
|
-
disabling. Run `webjs check` to validate, and `webjs check --rules` to
|
|
1354
|
-
list those checks.
|
|
1355
|
-
|
|
1356
|
-
---
|
|
1357
|
-
|
|
1358
|
-
## Scaffold
|
|
1359
|
-
|
|
1360
|
-
Create new projects with `webjs create`:
|
|
1361
|
-
|
|
1362
|
-
```sh
|
|
1363
|
-
webjs create <name> # full-stack (default)
|
|
1364
|
-
webjs create <name> --template api # backend-only API
|
|
1365
|
-
webjs create <name> --template saas # auth + dashboard + Drizzle User model
|
|
1366
|
-
```
|
|
1367
|
-
|
|
1368
|
-
**Route-wrapping pattern (especially for `--template api` apps):**
|
|
1369
|
-
Routes are thin wrappers over typed server actions. Business logic lives in
|
|
1370
|
-
`modules/`, routes just import and call the action/query:
|
|
1371
|
-
|
|
1372
|
-
```ts
|
|
1373
|
-
// app/api/users/route.ts: thin wrapper
|
|
1374
|
-
import { listUsers } from '#modules/users/queries/list-users.server.ts';
|
|
1375
|
-
import { createUser } from '#modules/users/actions/create-user.server.ts';
|
|
1376
|
-
|
|
1377
|
-
export async function GET() { return Response.json(await listUsers()); }
|
|
1378
|
-
export async function POST(req: Request) {
|
|
1379
|
-
const result = await createUser(await req.json());
|
|
1380
|
-
if (!result.success) return Response.json({ error: result.error }, { status: result.status });
|
|
1381
|
-
return Response.json(result.data, { status: 201 });
|
|
1382
|
-
}
|
|
1383
|
-
```
|
|
1
|
+
# Conventions for {{APP_NAME}}
|
|
2
|
+
|
|
3
|
+
The conventions for building a WebJs app live in the agent skill. **Read
|
|
4
|
+
`AGENTS.md` first, then `.agents/skills/webjs/SKILL.md`** (it routes to focused
|
|
5
|
+
references under `.agents/skills/webjs/references/`, loaded on demand). This file
|
|
6
|
+
is the short version.
|
|
7
|
+
|
|
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 feature gallery to learn from.** Single-concept demos
|
|
21
|
+
under `app/features/` plus the `app/examples/todo` app, with logic in
|
|
22
|
+
`modules/`. When you build a real app: learn from the gallery FIRST (skim the
|
|
23
|
+
demos relevant to your task; the skill teaches the same and survives the
|
|
24
|
+
clear), then run `npm run gallery:clear` to shed the demos and reset the home,
|
|
25
|
+
then grow the app in place.
|
|
26
|
+
- **Progressive enhancement is the default.** Pages render as HTML, `<a>`
|
|
27
|
+
navigates, `<form>` + a page action submits, all with JavaScript off; opt into
|
|
28
|
+
interactivity per behaviour inside a component.
|
|
29
|
+
- **Commit per logical unit** as soon as it is complete, and never push to `main`.
|
|
30
|
+
|
|
31
|
+
Everything else (the module architecture, the `ActionResult` envelope, styling,
|
|
32
|
+
testing, the client router, optimistic UI) is in the skill's references.
|