@webjsdev/mcp 0.1.10 → 0.1.12
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 +4 -0
- package/package.json +2 -2
- package/resources/AGENTS.md +117 -45
- package/resources/SKILL.md +34 -16
- package/resources/references/auth-and-sessions.md +86 -21
- package/resources/references/built-ins.md +56 -10
- package/resources/references/client-router-and-streaming.md +69 -9
- package/resources/references/components.md +167 -9
- package/resources/references/data-and-actions.md +115 -12
- package/resources/references/muscle-memory-gotchas.md +124 -3
- package/resources/references/optimistic-ui.md +49 -10
- package/resources/references/routing-and-pages.md +62 -18
- package/resources/references/runtime.md +10 -1
- package/resources/references/service-worker.md +5 -3
- package/resources/references/styling.md +116 -2
- package/resources/references/testing.md +86 -9
- package/resources/references/typescript.md +75 -6
- package/resources/references/ui-kit.md +18 -10
- package/src/mcp.js +18 -1
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
- TypeScript at runtime with **no build step**: `.ts` / `.mts` is stripped in place, not compiled.
|
|
6
6
|
- **Erasable syntax only** (`erasableSyntaxOnly: true`) and the exact list of banned constructs, with their allowed rewrites.
|
|
7
7
|
- The **pluggable stripper** (Node 24+ built-in vs `amaro` on Bun) and how the browser gets stripped source.
|
|
8
|
-
- **Full-stack type safety**: server-action types
|
|
9
|
-
- **Typing pages, layouts, and route handlers** with `PageProps` / `LayoutProps` / `RouteHandlerContext` and the generated route union (`
|
|
8
|
+
- **Full-stack type safety**: the rule (derive the type at every boundary, never `unknown` / `any`), server-action types flowing to the call site, and the `import type` carrier rule across the `.server` boundary.
|
|
9
|
+
- **Typing pages, layouts, and route handlers** with `PageProps` / `LayoutProps` / `RouteHandlerContext` and the generated route union (`npx webjsdev types`).
|
|
10
10
|
|
|
11
11
|
Read this when you are writing `.ts` in a WebJs app, hit a strip-time 500, or want typed params and hrefs. For action signatures and the serializer wire see `data-and-actions.md`. For typing reactive props see `components.md`.
|
|
12
12
|
|
|
@@ -77,6 +77,75 @@ Prefer explicit `.ts` extensions in imports. A `.js` specifier pointing at a `.t
|
|
|
77
77
|
|
|
78
78
|
## Full-stack type safety
|
|
79
79
|
|
|
80
|
+
### The rule: derive the type, never `unknown` or `any`
|
|
81
|
+
|
|
82
|
+
Every value crossing an app boundary in WebJs already has a type you can reach, so reach for it. Writing `unknown` or `any` at a boundary throws away the framework's central guarantee, and it does so silently: the app still runs, the checker just stops helping. Nothing catches this for you. Both are valid TypeScript, so `webjs check` will not flag them (it is a correctness tool, and this is a convention), and `tsc --noEmit` passes happily.
|
|
83
|
+
|
|
84
|
+
`unknown` is the more dangerous of the two, because it reads as the safe choice. It is safe only in the sense that it forces a narrow at the read site. As a boundary type it is exactly as uninformative as `any`, and it pushes a cast into every consumer.
|
|
85
|
+
|
|
86
|
+
The ladder, in the order to climb it:
|
|
87
|
+
|
|
88
|
+
| Boundary | Write this | Not this |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| A database row | `export type Todo = typeof todos.$inferSelect` in `db/schema.server.ts` (`$inferInsert` for a write) | a hand-written interface that drifts from the schema |
|
|
91
|
+
| That row inside a shipping component | `import type { Todo } from '#db/schema.server.ts'` | `any[]`, or re-declaring the shape by hand |
|
|
92
|
+
| A server action's input | a named `interface CreatePostInput` | `input: unknown` / `input: any` |
|
|
93
|
+
| A server action's result | `Promise<ActionResult<Post>>` | `Promise<any>` |
|
|
94
|
+
| `export const validate` | `(input: unknown)`, narrowed in the body, returning `data` that `satisfies` the action's input type | `(input: any)`, which un-types the returned `data` too |
|
|
95
|
+
| A page | `PageProps<'/blog/[slug]'>` | `{ params: Record<string, any> }` |
|
|
96
|
+
| A layout | `LayoutProps` (whose `children` is a `TemplateResult`) | `{ children: unknown }` |
|
|
97
|
+
| A route handler's 2nd argument | `RouteHandlerContext<'/api/users/[id]'>` | `{ params: any }` |
|
|
98
|
+
| A client-router href | the generated `Route` union (`npx webjsdev types`; `npm run dev` also emits it) | a bare `string` |
|
|
99
|
+
| A reactive property | `prop<Student>(Object)`, `prop<Tag[]>(Array)` | `prop(Object)` plus a cast at every read |
|
|
100
|
+
| An optimistic temp row | the pending shape on the row type (`pending?: boolean`) | `as any` on the temp id |
|
|
101
|
+
|
|
102
|
+
One flow, end to end, with no escape hatch anywhere in it:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
// db/schema.server.ts
|
|
106
|
+
export const posts = table('posts', { id: uuidPk(), title: text().notNull(), body: text().notNull() });
|
|
107
|
+
export type Post = typeof posts.$inferSelect; // derived, never hand-written
|
|
108
|
+
|
|
109
|
+
// modules/posts/actions/create-post.server.ts
|
|
110
|
+
'use server';
|
|
111
|
+
import type { Post } from '#db/schema.server.ts'; // type-only: erased before the browser sees it
|
|
112
|
+
export interface CreatePostInput { title: string; body: string }
|
|
113
|
+
export const validate = (input: unknown) => { /* narrows, returns data satisfying CreatePostInput */ };
|
|
114
|
+
export async function createPost(input: CreatePostInput): Promise<ActionResult<Post>> { /* ... */ }
|
|
115
|
+
|
|
116
|
+
// modules/posts/components/new-post.ts
|
|
117
|
+
import { createPost } from '#modules/posts/actions/create-post.server.ts';
|
|
118
|
+
const r = await createPost({ title, body });
|
|
119
|
+
if (r.success && r.data) r.data.title; // Post.title: string, checked at the call site
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The payoff is not stylistic. A typo in `r.data.titel`, a renamed column, a changed action signature, and a page reading `params.slugg` are all compile errors in that version and all silent runtime `undefined` in the `unknown` version.
|
|
123
|
+
|
|
124
|
+
### Where `unknown` is still the right type
|
|
125
|
+
|
|
126
|
+
There are two cases, and only the first is about narrowing.
|
|
127
|
+
|
|
128
|
+
**Case one: a value that genuinely has no type yet, at the moment before it is narrowed.**
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// app/api/webhook/route.ts
|
|
132
|
+
export async function POST(req: Request) {
|
|
133
|
+
const body: unknown = await req.json(); // correct: nothing has vouched for this yet
|
|
134
|
+
const parsed = parseWebhook(body); // narrowed on the very next line
|
|
135
|
+
return Response.json({ ok: parsed.kind });
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The test is what the next line does. Correct `unknown` here is narrowed immediately by a parse, a validator, or a type guard, and the narrowed type is what the rest of the function sees. Anything standing at that boundary qualifies: a `route.ts` handler's `await req.json()` (above), a `catch (e)` binding (already `unknown` under `strict`), an action's `export const validate`, and any validator function those delegate to. In a validator, returning `data` that `satisfies` the action's input type is what carries a real type into the action body. See `data-and-actions.md`.
|
|
140
|
+
|
|
141
|
+
**Case two: a parameter of YOUR OWN helper that forwards its argument into an `html` template hole, which is NOT narrowed.** A hole renders a string, a number, a `TemplateResult`, an array of those, a directive result, or nothing, so a helper like `lede(content: unknown)` is correctly typed and `TemplateResult` alone would be too narrow. Narrow it only when the helper genuinely accepts one shape (`backLink(href: string, ...)`).
|
|
142
|
+
|
|
143
|
+
This case is about a value YOU accept, never one the framework hands you. A layout's `children` also ends up in a hole, but the framework already types it (`LayoutProps.children` is a `TemplateResult`), so `{ children: unknown }` is a discarded type, not this carve-out.
|
|
144
|
+
|
|
145
|
+
Everywhere else `unknown` is a missing type, not a safe one: surviving into a return type, a component prop, a layout's `children`, or an action signature is the shape to fix.
|
|
146
|
+
|
|
147
|
+
`any` gets no carve-out at all in app code. It does not defer checking, it disables it, so a validator typed `(input: any)` un-types everything downstream of the call.
|
|
148
|
+
|
|
80
149
|
### Server actions type-check automatically
|
|
81
150
|
|
|
82
151
|
Calling a server action from a client component resolves at type-check time to the action's real source file. The runtime stub swap is invisible to the checker, and the RPC serializer makes runtime match the types (`Date` stays `Date`, `Map` stays `Map`, `BigInt` stays `BigInt`; see `data-and-actions.md` for the full supported set).
|
|
@@ -128,18 +197,18 @@ With no route literal (or before you generate route types), `params` is `Record<
|
|
|
128
197
|
|
|
129
198
|
Type page metadata with the exported `Metadata` type (and `MetadataContext` for the `generateMetadata` argument), the same ergonomics as Next.js's `import type { Metadata } from 'next'`.
|
|
130
199
|
|
|
131
|
-
### The generated route union (`
|
|
200
|
+
### The generated route union (`npx webjsdev types`)
|
|
132
201
|
|
|
133
|
-
Run `
|
|
202
|
+
Run `npx webjsdev types` to write `.webjs/routes.d.ts`, an opt-in overlay augmenting `@webjsdev/core` with one key per route in `app/`. It narrows two things at tsserver time:
|
|
134
203
|
|
|
135
204
|
- The `Route` href type: `navigate('/blog/anything')` passes, `navigate('/nonexistent')` is an error. Until you generate the types, `Route` is `string` (unconstrained, non-breaking for JSDoc and un-generated apps).
|
|
136
205
|
- Per-route `params`: `PageProps<'/blog/[slug]'>['params']` becomes `{ slug: string }`.
|
|
137
206
|
|
|
138
207
|
```sh
|
|
139
|
-
|
|
208
|
+
npx webjsdev types # writes .webjs/routes.d.ts (route count printed)
|
|
140
209
|
```
|
|
141
210
|
|
|
142
|
-
`
|
|
211
|
+
`npm run dev` emits it at startup and re-emits after each route rebuild, so the editor always has fresh types. The file is gitignored (regenerated per machine, like Next's `.next/types`); the scaffold `tsconfig.json` already lists it in `include`. To opt in for an existing app, run `npx webjsdev types` once and add `.webjs/routes.d.ts` to `include`. This is the WebJs no-build equivalent of Next 15's `typedRoutes`, achieved via interface declaration-merging rather than a bundler.
|
|
143
212
|
|
|
144
213
|
### The `webjs` config block and auth user
|
|
145
214
|
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# The `@webjsdev/ui` component kit
|
|
2
2
|
|
|
3
|
-
Load this when the app
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Load this when the app uses `@webjsdev/ui` (a `components.json` is present), OR
|
|
4
|
+
when you are about to add a UI primitive (button, card, input, badge) to a fresh
|
|
5
|
+
app that has not initialised the kit yet: running `npx webjsdev ui init` then
|
|
6
|
+
`npx webjsdev ui add <name>` is HOW the kit comes to exist, and it is the default for a
|
|
7
|
+
repeated primitive over hand-writing one from scratch. `@webjsdev/ui` is the shadcn-style
|
|
8
|
+
kit for WebJs. The source is copied into your repo (`components/ui/`), so you own
|
|
9
|
+
and edit it exactly as freely as code you wrote yourself, and can add, remove,
|
|
10
|
+
restructure, or theme it however your app needs. Two tiers:
|
|
6
11
|
|
|
7
12
|
- **Tier 1, class helpers (23 components).** Pure functions returning Tailwind
|
|
8
13
|
class strings (`buttonClass({ variant })`, `cardClass()`), composed with
|
|
@@ -27,25 +32,28 @@ paste-ready structure on demand:
|
|
|
27
32
|
kit inventory (each component's tier, helper signatures, npm deps); pass
|
|
28
33
|
`{ name: "accordion" }` for one component's helper signatures, the paste-ready
|
|
29
34
|
structural example, the accessibility header, and deps.
|
|
30
|
-
- **CLI**: `
|
|
35
|
+
- **CLI**: `npx webjsdev ui list` (inventory), `npx webjsdev ui view <name>` (the projected
|
|
31
36
|
view plus the full source). Same data as the MCP tool (one shared projector).
|
|
32
37
|
|
|
33
38
|
So the loop is: `add` the component, then query `ui <name>` (MCP) or
|
|
34
|
-
`
|
|
39
|
+
`npx webjsdev ui view <name>` for the accessible structure, paste it, and fill it in.
|
|
35
40
|
|
|
36
41
|
## Setup and resolution
|
|
37
42
|
|
|
38
|
-
- `
|
|
43
|
+
- `npx webjsdev ui init` writes `components.json`, `lib/utils/cn.ts`, and the CSS design
|
|
39
44
|
tokens the helpers render against (`--background`, `--foreground`,
|
|
40
45
|
`--destructive`, ...). It HARD-FAILS if the tokens cannot be written, so a
|
|
41
|
-
clean exit means the kit is styled.
|
|
46
|
+
clean exit means the kit is styled. Re-running it is safe on an existing
|
|
47
|
+
project: it keeps the aliases, stylesheet path, and base color already in
|
|
48
|
+
`components.json` and leaves an edited `cn.ts` / `dom.ts` alone, so it only
|
|
49
|
+
fills in what is missing (`--overwrite` resets them instead). `add` self-heals the tokens if they go
|
|
42
50
|
missing.
|
|
43
51
|
- Resolution is LOCAL-FIRST: `init` / `add` / `list` / `view` read the registry
|
|
44
52
|
that ships inside the installed `@webjsdev/ui`, with no network. This pins you
|
|
45
|
-
to the installed version; run `
|
|
53
|
+
to the installed version; run `npx webjsdev ui diff` to see where your local copies
|
|
46
54
|
drift from the upstream (that command alone compares against the live registry).
|
|
47
55
|
|
|
48
|
-
## Inventory (run `
|
|
56
|
+
## Inventory (run `npx webjsdev ui list` or the MCP `ui` tool for the authoritative, current set)
|
|
49
57
|
|
|
50
58
|
**Tier 1 (class helpers):** accordion, alert, aspect-ratio, avatar, badge,
|
|
51
59
|
breadcrumb, button, card, checkbox, collapsible, input, kbd, label,
|
|
@@ -61,7 +69,7 @@ dropdown-menu, hover-card, sonner, tabs, tooltip, plus toggle and toggle-group
|
|
|
61
69
|
- A helper is a function, so compose it: `class=${buttonClass({ variant: 'outline' })}`.
|
|
62
70
|
The unquoted `${...}` is a normal `html` attribute hole.
|
|
63
71
|
- Tier-1 helpers assume the design tokens exist; if a component paints unstyled,
|
|
64
|
-
the tokens are missing (re-run `
|
|
72
|
+
the tokens are missing (re-run `npx webjsdev ui init` or let `add` self-heal them).
|
|
65
73
|
- Custom elements are display-only-safe at SSR and hydrate in the browser, the
|
|
66
74
|
standard WebJs component model (`references/components.md`).
|
|
67
75
|
|
package/src/mcp.js
CHANGED
|
@@ -163,6 +163,12 @@ const TOOL_DEFS = [
|
|
|
163
163
|
'List registered custom-element tags: tag name, defining file, and class name. Read-only.',
|
|
164
164
|
inputSchema: APPDIR_SCHEMA,
|
|
165
165
|
},
|
|
166
|
+
{
|
|
167
|
+
name: 'list_elision',
|
|
168
|
+
description:
|
|
169
|
+
'Report the display-only elision verdict: every component module with whether it is elided (the browser never downloads it) or shipped plus the evidence that produced the verdict, every page/layout route module as inert / import-only / shipped (with the first client-effecting blocker that pins it), and any ORPHAN component class, one that either has no registration call at all or registers a computed tag, which the scanner cannot see either way, so it always loses its elision verdict, its tag-to-module registry entry, and its preload hint (and never upgrades at all with no registration call, or when a computed-tag class has no importer that ships whole). Identical to `webjs elision --json`. Read-only.',
|
|
170
|
+
inputSchema: APPDIR_SCHEMA,
|
|
171
|
+
},
|
|
166
172
|
{
|
|
167
173
|
name: 'check',
|
|
168
174
|
description:
|
|
@@ -298,7 +304,7 @@ export function extractActionConfig(src) {
|
|
|
298
304
|
* JSON-serialisable projection of an existing server data function. All are
|
|
299
305
|
* read-only.
|
|
300
306
|
*
|
|
301
|
-
* @param {{ buildRouteTable: Function, buildActionIndex: Function, hashFile: Function, scanComponents: Function, checkConventions: Function, projectCheck: Function, readFile: Function }} deps
|
|
307
|
+
* @param {{ buildRouteTable: Function, buildActionIndex: Function, hashFile: Function, scanComponents: Function, analyzeAppElision: Function, checkConventions: Function, projectCheck: Function, readFile: Function }} deps
|
|
302
308
|
*/
|
|
303
309
|
export function makeToolRunners(deps) {
|
|
304
310
|
const {
|
|
@@ -306,6 +312,7 @@ export function makeToolRunners(deps) {
|
|
|
306
312
|
buildActionIndex,
|
|
307
313
|
hashFile,
|
|
308
314
|
scanComponents,
|
|
315
|
+
analyzeAppElision,
|
|
309
316
|
checkConventions,
|
|
310
317
|
projectCheck,
|
|
311
318
|
readFile,
|
|
@@ -369,6 +376,15 @@ export function makeToolRunners(deps) {
|
|
|
369
376
|
.sort((a, b) => a.tag.localeCompare(b.tag));
|
|
370
377
|
},
|
|
371
378
|
|
|
379
|
+
async list_elision(appDir) {
|
|
380
|
+
// The whole report IS the contract: `analyzeAppElision` already returns
|
|
381
|
+
// an app-relative, sorted, JSON-serializable object, so unlike
|
|
382
|
+
// `list_routes` there is no projector leaf here and nothing to keep in
|
|
383
|
+
// sync. `webjs elision --json` prints this same object, and a drift test
|
|
384
|
+
// asserts the two are equal.
|
|
385
|
+
return analyzeAppElision(appDir);
|
|
386
|
+
},
|
|
387
|
+
|
|
372
388
|
async check(appDir) {
|
|
373
389
|
const violations = await checkConventions(appDir);
|
|
374
390
|
return projectCheck(violations);
|
|
@@ -475,6 +491,7 @@ export async function runMcpServer(opts) {
|
|
|
475
491
|
buildActionIndex: server.buildActionIndex,
|
|
476
492
|
hashFile: server.hashFile,
|
|
477
493
|
scanComponents: server.scanComponents,
|
|
494
|
+
analyzeAppElision: server.analyzeAppElision,
|
|
478
495
|
checkConventions: check.checkConventions,
|
|
479
496
|
projectCheck,
|
|
480
497
|
readFile,
|