@webjsdev/mcp 0.1.6 → 0.1.8

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.
Files changed (33) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/resources/AGENTS.md +169 -77
  4. package/resources/SKILL.md +226 -0
  5. package/resources/references/auth-and-sessions.md +220 -0
  6. package/resources/references/built-ins.md +200 -0
  7. package/resources/references/client-router-and-streaming.md +204 -0
  8. package/resources/references/components.md +167 -0
  9. package/resources/references/data-and-actions.md +187 -0
  10. package/resources/references/muscle-memory-gotchas.md +170 -0
  11. package/resources/references/optimistic-ui.md +128 -0
  12. package/resources/references/routing-and-pages.md +158 -0
  13. package/resources/references/runtime.md +80 -0
  14. package/resources/references/service-worker.md +78 -0
  15. package/resources/references/styling.md +123 -0
  16. package/resources/references/testing.md +125 -0
  17. package/resources/references/typescript.md +148 -0
  18. package/src/mcp-docs.js +27 -14
  19. package/src/mcp-source.js +12 -2
  20. package/src/mcp.js +4 -3
  21. package/resources/agent-docs/advanced.md +0 -1251
  22. package/resources/agent-docs/built-ins.md +0 -379
  23. package/resources/agent-docs/components.md +0 -712
  24. package/resources/agent-docs/configuration.md +0 -284
  25. package/resources/agent-docs/framework-dev.md +0 -65
  26. package/resources/agent-docs/lit-muscle-memory-gotchas.md +0 -470
  27. package/resources/agent-docs/metadata.md +0 -334
  28. package/resources/agent-docs/recipes.md +0 -528
  29. package/resources/agent-docs/runtime.md +0 -65
  30. package/resources/agent-docs/service-worker.md +0 -100
  31. package/resources/agent-docs/styling.md +0 -279
  32. package/resources/agent-docs/testing.md +0 -438
  33. package/resources/agent-docs/typescript.md +0 -402
