@webjsdev/cli 0.10.29 → 0.10.31
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 +1 -1
- package/lib/api-gallery.js +229 -0
- package/lib/create.js +452 -158
- package/lib/saas-template.js +39 -15
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +70 -2
- package/templates/.claude/hooks/check-server-imports.mjs +86 -0
- package/templates/.claude/hooks/check-server-imports.sh +26 -0
- package/templates/.claude/settings.json +9 -0
- package/templates/.cursorrules +41 -2
- package/templates/.github/copilot-instructions.md +41 -2
- package/templates/AGENTS.md +160 -10
- package/templates/CONVENTIONS.md +150 -11
- package/templates/gallery/app/examples/todo/page.ts +34 -0
- package/templates/gallery/app/features/async-render/page.ts +14 -0
- package/templates/gallery/app/features/broadcast/feed/route.ts +19 -0
- package/templates/gallery/app/features/broadcast/page.ts +24 -0
- package/templates/gallery/app/features/caching/page.ts +39 -0
- package/templates/gallery/app/features/client-router/page.ts +34 -0
- package/templates/gallery/app/features/client-router/second/page.ts +20 -0
- package/templates/gallery/app/features/components/page.ts +14 -0
- package/templates/gallery/app/features/directives/page.ts +14 -0
- package/templates/gallery/app/features/env/page.ts +36 -0
- package/templates/gallery/app/features/file-storage/file/[key]/route.ts +19 -0
- package/templates/gallery/app/features/file-storage/page.ts +62 -0
- package/templates/gallery/app/features/forms/page.ts +72 -0
- package/templates/gallery/app/features/metadata/page.ts +55 -0
- package/templates/gallery/app/features/optimistic-ui/page.ts +14 -0
- package/templates/gallery/app/features/rate-limit/page.ts +29 -0
- package/templates/gallery/app/features/rate-limit/ping/middleware.ts +8 -0
- package/templates/gallery/app/features/rate-limit/ping/route.ts +7 -0
- package/templates/gallery/app/features/route-handler/data/route.ts +8 -0
- package/templates/gallery/app/features/route-handler/page.ts +13 -0
- package/templates/gallery/app/features/routing/[id]/page.ts +25 -0
- package/templates/gallery/app/features/routing/page.ts +47 -0
- package/templates/gallery/app/features/server-actions/page.ts +14 -0
- package/templates/gallery/app/features/service-worker/page.ts +35 -0
- package/templates/gallery/app/features/websockets/echo/route.ts +19 -0
- package/templates/gallery/app/features/websockets/page.ts +25 -0
- package/templates/gallery/modules/async-render/components/server-clock.ts +26 -0
- package/templates/gallery/modules/async-render/queries/server-greeting.server.ts +9 -0
- package/templates/gallery/modules/broadcast/components/broadcast-feed.ts +61 -0
- package/templates/gallery/modules/components/components/counter-card.ts +35 -0
- package/templates/gallery/modules/directives/components/directive-demo.ts +53 -0
- package/templates/gallery/modules/file-storage/actions/store-upload.server.ts +18 -0
- package/templates/gallery/modules/optimistic-ui/actions/like-post.server.ts +9 -0
- package/templates/gallery/modules/optimistic-ui/components/like-button.ts +35 -0
- package/templates/gallery/modules/rate-limit/components/rate-probe.ts +49 -0
- package/templates/gallery/modules/server-actions/actions/greet.server.ts +12 -0
- package/templates/gallery/modules/server-actions/components/greeter.ts +30 -0
- package/templates/gallery/modules/server-actions/utils/format.server.ts +8 -0
- package/templates/gallery/modules/todo/actions/create-todo.server.ts +16 -0
- package/templates/gallery/modules/todo/actions/delete-todo.server.ts +15 -0
- package/templates/gallery/modules/todo/actions/toggle-todo.server.ts +21 -0
- package/templates/gallery/modules/todo/components/todo-app.ts +140 -0
- package/templates/gallery/modules/todo/queries/list-todos.server.ts +19 -0
- package/templates/gallery/modules/todo/types.ts +12 -0
- package/templates/gallery/modules/websockets/components/ws-echo.ts +62 -0
- package/templates/lib/utils/ui.ts +4 -4
- package/templates/test/hello/browser/hello.test.js +12 -6
- package/templates/test/hello/e2e/hello.test.ts +11 -9
- package/templates/test/hello/hello.test.ts +4 -6
- package/templates/web-test-runner.config.js +84 -9
package/lib/saas-template.js
CHANGED
|
@@ -106,7 +106,7 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
106
106
|
// add passwordHash (the column auth needs). Drizzle, dialect-agnostic.
|
|
107
107
|
await writeFile(join(appDir, 'db', 'schema.server.ts'), [
|
|
108
108
|
"import { defineRelations } from 'drizzle-orm';",
|
|
109
|
-
"import { table, pk, text, createdAt } from './columns.server.ts';",
|
|
109
|
+
"import { table, pk, uuidPk, text, bool, createdAt } from './columns.server.ts';",
|
|
110
110
|
"",
|
|
111
111
|
"export const users = table('users', {",
|
|
112
112
|
" id: pk(),",
|
|
@@ -116,7 +116,16 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
116
116
|
" createdAt: createdAt(),",
|
|
117
117
|
"});",
|
|
118
118
|
"",
|
|
119
|
-
"
|
|
119
|
+
"// Backs the example-gallery /examples/todo route (modules/todo). Delete it",
|
|
120
|
+
"// with the gallery when you prune the examples you do not use.",
|
|
121
|
+
"export const todos = table('todos', {",
|
|
122
|
+
" id: uuidPk(),",
|
|
123
|
+
" title: text().notNull(),",
|
|
124
|
+
" completed: bool().notNull().default(false),",
|
|
125
|
+
" createdAt: createdAt(),",
|
|
126
|
+
"});",
|
|
127
|
+
"",
|
|
128
|
+
"export const relations = defineRelations({ users, todos }, () => ({}));",
|
|
120
129
|
"",
|
|
121
130
|
"export type User = typeof users.$inferSelect;",
|
|
122
131
|
"",
|
|
@@ -132,12 +141,19 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
132
141
|
"import { db } from '#db/connection.server.ts';",
|
|
133
142
|
"import { users } from '#db/schema.server.ts';",
|
|
134
143
|
"import { hash } from '#lib/password.server.ts';",
|
|
135
|
-
"",
|
|
144
|
+
"import { signIn } from '#lib/auth.server.ts';",
|
|
145
|
+
"",
|
|
146
|
+
"// Creates the account, then signs the new user in and lands on the",
|
|
147
|
+
"// dashboard. signIn returns a 302 Response carrying the session cookie; the",
|
|
148
|
+
"// signup page action returns that Response as-is (a page action may return a",
|
|
149
|
+
"// Response). signIn lives in the server-only auth module, imported here",
|
|
150
|
+
"// server-to-server, so it never reaches the browser (the signup page only",
|
|
151
|
+
"// imports this action's RPC stub).",
|
|
136
152
|
"export async function signup(input: { name: string; email: string; password: string }) {",
|
|
137
153
|
" const exists = await db.query.users.findFirst({ where: { email: input.email }, columns: { id: true } });",
|
|
138
154
|
" if (exists) return { success: false as const, error: 'Email already registered', status: 409 };",
|
|
139
|
-
"
|
|
140
|
-
" return
|
|
155
|
+
" await db.insert(users).values({ name: input.name, email: input.email, passwordHash: await hash(input.password) });",
|
|
156
|
+
" return signIn('credentials', { email: input.email, password: input.password }, { redirectTo: '/dashboard' });",
|
|
141
157
|
"}",
|
|
142
158
|
"",
|
|
143
159
|
].join('\n'));
|
|
@@ -252,10 +268,11 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
252
268
|
" headers: { 'content-type': 'application/x-www-form-urlencoded' },",
|
|
253
269
|
" body: new URLSearchParams({ name: 'Harness', email, password }).toString(),",
|
|
254
270
|
" });",
|
|
255
|
-
" // Success
|
|
256
|
-
" //
|
|
257
|
-
" assert.ok([
|
|
258
|
-
" if (signupRes.status
|
|
271
|
+
" // Success auto-logs-in and 302s to /dashboard (carrying the session",
|
|
272
|
+
" // cookie); a 422 means validation failed. Either way the action ran.",
|
|
273
|
+
" assert.ok([302, 422].includes(signupRes.status), 'signup action ran');",
|
|
274
|
+
" if (signupRes.status === 302) assert.equal(signupRes.headers.get('location'), '/dashboard', 'signup lands on the dashboard');",
|
|
275
|
+
" if (signupRes.status !== 302) canSignup = false;",
|
|
259
276
|
" } catch {",
|
|
260
277
|
" // No migrated DB table -> the action throws. Skip the DB-backed assertions.",
|
|
261
278
|
" canSignup = false;",
|
|
@@ -270,6 +287,10 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
270
287
|
" assert.equal(dash.status, 200, 'the session cookie unlocks the dashboard');",
|
|
271
288
|
" const body = await dash.text();",
|
|
272
289
|
" assert.match(body, /Dashboard/, 'the dashboard content rendered');",
|
|
290
|
+
" // The greeting interpolates the real user, so the name renders and the",
|
|
291
|
+
" // literal template source never leaks (a counterfactual for the escaping bug).",
|
|
292
|
+
" assert.match(body, /Harness/, 'the dashboard greets the signed-in user by name');",
|
|
293
|
+
" assert.ok(!body.includes('${user'), 'the greeting interpolation is not a literal string');",
|
|
273
294
|
"});",
|
|
274
295
|
"",
|
|
275
296
|
].join('\n');
|
|
@@ -308,6 +329,8 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
308
329
|
" </div>",
|
|
309
330
|
" <div class=${cardContentClass()}>",
|
|
310
331
|
" <form method=\"POST\" action=\"/api/auth/signin/credentials\" class=\"flex flex-col gap-4\">",
|
|
332
|
+
" <!-- createAuth reads redirectTo from the posted form and 302s there after a successful signin. -->",
|
|
333
|
+
" <input type=\"hidden\" name=\"redirectTo\" value=\"/dashboard\">",
|
|
311
334
|
" <div class=\"flex flex-col gap-1.5\">",
|
|
312
335
|
" <label class=${labelClass()} for=\"email\">Email</label>",
|
|
313
336
|
" <input class=${inputClass()} id=\"email\" name=\"email\" type=\"email\" required>",
|
|
@@ -357,9 +380,10 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
357
380
|
" if (password.length < 8) fieldErrors.password = 'At least 8 characters';",
|
|
358
381
|
" if (Object.keys(fieldErrors).length) return { success: false, fieldErrors, values, status: 422 };",
|
|
359
382
|
" const result = await signup({ name, email, password });",
|
|
360
|
-
"
|
|
361
|
-
" //
|
|
362
|
-
"
|
|
383
|
+
" // On success signup returns signIn's 302 Response (auto-login -> /dashboard);",
|
|
384
|
+
" // a page action may return a Response, so pass it straight through.",
|
|
385
|
+
" if (result instanceof Response) return result;",
|
|
386
|
+
" return { success: false, fieldErrors: { email: result.error }, values, status: result.status };",
|
|
363
387
|
"}",
|
|
364
388
|
"",
|
|
365
389
|
"export default function SignupPage({ actionData }: { actionData?: { fieldErrors?: Record<string, string>; values?: Record<string, string> } }) {",
|
|
@@ -436,7 +460,7 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
436
460
|
" </div>",
|
|
437
461
|
" <div class=${cardClass()}>",
|
|
438
462
|
" <div class=${cardHeaderClass()}>",
|
|
439
|
-
" <h3 class=${cardTitleClass()}>Welcome, ${
|
|
463
|
+
" <h3 class=${cardTitleClass()}>Welcome, ${user?.name || user?.email}!</h3>",
|
|
440
464
|
" <p class=${cardDescriptionClass()}>You're authenticated. Replace this scaffold with your real app.</p>",
|
|
441
465
|
" </div>",
|
|
442
466
|
" <div class=${cardContentClass()}>",
|
|
@@ -468,9 +492,9 @@ export async function writeSaasFiles(appDir, opts = {}) {
|
|
|
468
492
|
" <div class=${cardContentClass()}>",
|
|
469
493
|
" <dl class=\"grid grid-cols-[max-content_1fr] gap-x-6 gap-y-2 text-sm\">",
|
|
470
494
|
" <dt class=\"text-muted-foreground\">Email</dt>",
|
|
471
|
-
" <dd>${
|
|
495
|
+
" <dd>${user?.email}</dd>",
|
|
472
496
|
" <dt class=\"text-muted-foreground\">Name</dt>",
|
|
473
|
-
" <dd>${
|
|
497
|
+
" <dd>${user?.name || 'Not set'}</dd>",
|
|
474
498
|
" </dl>",
|
|
475
499
|
" </div>",
|
|
476
500
|
" </div>",
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
You are working on a webjs app, an AI-first, no-build, web-components-first
|
|
4
4
|
framework. Read AGENTS.md for the full API reference and CONVENTIONS.md for
|
|
5
5
|
project-specific conventions before writing any code. When AGENTS.md does not
|
|
6
|
-
cover what you need, the full hosted docs are at **https://docs.webjs.
|
|
6
|
+
cover what you need, the full hosted docs are at **https://docs.webjs.dev**.
|
|
7
7
|
|
|
8
8
|
## Persistence + scaffold rules (non-negotiable)
|
|
9
9
|
|
|
@@ -19,6 +19,39 @@ cover what you need, the full hosted docs are at **https://docs.webjs.com**.
|
|
|
19
19
|
the example `User` model, the example users module, etc. with the app the
|
|
20
20
|
user actually asked for. Do not ship "Hello from <app-name>" as the
|
|
21
21
|
deliverable.
|
|
22
|
+
- **Study the gallery first, prune it second.** The full-stack and saas scaffolds
|
|
23
|
+
ship single-feature demos under `app/features/` plus one whole example app under
|
|
24
|
+
`app/examples/` (logic in `modules/`). It is your PRIMARY webjs reference: read
|
|
25
|
+
every demo end to end (code AND comments) to learn the idioms BEFORE you write
|
|
26
|
+
or delete anything. Only after internalising the patterns should you prune,
|
|
27
|
+
keeping and adapting what you need and deleting the rest (the
|
|
28
|
+
`app/features/<name>` or `app/examples/<name>` route AND its `modules/<name>`;
|
|
29
|
+
for the todo app, also the `todos` table). Never delete blindly up front. Each
|
|
30
|
+
route page has a `webjs-scaffold-placeholder` marker so `webjs check` fails
|
|
31
|
+
until you resolve it. Delete now-empty directories after pruning. The `api`
|
|
32
|
+
template ships a BACKEND-features showcase instead (endpoints under
|
|
33
|
+
`app/api/features/`: the `route()` adapter + validation, rate limiting,
|
|
34
|
+
streaming, file storage, WebSockets + broadcast, plus `env.ts` validation),
|
|
35
|
+
listed in the root `app/route.ts` index; prune it the same way.
|
|
36
|
+
- **Prune what the app does not use.** Keep the infrastructure the app USES
|
|
37
|
+
and delete the rest (files AND folders). No persistence means delete `db/`,
|
|
38
|
+
`drizzle.config.ts`, and the `db:*` scripts. No UI kit used means delete
|
|
39
|
+
`components/ui/`, `components.json`, and `lib/utils/cn.ts`. No PWA means
|
|
40
|
+
delete `public/sw.js` and `offline.html`. Always KEEP the durable knowledge
|
|
41
|
+
(`AGENTS.md`, `CONVENTIONS.md`, the rule files, the MCP), never prune it, so
|
|
42
|
+
removing example code never removes your context. Prune AFTER using the
|
|
43
|
+
features and examples as reference, never blindly up front. A no-op for the `api` template
|
|
44
|
+
(no UI kit, no PWA files).
|
|
45
|
+
- **`app/` is routing-only.** Only routing files live in `app/` (page, layout,
|
|
46
|
+
route, middleware, metadata routes). CSS, helpers, and constants do NOT:
|
|
47
|
+
`globals.css` is at `styles/`, browser-safe helpers at `lib/utils/`, feature
|
|
48
|
+
logic in `modules/`.
|
|
49
|
+
- **Use a unique design (UI apps).** When the app has a UI, give it a design of
|
|
50
|
+
your own (palette, layout, typography, chrome) that fits what the user asked
|
|
51
|
+
for. Do NOT mimic the scaffold's example look (its warm colors, the 760px
|
|
52
|
+
reading column, the example header/nav). The `api` template has no UI, so this
|
|
53
|
+
does not apply there. Keep the design tokens and theme setup in
|
|
54
|
+
`app/layout.ts`, those are infrastructure, and restyle on top of them.
|
|
22
55
|
- **Only three templates exist:** `webjs create <name>` (default full-stack),
|
|
23
56
|
`--template api`, `--template saas`. The CLI rejects any other `--template`
|
|
24
57
|
value. Pick:
|
|
@@ -135,17 +168,46 @@ self-review loop.
|
|
|
135
168
|
gradients); when unavoidable in a light-DOM component, prefix every class
|
|
136
169
|
selector with the component tag. Shadow-DOM components legitimately use
|
|
137
170
|
`static styles = css\`...\`` for scoped CSS.
|
|
171
|
+
- **One theme, canonical tokens.** The app has a SINGLE theme, defined once in
|
|
172
|
+
`app/layout.ts` using the standard `@webjsdev/ui` (shadcn-compatible)
|
|
173
|
+
semantic tokens set to the brand palette. Use the canonical utility names
|
|
174
|
+
everywhere, in the page chrome AND inside components: `bg-background`,
|
|
175
|
+
`text-foreground`, `bg-card`, `bg-muted`, `text-muted-foreground`,
|
|
176
|
+
`bg-primary`, `text-primary-foreground`, `bg-accent`, `text-accent-foreground`,
|
|
177
|
+
`border-border`, `ring-ring`. These are exactly the tokens a component copied
|
|
178
|
+
in by `webjs ui add <name>` reads, so a scaffolded page and a later-added ui
|
|
179
|
+
component share one theme with no wiring. NEVER invent a parallel token
|
|
180
|
+
vocabulary (`--fg`, `--bg`, `text-fg`, `bg-elev`, a separate `--brand`): it
|
|
181
|
+
collides with the ui tokens (the accent once flipped to neutral on navigation
|
|
182
|
+
for exactly this reason) and diverges from the shadcn conventions the kit and
|
|
183
|
+
AI agents expect. Reach for opacity modifiers (`bg-primary/10`,
|
|
184
|
+
`hover:bg-primary/90`) before adding a token; add one the canonical way (a
|
|
185
|
+
`--x` var plus a `--color-x: var(--x)` line in `@theme inline`).
|
|
138
186
|
- Custom-element tag names are passed to `.register('tag-name')`. They are NOT
|
|
139
187
|
a static field on the class.
|
|
140
188
|
- **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.
|
|
141
189
|
- One function per server action file (`*.server.ts`).
|
|
190
|
+
- **`.server.ts` vs `'use server'`, the decision.** Will the client call it?
|
|
191
|
+
Add `'use server'` and the file becomes an RPC action (browser import
|
|
192
|
+
rewritten to a typed stub). Is it server-only infra instead (a DB driver,
|
|
193
|
+
secrets, `node:*`)? Use NO directive, and NEVER import it into a
|
|
194
|
+
page/layout/component. Reach it from a `'use server'` action, `route.ts`, or
|
|
195
|
+
`middleware.ts`. A `.server.ts` WITHOUT the directive is a server-only utility
|
|
196
|
+
whose browser import throws at module load.
|
|
197
|
+
- **Label every interactive control.** Give each control an accessible name, and
|
|
198
|
+
make clickable text a `<label for="control-id">` (or the control itself) so a
|
|
199
|
+
text click activates the control on BOTH the JS path and the no-JS
|
|
200
|
+
form-submit path. Use `aria-label` and `aria-pressed` on icon-only controls.
|
|
201
|
+
`assertNoA11yViolations(el)` in a browser test catches missing labels.
|
|
142
202
|
- Server-only code (a DB driver like `pg`, `node:*`, anything that needs Node APIs)
|
|
143
203
|
goes only in `.server.{js,ts}` files, `route.ts` handlers, or
|
|
144
204
|
`middleware.ts`. Never in pages, layouts, or components. Wrap the access in
|
|
145
205
|
a `.server.{js,ts}` file; the framework rewrites that import into an RPC
|
|
146
206
|
stub for the browser. `lib/` holds both server-only infra
|
|
147
207
|
(the DB in `db/*.server.ts`) and browser-safe utilities (`lib/utils/cn.ts` with
|
|
148
|
-
`cn`); follow the same rule per file.
|
|
208
|
+
`cn`); follow the same rule per file. A TYPE-ONLY `import type { Todo } from
|
|
209
|
+
'#db/schema.server.ts'` is the exception, fine in a page or component because
|
|
210
|
+
the stripper erases it before it reaches the browser.
|
|
149
211
|
- Keep pages and layouts as pure carriers so their modules stay out of the
|
|
150
212
|
network tab. A page/layout never hydrates; the framework drops its module
|
|
151
213
|
from the browser as long as its only browser job is registering the
|
|
@@ -180,6 +242,12 @@ self-review loop.
|
|
|
180
242
|
paint), NOT from `fetch` calls in `connectedCallback`. For write-paths, prefer `<form action=...>` plus
|
|
181
243
|
server action over `fetch` plus click handler. The framework upgrades plain
|
|
182
244
|
forms to partial-swap submissions automatically.
|
|
245
|
+
- **Default to optimistic UI for feasible mutations.** Use `optimistic()` from
|
|
246
|
+
`@webjsdev/core` for create/toggle/like/reorder so the UI updates instantly
|
|
247
|
+
and rolls back on failure (no hand-written try-catch or temp-id bookkeeping).
|
|
248
|
+
Do NOT use it where it hurts: unpredictable or server-computed results,
|
|
249
|
+
side-effectful or OAuth/payment mutations, and destructive irreversible
|
|
250
|
+
actions (confirm-first instead).
|
|
183
251
|
- **Client navigation is auto-magic.** Real `<a href>` and `<form action>`
|
|
184
252
|
get partial-swap behavior with no opt-in. Because layouts persist across
|
|
185
253
|
navigation, put shared chrome (sidenav, header) in `layout.ts` and
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Node walker for check-server-imports.sh (#804). Reads the PreToolUse JSON
|
|
2
|
+
// payload (arg or stdin), extracts the file being edited and its proposed
|
|
3
|
+
// content, and warns when a browser-facing app module adds an import of a
|
|
4
|
+
// server-only `.server.*` utility (no `'use server'`). WARN by default; a
|
|
5
|
+
// clean edit prints nothing and exits 0.
|
|
6
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
7
|
+
import { dirname, resolve, join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
function readPayload() {
|
|
10
|
+
const arg = process.argv[2];
|
|
11
|
+
if (arg && arg.trim().startsWith('{')) return arg;
|
|
12
|
+
try { return readFileSync(0, 'utf8'); } catch { return ''; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let data;
|
|
16
|
+
try { data = JSON.parse(readPayload() || '{}'); } catch { process.exit(0); }
|
|
17
|
+
const input = data.tool_input || {};
|
|
18
|
+
const filePath = input.file_path || input.filePath || '';
|
|
19
|
+
if (!filePath) process.exit(0);
|
|
20
|
+
|
|
21
|
+
// Only browser-facing app modules matter. A `.server.*` file (the boundary) or a
|
|
22
|
+
// route.ts / middleware.ts (never shipped) is allowed to import server code.
|
|
23
|
+
const rel = filePath.replace(/\\/g, '/');
|
|
24
|
+
const isAppModule = /\/(app|components|modules|lib)\/.*\.(ts|js|mts|mjs)$/.test('/' + rel) || /(^|\/)(app|components|modules|lib)\//.test(rel);
|
|
25
|
+
if (!isAppModule) process.exit(0);
|
|
26
|
+
if (/\.server\.(ts|js|mts|mjs)$/.test(rel)) process.exit(0);
|
|
27
|
+
if (/(^|\/)(route|middleware)\.(ts|js|mts|mjs)$/.test(rel)) process.exit(0);
|
|
28
|
+
|
|
29
|
+
// Proposed content: Write has `content`; Edit has `new_string`; else read disk.
|
|
30
|
+
let content = input.content ?? input.new_string ?? '';
|
|
31
|
+
if (!content && existsSync(filePath)) { try { content = readFileSync(filePath, 'utf8'); } catch { /* ignore */ } }
|
|
32
|
+
if (!content) process.exit(0);
|
|
33
|
+
|
|
34
|
+
// Find the app root (walks up for a package.json with a `#*` imports map or a db/ dir).
|
|
35
|
+
function findAppRoot(start) {
|
|
36
|
+
let dir = dirname(resolve(start));
|
|
37
|
+
for (let i = 0; i < 8; i++) {
|
|
38
|
+
if (existsSync(join(dir, 'package.json')) && (existsSync(join(dir, 'app')) || existsSync(join(dir, 'db')))) return dir;
|
|
39
|
+
const up = dirname(dir);
|
|
40
|
+
if (up === dir) break;
|
|
41
|
+
dir = up;
|
|
42
|
+
}
|
|
43
|
+
return dirname(resolve(start));
|
|
44
|
+
}
|
|
45
|
+
const appRoot = findAppRoot(filePath);
|
|
46
|
+
|
|
47
|
+
// Collect import specifiers, skipping `import type` (erased by the stripper).
|
|
48
|
+
const specs = [];
|
|
49
|
+
const re = /(?:^|\n)\s*import\s+(type\s+)?[^;'"]*?from\s*['"]([^'"]+)['"]/g;
|
|
50
|
+
let m;
|
|
51
|
+
while ((m = re.exec(content))) { if (!m[1]) specs.push(m[2]); }
|
|
52
|
+
|
|
53
|
+
function resolveSpec(spec) {
|
|
54
|
+
if (spec.startsWith('#')) return join(appRoot, spec.slice(1).replace(/^\//, ''));
|
|
55
|
+
if (spec.startsWith('.')) return resolve(dirname(filePath), spec);
|
|
56
|
+
return null; // bare npm specifier
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const offenders = [];
|
|
60
|
+
for (const spec of specs) {
|
|
61
|
+
if (!/\.server\.(ts|js|mts|mjs)$/.test(spec)) continue;
|
|
62
|
+
const abs = resolveSpec(spec);
|
|
63
|
+
if (!abs || !existsSync(abs)) continue;
|
|
64
|
+
let src = '';
|
|
65
|
+
try { src = readFileSync(abs, 'utf8'); } catch { continue; }
|
|
66
|
+
const head = src.split('\n').slice(0, 5).join('\n');
|
|
67
|
+
const hasUseServer = /^\s*(['"])use server\1\s*;?\s*$/m.test(head);
|
|
68
|
+
if (!hasUseServer) offenders.push(spec);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (offenders.length === 0) process.exit(0);
|
|
72
|
+
|
|
73
|
+
const msg =
|
|
74
|
+
`A browser-facing module (${rel}) imports a server-only utility: ${offenders.join(', ')}. ` +
|
|
75
|
+
`A .server.{ts,js} file with NO 'use server' directive throws at load in the browser, ` +
|
|
76
|
+
`so this would crash the page (webjs check flags it as no-server-import-in-browser-module). ` +
|
|
77
|
+
`Fix: add 'use server' to make it an RPC action, or reach it from a 'use server' action / route.ts / ` +
|
|
78
|
+
`middleware.ts, or share only a type via 'import type'. See agent-docs/types-and-mutations.md.`;
|
|
79
|
+
|
|
80
|
+
if (process.env.WEBJS_SERVER_IMPORT_GATE === 'block') {
|
|
81
|
+
process.stderr.write(`BLOCKED: ${msg}\n`);
|
|
82
|
+
process.exit(2);
|
|
83
|
+
}
|
|
84
|
+
// WARN: surface as additionalContext, allow the edit.
|
|
85
|
+
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: msg } }) + '\n');
|
|
86
|
+
process.exit(0);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# PreToolUse hook (scaffolded by `webjs create`): WARN, at write time, when an
|
|
4
|
+
# Edit/Write to a browser-facing app module (a page / layout / component under
|
|
5
|
+
# app/ or components/ or modules/, NOT a `.server.*` file) adds an import of a
|
|
6
|
+
# server-only `.server.{ts,js}` utility (a `.server.*` file with NO `'use server'`
|
|
7
|
+
# directive). In the browser that import resolves to a throw-at-load stub, so the
|
|
8
|
+
# module crashes the moment it loads. This is the #804 first-pass iteration loop:
|
|
9
|
+
# `webjs check`'s `no-server-import-in-browser-module` catches it AFTER the file
|
|
10
|
+
# lands; this hook surfaces it BEFORE, so the agent never writes the wrong shape.
|
|
11
|
+
#
|
|
12
|
+
# WARN, not block (the convention-vs-check principle in this app's AGENTS.md):
|
|
13
|
+
# `webjs check` is the authoritative gate, and a pre-edit static peek cannot see
|
|
14
|
+
# the full elision verdict, so a hard block could false-positive on a display-only
|
|
15
|
+
# page the framework would elide. So this emits a loud reminder and allows the
|
|
16
|
+
# edit. Set WEBJS_SERVER_IMPORT_GATE=block to hard-block instead; set
|
|
17
|
+
# WEBJS_NO_SERVER_IMPORT_GATE=1 to skip.
|
|
18
|
+
#
|
|
19
|
+
# A `'use server'` action import is fine (it becomes a working RPC stub), and a
|
|
20
|
+
# `import type { ... } from './x.server.ts'` is fine (the stripper erases it), so
|
|
21
|
+
# both are ignored.
|
|
22
|
+
|
|
23
|
+
[ "$WEBJS_NO_SERVER_IMPORT_GATE" = "1" ] && exit 0
|
|
24
|
+
|
|
25
|
+
payload="$(cat)"
|
|
26
|
+
node "$(dirname "$0")/check-server-imports.mjs" "$payload"
|
package/templates/.cursorrules
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
You are working on a webjs app, an AI-first, no-build, web-components-first
|
|
4
4
|
framework. Read AGENTS.md for the full API reference and CONVENTIONS.md for
|
|
5
5
|
project-specific conventions before writing any code. When AGENTS.md doesn't
|
|
6
|
-
cover what you need, the full hosted docs are at **https://docs.webjs.
|
|
6
|
+
cover what you need, the full hosted docs are at **https://docs.webjs.dev**.
|
|
7
7
|
|
|
8
8
|
## Persistence + scaffold rules (non-negotiable)
|
|
9
9
|
|
|
@@ -18,6 +18,41 @@ cover what you need, the full hosted docs are at **https://docs.webjs.com**.
|
|
|
18
18
|
`app/page.ts`, the example `User` model, the example users module, etc.
|
|
19
19
|
with the app the user actually asked for. Don't ship "Hello from
|
|
20
20
|
<app-name>" as the deliverable.
|
|
21
|
+
- **Study the gallery first, prune it second.** The full-stack and saas scaffolds ship
|
|
22
|
+
single-feature demos under `app/features/` plus one whole example app under
|
|
23
|
+
`app/examples/` (logic in `modules/`). It is your PRIMARY webjs reference:
|
|
24
|
+
read every demo end to end (code AND comments) to learn the idioms BEFORE you
|
|
25
|
+
write or delete anything. Only after internalising the patterns should you
|
|
26
|
+
prune, keeping and adapting what you need and deleting the rest (the
|
|
27
|
+
`app/features/<name>` or `app/examples/<name>` route AND its `modules/<name>`;
|
|
28
|
+
for the todo app, also the `todos` table). Never delete blindly up front. Each
|
|
29
|
+
route page has a `webjs-scaffold-placeholder` marker so `webjs check` fails
|
|
30
|
+
until you resolve it. Delete now-empty directories after pruning. The `api`
|
|
31
|
+
template ships a BACKEND-features showcase instead (endpoints under
|
|
32
|
+
`app/api/features/`: the `route()` adapter + validation, rate limiting,
|
|
33
|
+
streaming, file storage, WebSockets + broadcast, plus `env.ts` validation),
|
|
34
|
+
listed in the root `app/route.ts` index; prune it the same way.
|
|
35
|
+
- **Prune what the app does not use.** Keep the infrastructure the app
|
|
36
|
+
USES and delete the rest (files AND folders). No persistence means
|
|
37
|
+
delete `db/`, `drizzle.config.ts`, and the `db:*` scripts. No UI kit
|
|
38
|
+
used means delete `components/ui/`, `components.json`, and
|
|
39
|
+
`lib/utils/cn.ts`. No PWA means delete `public/sw.js` and
|
|
40
|
+
`offline.html`. Always KEEP the durable knowledge (`AGENTS.md`,
|
|
41
|
+
`CONVENTIONS.md`, the rule files, the MCP), never prune it, so removing
|
|
42
|
+
example code never removes your context. Prune AFTER using the features
|
|
43
|
+
and examples as reference, never blindly up front. A no-op for the `api` template
|
|
44
|
+
(no UI kit, no PWA files).
|
|
45
|
+
- **`app/` is routing-only.** Only routing files live in `app/` (page,
|
|
46
|
+
layout, route, middleware, metadata routes). CSS, helpers, and constants
|
|
47
|
+
do NOT: `globals.css` is at `styles/`, browser-safe helpers at
|
|
48
|
+
`lib/utils/`, feature logic in `modules/`.
|
|
49
|
+
- **Use a unique design (UI apps).** When the app has a UI, give it a
|
|
50
|
+
design of your own (palette, layout, typography, chrome) that fits what
|
|
51
|
+
the user asked for. Do NOT mimic the scaffold's example look (its warm
|
|
52
|
+
colors, the 760px reading column, the example header/nav). The `api`
|
|
53
|
+
template has no UI, so this does not apply there. Keep the design tokens
|
|
54
|
+
and theme setup in `app/layout.ts`, those are infrastructure, and
|
|
55
|
+
restyle on top of them.
|
|
21
56
|
- **Only three templates exist:** `webjs create <name>` (default
|
|
22
57
|
full-stack), `--template api`, `--template saas`. The CLI rejects any
|
|
23
58
|
other `--template` value. Pick:
|
|
@@ -110,13 +145,17 @@ self-review loop.
|
|
|
110
145
|
- No build step: source files are served as ES modules
|
|
111
146
|
- **Erasable TypeScript only.** The runtime strips types via `module.stripTypeScriptTypes` (Node's built-in, or `amaro` on Bun) (whitespace replacement, byte-exact position preservation, no sourcemap). The scaffold's tsconfig.json sets `erasableSyntaxOnly: true`, so the TS compiler rejects `enum`, `namespace` with values, constructor parameter properties, legacy decorators with `emitDecoratorMetadata`, and `import = require`. Use erasable equivalents: `const X = { ... } as const` plus a derived union type instead of `enum`; explicit fields plus constructor body assignments instead of parameter properties. If `erasableSyntaxOnly` is disabled and non-erasable syntax is used, 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.
|
|
112
147
|
- **Tailwind-first styling.** Tailwind utilities are the strong default for pages AND light-DOM components (the default DOM mode): layout, spacing, color (via `@theme` tokens), typography, borders, radius, shadows, interaction states. Light DOM does not scope, so utilities apply directly. The lit reflex to scope CSS (`static styles = css`) or write an inline `<style>` with semantic class names (`.hero`, `.card`) in a light-DOM component is wrong: the scoped block needs `static shadow = true`, and inline class names leak globally. When a utility bundle repeats, extract a `lib/utils/ui.ts` helper returning an `html` fragment, not a CSS class. Reserve raw CSS for the allowlist (design tokens / `@theme`, `@property` + `@keyframes`, `::-webkit-scrollbar`, `prefers-reduced-motion`, complex `color-mix()` / gradients); when unavoidable in a light-DOM component, prefix every class selector with the component tag.
|
|
148
|
+
- **One theme, canonical tokens.** The app has a SINGLE theme, defined once in `app/layout.ts` using the standard `@webjsdev/ui` (shadcn-compatible) semantic tokens set to the brand palette. Use the canonical utility names everywhere, in the page chrome AND inside components: `bg-background`, `text-foreground`, `bg-card`, `bg-muted`, `text-muted-foreground`, `bg-primary`, `text-primary-foreground`, `bg-accent`, `text-accent-foreground`, `border-border`, `ring-ring`. These are exactly the tokens a component copied in by `webjs ui add <name>` reads, so a scaffolded page and a later-added ui component share one theme with no wiring. NEVER invent a parallel token vocabulary (`--fg`, `--bg`, `text-fg`, `bg-elev`, a separate `--brand`): it collides with the ui tokens (the accent once flipped to neutral on navigation for exactly this reason) and diverges from the shadcn conventions the kit and AI agents expect. Reach for opacity modifiers (`bg-primary/10`, `hover:bg-primary/90`) before adding a token; add one the canonical way (a `--x` var plus a `--color-x: var(--x)` line in `@theme inline`).
|
|
113
149
|
- Shadow-DOM components opt in with `static shadow = true` and use `static styles = css` for scoped CSS, not inline styles. That is the right home for scoped CSS.
|
|
114
150
|
- One function per server action file (*.server.ts)
|
|
151
|
+
- **`.server.ts` vs `'use server'`, the decision.** Will the client call it? Add `'use server'` and the file becomes an RPC action (browser import rewritten to a typed stub). Is it server-only infra instead (a DB driver, secrets, node:*)? Use NO directive, and NEVER import it into a page/layout/component. Reach it from a `'use server'` action, `route.ts`, or `middleware.ts`. A `.server.ts` WITHOUT the directive is a server-only utility whose browser import throws at module load.
|
|
152
|
+
- **Label every interactive control.** Give each control an accessible name, and make clickable text a `<label for="control-id">` (or the control itself) so a text click activates the control on BOTH the JS path and the no-JS form-submit path. Use `aria-label` and `aria-pressed` on icon-only controls. `assertNoA11yViolations(el)` in a browser test catches missing labels.
|
|
115
153
|
- Components must call customElements.define('tag', Class)
|
|
116
154
|
- **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.
|
|
117
|
-
- Server-only code (the DB driver `pg`, node:*, anything that needs Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap the access in a .server.{js,ts} file; the framework rewrites that import into an RPC stub for the browser. The DB lives in db/*.server.ts (db/connection.server.ts); lib/ holds other server-only infra and browser-safe utilities (lib/utils/cn.ts with cn); follow the same rule per file: if a lib/ file needs Node APIs, only import it from server-only files.
|
|
155
|
+
- Server-only code (the DB driver `pg`, node:*, anything that needs Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap the access in a .server.{js,ts} file; the framework rewrites that import into an RPC stub for the browser. The DB lives in db/*.server.ts (db/connection.server.ts); lib/ holds other server-only infra and browser-safe utilities (lib/utils/cn.ts with cn); follow the same rule per file: 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.
|
|
118
156
|
- Directives: webjs exports the lit directives with no clean native equivalent (`repeat`, `unsafeHTML`, `live`, `keyed`, `guard`, `cache`, `until`, `ref` / `createRef`, `templateContent`, `asyncAppend` / `asyncReplace`, `watch`). Lit's `classMap` / `styleMap` / `ifDefined` / `when` / `choose` are NOT exported. For those, use plain template-literal expressions (`class=${cond ? 'a' : 'b'}`, `${cond ? a : b}`) and lifecycle hooks (`this.query('#el')` in `firstUpdated`) instead.
|
|
119
157
|
- **Progressive enhancement is the default.** Pages AND every web component are SSR'd to real HTML. Write components so the first paint is the right content. Read SSR-meaningful defaults in `constructor()`. `connectedCallback` is never called on the server, so anything there only runs after hydration. Initial data for components comes from the page function (server-side fetch plus pass as attribute/property) OR from an `async render()` in the component itself (`const u = await getUser(this.uid)`, which SSR awaits so the data is in the first paint), NOT from `fetch` calls in `connectedCallback`. Prefer the co-located `async render()` over prop-drilling; `renderFallback()` is the optional re-fetch loading state (never first paint), and a `Task` is for genuinely client-only data. For write-paths, prefer `<form action=...>` plus server action over `fetch` plus click handler. The framework upgrades plain forms to partial-swap submissions automatically.
|
|
158
|
+
- **Default to optimistic UI for feasible mutations.** Use `optimistic()` from `@webjsdev/core` for create/toggle/like/reorder so the UI updates instantly and rolls back on failure (no hand-written try-catch or temp-id bookkeeping). Do NOT use it where it hurts: unpredictable or server-computed results, side-effectful or OAuth/payment mutations, and destructive irreversible actions (confirm-first instead).
|
|
120
159
|
- **Client navigation is auto-magic.** Real `<a href>` and `<form action>` get partial-swap behavior with no opt-in. Because layouts persist across navigation, put shared chrome (sidenav, header) in `layout.ts` and page-specific content in `page.ts`. For validation errors, return 4xx HTML from a `route.ts` POST handler; the router renders it in place preserving the user's input. For non-layout swap regions, wrap in `<webjs-frame id="...">`. See "Client navigation patterns" in AGENTS.md.
|
|
121
160
|
- **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 job is registering the components it imports. It starts shipping its own module (invisible in tests, an elision verdict) the moment its closure does any OTHER client work. Don't 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) or 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.
|
|
122
161
|
- See AGENTS.md for the complete directive decision guide
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
You are working on a webjs app, an AI-first, no-build, web-components-first
|
|
4
4
|
framework. Read AGENTS.md for the full API reference and CONVENTIONS.md for
|
|
5
5
|
project-specific conventions. When AGENTS.md doesn't cover what you need,
|
|
6
|
-
the full hosted docs are at **https://docs.webjs.
|
|
6
|
+
the full hosted docs are at **https://docs.webjs.dev**.
|
|
7
7
|
|
|
8
8
|
## Persistence + scaffold rules (non-negotiable)
|
|
9
9
|
|
|
@@ -18,6 +18,41 @@ the full hosted docs are at **https://docs.webjs.com**.
|
|
|
18
18
|
`app/page.ts`, the example `User` model, the example users module, etc.
|
|
19
19
|
with the app the user actually asked for. Don't ship "Hello from
|
|
20
20
|
<app-name>" as the deliverable.
|
|
21
|
+
- **Study the gallery first, prune it second.** The full-stack and saas scaffolds ship
|
|
22
|
+
single-feature demos under `app/features/` plus one whole example app under
|
|
23
|
+
`app/examples/` (logic in `modules/`). It is your PRIMARY webjs reference:
|
|
24
|
+
read every demo end to end (code AND comments) to learn the idioms BEFORE you
|
|
25
|
+
write or delete anything. Only after internalising the patterns should you
|
|
26
|
+
prune, keeping and adapting what you need and deleting the rest (the
|
|
27
|
+
`app/features/<name>` or `app/examples/<name>` route AND its `modules/<name>`;
|
|
28
|
+
for the todo app, also the `todos` table). Never delete blindly up front. Each
|
|
29
|
+
route page has a `webjs-scaffold-placeholder` marker so `webjs check` fails
|
|
30
|
+
until you resolve it. Delete now-empty directories after pruning. The `api`
|
|
31
|
+
template ships a BACKEND-features showcase instead (endpoints under
|
|
32
|
+
`app/api/features/`: the `route()` adapter + validation, rate limiting,
|
|
33
|
+
streaming, file storage, WebSockets + broadcast, plus `env.ts` validation),
|
|
34
|
+
listed in the root `app/route.ts` index; prune it the same way.
|
|
35
|
+
- **Prune what the app does not use.** Keep the infrastructure the app
|
|
36
|
+
USES and delete the rest (files AND folders). No persistence means
|
|
37
|
+
delete `db/`, `drizzle.config.ts`, and the `db:*` scripts. No UI kit
|
|
38
|
+
used means delete `components/ui/`, `components.json`, and
|
|
39
|
+
`lib/utils/cn.ts`. No PWA means delete `public/sw.js` and
|
|
40
|
+
`offline.html`. Always KEEP the durable knowledge (`AGENTS.md`,
|
|
41
|
+
`CONVENTIONS.md`, the rule files, the MCP), never prune it, so removing
|
|
42
|
+
example code never removes your context. Prune AFTER using the features
|
|
43
|
+
and examples as reference, never blindly up front. A no-op for the `api` template
|
|
44
|
+
(no UI kit, no PWA files).
|
|
45
|
+
- **`app/` is routing-only.** Only routing files live in `app/` (page,
|
|
46
|
+
layout, route, middleware, metadata routes). CSS, helpers, and constants
|
|
47
|
+
do NOT: `globals.css` is at `styles/`, browser-safe helpers at
|
|
48
|
+
`lib/utils/`, feature logic in `modules/`.
|
|
49
|
+
- **Use a unique design (UI apps).** When the app has a UI, give it a
|
|
50
|
+
design of your own (palette, layout, typography, chrome) that fits what
|
|
51
|
+
the user asked for. Do NOT mimic the scaffold's example look (its warm
|
|
52
|
+
colors, the 760px reading column, the example header/nav). The `api`
|
|
53
|
+
template has no UI, so this does not apply there. Keep the design tokens
|
|
54
|
+
and theme setup in `app/layout.ts`, those are infrastructure, and
|
|
55
|
+
restyle on top of them.
|
|
21
56
|
- **Only three templates exist:** `webjs create <name>` (default
|
|
22
57
|
full-stack), `--template api`, `--template saas`. The CLI rejects any
|
|
23
58
|
other `--template` value. Pick:
|
|
@@ -107,10 +142,14 @@ each change must include.
|
|
|
107
142
|
- **Erasable TypeScript only.** The runtime strips types via `module.stripTypeScriptTypes` (Node's built-in, or `amaro` on Bun) (whitespace replacement, byte-exact position preservation, no sourcemap). The scaffold's tsconfig.json sets `erasableSyntaxOnly: true`, so the TS compiler rejects `enum`, `namespace` with values, constructor parameter properties, legacy decorators with `emitDecoratorMetadata`, and `import = require`. Use erasable equivalents: `const X = { ... } as const` plus a derived union type instead of `enum`; explicit fields plus constructor body assignments instead of parameter properties. If `erasableSyntaxOnly` is disabled and non-erasable syntax is used, 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.
|
|
108
143
|
- Tagged template: html`<div>${value}</div>` with css`...` for styles.
|
|
109
144
|
- **Tailwind-first styling.** Tailwind utilities are the strong default for pages AND light-DOM components (the default DOM mode): layout, spacing, color (via `@theme` tokens), typography, borders, radius, shadows, interaction states. Light DOM does not scope, so utilities apply directly. The lit reflex to scope CSS (`static styles = css\`...\``) or write an inline `<style>` with semantic class names (`.hero`, `.card`) in a light-DOM component is wrong: the scoped block needs `static shadow = true`, and inline class names leak globally. When a utility bundle repeats, extract a `lib/utils/ui.ts` helper returning an `html` fragment, not a CSS class. Reserve raw CSS for the allowlist (design tokens / `@theme`, `@property` + `@keyframes`, `::-webkit-scrollbar`, `prefers-reduced-motion`, complex `color-mix()` / gradients); when unavoidable in a light-DOM component, prefix every class selector with the component tag. Shadow-DOM components (`static shadow = true`) legitimately author `static styles = css\`...\`` for scoped CSS; don't use inline `style="..."` there.
|
|
145
|
+
- **One theme, canonical tokens.** The app has a SINGLE theme, defined once in `app/layout.ts` using the standard `@webjsdev/ui` (shadcn-compatible) semantic tokens set to the brand palette. Use the canonical utility names everywhere, in the page chrome AND inside components: `bg-background`, `text-foreground`, `bg-card`, `bg-muted`, `text-muted-foreground`, `bg-primary`, `text-primary-foreground`, `bg-accent`, `text-accent-foreground`, `border-border`, `ring-ring`. These are exactly the tokens a component copied in by `webjs ui add <name>` reads, so a scaffolded page and a later-added ui component share one theme with no wiring. NEVER invent a parallel token vocabulary (`--fg`, `--bg`, `text-fg`, `bg-elev`, a separate `--brand`): it collides with the ui tokens (the accent once flipped to neutral on navigation for exactly this reason) and diverges from the shadcn conventions the kit and AI agents expect. Reach for opacity modifiers (`bg-primary/10`, `hover:bg-primary/90`) before adding a token; add one the canonical way (a `--x` var plus a `--color-x: var(--x)` line in `@theme inline`).
|
|
110
146
|
- Components: extend the `WebComponent({ ... })` factory to declare reactive properties (e.g. `extends WebComponent({ count: Number })`; per-prop options via `prop(Number, { reflect: true })`), add `static styles` for shadow-DOM components, call `Class.register('tag-name')` at the bottom of the file. The tag name is the argument to `.register()`, not a static field. A hand-written `static properties` throws at construction (`no-static-properties`); set defaults in the constructor, never a class-field initializer (`reactive-props-no-class-field`). Declare an array-typed prop with the `Array` constructor, not `Object` (`items: prop<Tag[]>(Array)`), flagged by `array-prop-uses-array-type`. **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.
|
|
111
147
|
- Async data in a component: prefer an `async render()` (`const u = await getUser(this.uid)`), which SSR awaits so the data is in the first paint, over prop-drilling from the page or fetching in `connectedCallback`. `renderFallback()` is the optional re-fetch loading state (never first paint); error isolation is automatic (`renderError()` customizes it); a `Task` is for genuinely client-only data.
|
|
148
|
+
- **Default to optimistic UI for feasible mutations.** Use `optimistic()` from `@webjsdev/core` for create/toggle/like/reorder so the UI updates instantly and rolls back on failure (no hand-written try-catch or temp-id bookkeeping). Do NOT use it where it hurts: unpredictable or server-computed results, side-effectful or OAuth/payment mutations, and destructive irreversible actions (confirm-first instead).
|
|
112
149
|
- Server actions: *.server.ts files with one exported async function each.
|
|
113
|
-
-
|
|
150
|
+
- **`.server.ts` vs `'use server'`, the decision.** Will the client call it? Add `'use server'` and the file becomes an RPC action (browser import rewritten to a typed stub). Is it server-only infra instead (a DB driver, secrets, node:*)? Use NO directive, and NEVER import it into a page/layout/component. Reach it from a `'use server'` action, route.ts, or middleware.ts. A `.server.ts` WITHOUT the directive is a server-only utility whose browser import throws at module load.
|
|
151
|
+
- **Label every interactive control.** Give each control an accessible name, and make clickable text a `<label for="control-id">` (or the control itself) so a text click activates the control on BOTH the JS path and the no-JS form-submit path. Use `aria-label` and `aria-pressed` on icon-only controls. `assertNoA11yViolations(el)` in a browser test catches missing labels.
|
|
152
|
+
- Server-only code (a DB driver like pg, node:*, anything needing Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap in a .server.{js,ts} file; the framework rewrites that import to an RPC stub for the browser. The DB lives in db/*.server.ts; lib/ holds other server-only infra and browser-safe utilities (lib/utils/cn.ts with cn); apply the same rule per file. 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.
|
|
114
153
|
- Directives: webjs exports the lit directives with no clean native equivalent (`repeat`, `unsafeHTML`, `live`, `keyed`, `guard`, `cache`, `until`, `ref` / `createRef`, `templateContent`, `asyncAppend` / `asyncReplace`, `watch`). Lit's `classMap` / `styleMap` / `ifDefined` / `when` / `choose` are NOT exported; use plain template-literal expressions instead.
|
|
115
154
|
- Context: import { createContext, ContextProvider, ContextConsumer } from '@webjsdev/core/context'
|
|
116
155
|
- Task: import { Task, TaskStatus } from '@webjsdev/core/task'
|