@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.
@@ -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: `static properties = { x: {...} }` + `declare x`, read `this.x` (the prop rides the `x` attribute) |
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` | `static properties` (the framework derives both) |
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 property
248
- (`static properties` + `declare`) is still the idiomatic path, but
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
- This looks fine in TypeScript. It silently breaks the framework's
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
- ```ts
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
- // right
292
- class StudentCard extends WebComponent {
293
- static properties = { student: { type: Object } };
294
- declare student: Student;
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 this via the `reactive-props-use-declare` rule, but
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
- Banned by framework invariant 10 (erasable TS). The replacement is
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 | `declare student: Student` plus constructor default |
467
- | `@property()` decorator | `static properties = { ... }` plus `declare` |
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.prisma` if the app does not actually have users (delete or replace
17
- > it). NEVER persist app data in JSON files (`data/todos.json`, `db.json`), in
18
- > a module-scope array or `Map`, or in `localStorage`. Those reset on every
19
- > reload and cannot scale. Every piece of stored data is a Prisma model.
20
-
21
- 1. **Edit `prisma/schema.prisma`** to the real domain. Replace the example
22
- `User` model with the models the app needs.
23
-
24
- ```prisma
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
- Start the dev server with `npm run dev` (the canonical command), NOT a bare
36
- `webjs dev`: `npm run dev` runs the `predev` hook (`prisma generate`) first,
37
- while `webjs dev` skips it and boots against an ungenerated client. A bare
38
- `webjs dev` will now warn and point you back at `npm run dev`, but use the
39
- npm script from the start.
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. **Migrate.** Run the npm script (not the `webjs`/`prisma` binary directly,
42
- so the `predev` / `db:*` hooks fire):
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:migrate -- --name add_post
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 { prisma } from '../../../lib/prisma.server.ts';
54
+ import { db } from '../../../db/connection.server.ts';
60
55
  export async function listPosts() {
61
- return prisma.post.findMany({ where: { published: true }, orderBy: { createdAt: 'desc' } });
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 { prisma } from '../../../lib/prisma.server.ts';
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 prisma.post.create({ data: { title, body: String(input?.body || '') } });
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
- `@prisma/client` into a page; reach the database through the query.
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 { prisma } from '../../../lib/prisma.server.ts';
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 prisma.user.update({ where: { id: me.id }, data: { name } });
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 { prisma } from '../../../lib/prisma.server.ts';
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 prisma.post.create({ data: input });
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 { prisma } from '../../../lib/prisma.server.ts';
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 prisma.user.update({ where: { id: 'me' }, data: { avatarKey: key } });
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 Prisma DB prepared for the blog, the same setup
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` → node tests (everything not under `browser/` or `e2e/`).
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): Node 24+'s built-in TypeScript type-stripping handles server-side `.ts` imports automatically (`process.features.typescript === 'strip'`). Browser-bound `.ts` requests go through `module.stripTypeScriptTypes` 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: Node ships the [`amaro`](https://github.com/nodejs/amaro) package internally, which wraps SWC's WASM TypeScript transform in a position-preserving strip-only mode. If the framework ever needs to run on a non-Node runtime (Bun, Deno) we will install `amaro` directly or an equivalent position-preserving stripper (Sucrase preserves lines but not columns; SWC's strip mode also works).
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 uses Node 24+'s built-in `module.stripTypeScriptTypes`,
14
- which only supports **erasable TypeScript**: type annotations,
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. Node 24+'s built-in
66
- type-stripping and the dev server's HTTP handler both key on the
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 { prisma } from '../../../lib/prisma.js'; // JS file unchanged
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 `declare count: T`; `@click=${fn}` must be callable), unquoted `@`/`.`/`?` bindings (invariant 4), and expressionless `.prop` bindings.
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 { prisma } from '../../../lib/prisma.server.ts';",
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
- ' return { success: true, data: await prisma.thing.create({ data: { name } }) };',
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
- '(use `declare` + `static properties`).',
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: Prisma init, DB connects).
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 or Prisma side effects.
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 Prisma/DB init, and no stray stdout
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);