@@ -0,0 +1,123 @@
1
+ # Styling
2
+
3
+ ## What This Covers
4
+
5
+ - Tailwind-first: the strong default for pages AND light-DOM components, and the Lit reflex it counters
6
+ - The light-DOM tag-prefix invariant when raw CSS is unavoidable
7
+ - Extracting a repeated Tailwind bundle into a `lib/utils/ui.ts` `html` fragment (not `@apply`)
8
+ - Design tokens: `:root` / `@theme` in the root layout
9
+ - Light-DOM host `display: block` behaviour (and shadow hosts via `:host`)
10
+ - When to use `static styles` (shadow DOM)
11
+ - `position: fixed`, not `sticky`, for a pinned header (the iOS WebKit flicker)
12
+ - Even-grid / no-reflow layout tips
13
+
14
+ Read this when a task touches a class list, a `<style>`, a design token, a pinned header, or a grid/board/card layout. Sibling ref: `components.md` (light vs shadow DOM, `static styles`, host behaviour in depth).
15
+
16
+ ## Tailwind-first is the strong default
17
+
18
+ Use Tailwind utilities for pages AND light-DOM components (the default DOM mode). Layout, spacing, color (via `@theme` tokens), typography, borders, radius, shadows, and interaction states (hover/focus/active/disabled, dark mode) are all utility-expressible. Light DOM does not scope styles, so a utility class on a light-DOM element resolves against the global stylesheet exactly as it does on a page. That is why utilities are the right tool there, not an exception.
19
+
20
+ ### The Lit muscle-memory trap
21
+
22
+ In Lit a component owns a shadow root and scopes its CSS with `static styles = css\`\``, so the reflex is to author scoped CSS or an inline `<style>` with semantic class names (`.hero`, `.card`, `.btn`) per component. In WebJs the default is light DOM, which does NOT scope. A `css` block does nothing without `static shadow = true` (the framework warns at runtime), and an inline `<style>` with bare semantic class names leaks those names into the global namespace. Reach for Tailwind utilities instead. When the same bundle repeats, extract a `lib/utils/ui.ts` helper returning an `html` fragment (below), NOT a CSS class.
23
+
24
+ ### The custom-CSS allowlist
25
+
26
+ Reserve raw CSS for what utilities genuinely cannot express. This is the exhaustive list, anything outside it should be a utility or a `lib/utils/ui.ts` helper:
27
+
28
+ - design-token `:root` + `@theme` definitions (palette, fonts, fluid type scale, motion durations, declared once in the root layout),
29
+ - `@property` animated custom properties paired with `@keyframes`,
30
+ - `::-webkit-scrollbar` and `scrollbar-color` (no utility surface),
31
+ - `prefers-reduced-motion` blocks,
32
+ - complex `color-mix()` or gradient effects a utility cannot spell.
33
+
34
+ When custom CSS IS unavoidable inside a light-DOM component, the tag-prefix invariant holds (every class selector is prefixed with the component tag). Shadow-DOM components (`static shadow = true`) legitimately author `static styles = css\`\``, the right home for scoped CSS. The Tailwind-first steer is about the LIGHT-DOM default, not shadow DOM.
35
+
36
+ ## DRY via a JS helper, not `@apply`
37
+
38
+ When the same Tailwind bundle repeats across 2+ places, extract it into a helper in `lib/utils/ui.ts` that returns an `html` fragment (SSR-time, no client runtime, output identical to inline classes):
39
+
40
+ ```ts
41
+ import { html } from '@webjsdev/core';
42
+
43
+ /** `● label` kicker: small caps, accent colour, above headings. */
44
+ export function rubric(label: string, mb: 'sm' | 'md' = 'md') {
45
+ const mbCls = mb === 'sm' ? 'mb-3' : 'mb-4';
46
+ return html`
47
+ <span class="block font-mono text-[11px] leading-none font-semibold tracking-[0.2em] uppercase text-primary ${mbCls}">● ${label}</span>
48
+ `;
49
+ }
50
+ ```
51
+
52
+ ```ts
53
+ // app/blog/[slug]/page.ts
54
+ import { rubric } from '#lib/utils/ui.ts';
55
+
56
+ export default function Post({ params }) {
57
+ return html`${rubric('post')}<h1 class="font-serif ...">${title}</h1>`;
58
+ }
59
+ ```
60
+
61
+ | Repeats | Action |
62
+ |---|---|
63
+ | Once | Inline the classes. |
64
+ | 2 to 3 times, identical | Extract to `lib/utils/ui.ts`. |
65
+ | Varies by 1 to 2 props | Extract with a small parameter (`mb: 'sm' \| 'md'`). |
66
+ | Radically different per call site | Keep inline, do not force-fit. |
67
+
68
+ Avoid `@apply`: it hides which utilities a class uses and creates a second source of truth. A JS helper keeps the bundle visible at the definition site, composes with conditional classes and active states, and runs at SSR time.
69
+
70
+ ## Design tokens
71
+
72
+ The default stack is a static compiled Tailwind stylesheet (`css:build` compiles `public/input.css` to the linked `public/tailwind.css`, so it works with JS off) plus `@theme` design tokens (palette, fonts, fluid type, motion durations) declared once in the root layout. Consume them as utility classes (`text-foreground`, `bg-card`, `font-serif`, `duration-fast`). If you wire your own theme switch, drive BOTH signals on `<html>` (the `data-theme` attribute for the app palette blocks AND the `.dark` class for the `@webjsdev/ui` kit), or half the UI renders stale tokens. Verify dark mode in a real browser, light mode passing proves nothing about dark.
73
+
74
+ ## Light-DOM host display, and shadow hosts
75
+
76
+ A custom element is `display: inline` in plain CSS, which collapses a component used as a block container to its content size. WebJs marks every LIGHT-DOM host `data-wj-host` and defaults it to `display: block` via one head rule in a low-priority cascade layer (`@layer webjs-host { :where([data-wj-host]) { display: block } }`), so a container component does not collapse. The layer keeps it overridable: any author style INCLUDING a Tailwind utility (`class="flex"`, `grid`, `hidden`) wins over it, and `[hidden]` still hides the host so `?hidden=${cond}` works. Opt into an inline light component with a tag-prefixed rule (`my-badge { display: inline }`).
77
+
78
+ Shadow-DOM hosts are NOT marked (a document rule would override the shadow tree's own `:host`), so a shadow component sets its host display the idiomatic way in `static styles`:
79
+
80
+ ```ts
81
+ static styles = css`:host { display: block }`; // a shadow host with no :host display stays inline
82
+ ```
83
+
84
+ **Size the HOST, not just an inner wrapper.** The host custom element is the box the parent lays out. A host that is a flex/grid item in a centering parent (`flex justify-center`, `grid place-items-center`) is sized to its content unless it carries width itself. Put the sizing classes on the host (`w-full max-w-[400px]`), not only on an inner `<div>`. Symptom: a board or card renders tiny even though its inner grid says `w-full max-w-[400px]`. Fix: move the sizing onto the host.
85
+
86
+ ## Even grids, no reflow
87
+
88
+ The reflow bug (a cell grows when it gets content while the others shrink) comes from `auto`-sized grid rows. Size the tracks explicitly so every cell is an equal fraction regardless of content:
89
+
90
+ ```html
91
+ <!-- a 3x3 board whose cells stay equal and square as it fills -->
92
+ <div class="grid gap-2 aspect-square [grid-template-columns:repeat(3,1fr)] [grid-template-rows:repeat(3,1fr)]">
93
+ ${cells.map((c) => html`
94
+ <button class="grid place-items-center min-h-0 overflow-hidden text-[clamp(1rem,8cqi,3rem)]">${c}</button>
95
+ `)}
96
+ </div>
97
+ ```
98
+
99
+ - `aspect-ratio` (e.g. `aspect-square`) on the CONTAINER plus `repeat(N,1fr)` columns AND rows keeps every cell an equal square that does not resize as marks are placed. Putting `aspect-square` on the CELLS is the common mistake that produces uneven rows.
100
+ - `min-h-0` + `overflow-hidden` on a cell stops its content forcing the track taller (a grid/flex child has an implicit `min-height: auto`).
101
+ - Size text relative to the cell (`clamp()`, container-query units `cqi`) so the glyph scales with the board rather than dictating the cell size.
102
+
103
+ Verify a layout by USING it, not by glancing at the first paint. A layout bug only shows mid-interaction: play through every state (fill the board, win, reload) and confirm nothing resizes.
104
+
105
+ ## Pin a header with `position: fixed`, never `sticky`
106
+
107
+ A `position: sticky` header (the common `sticky top-0` pattern) flickers its background for one frame on iOS WebKit (every iOS browser uses WebKit) during a client-router forward navigation. The router's scroll-to-top after the content swap drives a sticky recompute that WebKit mis-repaints. It is iOS-only (fine on desktop and Android, invisible in DevTools emulation), and neither compositor promotion (`translateZ(0)` / `will-change`) nor changing the swap paint timing fixes it. Preserving the header across nav is correct and standard, only the `sticky` positioning is the problem.
108
+
109
+ The fix is `position: fixed`. A fixed header is always pinned and never does the scroll-relative recompute, so the repaint bug never fires. Because fixed leaves normal flow, reserve the header height on the content below with a single `--header-h` custom property (kept exact with a `ResizeObserver`, degrading fine with no JS):
110
+
111
+ ```css
112
+ :root { --header-h: 56px; } /* sane SSR first-paint default */
113
+ header { position: fixed; inset-inline: 0; top: 0; }
114
+ body { padding-top: var(--header-h); }
115
+ ```
116
+ ```js
117
+ const hdr = document.querySelector('header');
118
+ const apply = () => document.documentElement.style.setProperty('--header-h', hdr.offsetHeight + 'px');
119
+ apply();
120
+ new ResizeObserver(apply).observe(hdr);
121
+ ```
122
+
123
+ For a dashboard, an alternative is an app-shell scroll container (a non-scrolling `100dvh` flex column with `<main>` as the internal scroller), which needs no offset but changes the scroll model.
@@ -0,0 +1,125 @@
1
+ # Testing
2
+
3
+ ## What This Covers
4
+
5
+ - The four test layers (unit, browser, e2e, smoke) and where each file lives.
6
+ - The `handle()` harness from `@webjsdev/server/testing` for driving the real request pipeline against a native `Response`.
7
+ - `webjs test` and `webjs test --browser`, plus when a browser or e2e test is REQUIRED (hydration, client router, slots, custom-element upgrade).
8
+ - Bun cross-runtime parity for runtime-sensitive code.
9
+ - Rendering the app and LOOKING for visual defects a static check cannot catch (a collapsed or reflowing layout).
10
+ - Convention validation with `webjs check`.
11
+
12
+ Read this when you are adding tests for a feature, deciding which layer a test belongs in, or verifying a UI or theming change. For component mount and hydration helpers see `components.md`. For testing actions and the `ActionResult` envelope see `data-and-actions.md`.
13
+
14
+ ## Test layers
15
+
16
+ Feature folders are primary, and the test kind is a subfolder inside the feature ONLY when that kind is present. Never create an empty `browser/` or `e2e/` folder.
17
+
18
+ | Kind | Location | What it does |
19
+ |---|---|---|
20
+ | unit + integration | `test/<feature>/<name>.test.{js,ts,mjs}` | Imports modules and asserts. No spawned process, no network. |
21
+ | browser | `test/<feature>/browser/<name>.test.js` | Real DOM, events, shadow / light DOM, in Chromium, Firefox, and WebKit. |
22
+ | e2e | `test/<feature>/e2e/<name>.test.{ts,mjs}` | Boots a real process and drives it over HTTP / browser / stdout. Opt in with `WEBJS_E2E=1`. |
23
+ | smoke | `test/<feature>/smoke/<name>.test.{js,ts}` | Fast deploy-time sanity check, a subset of e2e in spirit. |
24
+
25
+ Assert only on what the layer needs. A block that inspects only the HTTP response, the SSR HTML string, headers, or the importmap does NOT need a browser. Keep in the browser suite only blocks that genuinely need a DOM (live state via `page.evaluate`, hydration, client-router nav, slots, view transitions, streaming into the DOM, custom-element upgrade).
26
+
27
+ ## App runners (`webjs test`)
28
+
29
+ ```sh
30
+ webjs test # runtime test runner over everything not under browser/ or e2e/
31
+ webjs test --browser # web-test-runner against test/**/browser/**
32
+ WEBJS_E2E=1 webjs test # adds the e2e layer
33
+ ```
34
+
35
+ `webjs test` dispatches on the runtime (`node --test` on Node, `bun test` on Bun). The scaffold's `web-test-runner.config.js` globs `test/**/browser/**/*.test.js` and is already wired, so you do not set it up.
36
+
37
+ A scaffolded app has one root `test/` directory shaped the same way (feature first, kind second):
38
+
39
+ ```
40
+ test/
41
+ auth/
42
+ auth.test.ts # signup / login / currentUser
43
+ browser/login-form.test.js # only if exercising DOM
44
+ posts/
45
+ posts.test.ts
46
+ browser/post-editor.test.js
47
+ ```
48
+
49
+ ## The `handle()` harness (`@webjsdev/server/testing`)
50
+
51
+ `createRequestHandler({ appDir }).handle(request)` drives the FULL request pipeline (middleware, routing, SSR, page actions, server-action RPC, auth, CSRF) and returns a native `Response`. It is the same entry the framework's own suite uses, so the most realistic way to test an app is to fire a `Request` through it and assert on the `Response`, with no spawned process and no network. `@webjsdev/server/testing` ships thin builders over that `handle()`, each a few lines over native `Request` / `Response` that reuse the REAL cookie names, header names, and wire serializer. For a browser test that needs to drive the app in a real DOM, `createBrowserTestHandler()` from `@webjsdev/server/testing` exposes the same `handle()` pipeline to the WTR Chromium session.
52
+
53
+ ```js
54
+ import { createRequestHandler } from '@webjsdev/server';
55
+ import { testRequest, invokeActionForTest, rawActionRequest, loginAndGetCookies, withSessionCookie }
56
+ from '@webjsdev/server/testing';
57
+
58
+ const app = await createRequestHandler({ appDir: process.cwd(), dev: true });
59
+
60
+ const res = await testRequest(app.handle, '/about');
61
+ assert.equal(res.status, 200);
62
+ assert.match(await res.text(), /About/);
63
+ ```
64
+
65
+ A bare path is prefixed with a dummy origin (the pipeline reads only `pathname` and `search`); a full URL string or a pre-built `Request` also works. The optional third arg is a standard `RequestInit`.
66
+
67
+ ### Auth and session helpers
68
+
69
+ Server-action CSRF is an Origin / `Sec-Fetch-Site` check, so a test needs no CSRF setup. `loginAndGetCookies` drives the REAL credentials login through `handle()` and captures the genuine signed session cookie, so a follow-up request can hit a protected route as the logged-in user.
70
+
71
+ ```js
72
+ const gated = await testRequest(app.handle, '/dashboard');
73
+ assert.equal(gated.status, 302); // -> /login
74
+
75
+ const { cookies } = await loginAndGetCookies(app.handle, { email, password });
76
+ const dash = await testRequest(app.handle, '/dashboard', withSessionCookie({}, cookies));
77
+ assert.equal(dash.status, 200);
78
+ ```
79
+
80
+ ### `invokeActionForTest`: round-trip an action through the REAL endpoint
81
+
82
+ ```js
83
+ // modules/posts/actions/create.server.ts exports createPost
84
+ const out = await invokeActionForTest(app, 'modules/posts/actions/create.server.ts', 'createPost', [input]);
85
+ ```
86
+
87
+ It serializes the args with the WebJs serializer exactly as the generated client stub does, POSTs them same-origin to `/__webjs/action/<hash>/<fn>`, and parses the response. Prefer this over a direct import of the action. A direct import bypasses three production concerns the endpoint enforces (the wire serializer, CSRF, and prod error sanitization), so `invokeActionForTest` catches a regression a direct import cannot see. For negative cases, `rawActionRequest(app, file, fn, args, { crossOrigin: true })` returns the raw `Response` and never throws on a non-2xx (pass `{ omitCsrf: true }` to drop the CSRF pair).
88
+
89
+ ## When a browser or e2e test is REQUIRED
90
+
91
+ A unit test is necessary but NOT sufficient for any change to hydration, the client router, slots, or custom-element upgrade. The headline behaviour of these is a browser or e2e assertion, so ship one:
92
+
93
+ - Hydration and the SSR-then-hydrate agreement.
94
+ - Client-router navigation, form submissions through the router, prefetch.
95
+ - Slots and light / shadow DOM projection.
96
+ - Custom-element upgrade of the SSR'd tag.
97
+ - Progressive soft-nav streaming (assert the fallback is live at the moment the URL advances) belongs in e2e (`WEBJS_E2E=1`).
98
+
99
+ Component mount helpers (`fixture`, `ssrFixture`, `waitForUpdate`) come from `@webjsdev/core/testing`; see `components.md`.
100
+
101
+ ## Rendering the app and looking for UI defects
102
+
103
+ A layout bug (a board that collapses, cells of unequal size, a grid that resizes as it fills) is invisible to `webjs check`, `webjs typecheck`, and a glance at the empty first paint. Static tools give no signal for a visual defect, so render the app in a real browser and look.
104
+
105
+ - A browser test can measure real geometry with `getBoundingClientRect()` and FAIL on the defect. There is no framework helper (it is a few lines); write it against your component and assert its children stay the same size and do not resize as the grid fills. Ship one for any grid, board, or gallery layout.
106
+ - Test both light AND dark mode. Light mode passing proves nothing about dark mode. Emulate dark (a `newContext({ colorScheme: 'dark' })` or the theme toggle) and inspect a component's COMPUTED `background-color` and `color`, not just the page chrome.
107
+ - Read the screenshot. Capture `page.screenshot({ fullPage: true })` and open the PNG. White-on-white or a stray light box is obvious visually and invisible in the markup.
108
+
109
+ ## Bun cross-runtime parity
110
+
111
+ WebJs runs on Node 24+ or Bun. The Node suite is the source of truth; an additive Bun matrix re-runs the runtime-sensitive suite under Bun to catch the long tail of cross-runtime incompatibilities (a `node:*` API Bun implements differently, a crypto or stream edge case, an error-message-format quirk).
112
+
113
+ Bun parity is part of the definition of done. A change to a runtime-sensitive surface (the serializer, the `node:http` vs `Bun.serve` listener and request path, SSR / action / CSRF dispatch, streams, `node:crypto`, the TS stripper, auth / session / cors) is NOT done until you run the Bun matrix green AND add or update a `test/bun/<feature>.mjs` cross-runtime assertion. Run it with `node scripts/run-bun-tests.js` (needs `bun` on PATH).
114
+
115
+ ## Convention validation (`webjs check`)
116
+
117
+ `webjs check` is the correctness validator. Every rule catches code that is wrong to ship (a crash, a security leak, a type-strip failure), plus the `no-scaffold-placeholder` sentinel for unreplaced scaffold content. Run it and fix every violation before considering the change done (`webjs check --json` for an agent loop, `webjs check --rules` to list the rules). It is separate from `CONVENTIONS.md`, which carries the customizable project conventions you follow by judgment.
118
+
119
+ ## What NOT to do
120
+
121
+ - Do not recreate a top-level `test/{unit,browser,e2e}/` shape. Kind is a child of feature, never the reverse.
122
+ - Do not create empty kind folders.
123
+ - Do not import from another package's `test/` directory. Test code is not a public surface.
124
+ - Do not add `.unit` / `.integration` filename suffixes. The folder tells you the kind.
125
+ - Do not run WTR or Playwright inside a headless sandbox that lacks the transform plugins or native browser libraries. Instead, extract the reconciliation or optimistic-update logic into a pure browser-safe utility and cover it with a Node unit test.
@@ -0,0 +1,148 @@
1
+ # TypeScript
2
+
3
+ ## What This Covers
4
+
5
+ - TypeScript at runtime with **no build step**: `.ts` / `.mts` is stripped in place, not compiled.
6
+ - **Erasable syntax only** (`erasableSyntaxOnly: true`) and the exact list of banned constructs, with their allowed rewrites.
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 flow to the call site, `import type` crosses the `.server` boundary, plus the carrier rule.
9
+ - **Typing pages, layouts, and route handlers** with `PageProps` / `LayoutProps` / `RouteHandlerContext` and the generated route union (`webjs types`).
10
+
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
+
13
+ TypeScript is optional. JS + JSDoc gets the same call-site safety (the language server reads `@typedef` / `@param` / `@returns` identically). Add `"checkJs": true` to enforce it.
14
+
15
+ ## No build step: how `.ts` runs
16
+
17
+ `.ts` works everywhere `.js` does, same routing conventions, same server-action behaviour. There is no user-visible `tsc` run and no build output.
18
+
19
+ - **Server-side** `.ts` imports are stripped by the runtime automatically (Node exposes `process.features.typescript === 'strip'`, Bun runs `.ts` natively).
20
+ - **Browser-bound** `.ts` requests go through the pluggable stripper on the dev server, which does whitespace replacement (every source position maps to the same output position, so stack traces stay byte-exact with no sourcemap shipped). Cached by mtime.
21
+
22
+ The stripper backs onto **Node 24+'s built-in `module.stripTypeScriptTypes`** (itself SWC's WASM transform in strip-only mode) or, on **Bun**, `amaro` loaded directly (byte-identical output). Force one with `WEBJS_TS_STRIPPER=builtin|amaro`.
23
+
24
+ ## Erasable syntax only
25
+
26
+ The stripper supports **erasable TypeScript only**: type annotations, `interface`, `type`, `declare`, generics, `import type`, `as` casts, and `satisfies`. Non-erasable syntax is rejected at strip time (a 500 naming the file), so set `erasableSyntaxOnly: true` in `tsconfig.json` to catch it as an editor squiggle first. `webjs check`'s `erasable-typescript-only` rule verifies the flag is set.
27
+
28
+ Banned constructs and their erasable rewrites:
29
+
30
+ ```ts
31
+ // BANNED (rejected at compile + runtime)
32
+ enum Color { Red, Green, Blue }
33
+ class Foo { constructor(public x: number) {} } // parameter property
34
+ namespace Util { export const helper = 1; } // value namespace
35
+ import fs = require('fs'); // import = require
36
+ @legacyDecorator class C {} // legacy decorator + emitDecoratorMetadata
37
+
38
+ // ALLOWED (canonical erasable forms)
39
+ const Color = { Red: 'Red', Green: 'Green', Blue: 'Blue' } as const;
40
+ type Color = typeof Color[keyof typeof Color];
41
+
42
+ class Foo {
43
+ x: number;
44
+ constructor(x: number) { this.x = x; }
45
+ }
46
+
47
+ const Util = { helper: 1 };
48
+
49
+ import * as fs from 'fs';
50
+ ```
51
+
52
+ A third-party `.ts` dependency shipping non-erasable syntax fails the same way (rare, most npm packages publish compiled `.js`). WebJs is buildless end-to-end with no bundler fallback, so keep `erasableSyntaxOnly` on and your own code never hits it.
53
+
54
+ Prefer explicit `.ts` extensions in imports. A `.js` specifier pointing at a `.ts` sibling also resolves in the dev server, but explicit `.ts` is clearer.
55
+
56
+ ## Minimum `tsconfig.json`
57
+
58
+ ```json
59
+ {
60
+ "compilerOptions": {
61
+ "target": "ES2022",
62
+ "module": "NodeNext",
63
+ "moduleResolution": "NodeNext",
64
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
65
+ "strict": true,
66
+ "noEmit": true,
67
+ "checkJs": true,
68
+ "allowJs": true,
69
+ "allowImportingTsExtensions": true,
70
+ "skipLibCheck": true,
71
+ "erasableSyntaxOnly": true
72
+ }
73
+ }
74
+ ```
75
+
76
+ `erasableSyntaxOnly: true` is the non-negotiable line. It aligns the compiler's accepted syntax with the stripper's, so violations surface as diagnostics instead of a runtime 500.
77
+
78
+ ## Full-stack type safety
79
+
80
+ ### Server actions type-check automatically
81
+
82
+ 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).
83
+
84
+ ```ts
85
+ // modules/posts/actions/create-post.server.ts
86
+ export async function createPost(
87
+ input: { title: string; body: string },
88
+ ): Promise<ActionResult<PostFormatted>> { /* ... */ }
89
+
90
+ // modules/posts/components/new-post.ts
91
+ import { createPost } from '#modules/posts/actions/create-post.server.ts';
92
+ const r = await createPost({ title, body });
93
+ // ^ Promise<ActionResult<PostFormatted>>
94
+ if (r.success) r.data.title; // PostFormatted.title: string
95
+ ```
96
+
97
+ Class instances arrive as plain objects (prototypes and methods lost, matching React Server Actions). The opt-in `SerializableActionFn` annotation turns that silent loss into a compile error (`Serializable<T>` / `SerializableArgs` / `SerializableResult` are also exported, all types-only).
98
+
99
+ ### The carrier rule: `import type` across the `.server` boundary
100
+
101
+ A `.server.ts` file WITHOUT `'use server'` is a server-only utility whose browser stub throws at load. But a **type-only** `import type { Row } from '#db/schema.server.ts'` is safe: the stripper erases it before it can reach the browser, so sharing a derived row type from a `.server.ts` into a shipping component is fine and is not flagged. A **value** import of that same file into a shipping module is the crash the `no-server-import-in-browser-module` check catches. So carry TYPES over the boundary with `import type`, and carry DATA over it through a `'use server'` action (whose RPC stub loads safely client-side).
102
+
103
+ ### Typed page / layout / route-handler props
104
+
105
+ Type each routing entry with the exported helpers so a param typo is a compile error.
106
+
107
+ ```ts
108
+ import type { PageProps, LayoutProps, RouteHandlerContext } from '@webjsdev/core';
109
+
110
+ // Static route: params is Record<string, string>.
111
+ export default function About({ searchParams }: PageProps) { /* ... */ }
112
+
113
+ // Dynamic route: pass the route literal to narrow params.
114
+ export default function Post({ params }: PageProps<'/blog/[slug]'>) {
115
+ const slug = params.slug; // typed string
116
+ }
117
+
118
+ // Layout adds children.
119
+ export default function RootLayout({ children }: LayoutProps) { /* ... */ }
120
+
121
+ // Route handler's 2nd arg.
122
+ export async function GET(req: Request, ctx: RouteHandlerContext) {
123
+ return Response.json({ id: ctx.params.id });
124
+ }
125
+ ```
126
+
127
+ With no route literal (or before you generate route types), `params` is `Record<string, string>`, the runtime default. With `R` set to a generated dynamic route, `params` narrows to its exact shape (`{ slug: string }`, `{ rest: string[] }`, `{ slug?: string[] }`). These are pure types, erased at runtime.
128
+
129
+ 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
+
131
+ ### The generated route union (`webjs types`)
132
+
133
+ Run `webjs 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
+
135
+ - 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
+ - Per-route `params`: `PageProps<'/blog/[slug]'>['params']` becomes `{ slug: string }`.
137
+
138
+ ```sh
139
+ webjs types # writes .webjs/routes.d.ts (route count printed)
140
+ ```
141
+
142
+ `webjs 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 `webjs types` once and add `.webjs/routes.d.ts` to `include`. This is webjs's no-build equivalent of Next 15's `typedRoutes`, achieved via interface declaration-merging rather than a bundler.
143
+
144
+ ### The `webjs` config block and auth user
145
+
146
+ The `webjs` object in `package.json` has two typed references so a typo'd key is diagnosed instead of dropped: a JSON Schema (VS Code flags an unknown key while you edit) and the `WebjsConfig` type from `@webjsdev/core`. Type `auth()`'s session user by augmenting the `AuthUser` interface (types every `auth()` call) or by parameterising `createAuth<AppUser>(...)` (types one instance), both types-only. Un-augmented, `user` resolves to `Record<string, unknown>`.
147
+
148
+ Both `@webjsdev/core` and `@webjsdev/server` ship hand-authored `.d.ts` overlays with a `types` export condition, so a `strict` + `nodenext` app resolves real types for either import with no TS7016 error. The runtime stays plain `.js` + JSDoc; the overlays cost nothing at runtime.
package/src/mcp-docs.js CHANGED
@@ -4,20 +4,20 @@
4
4
  * `webjs mcp` (lib/mcp.js) started as four READ-ONLY introspection tools. This
