@webjsdev/cli 0.8.1
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 +71 -0
- package/bin/webjs.js +279 -0
- package/lib/create.js +898 -0
- package/lib/saas-template.js +397 -0
- package/package.json +39 -0
- package/templates/.claude/hooks/block-prose-punctuation.sh +236 -0
- package/templates/.claude/hooks/guard-branch-context.sh +39 -0
- package/templates/.claude/hooks/nudge-uncommitted.sh +46 -0
- package/templates/.claude/settings.json +35 -0
- package/templates/.claude.json +9 -0
- package/templates/.cursor/hooks/nudge-uncommitted.sh +38 -0
- package/templates/.cursor/hooks.json +8 -0
- package/templates/.cursorrules +99 -0
- package/templates/.editorconfig +18 -0
- package/templates/.env.example +27 -0
- package/templates/.gemini/hooks/nudge-uncommitted.sh +42 -0
- package/templates/.gemini/settings.json +15 -0
- package/templates/.github/copilot-instructions.md +85 -0
- package/templates/.github/pull_request_template.md +14 -0
- package/templates/.hooks/pre-commit +48 -0
- package/templates/.opencode/plugins/nudge-uncommitted.ts +62 -0
- package/templates/.windsurfrules +91 -0
- package/templates/AGENTS.md +816 -0
- package/templates/CLAUDE.md +2 -0
- package/templates/CONVENTIONS.md +901 -0
- package/templates/lib/utils/ui.ts +83 -0
- package/templates/public/tailwind-browser.js +947 -0
- package/templates/test/hello/browser/hello.test.js +40 -0
- package/templates/test/hello/e2e/hello.test.ts +87 -0
- package/templates/test/hello/hello.test.ts +24 -0
- package/templates/web-test-runner.config.js +33 -0
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
# AGENTS.md for {{APP_NAME}}
|
|
2
|
+
|
|
3
|
+
Read this before editing any file. This is a webjs app: AI-first, web-
|
|
4
|
+
components-first, no build step. The framework's own full API reference
|
|
5
|
+
lives at https://github.com/vivek7405/webjs/blob/main/AGENTS.md and the
|
|
6
|
+
full hosted documentation (every API, recipe, and example) lives at
|
|
7
|
+
**https://docs.webjs.com**. Treat this file as the app-scoped
|
|
8
|
+
companion and reach for docs.webjs.com whenever you need more detail.
|
|
9
|
+
|
|
10
|
+
## If you just scaffolded this app (AI agents, read first)
|
|
11
|
+
|
|
12
|
+
This project was created with `webjs create`. The files you see right
|
|
13
|
+
now (`app/page.ts` printing "Hello from {{APP_NAME}}", the example `User`
|
|
14
|
+
model in `prisma/schema.prisma`, the `theme-toggle` component, the
|
|
15
|
+
example users module in api/saas templates) are **starting-point
|
|
16
|
+
references, not the final product**. Your job is to replace them with
|
|
17
|
+
the app the user actually asked for.
|
|
18
|
+
|
|
19
|
+
**Non-negotiables for every webjs app:**
|
|
20
|
+
|
|
21
|
+
1. **Use Prisma + SQLite for persistence.** It's already wired up
|
|
22
|
+
(`prisma/schema.prisma`, `lib/prisma.server.ts`, `npm run db:migrate`,
|
|
23
|
+
`predev` hook running `prisma generate`). For any data the app
|
|
24
|
+
stores (todos, posts, messages, products, comments, anything),
|
|
25
|
+
define a Prisma model and persist there.
|
|
26
|
+
- **NEVER** store app data in JSON files (`data/todos.json`,
|
|
27
|
+
`db.json`, …). The convention check `no-json-data-files` flags
|
|
28
|
+
this and the user's prompt explicitly forbids it.
|
|
29
|
+
- **NEVER** use in-memory arrays or `Map`s as a substitute for the
|
|
30
|
+
database. They vanish on every dev-server reload and aren't
|
|
31
|
+
shared across processes.
|
|
32
|
+
- **NEVER** use `localStorage` to persist app data. It's per-browser
|
|
33
|
+
and doesn't reach the server.
|
|
34
|
+
2. **One of three scaffolds only.** The CLI exposes exactly three:
|
|
35
|
+
`full-stack` (default), `--template api`, `--template saas`. Don't
|
|
36
|
+
reach for a `--template blog` / `--template todo` / `--template
|
|
37
|
+
ecommerce`. They don't exist and the CLI will reject them.
|
|
38
|
+
3. **First step after scaffolding:** edit `prisma/schema.prisma` to the
|
|
39
|
+
app's real domain models (delete the example `User` model unless the
|
|
40
|
+
app actually needs users), run `webjs db migrate <name>`, then build
|
|
41
|
+
pages / actions / queries against those models.
|
|
42
|
+
|
|
43
|
+
**Picking the right scaffold from the user's prompt** (you do this BEFORE
|
|
44
|
+
running `webjs create`; if you're reading this you've already scaffolded.
|
|
45
|
+
Verify the choice was correct, otherwise re-scaffold in a fresh dir):
|
|
46
|
+
|
|
47
|
+
| User asks for… | Scaffold |
|
|
48
|
+
|---|---|
|
|
49
|
+
| Todo app, blog, notes, dashboard, marketplace, social feed, e-commerce, any product with a UI | `webjs create <name>` (default full-stack) |
|
|
50
|
+
| HTTP/JSON API only, no UI | `webjs create <name> --template api` |
|
|
51
|
+
| Anything with login / signup / accounts / protected pages / SaaS | `webjs create <name> --template saas` |
|
|
52
|
+
|
|
53
|
+
When in doubt, **full-stack is the default**. Pick `api` only if the user
|
|
54
|
+
is explicit about wanting a backend-only API. Pick `saas` only if the user
|
|
55
|
+
is explicit about auth / accounts / SaaS.
|
|
56
|
+
|
|
57
|
+
## Framework source is in `node_modules/`
|
|
58
|
+
|
|
59
|
+
No build step, no bundler, no minification. What you read is what
|
|
60
|
+
runs. When in doubt, grep the framework:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
node_modules/@webjsdev/
|
|
64
|
+
core/ renderer, WebComponent, directives, client router,
|
|
65
|
+
Task, context, testing helpers
|
|
66
|
+
src/component.js ← lifecycle, properties, light vs shadow DOM
|
|
67
|
+
src/render-client.js ← client-side DOM patching + hydration
|
|
68
|
+
src/render-server.js ← renderToString / renderToStream
|
|
69
|
+
src/router-client.js ← Turbo-Drive-style client navigation
|
|
70
|
+
src/directives.js ← unsafeHTML, live
|
|
71
|
+
src/context.js ← Context Protocol
|
|
72
|
+
src/task.js ← async data with states
|
|
73
|
+
server/ dev + prod server, SSR, file router, actions,
|
|
74
|
+
auth, sessions, cache, rate-limit, WebSocket
|
|
75
|
+
src/ssr.js ← how metadata becomes <head> tags
|
|
76
|
+
src/router.js ← file convention → route table
|
|
77
|
+
src/actions.js ← .server.ts scanner, RPC, expose()
|
|
78
|
+
src/auth.js, session.js, cache.js, rate-limit.js, csrf.js
|
|
79
|
+
cli/ webjs CLI (dev / start / build / test / check / create / db)
|
|
80
|
+
ts-plugin/ tsserver plugin: go-to-definition + diagnostic suppression
|
|
81
|
+
+ attribute auto-complete for Class.register('tag') elements
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Reaching straight for the source is the fastest way to resolve "why
|
|
85
|
+
doesn't X work?" with no documentation guesswork and no stale blog posts.
|
|
86
|
+
|
|
87
|
+
## Editor TS plugin: `@webjsdev/ts-plugin`
|
|
88
|
+
|
|
89
|
+
This scaffold's `tsconfig.json` lists a single tsserver plugin. It is
|
|
90
|
+
editor-only, not required for the framework to run.
|
|
91
|
+
|
|
92
|
+
```jsonc
|
|
93
|
+
// tsconfig.json (already wired by the scaffold)
|
|
94
|
+
"plugins": [
|
|
95
|
+
{ "name": "@webjsdev/ts-plugin" }
|
|
96
|
+
]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`@webjsdev/ts-plugin` bundles `ts-lit-plugin` internally (it's a runtime
|
|
100
|
+
dependency of the plugin) and loads it programmatically, so users
|
|
101
|
+
list one entry, not two. You get the full stack of template-literal
|
|
102
|
+
intelligence (type-checking, diagnostics, go-to-def inside
|
|
103
|
+
`` html`…` `` and `` css`…` `` templates) **plus** webjs-aware behaviour
|
|
104
|
+
layered on top:
|
|
105
|
+
|
|
106
|
+
- "Unknown tag/attribute" diagnostics are silenced for elements
|
|
107
|
+
registered via `Class.register('tag-name')`.
|
|
108
|
+
- Attribute auto-complete sourced from each component's
|
|
109
|
+
`static properties`.
|
|
110
|
+
- Attribute-value type-check against `declare propName: T` annotations.
|
|
111
|
+
|
|
112
|
+
See [docs.webjs.com → Editor setup](https://docs.webjs.com/docs/editor-setup)
|
|
113
|
+
for the full walkthrough.
|
|
114
|
+
|
|
115
|
+
## UI components: Webjs UI (preinstalled)
|
|
116
|
+
|
|
117
|
+
This scaffold ships with the standard Webjs UI component kit
|
|
118
|
+
**already installed at `components/ui/`**. The kit is **AI-first** and
|
|
119
|
+
splits into two tiers. Internalise the split. Picking the wrong tier
|
|
120
|
+
produces broken markup.
|
|
121
|
+
|
|
122
|
+
### Tier 1: class-helper functions (the majority)
|
|
123
|
+
|
|
124
|
+
Pure functions that return Tailwind class strings. You apply them to
|
|
125
|
+
**raw native HTML elements** that you write yourself. Examples:
|
|
126
|
+
`button`, `card`, `input`, `label`, `alert`, `badge`, `separator`,
|
|
127
|
+
`skeleton`, `kbd`, `table`, `breadcrumb`, `pagination`, `native-select`,
|
|
128
|
+
`avatar`, `checkbox`, `switch`, `radio-group`, `textarea`, `toggle`,
|
|
129
|
+
`aspect-ratio`.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import {
|
|
133
|
+
cardClass, cardHeaderClass, cardTitleClass,
|
|
134
|
+
cardContentClass, cardFooterClass,
|
|
135
|
+
} from '../../components/ui/card.ts';
|
|
136
|
+
import { inputClass } from '../../components/ui/input.ts';
|
|
137
|
+
import { labelClass } from '../../components/ui/label.ts';
|
|
138
|
+
import { buttonClass } from '../../components/ui/button.ts';
|
|
139
|
+
|
|
140
|
+
return html`
|
|
141
|
+
<div class=${cardClass()}>
|
|
142
|
+
<div class=${cardHeaderClass()}>
|
|
143
|
+
<h3 class=${cardTitleClass()}>Profile</h3>
|
|
144
|
+
</div>
|
|
145
|
+
<div class=${cardContentClass()}>
|
|
146
|
+
<label class=${labelClass()} for="name">Name</label>
|
|
147
|
+
<input class=${inputClass()} id="name" name="name">
|
|
148
|
+
</div>
|
|
149
|
+
<div class=${cardFooterClass()}>
|
|
150
|
+
<button class=${buttonClass()}>Save</button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
`;
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Helpers with variants take an options object:
|
|
157
|
+
`buttonClass({ variant: 'outline', size: 'sm' })`.
|
|
158
|
+
|
|
159
|
+
### Tier 2: stateful custom elements
|
|
160
|
+
|
|
161
|
+
For things the browser doesn't provide natively (focus traps, portaled
|
|
162
|
+
overlays, keyboard-navigated lists): `dialog`, `alert-dialog`, `popover`,
|
|
163
|
+
`tooltip`, `hover-card`, `tabs`, `accordion`, `collapsible`,
|
|
164
|
+
`dropdown-menu`, `progress`, `sonner`, `toggle-group`. These ARE custom
|
|
165
|
+
elements. Import them once (typically in `app/layout.ts`) and use
|
|
166
|
+
`<ui-X>` tags:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
// app/layout.ts (registers the custom elements for every page)
|
|
170
|
+
import '../components/ui/dialog.ts';
|
|
171
|
+
import '../components/ui/tabs.ts';
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
// app/some-page/page.ts (uses the registered elements)
|
|
176
|
+
import { buttonClass } from '../../components/ui/button.ts';
|
|
177
|
+
|
|
178
|
+
return html`
|
|
179
|
+
<ui-dialog>
|
|
180
|
+
<ui-dialog-trigger>
|
|
181
|
+
<button class=${buttonClass({ variant: 'outline' })}>Edit</button>
|
|
182
|
+
</ui-dialog-trigger>
|
|
183
|
+
<ui-dialog-content>
|
|
184
|
+
<h2>Edit profile</h2>
|
|
185
|
+
...
|
|
186
|
+
</ui-dialog-content>
|
|
187
|
+
</ui-dialog>
|
|
188
|
+
`;
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Adding more components
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
webjs ui add dialog dropdown-menu tabs progress
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Each `webjs ui add` call fetches the component source from
|
|
198
|
+
`https://ui.webjs.dev/registry/<name>.json`, copies it into
|
|
199
|
+
`components/ui/`, and installs any required npm deps. Run
|
|
200
|
+
`webjs ui list` to browse the catalogue or visit
|
|
201
|
+
[https://ui.webjs.dev](https://ui.webjs.dev).
|
|
202
|
+
|
|
203
|
+
### AI agents, picking the right tier
|
|
204
|
+
|
|
205
|
+
For forms, dashboards, settings pages, marketing layouts: **call the
|
|
206
|
+
Tier-1 class helpers on raw native elements**. You get accessibility,
|
|
207
|
+
visual consistency, and form submission semantics for free.
|
|
208
|
+
`<input class=${inputClass()}>` is a real `<input>` with native
|
|
209
|
+
autofill, browser validation, and `<form>` submission unchanged.
|
|
210
|
+
|
|
211
|
+
Because Tier-1 helpers wrap *real* HTML elements, a `buttonClass()`
|
|
212
|
+
button inside a `<form action="/posts" method="post">` participates
|
|
213
|
+
in the client router's partial-swap submission automatically. No JS
|
|
214
|
+
handler, no `fetch`. See *Client navigation patterns* below for the
|
|
215
|
+
full form-submission + 4xx-HTML-render-in-place pattern.
|
|
216
|
+
|
|
217
|
+
For modals, dropdowns, tooltips, tab strips, accordions: use the
|
|
218
|
+
Tier-2 `<ui-X>` custom element tags after importing the corresponding
|
|
219
|
+
module.
|
|
220
|
+
|
|
221
|
+
The composition style is deliberately **not** shadcn's
|
|
222
|
+
component-everything React API. We use native elements + class helpers
|
|
223
|
+
for the visual stuff because hiding a `<button>` inside a `<Button>`
|
|
224
|
+
wrapper adds zero value and obscures the real element from inspection,
|
|
225
|
+
form submission, and screen readers. Custom elements are reserved for
|
|
226
|
+
behavior the browser can't deliver natively.
|
|
227
|
+
|
|
228
|
+
## File conventions
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
app/ thin route adapters (import from modules/)
|
|
232
|
+
page.ts → /
|
|
233
|
+
layout.ts root layout, wraps every page
|
|
234
|
+
error.ts error boundary (render failures → user-friendly)
|
|
235
|
+
loading.ts Suspense fallback for sibling page
|
|
236
|
+
not-found.ts custom 404 page
|
|
237
|
+
middleware.ts global request middleware
|
|
238
|
+
[slug]/page.ts dynamic route segment
|
|
239
|
+
[...rest]/page.ts catch-all
|
|
240
|
+
(group)/ route group (parens not in URL)
|
|
241
|
+
_private/ underscore = not routable
|
|
242
|
+
api/
|
|
243
|
+
<path>/route.ts GET / POST / PUT / DELETE / WS handlers
|
|
244
|
+
sitemap.ts metadata route → /sitemap.xml
|
|
245
|
+
robots.ts metadata route → /robots.txt
|
|
246
|
+
opengraph-image.ts metadata route → /opengraph-image
|
|
247
|
+
components/ web components (extend WebComponent, call .register())
|
|
248
|
+
modules/<feature>/
|
|
249
|
+
actions/*.server.ts server actions (one function per file)
|
|
250
|
+
queries/*.server.ts data reads (one function per file)
|
|
251
|
+
components/*.ts feature-scoped components
|
|
252
|
+
utils/*.ts feature-scoped helpers
|
|
253
|
+
types.ts feature types
|
|
254
|
+
lib/
|
|
255
|
+
prisma.ts PrismaClient singleton (import from here, never `new PrismaClient()`)
|
|
256
|
+
... other cross-cutting infra (session, auth config, etc.)
|
|
257
|
+
prisma/
|
|
258
|
+
schema.prisma Prisma schema, SQLite by default, switch provider for Postgres/MySQL
|
|
259
|
+
dev.db SQLite file (gitignored); run `npm run db:migrate` to create
|
|
260
|
+
migrations/ generated migration SQL
|
|
261
|
+
public/ static assets, served at /public/*
|
|
262
|
+
test/<feature>/ feature-scoped tests, one folder per concern
|
|
263
|
+
<name>.test.ts node unit / integration test (node --test)
|
|
264
|
+
browser/<name>.test.js real-browser test (web-test-runner)
|
|
265
|
+
e2e/<name>.test.ts end-to-end test (full app boot, opt in via WEBJS_E2E=1)
|
|
266
|
+
smoke/<name>.test.ts fast post-deploy sanity check
|
|
267
|
+
middleware.ts root middleware (optional, outermost)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Database (Prisma + SQLite by default)
|
|
271
|
+
|
|
272
|
+
Every scaffold includes a Prisma setup pointed at a local SQLite file.
|
|
273
|
+
First-run workflow:
|
|
274
|
+
|
|
275
|
+
```sh
|
|
276
|
+
cp .env.example .env # DATABASE_URL is pre-filled for SQLite
|
|
277
|
+
npm run db:migrate # creates prisma/dev.db + migration
|
|
278
|
+
npm run dev # webjs dev + prisma generate via predev
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Always `npm run dev` / `npm start`, never `webjs dev` / `webjs start` directly
|
|
282
|
+
|
|
283
|
+
`webjs dev` and `webjs start` are framework primitives, they only run
|
|
284
|
+
the webjs server. They do **not** run `prisma generate`, do **not** run
|
|
285
|
+
`prisma migrate deploy`, do **not** spawn the Tailwind watcher, do
|
|
286
|
+
**not** run any other per-app process this `package.json` composes.
|
|
287
|
+
|
|
288
|
+
`npm run dev` and `npm start` are the app-level entrypoints. They run
|
|
289
|
+
the webjs server **plus** every other process the app needs, wired
|
|
290
|
+
together via `predev` / `prestart` hooks and (where present)
|
|
291
|
+
`concurrently` for parallel watchers. Skipping the npm wrapper produces
|
|
292
|
+
silent breakage: a stale Prisma client, missing `public/tailwind.css`,
|
|
293
|
+
an unmigrated database in production, etc.
|
|
294
|
+
|
|
295
|
+
Same split Rails 7+ uses: `bin/rails server` is the framework
|
|
296
|
+
primitive, `bin/dev` is the orchestrator. webjs uses npm scripts +
|
|
297
|
+
hooks for the same role, because as a no-build framework Tailwind /
|
|
298
|
+
Prisma / etc. cannot be bundler plugins.
|
|
299
|
+
|
|
300
|
+
In Docker / Railway, prefer `npm start` (or `node node_modules/.bin/npm
|
|
301
|
+
start`) as the CMD over `node ... webjs.js start ...`. The npm form
|
|
302
|
+
fires `prestart`; the direct binary form skips it.
|
|
303
|
+
|
|
304
|
+
Scripts:
|
|
305
|
+
|
|
306
|
+
- `npm run db:migrate`: `prisma migrate dev` (dev-time schema changes + migration + generate)
|
|
307
|
+
- `npm run db:generate`: `prisma generate` (regenerate client only)
|
|
308
|
+
- `npm run db:studio`: `prisma studio` (GUI)
|
|
309
|
+
- `predev` hook auto-runs `prisma generate` before `npm run dev`
|
|
310
|
+
- `prestart` hook runs `prisma migrate deploy` before `npm start` (idempotent in prod)
|
|
311
|
+
|
|
312
|
+
Always import the client from `lib/prisma.server.ts` (never `new PrismaClient()` directly -
|
|
313
|
+
the singleton avoids opening a new connection on every dev-server reload):
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
import { prisma } from '../../../lib/prisma.server.ts';
|
|
317
|
+
const users = await prisma.user.findMany();
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
To switch to Postgres or MySQL: change `provider` in `prisma/schema.prisma`
|
|
321
|
+
and the `DATABASE_URL` in `.env`.
|
|
322
|
+
|
|
323
|
+
## Imports
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
import { html, css, WebComponent } from '@webjsdev/core';
|
|
327
|
+
import '@webjsdev/core/client-router'; // enable SPA nav
|
|
328
|
+
import { unsafeHTML, live } from '@webjsdev/core/directives';
|
|
329
|
+
import { createContext } from '@webjsdev/core/context';
|
|
330
|
+
import { Task } from '@webjsdev/core/task';
|
|
331
|
+
import { fixture, waitForUpdate } from '@webjsdev/core/testing';
|
|
332
|
+
|
|
333
|
+
import { rateLimit, cache, createAuth, Credentials, Session } from '@webjsdev/server';
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Environment variables (server vs browser)
|
|
337
|
+
|
|
338
|
+
Server-only is the default. Any `process.env.X` read on the server stays on the server. Names that start with `WEBJS_PUBLIC_` are also exposed in the browser as `process.env.X`, via an inline script injected at SSR time. No build step.
|
|
339
|
+
|
|
340
|
+
```sh
|
|
341
|
+
# .env
|
|
342
|
+
DATABASE_URL=postgres://... # server-only
|
|
343
|
+
AUTH_SECRET=... # server-only
|
|
344
|
+
WEBJS_PUBLIC_API_URL=https://x.com # browser too
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
// Server-side (page function, action, middleware, route handler):
|
|
349
|
+
const dburl = process.env.DATABASE_URL; // works
|
|
350
|
+
|
|
351
|
+
// Browser-side (component render method, client-only utilities):
|
|
352
|
+
const url = process.env.WEBJS_PUBLIC_API_URL; // works
|
|
353
|
+
const secret = process.env.AUTH_SECRET; // undefined (fail-closed)
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
`process.env.NODE_ENV` is also defined in the browser (`'development'` in `webjs dev`, `'production'` in `webjs start`), so vendor bundles that probe it work without setup. Full docs: [Configuration](https://docs.webjs.com/docs/configuration).
|
|
357
|
+
|
|
358
|
+
## Component pattern
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
import { WebComponent, html, css } from '@webjsdev/core';
|
|
362
|
+
|
|
363
|
+
export class Counter extends WebComponent {
|
|
364
|
+
static properties = { count: { type: Number } };
|
|
365
|
+
static styles = css`button { padding: 8px 12px; }`; // shadow-DOM only
|
|
366
|
+
// static shadow = true; // opt into shadow DOM (default: light DOM)
|
|
367
|
+
// static lazy = true; // download JS only when scrolled into view
|
|
368
|
+
declare count: number; // TypeScript-only typed accessor
|
|
369
|
+
|
|
370
|
+
constructor() {
|
|
371
|
+
super();
|
|
372
|
+
this.count = 0; // SSR-meaningful default, see below
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
render() {
|
|
376
|
+
return html`
|
|
377
|
+
<button @click=${() => { this.count = this.count + 1; }}>
|
|
378
|
+
${this.count}
|
|
379
|
+
</button>
|
|
380
|
+
`;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
Counter.register('my-counter');
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**Progressive-enhancement rule for components.** Every webjs component
|
|
387
|
+
is SSR'd. The server constructs the component, applies attributes,
|
|
388
|
+
and runs `render()`. With JS disabled, the component's initial HTML
|
|
389
|
+
still paints (an unstyled counter still shows the number, and only
|
|
390
|
+
the click handler is inert). Two consequences for how you write code:
|
|
391
|
+
|
|
392
|
+
1. **Defaults for the first paint go in `constructor()`** (after
|
|
393
|
+
`super()`), never as class-field initializers (which break
|
|
394
|
+
reactivity) and never in `connectedCallback` (which the server
|
|
395
|
+
doesn't run). For Web Component properties with `declare`, set the
|
|
396
|
+
default in the constructor.
|
|
397
|
+
2. **`connectedCallback` is browser-only.** Use it for
|
|
398
|
+
`localStorage`, viewport size, online status, or anything that
|
|
399
|
+
genuinely can't be known on the server. Read the value, then
|
|
400
|
+
assign it to a reactive property (`this.items = stored`) or write
|
|
401
|
+
to a signal to refine the render. The SSR'd first paint shows the
|
|
402
|
+
constructor default. The browser refines after hydration.
|
|
403
|
+
3. **Server-known data goes through the page function**, not into
|
|
404
|
+
`connectedCallback`. Fetch in the page (which runs on the server),
|
|
405
|
+
pass the result down via `.prop=${value}` (custom elements) or
|
|
406
|
+
`attr=${string}` (native elements). For custom elements, the wire
|
|
407
|
+
serializer round-trips Array / Object / Date / Map / Set / BigInt
|
|
408
|
+
through the SSR `data-webjs-prop-*` side-channel, so the
|
|
409
|
+
component's first paint already has the rich-typed value with no
|
|
410
|
+
flash. The framework owns the attribute, applies it on
|
|
411
|
+
`connectedCallback`, then strips it from the live DOM. For native
|
|
412
|
+
elements use `value=${v}` / `checked=${b}` etc.; `.value` on a
|
|
413
|
+
native element drops at SSR (the property form is for client-only
|
|
414
|
+
re-render scenarios like controlled inputs via `.value=${live(v)}`).
|
|
415
|
+
4. **For write-paths, prefer `<form>` + server action over `fetch`.**
|
|
416
|
+
Plain forms POST without JS; the client router upgrades them to
|
|
417
|
+
partial-swaps automatically when scripts are active. One
|
|
418
|
+
implementation covers both.
|
|
419
|
+
|
|
420
|
+
See [Progressive Enhancement](https://docs.webjs.dev/docs/progressive-enhancement) for the full design rationale.
|
|
421
|
+
|
|
422
|
+
## Lit muscle-memory gotchas (read if you have written lit before)
|
|
423
|
+
|
|
424
|
+
Webjs's runtime API matches lit. The `WebComponent` base class,
|
|
425
|
+
`static properties`, the lifecycle hooks, ReactiveControllers, the
|
|
426
|
+
directive set, `html` / `css` tagged templates. The **rendering
|
|
427
|
+
model**, however, is different. Pure-lit patterns that work fine in a
|
|
428
|
+
client-only lit app break in webjs's SSR pipeline or its reactivity
|
|
429
|
+
system. Read this section before reaching for lit idioms.
|
|
430
|
+
|
|
431
|
+
### Mental model. JS opt-in per behavior, not per component
|
|
432
|
+
|
|
433
|
+
Lit hydrates per component. You decide at the component boundary
|
|
434
|
+
whether JS ships and runs for that island.
|
|
435
|
+
|
|
436
|
+
Webjs ships JS per **interactive behavior**, not per component. Every
|
|
437
|
+
component is server-rendered. JavaScript is requested by the specific
|
|
438
|
+
holes you write in the template.
|
|
439
|
+
|
|
440
|
+
- `@click=${...}`, `@input=${...}`, any event binding requests JS.
|
|
441
|
+
- A reactive property assignment (`this.count = …`) or a signal
|
|
442
|
+
`set()` that the component reads requests JS for reactive updates.
|
|
443
|
+
- `.prop=${richObject}` requests JS for property hydration.
|
|
444
|
+
- A controller like `Task` requests JS for that async behavior.
|
|
445
|
+
- A plain `<a href>`, a `<form action method>` submission, or a
|
|
446
|
+
purely display-time component (no event listeners, no property
|
|
447
|
+
mutations, no signal subscriptions, no property bindings) does
|
|
448
|
+
**not** request JS.
|
|
449
|
+
|
|
450
|
+
A single component can mix both. A product card with server-rendered
|
|
451
|
+
title, price, image, plus a "View" link (no JS) and an "Add to cart"
|
|
452
|
+
button with a `@click` (JS for that one behavior) is correct webjs
|
|
453
|
+
style. The framework loads JS for the component because of the
|
|
454
|
+
`@click` and runs it, while the rest of the card stays exactly as the
|
|
455
|
+
server painted it.
|
|
456
|
+
|
|
457
|
+
Practical consequences for agents writing webjs code.
|
|
458
|
+
|
|
459
|
+
1. Never reach for `fetch()` plus a `@click` handler when a `<form>`
|
|
460
|
+
plus a server action would do. The form is free (no JS), the
|
|
461
|
+
server action is typed and CSRF-protected, the result reaches the
|
|
462
|
+
page through normal navigation.
|
|
463
|
+
2. Never make first paint depend on hydration. A blank skeleton until
|
|
464
|
+
JS runs means the feature was written wrong.
|
|
465
|
+
3. Don't think binary about "static vs interactive components." Pick
|
|
466
|
+
interactive primitives per behavior. A page with ten components
|
|
467
|
+
can ship zero JS for eight of them and handlers only for the two
|
|
468
|
+
that need it.
|
|
469
|
+
|
|
470
|
+
### Gotchas at a glance
|
|
471
|
+
|
|
472
|
+
| Lit pattern | What breaks in webjs | Webjs equivalent |
|
|
473
|
+
|---|---|---|
|
|
474
|
+
| Fetch in `connectedCallback` / `firstUpdated` | Empty first paint (neither hook runs in SSR) | Fetch in the page function, pass as props |
|
|
475
|
+
| `Task` for initial-paint data | SSR ships the pending state, flashes to resolved on hydration | Page function fetch, pass as props (`Task` is fine for client-time async) |
|
|
476
|
+
| `window.X` / `document.X` in constructor or `render()` | SSR crash | Move to `connectedCallback` |
|
|
477
|
+
| Top-level `import` of a browser-only library | SSR crash | Dynamic `import()` inside `connectedCallback` |
|
|
478
|
+
| Class-field initializer for a reactive property (`student: Student = {...}`) | Silently breaks reactivity (overwrites the framework accessor) | `declare student: Student` plus constructor default |
|
|
479
|
+
| `@property()` decorator | Banned by invariant 10 (erasable TS) | `static properties = { ... }` plus `declare` |
|
|
480
|
+
| `static styles = css` block without `static shadow = true` | Styles leak globally; the framework warns at runtime | Add `static shadow = true`, or use Tailwind utilities |
|
|
481
|
+
| `willUpdate` computing SSR-visible derived state | Field is `undefined` in SSR HTML (hook is client-only) | Compute inline in `render()` |
|
|
482
|
+
| `ContextProvider` for server-known data | Default value during SSR, content shift on hydration | Pass via props from the page function |
|
|
483
|
+
|
|
484
|
+
The full annotated catalog with code examples lives in the framework
|
|
485
|
+
repo at
|
|
486
|
+
[`agent-docs/lit-muscle-memory-gotchas.md`](https://github.com/vivek7405/webjs/blob/main/agent-docs/lit-muscle-memory-gotchas.md).
|
|
487
|
+
|
|
488
|
+
## Server action pattern
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
// modules/posts/actions/create-post.server.ts
|
|
492
|
+
'use server';
|
|
493
|
+
import { prisma } from '../../../lib/prisma.server.ts';
|
|
494
|
+
|
|
495
|
+
export async function createPost(input: { title: string; body: string }) {
|
|
496
|
+
if (!input.title) return { success: false, error: 'title required', status: 400 };
|
|
497
|
+
const post = await prisma.post.create({ data: input });
|
|
498
|
+
return { success: true, data: post };
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Import it from a client component. The framework rewrites it into a
|
|
503
|
+
type-safe RPC stub automatically.
|
|
504
|
+
|
|
505
|
+
## Client navigation patterns (auto-magic)
|
|
506
|
+
|
|
507
|
+
The client router enables itself when the scaffolded root layout imports
|
|
508
|
+
`@webjsdev/core/client-router`. After that, **every `<a href>` and
|
|
509
|
+
`<form action>` on the page is enhanced into a partial-swap navigation
|
|
510
|
+
or submission automatically**. You don't call a router API. Write
|
|
511
|
+
standard HTML; the swap happens.
|
|
512
|
+
|
|
513
|
+
What this changes for how you write apps:
|
|
514
|
+
|
|
515
|
+
### 1. Put shared chrome in `layout.ts`, not in every page
|
|
516
|
+
|
|
517
|
+
When you navigate from `/posts` to `/posts/123`, the framework swaps
|
|
518
|
+
only the deepest layout's `${'${children}'}` slot. Outer layouts stay
|
|
519
|
+
mounted. The sidenav's scroll position, an open `<details>`, a focused
|
|
520
|
+
input, and an inflight `<video>` are all preserved across the navigation
|
|
521
|
+
without you writing any code.
|
|
522
|
+
|
|
523
|
+
The rule: anything that should persist across navigations within a
|
|
524
|
+
section lives in that section's `layout.ts`. Page-specific content
|
|
525
|
+
lives in `page.ts`. Don't duplicate a sidenav into every page.
|
|
526
|
+
|
|
527
|
+
### 2. Forms POST through `<form action>` (no `fetch` for write-paths)
|
|
528
|
+
|
|
529
|
+
A `<form action=${'${createPost}'} method="post">` works as a plain
|
|
530
|
+
HTML form when JS is disabled and as a partial-swap submission when JS
|
|
531
|
+
is active. **The same form covers both paths.** Don't reach for
|
|
532
|
+
`fetch` + a click handler unless you genuinely need to.
|
|
533
|
+
|
|
534
|
+
### 3. Server-side validation: re-render the form with errors
|
|
535
|
+
|
|
536
|
+
The router applies any `text/html` response to the DOM regardless of
|
|
537
|
+
status code (4xx, 422, etc.). This is the Rails / Django / Phoenix
|
|
538
|
+
server-side validation pattern. Pair a `<form action="/posts" method="post">`
|
|
539
|
+
with a `route.ts` POST handler:
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
// app/posts/route.ts
|
|
543
|
+
import { redirect, html } from '@webjsdev/core';
|
|
544
|
+
import { createPost } from '../../modules/posts/actions/create-post.server.ts';
|
|
545
|
+
|
|
546
|
+
export async function POST(req: Request) {
|
|
547
|
+
const form = await req.formData();
|
|
548
|
+
const result = await createPost({
|
|
549
|
+
title: String(form.get('title') ?? ''),
|
|
550
|
+
body: String(form.get('body') ?? ''),
|
|
551
|
+
});
|
|
552
|
+
if (!result.success) {
|
|
553
|
+
// Re-render the form page with the user's input + inline errors.
|
|
554
|
+
// The client router applies this HTML in place, no full reload.
|
|
555
|
+
return new Response(renderNewPostForm(result.errors, form), {
|
|
556
|
+
status: 422,
|
|
557
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
// Success → PRG redirect; fetch follows, history records /posts/<id>
|
|
561
|
+
redirect(`/posts/${result.data.id}`);
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
```html
|
|
566
|
+
<!-- The form: standard HTML, no JS handler needed -->
|
|
567
|
+
<form action="/posts" method="post">
|
|
568
|
+
<input name="title" required />
|
|
569
|
+
<textarea name="body" required></textarea>
|
|
570
|
+
<button>Publish</button>
|
|
571
|
+
</form>
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
With JS active: router intercepts the submit, sends the POST, applies
|
|
575
|
+
the response in place (2xx + redirect for success, 4xx HTML for
|
|
576
|
+
errors). With JS disabled: browser performs the same POST as a normal
|
|
577
|
+
form submission and renders the response page. Same code, both paths.
|
|
578
|
+
|
|
579
|
+
(For RPC-style server actions that return typed values to client
|
|
580
|
+
components. See *Server action pattern* above. The HTML-form pattern
|
|
581
|
+
here is for the "submit → server processes → render new page" flow.)
|
|
582
|
+
|
|
583
|
+
### 4. `<webjs-frame id="...">` for non-layout swap regions
|
|
584
|
+
|
|
585
|
+
For a widget that should swap on click but isn't a route boundary
|
|
586
|
+
(e.g. a tab strip inside a page), wrap it:
|
|
587
|
+
|
|
588
|
+
```ts
|
|
589
|
+
return html`
|
|
590
|
+
<nav>
|
|
591
|
+
<a href=${'${path + "?tab=overview"}'}>Overview</a>
|
|
592
|
+
<a href=${'${path + "?tab=stats"}'}>Stats</a>
|
|
593
|
+
</nav>
|
|
594
|
+
<webjs-frame id="tab-content">
|
|
595
|
+
${'${tab === "stats" ? renderStats() : renderOverview()}'}
|
|
596
|
+
</webjs-frame>
|
|
597
|
+
`;
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
The router's `closest('webjs-frame')` detection takes precedence over
|
|
601
|
+
layout markers. Only the frame's content swaps. Use this sparingly -
|
|
602
|
+
folder-based layouts handle 99% of cases.
|
|
603
|
+
|
|
604
|
+
### 5. `loading.ts` for per-segment skeletons
|
|
605
|
+
|
|
606
|
+
Drop a `loading.ts` in any route segment. The framework auto-wraps the
|
|
607
|
+
sibling `page.ts` in a Suspense boundary with `loading.ts`'s default
|
|
608
|
+
export as the fallback. On navigation, the client router clones the
|
|
609
|
+
deepest matching loading template into the swap slot immediately -
|
|
610
|
+
the user sees a skeleton during the fetch, then the real content.
|
|
611
|
+
|
|
612
|
+
### 6. `error.ts` for per-segment error boundaries
|
|
613
|
+
|
|
614
|
+
Drop an `error.ts` in any route segment. Render-time exceptions in
|
|
615
|
+
that segment's tree are caught and rendered through `error.ts`'s
|
|
616
|
+
default export, scoped to that boundary (outer layouts stay alive).
|
|
617
|
+
|
|
618
|
+
### What you do NOT need to write
|
|
619
|
+
|
|
620
|
+
- Manual fetch / DOM-swap code for SPA-style navigation
|
|
621
|
+
- An "active link" highlight handler. Use `aria-current="page"`
|
|
622
|
+
derived from the request URL on the server.
|
|
623
|
+
- Loading spinners on `<a>` clicks. `loading.ts` handles it.
|
|
624
|
+
- Cancellation when the user clicks faster than the network. The
|
|
625
|
+
router's nav-token + AbortController combo guarantees stale
|
|
626
|
+
responses never overwrite a newer settled page.
|
|
627
|
+
- Scroll-position save/restore for back/forward. The snapshot cache
|
|
628
|
+
handles window scroll. Inner scrollables persist via DOM identity.
|
|
629
|
+
|
|
630
|
+
Full reference: see the [Client Router docs](https://docs.webjs.dev/docs/client-router) and the framework AGENTS.md "Client navigation" section.
|
|
631
|
+
|
|
632
|
+
## Metadata (per-page)
|
|
633
|
+
|
|
634
|
+
The `metadata` export is Next.js-compatible. Common fields shown below;
|
|
635
|
+
the full surface includes `title.template / .default / .absolute`,
|
|
636
|
+
`metadataBase`, `alternates: { canonical, languages, media, types }`,
|
|
637
|
+
`robots`, `keywords`, `authors`, `creator`, `publisher`, `verification`,
|
|
638
|
+
`icons`, `manifest`, `appleWebApp`, `formatDetection`, `itunes`, and
|
|
639
|
+
the typed `other: { '<meta-name>': value }` escape hatch.
|
|
640
|
+
|
|
641
|
+
```ts
|
|
642
|
+
export const metadata = {
|
|
643
|
+
title: 'My page',
|
|
644
|
+
// OR: title: { template: '%s | {{APP_NAME}}', default: '{{APP_NAME}}' }
|
|
645
|
+
description: 'A page in {{APP_NAME}}',
|
|
646
|
+
metadataBase: 'https://example.com', // base for relative URLs below
|
|
647
|
+
openGraph: { type: 'website', image: '/og.png' },
|
|
648
|
+
twitter: { card: 'summary_large_image' },
|
|
649
|
+
icons: { icon: '/favicon.svg', apple: '/apple.png' },
|
|
650
|
+
alternates: { canonical: '/post' }, // → <link rel="canonical">
|
|
651
|
+
robots: { index: true, follow: true },
|
|
652
|
+
cacheControl: 'public, max-age=60', // opt into caching (default: no-store)
|
|
653
|
+
};
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
Use `generateMetadata(ctx)` when you need request-scoped values (e.g.
|
|
657
|
+
absolute URLs from `ctx.url`):
|
|
658
|
+
|
|
659
|
+
```ts
|
|
660
|
+
export function generateMetadata(ctx: { url: string }) {
|
|
661
|
+
return { metadataBase: new URL(ctx.url).origin, title: 'Hello' };
|
|
662
|
+
}
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
Viewport may be split into its own export (Next.js 14+ pattern):
|
|
666
|
+
|
|
667
|
+
```ts
|
|
668
|
+
export const viewport = {
|
|
669
|
+
width: 'device-width',
|
|
670
|
+
initialScale: 1,
|
|
671
|
+
themeColor: '#1c1613',
|
|
672
|
+
colorScheme: 'light dark',
|
|
673
|
+
};
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
## Document shell (`<html>` / `<head>` / `<body>`)
|
|
677
|
+
|
|
678
|
+
The framework owns the shell by default. The SSR pipeline auto-emits
|
|
679
|
+
`<!doctype html><html lang="en"><head>…</head><body>` around every
|
|
680
|
+
composition, and auto-hoists `<link>` / `<style>` / `<meta>` / `<script>`
|
|
681
|
+
tags returned anywhere in a layout/page into the real `<head>`. The
|
|
682
|
+
`metadata` export drives `<title>` and `<meta>` tags.
|
|
683
|
+
|
|
684
|
+
**Only `app/layout.ts` (the root layout)** may optionally write its
|
|
685
|
+
own `<!doctype><html><head>…</head><body>` shell to override `<html lang>`,
|
|
686
|
+
`<html dir>`, `<html data-*>`, `<body class>`, or add a custom
|
|
687
|
+
`<link rel="preconnect">` etc. When the root layout supplies a shell,
|
|
688
|
+
the framework respects it and splices its required tags into the
|
|
689
|
+
user's `<head>`.
|
|
690
|
+
|
|
691
|
+
```ts
|
|
692
|
+
// app/layout.ts (root, optionally owning the shell)
|
|
693
|
+
export default function RootLayout({ children }) {
|
|
694
|
+
return html`
|
|
695
|
+
<!doctype html>
|
|
696
|
+
<html lang="es" data-theme="dark">
|
|
697
|
+
<head>
|
|
698
|
+
<link rel="preconnect" href="https://cdn.example.com">
|
|
699
|
+
</head>
|
|
700
|
+
<body class="min-h-screen bg-bg">
|
|
701
|
+
<main>${children}</main>
|
|
702
|
+
</body>
|
|
703
|
+
</html>
|
|
704
|
+
`;
|
|
705
|
+
}
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
**Non-root layouts** (`app/<segment>/layout.ts`) and **pages**
|
|
709
|
+
(`app/**/page.ts`) **must NOT** write `<!doctype>` / `<html>` / `<head>`
|
|
710
|
+
/ `<body>`. The framework auto-emits the wrapper around the whole
|
|
711
|
+
composition, so a nested shell ends up dropped by the HTML parser.
|
|
712
|
+
`webjs check` enforces this via the `shell-in-non-root-layout` rule.
|
|
713
|
+
|
|
714
|
+
## Invariants (do not violate)
|
|
715
|
+
|
|
716
|
+
1. Custom element tags must contain a hyphen. Pass the tag to `.register('tag-name')` at the bottom of the file. The tag is not a static field.
|
|
717
|
+
2. **Server-only code goes in `.server.{js,ts}` files, `route.ts`
|
|
718
|
+
handlers, or `middleware.ts`. Never in pages, layouts, or
|
|
719
|
+
components.** Direct imports of `@prisma/client`, `node:*`, or any
|
|
720
|
+
server-only dependency from a page, layout, loading.ts, error.ts,
|
|
721
|
+
not-found.ts, or component will crash the browser at module load.
|
|
722
|
+
Wrap the access in a `.server.{js,ts}` file; the framework
|
|
723
|
+
rewrites that import into an RPC stub for the browser. `lib/`
|
|
724
|
+
holds both server-only infra (`lib/prisma.server.ts`, `lib/session.server.ts`)
|
|
725
|
+
and browser-safe utilities (`lib/utils/cn.ts` with `cn`, design-
|
|
726
|
+
system helpers). Server-only `lib/*` files must only be imported
|
|
727
|
+
from `.server.ts`/`route.ts`/`middleware.ts`; browser-safe `lib/*`
|
|
728
|
+
files (like `lib/utils/cn.ts`) can be imported anywhere.
|
|
729
|
+
3. Event / property / boolean holes in `` html`` `` are unquoted:
|
|
730
|
+
`@click=${fn}`, not `@click="${fn}"`.
|
|
731
|
+
4. Component state lives in signals. Import `signal` from
|
|
732
|
+
`@webjsdev/core`, read with `signal.get()` inside `render()`, and
|
|
733
|
+
write with `signal.set(value)`. Module-scope signals share state
|
|
734
|
+
across components; instance signals (created in the constructor)
|
|
735
|
+
carry component-local state. Reactive properties (`static
|
|
736
|
+
properties = { ... }` with a sibling `declare`) are for values
|
|
737
|
+
that ride an HTML attribute or `.prop=${...}` SSR hydration.
|
|
738
|
+
5. Pages / layouts / metadata routes default-export a server-only function.
|
|
739
|
+
6. One exported function per action / query file. Name the file after it.
|
|
740
|
+
7. **Components must render meaningful HTML on first paint** (SSR
|
|
741
|
+
uses constructor defaults + attributes, while `connectedCallback` is
|
|
742
|
+
browser-only). Never fetch initial data in `connectedCallback` /
|
|
743
|
+
`firstUpdated`. Fetch in the page function (server) and pass it as
|
|
744
|
+
a prop. See *Component pattern* above.
|
|
745
|
+
8. **Erasable TypeScript only.** Node 24+ strips types via
|
|
746
|
+
`module.stripTypeScriptTypes` (whitespace replacement, byte-exact
|
|
747
|
+
line and column position preservation, no sourcemap shipped to the
|
|
748
|
+
browser). Your `tsconfig.json` sets `erasableSyntaxOnly: true`, so
|
|
749
|
+
the TS compiler rejects: `enum`, `namespace` with values,
|
|
750
|
+
constructor parameter properties, legacy decorators with
|
|
751
|
+
`emitDecoratorMetadata`, and `import = require`. Use the erasable
|
|
752
|
+
equivalents:
|
|
753
|
+
|
|
754
|
+
```ts
|
|
755
|
+
// ❌ enum
|
|
756
|
+
enum Color { Red, Green, Blue }
|
|
757
|
+
|
|
758
|
+
// ✅ const object + union type
|
|
759
|
+
const Color = { Red: 'Red', Green: 'Green', Blue: 'Blue' } as const;
|
|
760
|
+
type Color = typeof Color[keyof typeof Color];
|
|
761
|
+
|
|
762
|
+
// ❌ parameter property
|
|
763
|
+
class Foo { constructor(public x: number) {} }
|
|
764
|
+
|
|
765
|
+
// ✅ explicit field + assignment
|
|
766
|
+
class Foo {
|
|
767
|
+
x: number;
|
|
768
|
+
constructor(x: number) { this.x = x; }
|
|
769
|
+
}
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
If you turn `erasableSyntaxOnly` off and use non-erasable syntax,
|
|
773
|
+
the dev server falls back to esbuild and emits inline sourcemaps
|
|
774
|
+
for those specific files: roughly 3x wire bytes per request, and
|
|
775
|
+
stack-trace positions are no longer byte-exact. The
|
|
776
|
+
`erasable-typescript-only` convention check warns when the flag
|
|
777
|
+
is missing or set to false.
|
|
778
|
+
9. **No em-dashes (U+2014) anywhere, and no hyphen or semicolon used
|
|
779
|
+
as a pause-punctuation substitute.** Prose, comments, code, JSON
|
|
780
|
+
descriptions, commit messages. Rewrite the sentence so no
|
|
781
|
+
pause-punctuation crutch is needed. Banned as pause punctuation:
|
|
782
|
+
the em-dash (`-`), a plain hyphen used in place of one (` - `), and
|
|
783
|
+
a semicolon used in place of one (` ; `). Use a period, comma,
|
|
784
|
+
colon, parentheses, or a restructured phrasing. Plain hyphens stay
|
|
785
|
+
fine in compound words (`AI-first`), CLI flags (`--http2`),
|
|
786
|
+
filenames, and ranges. Semicolons stay fine inside code.
|
|
787
|
+
|
|
788
|
+
## Workflow expectations for AI agents
|
|
789
|
+
|
|
790
|
+
1. Branch before editing. Never push to `main` directly.
|
|
791
|
+
2. Every code change comes with: unit test(s), AGENTS.md / docs updates if
|
|
792
|
+
the feature surface changed, `webjs check` passing.
|
|
793
|
+
3. Commit and push **per logical unit**, not at the end. A logical unit is one
|
|
794
|
+
feature, one fix, one rename, one doc rewrite. If you have 5+ unstaged files
|
|
795
|
+
spanning different concerns, commit the current group before continuing.
|
|
796
|
+
The framework ships a `nudge-uncommitted` hook for several agents that
|
|
797
|
+
fires at threshold 4:
|
|
798
|
+
|
|
799
|
+
| Agent | Hook path | Doc |
|
|
800
|
+
|---|---|---|
|
|
801
|
+
| Claude Code | `.claude/hooks/nudge-uncommitted.sh` (`PostToolUse`) | `.claude/settings.json` |
|
|
802
|
+
| Gemini CLI | `.gemini/hooks/nudge-uncommitted.sh` (`AfterTool`) | `.gemini/settings.json` |
|
|
803
|
+
| Cursor 1.7+ | `.cursor/hooks/nudge-uncommitted.sh` (`afterFileEdit`) | `.cursor/hooks.json` |
|
|
804
|
+
| OpenCode | `.opencode/plugins/nudge-uncommitted.ts` (`tool.execute.after`) | `.opencode/plugins/` |
|
|
805
|
+
| Windsurf | text rule only (post-write hooks cannot inject context) | `.windsurfrules` |
|
|
806
|
+
| GitHub Copilot | text rule only (no hooks API) | `.github/copilot-instructions.md` |
|
|
807
|
+
| Google Antigravity | text rule only (no hooks API) | `AGENTS.md` |
|
|
808
|
+
|
|
809
|
+
Tool-agnostic fallback: `.hooks/pre-commit` runs `webjs test` + `webjs check`
|
|
810
|
+
on every commit, regardless of which agent (or human) made it. No AI
|
|
811
|
+
attribution trailers in commit messages.
|
|
812
|
+
4. When unsure how a framework feature works, `grep` or `cat` the
|
|
813
|
+
relevant `node_modules/@webjsdev/*/src/` file before asking the user.
|
|
814
|
+
|
|
815
|
+
Project-specific conventions and overrides live in
|
|
816
|
+
[CONVENTIONS.md](./CONVENTIONS.md).
|