@webjskit/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/bin/webjs.js +226 -0
- package/lib/create.js +519 -0
- package/lib/saas-template.js +238 -0
- package/package.json +35 -0
- package/templates/.claude/hooks/guard-branch-context.sh +39 -0
- package/templates/.claude/hooks/guard-main-merge.sh +44 -0
- package/templates/.claude/settings.json +24 -0
- package/templates/.claude.json +9 -0
- package/templates/.cursorrules +63 -0
- package/templates/.editorconfig +18 -0
- package/templates/.env.example +27 -0
- package/templates/.github/copilot-instructions.md +59 -0
- package/templates/.github/pull_request_template.md +14 -0
- package/templates/.hooks/pre-commit +24 -0
- package/templates/.windsurfrules +53 -0
- package/templates/CLAUDE.md +70 -0
- package/templates/CONVENTIONS.md +589 -0
- package/templates/app/_utils/ui.ts +83 -0
- package/templates/public/tailwind-browser.js +947 -0
- package/templates/test/browser/example.test.js +40 -0
- package/templates/test/e2e/example.test.ts +87 -0
- package/templates/test/unit/example.test.ts +24 -0
- package/templates/web-test-runner.config.js +26 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# CLAUDE.md — {{APP_NAME}}
|
|
2
|
+
|
|
3
|
+
This file instructs AI coding agents on how to work in this project.
|
|
4
|
+
**Do not duplicate content here** — reference the authoritative sources below.
|
|
5
|
+
|
|
6
|
+
## Required reading (in this order)
|
|
7
|
+
|
|
8
|
+
1. **[AGENTS.md](./AGENTS.md)** — Full webjs API reference, file conventions,
|
|
9
|
+
invariants, recipes, directives, lifecycle, controllers, context, task.
|
|
10
|
+
2. **[CONVENTIONS.md](./CONVENTIONS.md)** — Project-specific conventions for
|
|
11
|
+
module architecture, testing rules, component patterns, code style.
|
|
12
|
+
Users may override sections.
|
|
13
|
+
|
|
14
|
+
## AI-driven development workflow
|
|
15
|
+
|
|
16
|
+
**CRITICAL: Every code change MUST include the following — automatically,
|
|
17
|
+
without the user having to ask:**
|
|
18
|
+
|
|
19
|
+
### 1. Commit and push often (mandatory, never skip)
|
|
20
|
+
|
|
21
|
+
**Commit AND push after each logical unit of work** — a completed
|
|
22
|
+
feature, a passing test, a doc update. Don't accumulate uncommitted
|
|
23
|
+
or unpushed changes. Small focused commits with meaningful messages.
|
|
24
|
+
No AI attribution trailers. Always `git push` after committing.
|
|
25
|
+
This is automatic — the user should never have to ask.
|
|
26
|
+
|
|
27
|
+
### 2. Tests (mandatory, never skip)
|
|
28
|
+
|
|
29
|
+
- **New server action or query** → add unit test in `test/unit/<module>.test.ts`
|
|
30
|
+
- **New or modified component** → add unit test (SSR rendering via `renderToString`)
|
|
31
|
+
- **New or modified page/route** → add E2E test in `test/browser/<feature>.test.ts`
|
|
32
|
+
- **Bug fix** → add regression test proving the fix
|
|
33
|
+
- **Refactor** → run existing tests, ensure they pass
|
|
34
|
+
|
|
35
|
+
After writing code, ALWAYS run `npx webjs test`. If E2E-relevant,
|
|
36
|
+
also run `npx webjs test --browser`. Never report a task as done with
|
|
37
|
+
failing tests.
|
|
38
|
+
|
|
39
|
+
### 3. Documentation (mandatory, never skip)
|
|
40
|
+
|
|
41
|
+
When adding or modifying features, update:
|
|
42
|
+
|
|
43
|
+
- **AGENTS.md** — API surface table, directive reference, recipes, or
|
|
44
|
+
relevant sections. This is the source of truth for the framework.
|
|
45
|
+
- **CONVENTIONS.md** — Only if the change introduces or modifies a convention.
|
|
46
|
+
|
|
47
|
+
If this project has a **docs/** directory, also:
|
|
48
|
+
- Add or update the relevant documentation page under `docs/`.
|
|
49
|
+
|
|
50
|
+
If this project has a **website/** directory, also:
|
|
51
|
+
- Update the website landing page if the feature is user-facing/marketable.
|
|
52
|
+
|
|
53
|
+
### 4. Convention validation
|
|
54
|
+
|
|
55
|
+
After making changes, run `npx webjs check` and fix any violations before
|
|
56
|
+
reporting the task as done.
|
|
57
|
+
|
|
58
|
+
## Quick reference
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
npx webjs dev # dev server with live reload
|
|
62
|
+
npx webjs test # run unit tests
|
|
63
|
+
npx webjs test --browser # run unit + E2E tests
|
|
64
|
+
npx webjs check # validate conventions
|
|
65
|
+
npx webjs build # (optional) production bundle
|
|
66
|
+
npx webjs start # production server
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
All API details, recipes, and feature documentation → see **[AGENTS.md](./AGENTS.md)**.
|
|
70
|
+
All project conventions and overrides → see **[CONVENTIONS.md](./CONVENTIONS.md)**.
|
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
# CONVENTIONS.md — {{APP_NAME}}
|
|
2
|
+
|
|
3
|
+
This file defines the conventions for this webjs app. **AI agents MUST read
|
|
4
|
+
this file before writing any code.** It is the single source of truth for
|
|
5
|
+
how code should be structured, tested, and organized.
|
|
6
|
+
|
|
7
|
+
Sections marked `<!-- OVERRIDE -->` contain defaults you can customize.
|
|
8
|
+
Edit the content below the marker to change the convention for your project.
|
|
9
|
+
The `webjs check` command validates your code against these conventions.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## AI agent workflow (non-negotiable)
|
|
14
|
+
|
|
15
|
+
**These rules apply to ALL AI agents (Claude, Cursor, Copilot, etc.)
|
|
16
|
+
working on this codebase. They are not optional and must not be skipped
|
|
17
|
+
even if the user doesn't explicitly ask.**
|
|
18
|
+
|
|
19
|
+
### Before starting ANY work — verify and sync the branch:
|
|
20
|
+
|
|
21
|
+
1. Check `git branch --show-current`
|
|
22
|
+
2. If on `main`/`master` → create a feature branch first
|
|
23
|
+
3. If on a feature branch → verify it matches the current task
|
|
24
|
+
4. Sync with parent: `git fetch origin && git rebase origin/main` if behind
|
|
25
|
+
5. Don't mix unrelated work on the wrong branch
|
|
26
|
+
|
|
27
|
+
### Every code change must include:
|
|
28
|
+
|
|
29
|
+
1. **Commit and push** — Commit AND push after each logical unit of work.
|
|
30
|
+
Small, focused commits with meaningful messages. Always `git push`
|
|
31
|
+
after committing. Don't accumulate uncommitted or unpushed changes.
|
|
32
|
+
This is automatic — the user should never have to ask.
|
|
33
|
+
|
|
34
|
+
2. **Tests** — Unit test for logic, E2E test for user-facing behavior.
|
|
35
|
+
See the "Testing" section below for what type of test each change needs.
|
|
36
|
+
Run `npx webjs test` after every change. Never mark work as done with
|
|
37
|
+
failing tests.
|
|
38
|
+
|
|
39
|
+
3. **Documentation updates** — When adding or modifying features:
|
|
40
|
+
- Update `AGENTS.md` if the change affects the framework API surface.
|
|
41
|
+
- Update `CONVENTIONS.md` only if the change introduces a new convention.
|
|
42
|
+
- If a `docs/` directory exists, add or update the relevant doc page.
|
|
43
|
+
- If a `website/` directory exists, update the landing page for
|
|
44
|
+
user-facing features.
|
|
45
|
+
|
|
46
|
+
3. **Convention check** — Run `npx webjs check` after changes and fix
|
|
47
|
+
any violations before reporting the task as done.
|
|
48
|
+
|
|
49
|
+
### Autonomous mode (sandbox / bypass permissions)
|
|
50
|
+
|
|
51
|
+
When running without interactive approval, agents must NOT ask questions.
|
|
52
|
+
Instead, auto-decide using best practices:
|
|
53
|
+
- On `main`? → Auto-create `feature/<task-slug>` branch
|
|
54
|
+
- Parent branch has new commits? → Auto-rebase before starting
|
|
55
|
+
- Ready to merge? → Auto-merge, delete feature/fix branches, keep
|
|
56
|
+
long-lived branches (dev, staging, release/*)
|
|
57
|
+
- Commit message? → Auto-generate: what changed and why
|
|
58
|
+
- Tests failing? → Fix them, don't report the failure and stop
|
|
59
|
+
- Convention violations? → Fix them silently
|
|
60
|
+
|
|
61
|
+
The quality bar is the same. Autonomous mode means faster, not sloppier.
|
|
62
|
+
|
|
63
|
+
### What "automatically" means:
|
|
64
|
+
|
|
65
|
+
When a user says "add a contact page" or "add a delete button to posts",
|
|
66
|
+
the AI agent must deliver:
|
|
67
|
+
- The implementation (page, component, action, etc.)
|
|
68
|
+
- Unit tests for any new server actions/queries/components
|
|
69
|
+
- E2E test if the feature involves user interaction
|
|
70
|
+
- Documentation updates if applicable
|
|
71
|
+
|
|
72
|
+
The user should never have to say "also write tests" or "also update the
|
|
73
|
+
docs" — that is the agent's default behavior in a webjs project.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Sensible defaults
|
|
78
|
+
|
|
79
|
+
<!-- OVERRIDE -->
|
|
80
|
+
webjs uses sensible defaults. Environment
|
|
81
|
+
variables control infrastructure — no config files needed:
|
|
82
|
+
|
|
83
|
+
| Environment variable | Effect |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `REDIS_URL` | Cache, sessions, rate limiting, and pub/sub all use Redis |
|
|
86
|
+
| `AUTH_SECRET` | Required for auth JWT signing (32+ random chars) |
|
|
87
|
+
| `AUTH_GOOGLE_ID` | Google OAuth client ID (optional) |
|
|
88
|
+
| `AUTH_GITHUB_ID` | GitHub OAuth client ID (optional) |
|
|
89
|
+
| `PORT` | Server port (default: 3000) |
|
|
90
|
+
|
|
91
|
+
**Development:** zero env vars needed. Everything works with memory/cookie/disk.
|
|
92
|
+
**Production:** set `REDIS_URL` + `SESSION_SECRET`. That's it.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Architecture: Modules
|
|
97
|
+
|
|
98
|
+
<!-- OVERRIDE -->
|
|
99
|
+
This app uses the **modules architecture** for feature-scoped code:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
modules/
|
|
103
|
+
<feature>/
|
|
104
|
+
actions/ Server mutations — one async function per file (*.server.ts)
|
|
105
|
+
queries/ Server reads — one async function per file (*.server.ts)
|
|
106
|
+
components/ Feature-owned web components
|
|
107
|
+
utils/ Pure helper functions
|
|
108
|
+
types.ts Shared TypeScript types / JSDoc typedefs
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Rules:**
|
|
112
|
+
- One exported function per server action/query file
|
|
113
|
+
- Server actions must use `'use server'` pragma or `.server.ts` extension
|
|
114
|
+
- Components must call `Class.register('tag')`
|
|
115
|
+
- Never import `@prisma/client`, `node:*`, or `lib/` directly from components — use server actions
|
|
116
|
+
- Routes (`app/**/page.ts`, `app/**/route.ts`) must be thin: import logic from modules
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Architecture: Routes
|
|
121
|
+
|
|
122
|
+
<!-- OVERRIDE -->
|
|
123
|
+
Routes live under `app/` and follow NextJs App Router conventions:
|
|
124
|
+
|
|
125
|
+
- `app/page.ts` — Homepage
|
|
126
|
+
- `app/<segment>/page.ts` — Static route
|
|
127
|
+
- `app/[param]/page.ts` — Dynamic route
|
|
128
|
+
- `app/[...rest]/page.ts` — Catch-all
|
|
129
|
+
- `app/(group)/...` — Route group (folder not in URL)
|
|
130
|
+
- `app/**/route.ts` — API endpoint
|
|
131
|
+
- `app/**/layout.ts` — Layout wrapper
|
|
132
|
+
- `app/**/error.ts` — Error boundary
|
|
133
|
+
- `app/**/middleware.ts` — Per-segment middleware
|
|
134
|
+
|
|
135
|
+
**Special route files:**
|
|
136
|
+
- `app/**/error.ts` — Error boundary. Default export receives `{ error }`, returns `TemplateResult`. Nearest boundary catches errors from pages below it.
|
|
137
|
+
- `app/**/loading.ts` — Loading state. Auto-wraps the sibling page in a `Suspense` boundary. Shown while async page functions resolve.
|
|
138
|
+
- `app/**/not-found.ts` — 404 page. Nearest wins when `notFound()` is thrown.
|
|
139
|
+
- `app/sitemap.ts` — Dynamic sitemap at `/sitemap.xml`. Export a function returning an array of `{ url, lastModified }`.
|
|
140
|
+
- `app/robots.ts` — Dynamic robots.txt at `/robots.txt`.
|
|
141
|
+
- `app/manifest.ts` — Web app manifest at `/manifest.json`.
|
|
142
|
+
|
|
143
|
+
**Rules:**
|
|
144
|
+
- A folder cannot have both `page.ts` and `route.ts`
|
|
145
|
+
- Page/layout default exports must be functions (possibly async)
|
|
146
|
+
- Route handlers export named methods: `GET`, `POST`, `PUT`, `DELETE`, `WS`
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Testing
|
|
151
|
+
|
|
152
|
+
<!-- OVERRIDE -->
|
|
153
|
+
Every feature module should have corresponding tests:
|
|
154
|
+
|
|
155
|
+
### Unit tests — `test/unit/`
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
test/
|
|
159
|
+
unit/
|
|
160
|
+
<feature>.test.ts One test file per module feature
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- Run with: `webjs test` or `node --test test/unit/*.test.ts`
|
|
164
|
+
- Use `node:test` and `node:assert/strict`
|
|
165
|
+
- Test server actions by importing and calling them directly
|
|
166
|
+
- Test component rendering with `renderToString` from webjs
|
|
167
|
+
- Test utility functions with simple assertions
|
|
168
|
+
|
|
169
|
+
**Naming:** `test/unit/<module-name>.test.ts` (e.g., `test/unit/auth.test.ts`)
|
|
170
|
+
|
|
171
|
+
### Browser tests — `test/browser/`
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
test/
|
|
175
|
+
browser/
|
|
176
|
+
<feature>.test.js Real-browser tests per feature
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
- Run with: `webjs test --browser` or `npx wtr`
|
|
180
|
+
- Uses **Web Test Runner (WTR) + Playwright** — tests run in real Chromium
|
|
181
|
+
- Full Shadow DOM, events, adoptedStyleSheets, IntersectionObserver
|
|
182
|
+
- Test components, user interactions, navigation, form submission
|
|
183
|
+
|
|
184
|
+
**Naming:** `test/browser/<feature>.test.js` (e.g., `test/browser/auth.test.js`)
|
|
185
|
+
|
|
186
|
+
### Debugging with Playwright MCP
|
|
187
|
+
|
|
188
|
+
This project includes a Playwright MCP server (`.claude.json`). When
|
|
189
|
+
debugging UI issues, AI agents can use the Playwright MCP tools to:
|
|
190
|
+
- Navigate to pages in a real browser
|
|
191
|
+
- Click elements, fill forms, interact with the UI
|
|
192
|
+
- Take screenshots to see what the user sees
|
|
193
|
+
- Inspect the accessibility tree for element discovery
|
|
194
|
+
|
|
195
|
+
Use `Playwright MCP` tools instead of writing one-shot Bash scripts
|
|
196
|
+
with puppeteer or playwright imports.
|
|
197
|
+
|
|
198
|
+
### When to write tests
|
|
199
|
+
|
|
200
|
+
| Change | Server test (node:test) | Browser test (WTR) |
|
|
201
|
+
|--------|------------------------|-------------------|
|
|
202
|
+
| New server action | Required | — |
|
|
203
|
+
| New component | Required (SSR output) | Required (interaction) |
|
|
204
|
+
| New page/route | — | Required |
|
|
205
|
+
| Bug fix | Required (regression) | If user-facing |
|
|
206
|
+
| Refactor | Existing tests must pass | Existing tests must pass |
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Components
|
|
211
|
+
|
|
212
|
+
<!-- OVERRIDE -->
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { WebComponent, html } from '@webjskit/core';
|
|
216
|
+
|
|
217
|
+
export class MyWidget extends WebComponent {
|
|
218
|
+
static properties = { label: { type: String }, count: { type: Number } };
|
|
219
|
+
declare label: string;
|
|
220
|
+
declare count: number;
|
|
221
|
+
// Light DOM is the default; Tailwind utility classes apply directly.
|
|
222
|
+
|
|
223
|
+
render() {
|
|
224
|
+
return html`
|
|
225
|
+
<div class="p-4 border border-border rounded-lg">
|
|
226
|
+
<p class="font-serif text-fg">${this.label}: ${this.count}</p>
|
|
227
|
+
</div>
|
|
228
|
+
`;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
MyWidget.register('my-widget');
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`static properties` is the runtime declaration (reactive accessor,
|
|
235
|
+
attribute coercion, reflection). `declare` types the field for
|
|
236
|
+
TypeScript without emitting a class-field initializer that would
|
|
237
|
+
clobber the reactive accessor at construction time. The two
|
|
238
|
+
declarations together give you full intelligence in any tsserver-backed
|
|
239
|
+
editor — see the Editor Setup docs for `ts-lit-plugin` setup that
|
|
240
|
+
extends this to tag / attribute intelligence inside `html\`…\``
|
|
241
|
+
templates.
|
|
242
|
+
|
|
243
|
+
**Rules:**
|
|
244
|
+
- One component per file
|
|
245
|
+
- **Light DOM by default.** Opt in to shadow DOM with `static shadow = true` when you need scoped styles, `<slot>` projection, or third-party-embed isolation.
|
|
246
|
+
- Prefer Tailwind utility classes for styling. They're unique by construction (`p-4`, `font-semibold`) so they can't collide across components.
|
|
247
|
+
- **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:
|
|
248
|
+
- `.my-widget__body`, `.my-widget__title` (BEM-ish)
|
|
249
|
+
- `my-widget .body`, `my-widget .title` (descendant selector)
|
|
250
|
+
- Tag name must contain a hyphen (HTML spec)
|
|
251
|
+
- Always call `Class.register('tag')` — the standard DOM API
|
|
252
|
+
- Use `setState()` for state changes, never mutate `this.state` directly
|
|
253
|
+
- Use lifecycle hooks (`firstUpdated`, `updated`) only when needed
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Components: Light DOM (default) vs Shadow DOM (opt-in)
|
|
258
|
+
|
|
259
|
+
<!-- OVERRIDE -->
|
|
260
|
+
|
|
261
|
+
| Use case | Mode | How |
|
|
262
|
+
|---|---|---|
|
|
263
|
+
| Global / Tailwind CSS, simple composition | **Light DOM** (default) | Write `class="..."` in your template. Plain children, global styles apply. |
|
|
264
|
+
| Scoped styles via `static styles = css\`\`` | Shadow DOM | Set `static shadow = true`. `adoptedStyleSheets` scopes bare selectors. |
|
|
265
|
+
| `<slot>` content projection | Shadow DOM | Slots only work inside shadow roots. |
|
|
266
|
+
| Third-party embed isolation | Shadow DOM | CSS can't leak in or out. |
|
|
267
|
+
|
|
268
|
+
**Light DOM** = the component renders as plain HTML. Global CSS and
|
|
269
|
+
Tailwind utility classes apply directly. Use `document.querySelector`
|
|
270
|
+
to find elements. No `:host`, no `::part`, no CSS-variable plumbing.
|
|
271
|
+
|
|
272
|
+
**Shadow DOM** = opt-in style encapsulation. Declare `static shadow = true`
|
|
273
|
+
and author styles via `static styles = css\`...\`` (adopted via
|
|
274
|
+
`adoptedStyleSheets`). The browser enforces the boundary; nothing leaks
|
|
275
|
+
in or out.
|
|
276
|
+
|
|
277
|
+
Both modes are fully SSR'd. Light DOM emits content as direct children
|
|
278
|
+
with a `<!--webjs-hydrate-->` marker. Shadow DOM emits a
|
|
279
|
+
`<template shadowrootmode="open">` that the browser attaches automatically.
|
|
280
|
+
Both hydrate without flash on the client.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Styling: Tailwind + JS helpers
|
|
285
|
+
|
|
286
|
+
<!-- OVERRIDE -->
|
|
287
|
+
|
|
288
|
+
The scaffold ships with the **Tailwind CSS browser runtime** + `@theme`
|
|
289
|
+
design tokens defined in the root layout. Every colour, font family,
|
|
290
|
+
fluid type scale value, and motion duration is declared once in `@theme`
|
|
291
|
+
and available everywhere via utility classes (`text-fg`, `bg-bg-elev`,
|
|
292
|
+
`font-serif`, `duration-fast`, `text-display`).
|
|
293
|
+
|
|
294
|
+
**Dedup repeated Tailwind class bundles with JS helpers, not `@apply`.**
|
|
295
|
+
When the same string of classes appears in 2+ places, extract it into a
|
|
296
|
+
small function in `app/_utils/ui.ts`:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
// app/_utils/ui.ts
|
|
300
|
+
import { html } from '@webjskit/core';
|
|
301
|
+
|
|
302
|
+
export function rubric(label: string) {
|
|
303
|
+
return html`
|
|
304
|
+
<span class="block font-mono text-[11px] leading-none font-semibold tracking-[0.2em] uppercase text-accent mb-4">● ${label}</span>
|
|
305
|
+
`;
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Consume:
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
// app/page.ts
|
|
313
|
+
import { rubric } from './_utils/ui.ts';
|
|
314
|
+
|
|
315
|
+
export default function Home() {
|
|
316
|
+
return html`
|
|
317
|
+
${rubric('welcome')}
|
|
318
|
+
<h1 class="font-serif text-display">Hello</h1>
|
|
319
|
+
`;
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Helpers run at SSR time inside `html\`\``, so the output is identical
|
|
324
|
+
to writing the classes inline — no client-side runtime.
|
|
325
|
+
|
|
326
|
+
**Why not `@apply`?** `@apply` hides which utilities back a class and
|
|
327
|
+
creates a second source of truth. JS helpers keep the class bundle
|
|
328
|
+
visible at the definition site and compose naturally with conditional
|
|
329
|
+
classes and active states.
|
|
330
|
+
|
|
331
|
+
**Custom CSS is still supported** — plain `<style>` blocks, CSS modules,
|
|
332
|
+
or a build-step pipeline. The framework has no hard dependency on Tailwind.
|
|
333
|
+
If you mix custom CSS into a light-DOM component, apply the class-prefix
|
|
334
|
+
rule (see Components section above).
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Styling alternative: vanilla CSS end-to-end
|
|
339
|
+
|
|
340
|
+
<!-- OVERRIDE -->
|
|
341
|
+
|
|
342
|
+
If you'd rather skip Tailwind, webjs works with plain CSS as long as you
|
|
343
|
+
wrap pages, layouts, and components so class names don't collide in the
|
|
344
|
+
global light-DOM namespace.
|
|
345
|
+
|
|
346
|
+
**Convention — three scopes:**
|
|
347
|
+
|
|
348
|
+
| Scope | Wrapper | Derivation |
|
|
349
|
+
|---|---|---|
|
|
350
|
+
| **Component** | Custom-element tag | Tag is already unique |
|
|
351
|
+
| **Page** | `.page-<route>` | `app/dashboard/page.ts` → `.page-dashboard`; `app/blog/[slug]/page.ts` → `.page-blog-slug`; root `app/page.ts` → `.page-home` |
|
|
352
|
+
| **Layout** | `.layout-<name>` | `app/layout.ts` → `.layout-root`; `app/admin/layout.ts` → `.layout-admin` |
|
|
353
|
+
|
|
354
|
+
Every page wraps its output in `<div class="page-<route>">`. Every
|
|
355
|
+
layout wraps in `<div class="layout-<name>">`. Components scope via
|
|
356
|
+
their tag. Styles colocate as `const STYLES = css\`…\`` + `<style>${'$'}{STYLES.text}</style>`.
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
// app/dashboard/page.ts
|
|
360
|
+
import { html, css } from '@webjskit/core';
|
|
361
|
+
|
|
362
|
+
const STYLES = css\`
|
|
363
|
+
.page-dashboard {
|
|
364
|
+
.actions { display: flex; gap: 12px; }
|
|
365
|
+
.btn { padding: 12px 24px; border-radius: 999px; }
|
|
366
|
+
.btn-primary { background: var(--accent); color: var(--accent-fg); }
|
|
367
|
+
}
|
|
368
|
+
\`;
|
|
369
|
+
|
|
370
|
+
export default function Dashboard() {
|
|
371
|
+
return html\`
|
|
372
|
+
<style>${'$'}{STYLES.text}</style>
|
|
373
|
+
<div class="page-dashboard">
|
|
374
|
+
<div class="actions">
|
|
375
|
+
<a class="btn btn-primary" href="/new">+ New</a>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
\`;
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Inside each scope, `.btn` / `.input` / `.form` / `.item` are free
|
|
383
|
+
names — CSS descendant combinators stop them at the scope boundary.
|
|
384
|
+
A small curated set of **primitives** (`rubric`, `banner`,
|
|
385
|
+
`accent-link`, `display-h1`, …) can live global in the root layout
|
|
386
|
+
as your design system.
|
|
387
|
+
|
|
388
|
+
**When you'd pick this over Tailwind:**
|
|
389
|
+
- You want zero runtime scripts and zero build step.
|
|
390
|
+
- You prefer idiomatic CSS and plain-cascade debugging.
|
|
391
|
+
- You already have a design system in CSS custom properties.
|
|
392
|
+
|
|
393
|
+
**Costs:**
|
|
394
|
+
- Write more per-file CSS (no utility ecosystem).
|
|
395
|
+
- Discipline: every page/layout remembers to wrap.
|
|
396
|
+
- Renaming a route folder = 2 textual edits in one file (the wrapper class + the matching `class=` attribute).
|
|
397
|
+
|
|
398
|
+
Pick one convention per project and stay consistent.
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## Rate limiting & middleware
|
|
403
|
+
|
|
404
|
+
<!-- OVERRIDE -->
|
|
405
|
+
Use `rateLimit()` as per-segment middleware to protect routes:
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
// app/api/auth/middleware.ts — protect auth endpoints
|
|
409
|
+
import { rateLimit } from '@webjskit/server';
|
|
410
|
+
export default rateLimit({ window: '10s', max: 5 });
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Place `middleware.ts` at any route level — it applies to that subtree only.
|
|
414
|
+
Chain runs outermost → innermost.
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## Lazy loading
|
|
419
|
+
|
|
420
|
+
<!-- OVERRIDE -->
|
|
421
|
+
For below-the-fold components with heavy JS, defer loading until visible:
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
class HeavyChart extends WebComponent {
|
|
425
|
+
static lazy = true; // module loaded on scroll, not on page load
|
|
426
|
+
// ...
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
SSR content is visible immediately — only the JS download is deferred.
|
|
431
|
+
**Do NOT use** for above-the-fold or critical UI (navigation, forms).
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## expose() — REST endpoints from server actions
|
|
436
|
+
|
|
437
|
+
<!-- OVERRIDE -->
|
|
438
|
+
Tag a server action to also be reachable over HTTP:
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
import { expose } from '@webjskit/core';
|
|
442
|
+
export const createPost = expose('POST /api/posts', async ({ title, body }) => {
|
|
443
|
+
return prisma.post.create({ data: { title, body } });
|
|
444
|
+
});
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
The same function works via RPC (from components) and HTTP (for external
|
|
448
|
+
callers). Use `expose()` when mobile apps, webhooks, or third parties need
|
|
449
|
+
to call your action. For internal-only actions, plain server actions are
|
|
450
|
+
simpler and CSRF-protected.
|
|
451
|
+
|
|
452
|
+
**Security:** `expose()`d endpoints are NOT CSRF-protected. Authenticate
|
|
453
|
+
via bearer tokens, API keys, or auth middleware.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## Server actions
|
|
458
|
+
|
|
459
|
+
<!-- OVERRIDE -->
|
|
460
|
+
|
|
461
|
+
```ts
|
|
462
|
+
// modules/posts/actions/create-post.server.ts
|
|
463
|
+
'use server';
|
|
464
|
+
import { prisma } from '../../../lib/prisma.ts';
|
|
465
|
+
import type { ActionResult } from '../types.ts';
|
|
466
|
+
|
|
467
|
+
export async function createPost(input: {
|
|
468
|
+
title: string;
|
|
469
|
+
body: string;
|
|
470
|
+
}): Promise<ActionResult<Post>> {
|
|
471
|
+
// validate, create, return
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
**Rules:**
|
|
476
|
+
- One function per file (greppable, AI-agent friendly)
|
|
477
|
+
- File name matches function name: `create-post.server.ts` → `createPost`
|
|
478
|
+
- Return `ActionResult<T>` envelope for actions that can fail
|
|
479
|
+
- Never throw for expected errors — return `{ success: false, error, status }`
|
|
480
|
+
- Validate input at the top of the function
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## Code style
|
|
485
|
+
|
|
486
|
+
<!-- OVERRIDE -->
|
|
487
|
+
- TypeScript with explicit `.ts` extensions in imports
|
|
488
|
+
- No semicolons (or with — pick one and be consistent)
|
|
489
|
+
- `const` by default, `let` when needed, never `var`
|
|
490
|
+
- Prefer `async/await` over `.then()` chains
|
|
491
|
+
- Minimal comments — code should be self-documenting
|
|
492
|
+
- No barrel files (`index.ts` re-exporting everything) — import from the source directly
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## Git workflow
|
|
497
|
+
|
|
498
|
+
<!-- OVERRIDE -->
|
|
499
|
+
|
|
500
|
+
This project enforces a git workflow via agent-specific config files
|
|
501
|
+
(`.claude/settings.json`, `.cursorrules`, `.windsurfrules`,
|
|
502
|
+
`.github/copilot-instructions.md`). These rules apply to ALL AI agents:
|
|
503
|
+
|
|
504
|
+
**Commit rules:**
|
|
505
|
+
- **Commit often** — after each logical unit of work, not at the end
|
|
506
|
+
- **Meaningful messages** — imperative mood, what changed and why
|
|
507
|
+
(e.g., `Add contact form with email validation`)
|
|
508
|
+
- **NEVER add AI attribution** — no `Co-Authored-By: Claude`, no
|
|
509
|
+
`Generated by AI`, no `AI-assisted` trailers or prefixes
|
|
510
|
+
- **Small, focused commits** — don't batch unrelated changes
|
|
511
|
+
- **Committing is automatic** — the user should never have to ask
|
|
512
|
+
"please commit". Commit after completing each task.
|
|
513
|
+
|
|
514
|
+
**Branch rules:**
|
|
515
|
+
- **Feature branches** — never commit directly to main
|
|
516
|
+
- **Branch naming** — `feature/<name>`, `fix/<name>`, `refactor/<name>`
|
|
517
|
+
- **Pull requests** — always create a PR, never push to main directly
|
|
518
|
+
- **NEVER merge without user permission** — before merging ANY branch
|
|
519
|
+
into ANY other branch, ask: "Ready to merge `<branch>` into `<target>`?
|
|
520
|
+
Delete or keep `<branch>` after?" Wait for approval AND the preference.
|
|
521
|
+
- **Claude Code hook** (`.claude/hooks/guard-main-merge.sh`) enforces
|
|
522
|
+
merge/push-to-main approval programmatically for Claude agents.
|
|
523
|
+
Other agents enforce this via `.cursorrules`, `.windsurfrules`,
|
|
524
|
+
`.github/copilot-instructions.md`.
|
|
525
|
+
|
|
526
|
+
**Pre-commit checks:**
|
|
527
|
+
- `npx webjs test` must pass
|
|
528
|
+
- `npx webjs check` must pass
|
|
529
|
+
- No unrelated files in the commit
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## Overriding conventions
|
|
534
|
+
|
|
535
|
+
To disable a convention check, add to your `package.json`:
|
|
536
|
+
|
|
537
|
+
```json
|
|
538
|
+
{
|
|
539
|
+
"webjs": {
|
|
540
|
+
"conventions": {
|
|
541
|
+
"actions-in-modules": false,
|
|
542
|
+
"one-function-per-action": false,
|
|
543
|
+
"tests-exist": false
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Or create `webjs.config.js`:
|
|
550
|
+
|
|
551
|
+
```js
|
|
552
|
+
export default {
|
|
553
|
+
conventions: {
|
|
554
|
+
'actions-in-modules': false,
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Run `webjs check` to validate your app against these conventions.
|
|
560
|
+
Run `webjs check --fix` to see suggested fixes for violations.
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## Scaffold
|
|
565
|
+
|
|
566
|
+
Create new projects with `webjs create`:
|
|
567
|
+
|
|
568
|
+
```sh
|
|
569
|
+
webjs create <name> # full-stack (default)
|
|
570
|
+
webjs create <name> --template api # backend-only API
|
|
571
|
+
webjs create <name> --template saas # auth + dashboard + Prisma User model
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
**Route-wrapping pattern (especially for `--template api` apps):**
|
|
575
|
+
Routes are thin wrappers over typed server actions. Business logic lives in
|
|
576
|
+
`modules/`, routes just import and call the action/query:
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
// app/api/users/route.ts — thin wrapper
|
|
580
|
+
import { listUsers } from '../../../modules/users/queries/list-users.server.ts';
|
|
581
|
+
import { createUser } from '../../../modules/users/actions/create-user.server.ts';
|
|
582
|
+
|
|
583
|
+
export async function GET() { return Response.json(await listUsers()); }
|
|
584
|
+
export async function POST(req: Request) {
|
|
585
|
+
const result = await createUser(await req.json());
|
|
586
|
+
if (!result.success) return Response.json({ error: result.error }, { status: result.status });
|
|
587
|
+
return Response.json(result.data, { status: 201 });
|
|
588
|
+
}
|
|
589
|
+
```
|