5
5
  * module adds the second layer the Next.js MCP (`next-devtools-mcp`) showed is
6
6
  * the high-leverage one: the framework knowledge an agent needs to author
7
- * idiomatic webjs code, surfaced as MCP RESOURCES (the `agent-docs/*.md` corpus
7
+ * idiomatic WebJs code, surfaced as MCP RESOURCES (the WebJs skill references
8
8
  * plus the `AGENTS.md` contract), an `init` "read first" primer that fights the
9
9
  * React/RSC mental model, a `docs` retrieval tool, and guided-workflow PROMPTS
10
10
  * built from the recipes.
11
11
  *
12
12
  * It stays hand-rolled and ZERO-dependency (no `@modelcontextprotocol/sdk`),
13
- * consistent with webjs being buildless + minimal-deps. Everything here is PURE
13
+ * consistent with WebJs being buildless + minimal-deps. Everything here is PURE
14
14
  * given its injected `{ docsDir, agentsPath, readFile }` deps, so it is testable
15
15
  * in-process without booting a server or touching the real filesystem.
16
16
  *
17
17
  * Docs resolution (so `npx @webjsdev/cli mcp` is self-contained): a published
18
- * install reads the corpus bundled under `<cli>/resources/agent-docs` (copied
18
+ * install reads the corpus bundled under `<cli>/resources/references` (copied
19
19
  * at `prepack`, see `scripts/copy-mcp-resources.js`); a monorepo dev run falls
20
- * back to the repo-root `agent-docs/`. {@link resolveDocsLocation} encodes that
20
+ * back to the repo-root skill. {@link resolveDocsLocation} encodes that
21
21
  * two-path lookup so source stays single (no committed duplicate docs).
22
22
  *
23
23
  * @module mcp-docs
@@ -33,8 +33,8 @@ const DOCS_SCHEME = 'webjs-docs://';
33
33
  /**
34
34
  * Resolve where the framework-docs corpus lives, plus the `AGENTS.md` contract
35
35
  * path. Tries the BUNDLED location first (a published `@webjsdev/mcp` ships
36
- * `resources/agent-docs/` + `resources/AGENTS.md` via `prepack`), then falls
37
- * back to the monorepo-root layout (`agent-docs/` + `AGENTS.md`) used in dev and
36
+ * `resources/references/` + `resources/SKILL.md` + `resources/AGENTS.md` via `prepack`), then falls
37
+ * back to the monorepo-root skill layout used in dev and
38
38
  * tests. Returns `{ docsDir, agentsPath }`; either path may not exist, callers
39
39
  * fail soft (an empty corpus is valid, never a crash).
40
40
  *
@@ -46,11 +46,20 @@ export function resolveDocsLocation(moduleUrl) {
46
46
  const pkgRoot = resolve(here, '..'); // packages/mcp/src -> packages/mcp
47
47
  const repoRoot = resolve(here, '..', '..', '..'); // -> monorepo root
48
48
 
49
- const bundledDocs = join(pkgRoot, 'resources', 'agent-docs');
50
- if (existsSync(bundledDocs)) {
51
- return { docsDir: bundledDocs, agentsPath: join(pkgRoot, 'resources', 'AGENTS.md') };
49
+ const bundledRefs = join(pkgRoot, 'resources', 'references');
50
+ if (existsSync(bundledRefs)) {
51
+ return {
52
+ docsDir: bundledRefs,
53
+ agentsPath: join(pkgRoot, 'resources', 'AGENTS.md'),
54
+ skillPath: join(pkgRoot, 'resources', 'SKILL.md'),
55
+ };
52
56
  }
53
- return { docsDir: join(repoRoot, 'agent-docs'), agentsPath: join(repoRoot, 'AGENTS.md') };
57
+ const skill = join(repoRoot, '.agents', 'skills', 'webjs');
58
+ return {
59
+ docsDir: join(skill, 'references'),
60
+ agentsPath: join(repoRoot, 'AGENTS.md'),
61
+ skillPath: join(skill, 'SKILL.md'),
62
+ };
54
63
  }
55
64
 
56
65
  /**
@@ -74,6 +83,7 @@ function docName(file) {
74
83
  */
75
84
  function titleFor(name) {
76
85
  if (name === 'AGENTS') return 'AGENTS.md (the framework contract + invariants)';
86
+ if (name === 'SKILL') return 'SKILL.md (the WebJs agent skill: how to build)';
77
87
  return name
78
88
  .split('-')
79
89
  .map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w))
@@ -89,12 +99,15 @@ function titleFor(name) {
89
99
  * @returns {Array<{ name: string, file: string, uri: string, title: string }>}
90
100
  */
91
101
  export function catalogue(deps) {
92
- const { docsDir, agentsPath, listDir, exists } = deps;
102
+ const { docsDir, agentsPath, skillPath, listDir, exists } = deps;
93
103
  /** @type {Array<{ name: string, file: string, uri: string, title: string }>} */
94
104
  const out = [];
95
105
  if (exists(agentsPath)) {
96
106
  out.push({ name: 'AGENTS', file: agentsPath, uri: `${DOCS_SCHEME}AGENTS`, title: titleFor('AGENTS') });
97
107
  }
108
+ if (skillPath && exists(skillPath)) {
109
+ out.push({ name: 'SKILL', file: skillPath, uri: `${DOCS_SCHEME}SKILL`, title: titleFor('SKILL') });
110
+ }
98
111
  let files = [];
99
112
  try { files = listDir(docsDir).filter((f) => /\.md$/i.test(f)).sort(); } catch { files = []; }
100
113
  for (const f of files) {
@@ -275,7 +288,7 @@ export async function searchDocs(deps, args) {
275
288
 
276
289
  /**
277
290
  * The guided-workflow PROMPTS, built from the recipes. Each is a single-message
278
- * prompt that hands the agent the canonical webjs recipe plus the invariants it
291
+ * prompt that hands the agent the canonical WebJs recipe plus the invariants it
279
292
  * must not break, then tells it to pull `webjs-docs://recipes` for the full set.
280
293
  * Static metadata; {@link getPrompt} fills the message text.
281
294
  */
@@ -291,7 +304,7 @@ export const PROMPTS = [
291
304
  /**
292
305
  * The canonical recipe snippet + invariant reminders for each prompt. Kept
293
306
  * compact on purpose: the prompt orients + shows the shape, then points at the
294
- * full `webjs-docs://recipes` resource. The shapes mirror `agent-docs/recipes.md`.
307
+ * full `webjs-docs://recipes` resource. The shapes mirror `.agents/skills/webjs/references/data-and-actions.md`.
295
308
  *
296
309
  * @type {Record<string, string>}
297
310
  */
@@ -417,7 +430,7 @@ export function getPrompt(name, args) {
417
430
  ? `Context: ${provided.map(([k, v]) => `${k}=${v}`).join(', ')}.\n\n`
418
431
  : '';
419
432
 
420
- const text = `${argLine}${body}\n\nSee \`webjs-docs://recipes\` for the full recipe set and \`webjs-docs://AGENTS\` for the invariants.`;
433
+ const text = `${argLine}${body}\n\nSee \`webjs-docs://SKILL\` for the full guide (it routes to the reference for each task) and \`webjs-docs://AGENTS\` for the invariants.`;
421
434
  return {
422
435
  description: meta.description,
423
436
  messages: [{ role: 'user', content: { type: 'text', text } }],
package/src/mcp-source.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * The `source` tool for `webjs mcp` (#378): read-only access to the FRAMEWORK
3
3
  * source itself.
4
4
  *
5
- * webjs is buildless, so every app's `node_modules/@webjsdev/<pkg>/src` holds the
5
+ * WebJs is buildless, so every app's `node_modules/@webjsdev/<pkg>/src` holds the
6
6
  * authored JSDoc `.js`, and server-side that source runs directly. (The one built
7
7
  * artifact is the `@webjsdev/core` browser bundle in `dist/`, which this tool
8
8
  * deliberately skips: it surfaces only the authored `src/`.) That is a real
@@ -153,6 +153,13 @@ export function listSources(deps, pkgFilter) {
153
153
  export async function grepSources(deps, query) {
154
154
  const q = String(query).toLowerCase();
155
155
  if (!q) return 'Provide a non-empty `query`.';
156
+ // No resolvable packages means NOTHING was searched. Returning "no matches"
157
+ // here would read as authoritative absence when the search never ran, which
158
+ // is the #837 dogfood failure: an odd/shimmed node_modules layout resolved
159
+ // zero roots, so a real symbol looked missing and the agent lost trust.
160
+ if (!deps.roots.length) {
161
+ return 'No @webjsdev/* source is resolvable here, so NOTHING was searched (run inside a webjs app or the monorepo). This is not the same as "not found".';
162
+ }
156
163
  /** @type {string[]} */
157
164
  const hits = [];
158
165
  let capped = false;
@@ -170,7 +177,10 @@ export async function grepSources(deps, query) {
170
177
  }
171
178
  }
172
179
  }
173
- if (!hits.length) return `No matches for "${query}" in the @webjsdev/* source.`;
180
+ // Disclose the SEARCHED scope on a no-match, so "no matches" reads as a
181
+ // comprehensive-search result the agent can trust, not a silent gap (#837).
182
+ const searched = deps.roots.map((r) => r.pkg).join(', ');
183
+ if (!hits.length) return `No matches for "${query}" in the searched @webjsdev/* source (${searched}).`;
174
184
  if (capped) hits.push(`... (truncated at ${MAX_HITS} matches; narrow the query or read a file with \`path\`)`);
175
185
  return hits.join('\n');
176
186
  }
package/src/mcp.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * `webjs mcp`: a minimal, READ-ONLY Model Context Protocol server (#262).
3
3
  *
4
4
  * Exposes the live introspection surface an AI agent needs while editing a
5
- * webjs app (the route table, registered server actions with their RPC
5
+ * WebJs app (the route table, registered server actions with their RPC
6
6
  * endpoints, registered custom-element tags, and the structured `webjs check`
7
7
  * violations) over MCP's stdio transport. Every tool REUSES an existing
8
8
  * `@webjsdev/server` data function and MUTATES NOTHING. The prior art is
@@ -14,7 +14,7 @@
14
14
  * to stdout. STDOUT IS THE PROTOCOL CHANNEL, so this module writes ONLY
15
15
  * JSON-RPC frames there and routes every diagnostic to stderr. A malformed
16
16
  * input line is answered with a JSON-RPC parse error and never crashes the
17
- * loop. Hand-rolled with zero new dependency (webjs is buildless +
17
+ * loop. Hand-rolled with zero new dependency (WebJs is buildless +
18
18
  * minimal-deps).
19
19
  *
20
20
  * @module mcp
@@ -417,7 +417,7 @@ function rpcError(id, code, message) {
417
417
  }
418
418
 
419
419
  /**
420
- * Run the read-only webjs MCP server over the given streams. Reads
420
+ * Run the read-only WebJs MCP server over the given streams. Reads
421
421
  * newline-delimited JSON-RPC from `stdin`, writes one response line per request
422
422
  * to `stdout`, and logs diagnostics to `stderr` ONLY (stdout is the protocol
423
423
  * channel). Resolves when stdin ends (clean shutdown).
@@ -470,6 +470,7 @@ export async function runMcpServer(opts) {
470
470
  docsDeps = {
471
471
  docsDir: loc.docsDir,
472
472
  agentsPath: loc.agentsPath,
473
+ skillPath: loc.skillPath,
473
474
  listDir: readdirSync,
474
475
  exists: existsSync,
475
476
  readFile,