@webjsdev/mcp 0.1.2 → 0.1.4
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/package.json +1 -1
- package/resources/AGENTS.md +68 -27
- package/resources/agent-docs/advanced.md +29 -12
- package/resources/agent-docs/built-ins.md +10 -8
- package/resources/agent-docs/components.md +52 -19
- package/resources/agent-docs/configuration.md +31 -0
- package/resources/agent-docs/framework-dev.md +1 -1
- package/resources/agent-docs/lit-muscle-memory-gotchas.md +21 -27
- package/resources/agent-docs/recipes.md +90 -46
- package/resources/agent-docs/testing.md +48 -3
- package/resources/agent-docs/typescript.md +8 -7
- package/src/mcp-docs.js +6 -6
- package/src/mcp.js +3 -3
- package/resources/agent-docs/ssr-partial-nav-design.md +0 -214
|
@@ -77,14 +77,14 @@ lit form unless the vanilla API is genuinely unavoidable.
|
|
|
77
77
|
|
|
78
78
|
| Vanilla muscle memory | Lit-style webjs form |
|
|
79
79
|
|---|---|
|
|
80
|
-
| `this.getAttribute('x')` / `this.hasAttribute('x')` for own config | a reactive prop: `
|
|
80
|
+
| `this.getAttribute('x')` / `this.hasAttribute('x')` for own config | a reactive prop declared in the factory: `extends WebComponent({ x: String })`, read `this.x` (the prop rides the `x` attribute) |
|
|
81
81
|
| `this.setAttribute('x', v)` / `removeAttribute` to reflect own state | a reactive prop with `reflect: true`, or for non-attribute state a `signal` |
|
|
82
82
|
| `state: true` reactive prop for internal state | a `signal` (instance signal in the constructor, or module-scope) |
|
|
83
83
|
| `this.classList.add/toggle(...)` on self | a `class=${...}` binding in `render()` |
|
|
84
84
|
| `this.innerHTML = ...` / `appendChild` / `document.createElement` | return the markup from `render()` as `` html`...` `` |
|
|
85
85
|
| `this.addEventListener('click', ...)` on own/child elements | a `@click=${...}` binding in the template |
|
|
86
86
|
| `this.querySelector(...)` to reach own rendered DOM | the `ref()` directive + `createRef()`, or read a `<form>` with `new FormData(form)` |
|
|
87
|
-
| manual `observedAttributes` + `attributeChangedCallback` | `
|
|
87
|
+
| manual `observedAttributes` + `attributeChangedCallback` | a factory-declared reactive prop `WebComponent({ ... })` (the framework derives both) |
|
|
88
88
|
| manual `customElements.define('x', C)` | `C.register('x')` |
|
|
89
89
|
|
|
90
90
|
Emitting an event with `this.dispatchEvent(new CustomEvent(...))` is the
|
|
@@ -244,8 +244,8 @@ the event methods (`addEventListener` / `removeEventListener` /
|
|
|
244
244
|
`dispatchEvent`), and `this.attachInternals()` ARE backed by a server shim,
|
|
245
245
|
so reading an attribute in `render()`, wiring a delegated listener in the
|
|
246
246
|
constructor, or reflecting a property during the SSR update cycle all work.
|
|
247
|
-
Reading attributes that drive render through a reactive
|
|
248
|
-
(`
|
|
247
|
+
Reading attributes that drive render through a factory-declared reactive
|
|
248
|
+
property (`WebComponent({ ... })`) is still the idiomatic path, but
|
|
249
249
|
`this.hasAttribute(...)` no longer crashes.
|
|
250
250
|
|
|
251
251
|
Two guards catch the browser-only cases. `webjs check` flags browser
|
|
@@ -278,20 +278,20 @@ connectedCallback() {
|
|
|
278
278
|
|
|
279
279
|
### 5. Class-field initializers for reactive properties
|
|
280
280
|
|
|
281
|
-
|
|
282
|
-
accessor.
|
|
281
|
+
In Lit, class-field initializers (like `student: Student = { name: '', email: '' }`) are commonly used. In webjs, the base class installs reactive accessors on `this` inside the constructor via `Object.defineProperty` (to support SSR property hydration and signals). Under modern V8 class-field semantics, a class-field initializer compiles to an assignment after `super()`, which uses `[[Define]]` and overwrites the accessor, silently breaking reactivity. The footgun is still live with factory-declared props, so it stays a gotcha.
|
|
283
282
|
|
|
284
|
-
|
|
285
|
-
// wrong (the initializer overwrites the framework accessor after super())
|
|
286
|
-
class StudentCard extends WebComponent {
|
|
287
|
-
static properties = { student: { type: Object } };
|
|
288
|
-
student: Student = { name: '', email: '' };
|
|
289
|
-
}
|
|
283
|
+
The fix is to declare the prop in the base-class factory `WebComponent({ ... })` (which types it automatically, no `declare` line) and set its default via the `default` option or in the constructor after `super()`, never as a class-field initializer:
|
|
290
284
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
285
|
+
```ts
|
|
286
|
+
// default via the option (a function default = fresh object per instance)
|
|
287
|
+
class StudentCard extends WebComponent({
|
|
288
|
+
student: prop<Student>(Object, { default: () => ({ name: '', email: '' }) }),
|
|
289
|
+
}) {}
|
|
290
|
+
|
|
291
|
+
// or set the default in the constructor (fully typed, no declare needed)
|
|
292
|
+
class StudentCard extends WebComponent({
|
|
293
|
+
student: prop<Student>(Object),
|
|
294
|
+
}) {
|
|
295
295
|
constructor() {
|
|
296
296
|
super();
|
|
297
297
|
this.student = { name: '', email: '' };
|
|
@@ -299,17 +299,11 @@ class StudentCard extends WebComponent {
|
|
|
299
299
|
}
|
|
300
300
|
```
|
|
301
301
|
|
|
302
|
-
`webjs check` flags
|
|
303
|
-
AI agents emit the broken form on autopilot. The convention check is
|
|
304
|
-
the safety net, not the primary defense. Authoring code should use
|
|
305
|
-
`declare` plus constructor defaults from the start.
|
|
302
|
+
`webjs check` flags a class-field initializer on a factory-declared prop via the `reactive-props-no-class-field` rule.
|
|
306
303
|
|
|
307
|
-
### 6. The `@property()` decorator
|
|
304
|
+
### 6. The `@property()` decorator and a direct `static properties` block
|
|
308
305
|
|
|
309
|
-
|
|
310
|
-
`static properties = { ... }` plus a matching `declare` for the typed
|
|
311
|
-
accessor, as shown above. Decorators are non-erasable, so they would
|
|
312
|
-
force the framework to depend on a build step.
|
|
306
|
+
The `@property()` decorator is banned by framework invariant 10 (erasable TS): decorators are non-erasable, so they would force the framework to depend on a build step. A direct `static properties = { ... }` block is also gone, and webjs THROWS at runtime if a class body declares one (flagged by the `no-static-properties` rule). The single replacement for both is the declare-free base-class factory `WebComponent({ ... })` (the `prop()` helper carries options), as shown above.
|
|
313
307
|
|
|
314
308
|
## Patterns that produce different visual output
|
|
315
309
|
|
|
@@ -463,8 +457,8 @@ must move with it, use `repeat()` with a stable key.
|
|
|
463
457
|
| `Task` for client-time async | `Task` (no change, that's its job) |
|
|
464
458
|
| `window.X` or `document.X` in constructor or `render()` | Move to `connectedCallback` |
|
|
465
459
|
| Top-level `import` of browser-only library | Dynamic `import()` inside `connectedCallback` |
|
|
466
|
-
| `student: Student = { ... }` field initializer | `
|
|
467
|
-
| `@property()` decorator
|
|
460
|
+
| `student: Student = { ... }` field initializer | Base-class factory `WebComponent({ student: Object })`, default via the `default` option or the constructor |
|
|
461
|
+
| `@property()` decorator / `static properties` block | Base-class factory `WebComponent({ ... })` (the `prop()` helper carries options) |
|
|
468
462
|
| `static styles = css` / inline `<style>` with semantic class names in a light-DOM component | Tailwind utilities (the default); or `static shadow = true` for genuinely scoped CSS |
|
|
469
463
|
| Plain `.map()` for an interactive/stateful list | Works (reconciles in place, keeps node identity); use `repeat(items, key, t)` only when the list **reorders** |
|
|
470
464
|
| `willUpdate` for SSR-visible derived state | Works (runs at SSR); keep it a pure derivation |
|
|
@@ -13,41 +13,36 @@ schema with the real domain models, then build features on top. This is the
|
|
|
13
13
|
transition agents most often get wrong, so it is the first recipe.
|
|
14
14
|
|
|
15
15
|
> **Two non-negotiables.** NEVER leave the example `User` model in
|
|
16
|
-
> `schema.
|
|
17
|
-
> it). NEVER persist app data in JSON files (`data/todos.json`,
|
|
18
|
-
> a module-scope array or `Map`, or in `localStorage`. Those
|
|
19
|
-
> reload and cannot scale. Every piece of stored data is a
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// prisma/schema.prisma
|
|
26
|
-
model Post {
|
|
27
|
-
id String @id @default(cuid())
|
|
28
|
-
title String
|
|
29
|
-
body String
|
|
30
|
-
published Boolean @default(false)
|
|
31
|
-
createdAt DateTime @default(now())
|
|
32
|
-
}
|
|
33
|
-
```
|
|
16
|
+
> `db/schema.server.ts` if the app does not actually have users (delete or
|
|
17
|
+
> replace it). NEVER persist app data in JSON files (`data/todos.json`,
|
|
18
|
+
> `db.json`), in a module-scope array or `Map`, or in `localStorage`. Those
|
|
19
|
+
> reset on every reload and cannot scale. Every piece of stored data is a
|
|
20
|
+
> Drizzle table.
|
|
21
|
+
|
|
22
|
+
1. **Edit `db/schema.server.ts`** to the real domain. Replace the example
|
|
23
|
+
`User` model with the models the app needs (the helpers come from
|
|
24
|
+
`db/columns.server.ts`).
|
|
34
25
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
```ts
|
|
27
|
+
// db/schema.server.ts
|
|
28
|
+
import { table, pk, text, bool, createdAt, index } from './columns.server.ts';
|
|
29
|
+
export const posts = table('posts', {
|
|
30
|
+
id: pk(),
|
|
31
|
+
title: text().notNull(),
|
|
32
|
+
body: text().notNull(),
|
|
33
|
+
published: bool().notNull().default(false),
|
|
34
|
+
createdAt: createdAt(),
|
|
35
|
+
}, (t) => [index(t.createdAt)]);
|
|
36
|
+
```
|
|
40
37
|
|
|
41
|
-
2. **
|
|
42
|
-
|
|
38
|
+
2. **Generate + migrate.** Drizzle has no codegen; `generate` turns the schema
|
|
39
|
+
into SQL, `migrate` applies it:
|
|
43
40
|
|
|
44
41
|
```sh
|
|
45
|
-
npm run db:
|
|
42
|
+
npm run db:generate # schema -> SQL migration (drizzle-kit)
|
|
43
|
+
npm run db:migrate # apply it to the dev SQLite database
|
|
46
44
|
```
|
|
47
45
|
|
|
48
|
-
This creates the migration, applies it to the dev SQLite database, and
|
|
49
|
-
regenerates the Prisma client.
|
|
50
|
-
|
|
51
46
|
3. **Generate one query and one action per operation**, one exported function
|
|
52
47
|
per file, named after the file, under the feature module. Reads go in
|
|
53
48
|
`queries/`, mutations in `actions/`. Both are `.server.ts` with
|
|
@@ -56,27 +51,28 @@ transition agents most often get wrong, so it is the first recipe.
|
|
|
56
51
|
```ts
|
|
57
52
|
// modules/posts/queries/list-posts.server.ts
|
|
58
53
|
'use server';
|
|
59
|
-
import {
|
|
54
|
+
import { db } from '../../../db/connection.server.ts';
|
|
60
55
|
export async function listPosts() {
|
|
61
|
-
return
|
|
56
|
+
return db.query.posts.findMany({ where: { published: true }, orderBy: { createdAt: 'desc' } });
|
|
62
57
|
}
|
|
63
58
|
```
|
|
64
59
|
|
|
65
60
|
```ts
|
|
66
61
|
// modules/posts/actions/create-post.server.ts
|
|
67
62
|
'use server';
|
|
68
|
-
import {
|
|
63
|
+
import { db } from '../../../db/connection.server.ts';
|
|
64
|
+
import { posts } from '../../../db/schema.server.ts';
|
|
69
65
|
export async function createPost(input: { title: string; body: string }) {
|
|
70
66
|
const title = String(input?.title || '').trim();
|
|
71
67
|
if (!title) return { success: false, error: 'title required', status: 400 };
|
|
72
|
-
const post = await
|
|
68
|
+
const [post] = await db.insert(posts).values({ title, body: String(input?.body || '') }).returning();
|
|
73
69
|
return { success: true, data: post };
|
|
74
70
|
}
|
|
75
71
|
```
|
|
76
72
|
|
|
77
73
|
4. **Wire it into a page** by calling the query (the page runs on the server,
|
|
78
74
|
so it imports the `.server` query directly and awaits it). Never import
|
|
79
|
-
|
|
75
|
+
the DB driver into a page; reach the database through the query.
|
|
80
76
|
|
|
81
77
|
```ts
|
|
82
78
|
// app/posts/page.ts
|
|
@@ -118,11 +114,13 @@ export default async function User({ params }: { params: { id: string } }) {
|
|
|
118
114
|
```ts
|
|
119
115
|
// modules/users/actions/update-profile.server.ts
|
|
120
116
|
'use server';
|
|
121
|
-
import {
|
|
117
|
+
import { eq } from 'drizzle-orm';
|
|
118
|
+
import { db } from '../../../db/connection.server.ts';
|
|
119
|
+
import { users } from '../../../db/schema.server.ts';
|
|
122
120
|
export async function updateProfile(input: { name: string }) {
|
|
123
121
|
const name = String(input?.name || '').trim();
|
|
124
122
|
if (!name) return { success: false, error: 'name required', status: 400 };
|
|
125
|
-
const row = await
|
|
123
|
+
const [row] = await db.update(users).set({ name }).where(eq(users.id, me.id)).returning();
|
|
126
124
|
return { success: true, data: row };
|
|
127
125
|
}
|
|
128
126
|
```
|
|
@@ -143,7 +141,8 @@ validator is a plain function (or a three-line zod adapter).
|
|
|
143
141
|
```ts
|
|
144
142
|
// modules/posts/actions/create-post.server.ts
|
|
145
143
|
'use server';
|
|
146
|
-
import {
|
|
144
|
+
import { db } from '../../../db/connection.server.ts';
|
|
145
|
+
import { posts } from '../../../db/schema.server.ts';
|
|
147
146
|
|
|
148
147
|
// the validator: receives the action's FIRST argument
|
|
149
148
|
export const validate = (input: any) => {
|
|
@@ -157,7 +156,7 @@ export const validate = (input: any) => {
|
|
|
157
156
|
|
|
158
157
|
// the action body: runs ONLY when validation passes
|
|
159
158
|
export async function createPost(input: { title: string; body: string }) {
|
|
160
|
-
const row = await
|
|
159
|
+
const [row] = await db.insert(posts).values(input).returning();
|
|
161
160
|
return { success: true, data: row };
|
|
162
161
|
}
|
|
163
162
|
```
|
|
@@ -201,6 +200,53 @@ option: `export const POST = route(createPost, { validate })`. A
|
|
|
201
200
|
validator that THROWS (the classic `Schema.parse` style) becomes a 400, and a
|
|
202
201
|
non-envelope return transforms the input.
|
|
203
202
|
|
|
203
|
+
## HTTP verbs, caching, streaming, and cancellation (#488, #489, #490, #492)
|
|
204
|
+
|
|
205
|
+
A `'use server'` action is a POST by default. Reserved sibling exports, read
|
|
206
|
+
statically (the same way a page reads `export const revalidate`), change its
|
|
207
|
+
HTTP semantics WITHOUT changing the call site (you still write `await
|
|
208
|
+
getUser(7)`). One callable function per configured file (a second callable is a
|
|
209
|
+
`webjs check` error).
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
// modules/users/queries/get-user.server.ts: a cached, tagged GET read
|
|
213
|
+
'use server';
|
|
214
|
+
export const method = 'GET'; // absent = POST
|
|
215
|
+
export const cache = 60; // seconds, or { maxAge, swr, public }
|
|
216
|
+
export const tags = (id: number) => ['user:' + id];
|
|
217
|
+
export async function getUser(id: number) { return db.query.users.findFirst({ where: { id } }); }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
// a mutation evicts the tags it touches
|
|
222
|
+
'use server';
|
|
223
|
+
export const invalidates = (id: number) => ['user:' + id];
|
|
224
|
+
export const middleware = [requireAuth]; // #490: async (ctx, next) => result; actionContext()
|
|
225
|
+
export async function updateUser(id: number, patch: Partial<User>) { /* ... */ }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
- A **GET** rides args in the URL (POST fallback over a 4KB cap), is CSRF-exempt,
|
|
229
|
+
and carries `Cache-Control` + a weak `ETag` (304 on `If-None-Match`) +
|
|
230
|
+
`X-Webjs-Tags`. A **mutation** (POST/PUT/PATCH/DELETE) sends the rich body
|
|
231
|
+
(DELETE rides the URL), is CSRF-protected, and on success evicts its
|
|
232
|
+
`invalidates` tags and reports them via `X-Webjs-Invalidate`. A method mismatch
|
|
233
|
+
is a `405` + `Allow`.
|
|
234
|
+
- **SAFETY:** `cache` with `public: true` SHARES one response across all users,
|
|
235
|
+
keyed only by URL + args. Use it ONLY for data identical for every visitor (the
|
|
236
|
+
same rule as a page's `export const revalidate`).
|
|
237
|
+
- **Streaming (#489):** return a `ReadableStream` / async iterable / async
|
|
238
|
+
generator (any verb) and the call site does `for await (const c of await
|
|
239
|
+
streamTokens(8))`. Each chunk is rich-serialized as yielded, back-pressured,
|
|
240
|
+
and the source is cancelled on disconnect. A streamed result is never cached /
|
|
241
|
+
ETagged / seeded.
|
|
242
|
+
- **Cancellation (#492):** read the request `AbortSignal` via `actionSignal()`
|
|
243
|
+
(from `@webjsdev/server`) to stop work on disconnect; a superseded client
|
|
244
|
+
`async render()` auto-aborts the previous in-flight fetch.
|
|
245
|
+
- **SSR seeding (#472):** an action result invoked during a non-streamed SSR
|
|
246
|
+
render is serialized into the page; the stub reads that seed on its first client
|
|
247
|
+
call, so a shipping component does not re-fetch on hydration (consume-once,
|
|
248
|
+
fail-open; opt out with `"webjs": { "seed": false }`).
|
|
249
|
+
|
|
204
250
|
## Add a component
|
|
205
251
|
|
|
206
252
|
```ts
|
|
@@ -218,9 +264,7 @@ A leaf component fetches its own server data into the first paint, co-located, w
|
|
|
218
264
|
|
|
219
265
|
```ts
|
|
220
266
|
// (a) blocking async render: real data in the first paint, the common case
|
|
221
|
-
class UserProfile extends WebComponent {
|
|
222
|
-
static properties = { uid: { type: String } };
|
|
223
|
-
declare uid: string;
|
|
267
|
+
class UserProfile extends WebComponent({ uid: String }) {
|
|
224
268
|
async render() {
|
|
225
269
|
const u = await getUser(this.uid);
|
|
226
270
|
return html`<h3>${u.name}</h3>`;
|
|
@@ -236,9 +280,7 @@ html`
|
|
|
236
280
|
`;
|
|
237
281
|
|
|
238
282
|
// (c) renderFallback() = the CLIENT re-fetch loading state (never the first paint)
|
|
239
|
-
class UserActivity extends WebComponent {
|
|
240
|
-
static properties = { uid: { type: String } };
|
|
241
|
-
declare uid: string;
|
|
283
|
+
class UserActivity extends WebComponent({ uid: String }) {
|
|
242
284
|
renderFallback() { return html`<div class="skeleton h-24"></div>`; }
|
|
243
285
|
async render() {
|
|
244
286
|
const items = await getActivity(this.uid);
|
|
@@ -410,7 +452,9 @@ the user-supplied filename as a key; `generateKey` makes an opaque one.
|
|
|
410
452
|
// modules/avatar/actions/save-avatar.server.ts
|
|
411
453
|
'use server';
|
|
412
454
|
import { getFileStore, generateKey } from '@webjsdev/server';
|
|
413
|
-
import {
|
|
455
|
+
import { eq } from 'drizzle-orm';
|
|
456
|
+
import { db } from '../../../db/connection.server.ts';
|
|
457
|
+
import { users } from '../../../db/schema.server.ts';
|
|
414
458
|
|
|
415
459
|
export async function saveAvatar(file: File) {
|
|
416
460
|
const key = generateKey(file.name); // <uuid>.<ext>, safe
|
|
@@ -419,7 +463,7 @@ export async function saveAvatar(file: File) {
|
|
|
419
463
|
await getFileStore().delete(key);
|
|
420
464
|
return { success: false, fieldErrors: { avatar: 'Max 5 MB' }, status: 422 };
|
|
421
465
|
}
|
|
422
|
-
await
|
|
466
|
+
await db.update(users).set({ avatarKey: key }).where(eq(users.id, 'me'));
|
|
423
467
|
return { success: true, data: { key, contentType } };
|
|
424
468
|
}
|
|
425
469
|
```
|
|
@@ -108,6 +108,51 @@ cross-package suite (`packages/*/test/` + `test/`). They do NOT walk the in-repo
|
|
|
108
108
|
apps' own test dirs (`website/test/`, `examples/blog/test/`), so those run from
|
|
109
109
|
each app's OWN `webjs test` script, not the root runner.
|
|
110
110
|
|
|
111
|
+
### The Bun test matrix (#509)
|
|
112
|
+
|
|
113
|
+
webjs runs on Node 24+ or Bun (#508). The **Node suite (`npm test`) is the
|
|
114
|
+
source of truth**; a separate, additive **Bun matrix** re-runs the
|
|
115
|
+
runtime-sensitive suite under Bun to catch the long tail of cross-runtime
|
|
116
|
+
incompatibilities (a `node:*` API Bun implements differently, a crypto/stream
|
|
117
|
+
edge case, an error-message-format quirk).
|
|
118
|
+
|
|
119
|
+
- `node scripts/run-bun-tests.js` (needs `bun` on PATH; `BUN=…` overrides) runs
|
|
120
|
+
the `node:test` files under `test/`, `packages/core/test/`, and
|
|
121
|
+
`packages/server/test/` (excluding `browser/`, `e2e/`, the network-bound
|
|
122
|
+
`vendor/`) file by file via `bun test <file>`, and CLASSIFIES each result:
|
|
123
|
+
**pass**, **skip(node-only)** (a documented file that asserts Node-only
|
|
124
|
+
behavior or trips a Bun-test-runner quirk, listed with a reason in the
|
|
125
|
+
script's `DENYLIST`, its Bun behavior covered elsewhere), **skip(harness)**
|
|
126
|
+
(a Bun `node:test` compat gap, auto-detected by error signature),
|
|
127
|
+
**skip(env)** (needs Redis / a DOM), and **genuine fail** (a real Bun
|
|
128
|
+
failure, which fails the job). Set `WEBJS_BUN_TESTS=<substr,…>` to scope a
|
|
129
|
+
local run.
|
|
130
|
+
- CI runs it in the `bun` job alongside `test/bun/smoke.mjs` (#508) and
|
|
131
|
+
`test/bun/listener.mjs` (#511, the listener-shell parity).
|
|
132
|
+
- Two cross-runtime test scripts also run under BOTH runtimes: `test/bun/smoke.mjs`
|
|
133
|
+
(boot + SSR + TS strip + a server-action RPC) and `test/bun/listener.mjs`
|
|
134
|
+
(`startServer` over a real socket: SSR + route + SSE + WebSocket). Plain assert
|
|
135
|
+
scripts (not `node:test`) so the same file runs identically on each runtime.
|
|
136
|
+
|
|
137
|
+
**Dev hot-reload of server modules (cross-runtime, #514).** webjs's dev server
|
|
138
|
+
re-imports a `route.ts` / `.server.ts` / page module per request with a
|
|
139
|
+
`?t=<timestamp>` query cache-bust to pick up edits. Node honors that query and
|
|
140
|
+
re-imports, so under `node --watch` an edit is picked up live. Bun's ESM loader
|
|
141
|
+
IGNORES the query string and exposes no module-eviction API, so that mechanism
|
|
142
|
+
alone leaves a server-side edit stale on Bun. The CLI closes the gap by
|
|
143
|
+
re-execing `webjs dev` under `bun --hot` on Bun (vs `node --watch` on Node),
|
|
144
|
+
whose file-watching cache invalidation makes the next re-import fresh with no
|
|
145
|
+
restart (`Bun.serve` is reused across hot reloads, so the listener is not
|
|
146
|
+
duplicated). The cross-runtime test `test/bun/dev-hot-reload.mjs` proves the
|
|
147
|
+
edit-picked-up behaviour on BOTH runtimes (and runs as a dedicated CI step on
|
|
148
|
+
Bun). The `api` dev-cache-bust UNIT test asserts the bare server-level `?t=`
|
|
149
|
+
mechanism directly (no supervisor), which Bun ignores by design, so it stays on
|
|
150
|
+
the Bun-matrix denylist; the user-facing hot reload it underpins is covered on
|
|
151
|
+
Bun by the dev-hot-reload script instead. A `--no-hot` flag opts out of the
|
|
152
|
+
supervisor on either runtime (run the server in-process, edits need a manual
|
|
153
|
+
restart). Component / page / layout SOURCE edits already hot-reload on both
|
|
154
|
+
runtimes (the served `.ts`/`.js` is read from disk per request, not imported).
|
|
155
|
+
|
|
111
156
|
### In-repo app tests in CI (#342)
|
|
112
157
|
|
|
113
158
|
Each in-repo app (`website`, `examples/blog`) carries its own test suite under
|
|
@@ -116,7 +161,7 @@ its `test/` dir and runs it through its own `webjs test` script (the website's
|
|
|
116
161
|
not discover these, so a dedicated `.github/workflows/ci.yml` job, **In-repo app
|
|
117
162
|
tests (website + blog)**, runs `npm test --workspace=@webjsdev/website` and
|
|
118
163
|
`npm test --workspace=@webjsdev/example-blog` (with Playwright installed for the
|
|
119
|
-
website browser tests and the
|
|
164
|
+
website browser tests and the SQLite DB prepared for the blog, the same setup
|
|
120
165
|
the `unit` + `e2e` jobs use). It is a required status check, so a regression in
|
|
121
166
|
an app's tests gates the merge. The app test dirs are not walked by the root
|
|
122
167
|
runner, so the framework-package tests never double-run. `docs` and the
|
|
@@ -307,8 +352,8 @@ test/
|
|
|
307
352
|
|
|
308
353
|
App-side runners:
|
|
309
354
|
|
|
310
|
-
- `webjs test` →
|
|
311
|
-
- `webjs test --browser` → web-test-runner against `test/**/browser
|
|
355
|
+
- `webjs test` → the runtime's test runner over everything not under `browser/` or `e2e/` (#570): `node --test` on Node, `bun test` on Bun (dispatched on `process.versions.bun`, since `bun --test` is invalid).
|
|
356
|
+
- `webjs test --browser` → web-test-runner against `test/**/browser/**` (the app's resolved `wtr` bin, spawned with the current runtime, no `npx`).
|
|
312
357
|
- `WEBJS_E2E=1 webjs test` adds e2e.
|
|
313
358
|
|
|
314
359
|
App AI agents read this convention through the scaffold's
|
|
@@ -6,12 +6,13 @@ run is part of the user-visible workflow, no separate build step:
|
|
|
6
6
|
|
|
7
7
|
- **Editor** (VS Code) runs the TypeScript language server continuously. Red-squiggle on wrong types, including non-erasable syntax (see below).
|
|
8
8
|
- **CI** (optional) runs `tsc --noEmit` against `tsconfig.json`. Type-check only. Also catches non-erasable syntax via `erasableSyntaxOnly`.
|
|
9
|
-
- **Dev + prod server** (runtime, both directions):
|
|
9
|
+
- **Dev + prod server** (runtime, both directions): the runtime's TypeScript type-stripping handles server-side `.ts` imports automatically (on Node, `process.features.typescript === 'strip'`; Bun runs `.ts` natively). Browser-bound `.ts` requests go through the pluggable stripper in `packages/server/src/ts-strip.js` (#508) on the dev server, which performs whitespace replacement: every (line, column) in the source maps to the same position in the stripped output, so no sourcemap needs to be shipped and stack traces are byte-exact. The transform is cached by mtime (~microseconds per cache hit). Implementation backing: on **Node 24+** it is the built-in `module.stripTypeScriptTypes` (which itself wraps the [`amaro`](https://github.com/nodejs/amaro) package, SWC's WASM TypeScript transform in position-preserving strip-only mode); on **Bun** (no such built-in) it is `amaro` loaded directly, an `optionalDependency` of `@webjsdev/server`, producing byte-identical output. `WEBJS_TS_STRIPPER=builtin|amaro` forces a backend. Edge runtimes without a filesystem are a separate, later target.
|
|
10
10
|
|
|
11
11
|
## TypeScript feature support: erasable only
|
|
12
12
|
|
|
13
|
-
The framework
|
|
14
|
-
which only supports **erasable
|
|
13
|
+
The framework strips with Node 24+'s built-in `module.stripTypeScriptTypes`
|
|
14
|
+
(or `amaro` on Bun, the same engine), which only supports **erasable
|
|
15
|
+
TypeScript**: type annotations,
|
|
15
16
|
`interface`, `type`, `declare`, generics, `import type`, `as` casts,
|
|
16
17
|
and `satisfies`. Non-erasable syntax is rejected.
|
|
17
18
|
|
|
@@ -62,15 +63,15 @@ off so you catch the configuration drift before runtime.
|
|
|
62
63
|
|
|
63
64
|
## Import convention
|
|
64
65
|
|
|
65
|
-
Use explicit `.ts` extensions in imports.
|
|
66
|
-
|
|
66
|
+
Use explicit `.ts` extensions in imports. The runtime's type-stripping
|
|
67
|
+
and the dev server's HTTP handler both key on the
|
|
67
68
|
file URL ending in `.ts` / `.mts`. For mixed codebases, `.js` imports
|
|
68
69
|
that point at a `.ts` sibling also resolve in the dev server. Still
|
|
69
70
|
prefer explicit `.ts`.
|
|
70
71
|
|
|
71
72
|
```ts
|
|
72
73
|
// modules/posts/queries/list-posts.server.ts
|
|
73
|
-
import {
|
|
74
|
+
import { db } from '../../../db/connection.server.js'; // .js specifier resolves the .ts sibling
|
|
74
75
|
import { formatPost } from '../utils/slugify.ts'; // TS file
|
|
75
76
|
```
|
|
76
77
|
|
|
@@ -385,7 +386,7 @@ A single plugin with its OWN in-template intelligence (no Lit dependency; the `w
|
|
|
385
386
|
Gives you, inside `` html`` `` templates:
|
|
386
387
|
- **Go-to-definition** from `<my-counter>` to the class registered via `MyCounter.register('my-counter')`, from an attribute / property / event name to its class member, and from a CSS class in `class="…"` to its `` css`` `` rule.
|
|
387
388
|
- **Completions**: reachable custom-element tag names after `<`, and binding-aware attribute completions: `.` offers property names, plain / `?` offer hyphenated attribute names (`maxLength` → `max-length`), `@event` is permissive.
|
|
388
|
-
- **Diagnostics**: attribute / property value type-checks (`<my-counter .count=${expr}>` assignability-checks `typeof expr` against `
|
|
389
|
+
- **Diagnostics**: attribute / property value type-checks (`<my-counter .count=${expr}>` assignability-checks `typeof expr` against the property's type from the `WebComponent({ count: Number })` factory shape, and `@click=${fn}` must be callable), unquoted `@`/`.`/`?` bindings (invariant 4), and expressionless `.prop` bindings.
|
|
389
390
|
- **Hover**: a tag shows its component class; an attribute / property / event shows its declared type.
|
|
390
391
|
|
|
391
392
|
Every feature is gated on import-graph reachability: a tag is recognized only if the file registering it is reachable from the file you're editing. There is deliberately no blanket "unknown tag / attribute" diagnostic (webjs has no element type map, so it would false-positive on third-party custom elements).
|
package/src/mcp-docs.js
CHANGED
|
@@ -333,11 +333,13 @@ const PROMPT_BODIES = {
|
|
|
333
333
|
'```ts',
|
|
334
334
|
'// modules/<feature>/actions/<name>.server.ts',
|
|
335
335
|
"'use server';",
|
|
336
|
-
"import {
|
|
336
|
+
"import { db } from '../../../db/connection.server.ts';",
|
|
337
|
+
"import { things } from '../../../db/schema.server.ts';",
|
|
337
338
|
'export async function doThing(input: { name: string }) {',
|
|
338
339
|
" const name = String(input?.name || '').trim();",
|
|
339
340
|
" if (!name) return { success: false, error: 'name required', status: 400 };",
|
|
340
|
-
'
|
|
341
|
+
' const [row] = await db.insert(things).values({ name }).returning();',
|
|
342
|
+
' return { success: true, data: row };',
|
|
341
343
|
'}',
|
|
342
344
|
'```',
|
|
343
345
|
'',
|
|
@@ -363,16 +365,14 @@ const PROMPT_BODIES = {
|
|
|
363
365
|
'Tag MUST contain a hyphen (invariant 3). Event/property/boolean holes are',
|
|
364
366
|
'unquoted (invariant 4). Read `webjs-docs://lit-muscle-memory-gotchas` first:',
|
|
365
367
|
'a class-field initializer that overwrites a reactive accessor breaks reactivity',
|
|
366
|
-
'(
|
|
368
|
+
'(declare reactive props via the `extends WebComponent({ … })` factory).',
|
|
367
369
|
].join('\n'),
|
|
368
370
|
fetch_data_in_component: [
|
|
369
371
|
'Fetch server data IN the component that needs it, with an async render() (#469).',
|
|
370
372
|
'No prop-drilling: SSR awaits the render, so the DATA is in the first paint.',
|
|
371
373
|
'',
|
|
372
374
|
'```ts',
|
|
373
|
-
'class UserProfile extends WebComponent {',
|
|
374
|
-
' static properties = { uid: { type: String } };',
|
|
375
|
-
' declare uid: string;',
|
|
375
|
+
'class UserProfile extends WebComponent({ uid: String }) {',
|
|
376
376
|
' async render() {',
|
|
377
377
|
' const u = await getUser(this.uid); // a `use server` action: real fn at SSR, RPC stub on the client',
|
|
378
378
|
' return html`<h3>${u.name}</h3>`;',
|
package/src/mcp.js
CHANGED
|
@@ -155,7 +155,7 @@ const TOOL_DEFS = [
|
|
|
155
155
|
/**
|
|
156
156
|
* Lexically extract the names exported from a module source. Recognises the
|
|
157
157
|
* common forms a server-action / route file uses without LOADING the module
|
|
158
|
-
* (loading would run its top-level side effects:
|
|
158
|
+
* (loading would run its top-level side effects: DB init, connections).
|
|
159
159
|
*
|
|
160
160
|
* export async function foo() {} export function foo() {}
|
|
161
161
|
* export const foo = ... export let/var foo = ...
|
|
@@ -216,7 +216,7 @@ export function extractRouteMethods(src) {
|
|
|
216
216
|
/**
|
|
217
217
|
* Lexically extract the HTTP-verb action config declared as reserved sibling
|
|
218
218
|
* exports in a `'use server'` file (#488). Reads source text only (no module
|
|
219
|
-
* load) so there are no DB-init
|
|
219
|
+
* load) so there are no DB-init side effects.
|
|
220
220
|
*
|
|
221
221
|
* Returned shape:
|
|
222
222
|
* {
|
|
@@ -343,7 +343,7 @@ export function makeToolRunners(deps) {
|
|
|
343
343
|
|
|
344
344
|
async list_actions(appDir) {
|
|
345
345
|
// buildActionIndex is a pure file -> hash mapping that imports no module,
|
|
346
|
-
// so this stays truly read-only (no
|
|
346
|
+
// so this stays truly read-only (no DB init, and no stray stdout
|
|
347
347
|
// from a loaded module corrupting the JSON-RPC channel). The RPC hash is
|
|
348
348
|
// over the file path only, so no module load is needed.
|
|
349
349
|
const idx = await buildActionIndex(appDir, false);
|