@webjsdev/mcp 0.1.18 → 0.1.20

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 CHANGED
@@ -1,9 +1,9 @@
1
1
  # @webjsdev/mcp
2
2
 
3
- The webjs **Model Context Protocol server** for AI coding agents. A read-only
3
+ The WebJs **Model Context Protocol server** for AI coding agents. A read-only
4
4
  MCP server (newline-delimited JSON-RPC 2.0 over stdio) that gives an agent the
5
- live introspection surface plus the framework knowledge layer it needs while
6
- editing a webjs app.
5
+ WebJs brief, the live introspection surface, and the framework source it needs
6
+ while editing a WebJs app.
7
7
 
8
8
  ## Run it
9
9
 
@@ -19,28 +19,30 @@ no install:
19
19
  }
20
20
  ```
21
21
 
22
- Every webjs scaffold wires this entry automatically. `webjs mcp` (the CLI
22
+ Every WebJs scaffold wires this entry automatically. `webjs mcp` (the CLI
23
23
  subcommand) delegates to this same server, so both routes run identical code.
24
24
 
25
25
  ## What it exposes
26
26
 
27
+ - **`init` tool**: the WebJs brief, the short list of what differs from Next.js
28
+ App Router routing and Lit components, plus what each other tool is for.
29
+ WebJs is designed so Lit and Next.js habits produce correct code, and where
30
+ one cannot, `webjs check` and the runtime refuse it with the fix in the
31
+ message, so the brief is the whole of the agent-facing documentation. The
32
+ human docs live at https://webjs.dev/docs. `init` prepends a warning when the
33
+ app's installed `@webjsdev/mcp` is strictly newer than the running server's,
34
+ naming every way the server may have been started (a global install, the
35
+ `npx` package cache, `@webjsdev/cli` behind `webjs mcp`, or a checkout).
27
36
  - **Introspection tools** (read-only, scoped to an `appDir`): `list_routes`,
28
37
  `list_actions` (RPC endpoints plus the full data contract: HTTP verb, cache
29
38
  config, and boolean flags for tags/invalidates/validate/middleware; reserved
30
39
  config exports are excluded from the callable-action list), `list_components`,
31
- `list_elision` (the display-only elision verdict: which component modules the
32
- browser never downloads, the evidence behind each one that ships, every
33
- page/layout as inert / import-only / shipped, and any orphan class that gets
34
- no verdict at all; identical to `webjs elision --json`),
35
- `check` (the structured `webjs check` violations). Each projects an existing
36
- `@webjsdev/server` data function and mutates nothing.
37
- - **Knowledge layer**: an `init` mental-model primer (which also names the docs
38
- corpus it is serving, and warns when this server's copy looks stale against
39
- the app's), a `docs` retrieval tool, MCP `resources` (the skill references +
40
- `SKILL.md` + `AGENTS.md` as `webjs-docs://*`), and `prompts` (the recipes as
41
- guided workflows).
40
+ `check` (the structured `webjs check` violations, each with its fix). Each
41
+ projects an existing `@webjsdev/server` data function and mutates nothing.
42
42
  - **`source` tool**: reads the framework's own no-build source from
43
- `node_modules/@webjsdev/*/src` (read-only, traversal-guarded).
43
+ `node_modules/@webjsdev/*/src` (read-only, traversal-guarded). Pass `export`
44
+ for one export's signature plus doc comment, the cheapest way to check a
45
+ contract.
44
46
  - **`ui` tool**: the `@webjsdev/ui` kit. With no args it returns the
45
47
  component inventory plus the block and chart NAMES grouped by category (a
46
48
  block is a ready-made view you install and then edit: a login or signup
@@ -54,87 +56,4 @@ subcommand) delegates to this same server, so both routes run identical code.
54
56
  a `cn-*` name, and a test caps the no-args payload size. Kit-scoped (not
55
57
  `appDir`-scoped); shares one projector with `webjsui view`.
56
58
 
57
- ## Which docs you get
58
-
59
- The corpus is resolved in three rungs, highest first:
60
-
61
- 1. `<appDir>/node_modules/@webjsdev/mcp/resources`, the app's own installed copy.
62
- 2. `<pkg>/resources`, this server's bundled snapshot, so `npx @webjsdev/mcp` is
63
- self-contained.
64
- 3. Whatever the server resolves locally, which is the repo-root skill on the
65
- monorepo dev path. This rung is an unconditional fallback rather than a
66
- checkout probe, so a published install whose `resources/` bundle is missing
67
- lands here too, with an empty corpus. That is why neither the corpus line nor
68
- the warning asserts a checkout.
69
-
70
- The app's copy wins because it is version-matched to the framework you are
71
- editing, and so is the only corpus that can be correct about that app. This
72
- matters most for a GLOBAL install (`npm i -g` / `bun add -g`), which otherwise
73
- keeps serving the docs it was published with forever, contradicting the copy
74
- sitting in your own `node_modules`. That is a real incident, not a hypothetical:
75
- a server published one day before the client router stopped needing an explicit
76
- `import '@webjsdev/core/client-router'` kept teaching that import for months, and
77
- an agent following it wrote dead imports into three layouts, each of which the
78
- elision analyser then correctly refused to strip.
79
-
80
- `appDir` is a per-call argument, so the corpus follows it. `resources/list` and
81
- `resources/read` carry no `appDir` in the MCP protocol and resolve from the
82
- server's working directory, which is what a `tools/call` with no `appDir`
83
- defaults to, so in the ordinary case every surface reads one corpus.
84
-
85
- `init` names the corpus it served on its first line, and prepends a warning when
86
- the app's installed `@webjsdev/mcp` is strictly newer than the running server's:
87
-
88
- ```
89
- Warning: this MCP server is @webjsdev/mcp@0.1.4, but this app has @webjsdev/mcp@0.1.12,
90
- so the server may be stale. The docs below come from this app's own copy, so they
91
- match it; update the server so its TOOLS match too. Update whichever copy runs this
92
- server: a global install (npm i -g @webjsdev/mcp@latest, or bun add -g @webjsdev/mcp),
93
- the package cache behind npx @webjsdev/mcp, @webjsdev/cli when the server is started
94
- as webjs mcp, or the checkout it runs from.
95
- Docs corpus: @webjsdev/mcp@0.1.12, copied from webjsdev/webjs e5806e2 on 2026-08-08.
96
- ```
97
-
98
- It warns rather than refusing to answer, because a knowledge tool that declines
99
- sends the agent back to its training data, which is what the caveat exists to
100
- correct. Equal versions, an older app, and an unreadable app manifest are all
101
- silent.
102
-
103
- Two details the wording is deliberate about. It does not name a global install as
104
- the thing to fix, because nothing observes how the server was started and the two
105
- shipped configurations are an `npx` invocation (whose staleness is a package
106
- cache) and `webjs mcp` (whose staleness is the CLI's own dependency). And it does
107
- not promise the docs below came from the app: the warning reads the app's
108
- `package.json` while the corpus rung probes that install's `resources/`, so a
109
- workspace-linked install can have a manifest to compare and no corpus to serve.
110
- In that case the corpus falls through to the server's snapshot and the warning
111
- says so, rather than contradicting the corpus line printed under it.
112
-
113
- ## The build stamp
114
-
115
- The bundle is a snapshot frozen at publish time, so a published tarball keeps
116
- serving the docs as they read on the day it was cut. `prepack` stamps it with
117
- `resources/corpus.json` so the snapshot can say which docs it holds:
118
-
119
- ```json
120
- {
121
- "package": "@webjsdev/mcp",
122
- "version": "0.1.12",
123
- "sha": "e5806e2400000000000000000000000000000000",
124
- "copiedAt": "2026-08-08T09:14:22.031Z"
125
- }
126
- ```
127
-
128
- `sha` is the full commit the docs were copied from, so it resolves straight to a
129
- GitHub diff. Every field is `null` rather than a plausible-looking default when
130
- it cannot be established, so a consumer can always tell a real answer from no
131
- answer: `sha` when the source tree is not itself a git checkout root, and
132
- `package` / `version` when the manifest cannot be read. None of those fails the
133
- publish. A dev checkout has no bundle and so no stamp.
134
-
135
- The SHA is deliberately refused when the tree merely SITS inside some other
136
- checkout, because `git rev-parse` walks up to an ancestor and would otherwise
137
- report an unrelated repository's HEAD as the commit these docs came from. That
138
- answer is a well-formed SHA, so nothing downstream could catch it.
139
-
140
59
  STDOUT is the JSON-RPC channel; every diagnostic goes to stderr.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@webjsdev/mcp",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "type": "module",
5
- "description": "The Model Context Protocol server for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Gives AI coding agents live app introspection over routes, actions, components, and convention checks, plus a knowledge layer of docs, recipes, and framework source.",
5
+ "description": "The Model Context Protocol server for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Gives AI coding agents the WebJs brief, live app introspection over routes, actions, components, and convention checks, and the framework source.",
6
6
  "bin": {
7
7
  "webjs-mcp": "bin/webjs-mcp.js"
8
8
  },
@@ -21,12 +21,11 @@
21
21
  "files": [
22
22
  "bin",
23
23
  "src",
24
- "README.md",
25
- "resources"
24
+ "README.md"
26
25
  ],
27
26
  "dependencies": {
28
- "@webjsdev/server": "^0.9.0",
29
- "@webjsdev/ui": "^0.4.0"
27
+ "@webjsdev/server": "^0.10.1",
28
+ "@webjsdev/ui": "^0.5.1"
30
29
  },
31
30
  "publishConfig": {
32
31
  "access": "public"
@@ -48,9 +47,5 @@
48
47
  ],
49
48
  "engines": {
50
49
  "node": ">=24.0.0"
51
- },
52
- "scripts": {
53
- "prepack": "node scripts/copy-mcp-resources.js",
54
- "postpack": "node scripts/clean-mcp-resources.js"
55
50
  }
56
51
  }
package/src/brief.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The WebJs brief: the whole of what an agent needs to know beyond its Lit and
3
+ * Next.js training data. Lit and Next habits are meant to produce correct WebJs
4
+ * code, and where one cannot, `webjs check` and the runtime refuse it with the
5
+ * fix in the message, so the agent-facing docs are this brief and nothing more.
6
+ *
7
+ * Single source. The MCP `init` tool returns it, and the root `AGENTS.md` plus
8
+ * the scaffold's `packages/cli/templates/AGENTS.md` carry it VERBATIM under the
9
+ * `## The WebJs brief` heading. `test/repo-health/brief-parity.test.mjs`
10
+ * asserts all three agree, so edit it here and paste it there.
11
+ *
12
+ * @module brief
13
+ */
14
+
15
+ /** The heading both `AGENTS.md` files place the brief under. */
16
+ export const BRIEF_HEADING = '## The WebJs brief';
17
+
18
+ /** The brief itself, as markdown, without its heading. */
19
+ export const BRIEF = [
20
+ 'WebJs is a no-build full-stack framework. Write it the way you write Next.js',
21
+ 'routing and Lit components, with the differences below.',
22
+ '',
23
+ '- Routing follows the Next.js App Router: `app/page.ts`, `layout.ts`,',
24
+ ' `route.ts`, `middleware.ts`, `[param]`, `[...slug]`, `(group)`,',
25
+ ' `loading.ts`, `error.ts`, `not-found.ts`, `metadata` and',
26
+ ' `generateMetadata`, and server actions.',
27
+ '- Components use Lit\'s APIs (`html`, `css`, reactive properties through',
28
+ ' `static properties` plus `declare` or the `WebComponent({ ... })` factory,',
29
+ ' the lifecycle, directives, context, task), imported from `@webjsdev/core`',
30
+ ' and its `/directives`, `/context`, `/task` and `/signals` subpaths. The base',
31
+ ' class is `WebComponent`. Register with `Class.register(\'my-tag\')` or',
32
+ ' `customElements.define`.',
33
+ '- Components render light DOM by default and `<slot>` works there too. Keep',
34
+ ' them light: page styles such as Tailwind and the UI kit do not reach into a',
35
+ ' shadow root. Use `static shadow = true` only when light DOM cannot work.',
36
+ '- TypeScript must be erasable, so no decorators and no enums.',
37
+ '- Server-only code and server actions live in `*.server.ts` files. With',
38
+ ' `\'use server\'` at the top, their exports are callable from components and',
39
+ ' bindable as `<form action=${fn}>`. A failed form action re-renders the page',
40
+ ' with its result on `actionData`.',
41
+ '- `cookies()`, `headers()` and `revalidatePath()` come from `@webjsdev/server`,',
42
+ ' and so does `createAuth()`, which is Auth.js\'s API. Files in `public/` are',
43
+ ' served from the site root.',
44
+ '- Never import `lit`, `next` or `react`.',
45
+ '',
46
+ 'Before hand-writing auth, sessions, caching, rate limits, uploads, realtime or',
47
+ 'SEO, run `npx webjs source`, which lists what WebJs provides for each need.',
48
+ '`npx webjs source <Export>` then prints one export\'s signature and example.',
49
+ 'After each edit run `npx webjs check` and `npx webjs status`. The framework',
50
+ 'source is in `node_modules/@webjsdev/*` (no build, so it is the code that',
51
+ 'runs) and the human docs are at https://webjs.dev/docs.',
52
+ ].join('\n');
package/src/catalog.js ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The feature catalogue: what WebJs provides beyond the brief, grouped by what
3
+ * an app needs (auth, sessions, caching, uploads, realtime, ...). An agent that
4
+ * does not know an export's name cannot ask `source` for it, so before #1686 a
5
+ * feature the brief does not name (rate limiting, file storage, broadcast) was
6
+ * found only by reading the framework source, or hand-written instead.
7
+ *
8
+ * `npx webjs source` with no name prints it, and so does the MCP `source` tool
9
+ * with no arguments. Every name here resolves through `lookupExport`, so the
10
+ * second call (`npx webjs source <Name>`) returns the signature and example;
11
+ * `packages/mcp/test/catalog.test.mjs` holds that, and holds each name to be a
12
+ * real export of the module named for its group.
13
+ *
14
+ * Curated rather than derived: the export lists mix the app-facing surface
15
+ * with framework plumbing (`buildModuleGraph`, `jspmGenerate`), and a need is
16
+ * not a property any export carries. Add an entry when an app-facing export
17
+ * lands; the test fails on one that disappears.
18
+ *
19
+ * @module catalog
20
+ */
21
+
22
+ /**
23
+ * @typedef {{ need: string, names: string[], note?: string }} CatalogGroup
24
+ * @typedef {{ module: string, groups: CatalogGroup[] }} CatalogSection
25
+ */
26
+
27
+ /** @type {CatalogSection[]} */
28
+ export const FEATURE_CATALOG = [
29
+ {
30
+ module: '@webjsdev/server',
31
+ groups: [
32
+ { need: 'Auth', names: ['createAuth', 'Credentials', 'GitHub', 'Google', 'AuthError'], note: "Auth.js's API: auth(), signIn(), signOut(), handlers" },
33
+ { need: 'Sessions and cookies', names: ['session', 'getSession', 'cookies', 'headers'] },
34
+ { need: 'Redirects and errors', names: ['redirect', 'permanentRedirect', 'notFound', 'forbidden', 'unauthorized'] },
35
+ { need: 'Caching', names: ['cache', 'revalidatePath', 'revalidateTag', 'revalidateAll'] },
36
+ { need: 'Rate limits and CORS', names: ['rateLimit', 'cors', 'clientIp'] },
37
+ { need: 'File uploads', names: ['getFileStore', 'setFileStore', 'diskStore', 'generateKey', 'signedUrl', 'verifySignedUrl'] },
38
+ { need: 'Realtime', names: ['broadcast'], note: 'a route.ts exporting WS handles WebSocket connections' },
39
+ { need: 'Streaming responses', names: ['stream', 'streamResponse', 'acceptsStream'] },
40
+ { need: 'Route handlers', names: ['json', 'readBody', 'route'] },
41
+ { need: 'SEO', names: ['sitemap', 'sitemapIndex', 'robots'] },
42
+ { need: 'Env validation', names: ['validateEnv'], note: 'an env.ts schema checks process.env at boot' },
43
+ { need: 'Server tests', names: ['testRequest', 'submitForm', 'invokeActionForTest', 'loginAndGetCookies', 'createRequestHandler'] },
44
+ ],
45
+ },
46
+ {
47
+ module: '@webjsdev/core',
48
+ groups: [
49
+ { need: 'Components', names: ['WebComponent', 'html', 'css', 'nothing', 'unsafeHTML'] },
50
+ { need: 'Directives', names: ['classMap', 'styleMap', 'repeat', 'map', 'when', 'choose', 'ref', 'live', 'until', 'guard', 'keyed', 'ifDefined'] },
51
+ { need: 'State', names: ['signal', 'computed', 'effect', 'Task', 'createContext'] },
52
+ { need: 'Client navigation and data', names: ['navigate', 'refreshPage', 'revalidate', 'optimistic', 'connectWS'] },
53
+ { need: 'Streaming and assets', names: ['Suspense', 'asset', 'cspNonce'] },
54
+ ],
55
+ },
56
+ {
57
+ module: '@webjsdev/core/testing',
58
+ groups: [
59
+ { need: 'Component tests', names: ['fixture', 'ssrFixture', 'waitForUpdate', 'click'] },
60
+ ],
61
+ },
62
+ ];
63
+
64
+ /**
65
+ * The catalogue as the text `npx webjs source` and the MCP `source` tool print.
66
+ * @param {CatalogSection[]} [catalog]
67
+ * @returns {string}
68
+ */
69
+ export function featureCatalog(catalog = FEATURE_CATALOG) {
70
+ const out = [
71
+ 'What WebJs provides, by need. `npx webjs source <Name>` (or the MCP `source`',
72
+ 'tool with `export`) prints one name\'s signature and a working example. Use',
73
+ 'these before writing your own.',
74
+ ];
75
+ for (const section of catalog) {
76
+ out.push('', section.module);
77
+ for (const g of section.groups) {
78
+ out.push(` ${g.need}: ${g.names.join(', ')}${g.note ? ` (${g.note})` : ''}`);
79
+ }
80
+ }
81
+ return out.join('\n');
82
+ }
package/src/guides.js ADDED
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Pattern guides: how WebJs wants a feature built, one short page per need
3
+ * (routing, the module layout, progressive enhancement, optimistic UI, type
4
+ * safety, data flow, authentication, testing, uploads). `npx webjs source <guide>` prints one,
5
+ * as does the MCP `source` tool given the guide's name as `export`.
6
+ *
7
+ * These are not a docs corpus (the removed `docs` tool served the whole site).
8
+ * Each guide is capped (under 40 lines, held by test/guides.test.mjs) and
9
+ * ends in actions, never prose: the exports to look up next and the docs page
10
+ * for the long version. Every export a guide names is checked to exist. A
11
+ * guide is printed only when asked for by name; nothing lists them up front.
12
+ *
13
+ * Guide names are slugs no package exports (`optimistic-ui`, not
14
+ * `optimistic`), so a lookup of an export never lands on a guide.
15
+ *
16
+ * @module guides
17
+ */
18
+
19
+ /** @type {Record<string, { need: string, text: string }>} */
20
+ export const GUIDES = {
21
+ routing: {
22
+ need: 'the app/ file conventions: pages, layouts, route.ts, middleware.ts, boundaries',
23
+ text: `Guide: routing (the app/ file conventions, as in the Next.js App Router)
24
+
25
+ app/page.ts / default export (ctx) => html, server only
26
+ app/layout.ts wraps every page below it; only the root one writes <html>
27
+ app/posts/[id]/page.ts /posts/1 params.id, or const { id } = await params
28
+ app/docs/[...slug]/page.ts /docs/a/b params.slug is a string[] ([[...slug]] matches /docs too)
29
+ app/(admin)/users/page.ts /users a (group) adds no URL segment; _folders are not routed
30
+ app/api/posts/[id]/route.ts export async function GET(req: Request, { params }) {
31
+ const { id } = await params; return Response.json(...) }
32
+ GET, POST, PUT, PATCH, DELETE, each returning a Response
33
+ app/middleware.ts export default async function middleware(request: Request),
34
+ run before every page, form post and route.ts under it;
35
+ return a Response to answer, nothing to continue
36
+ app/admin/middleware.ts the same for /admin and everything under it
37
+ middleware.ts beside app/ the app-wide one, which also sees action RPC calls
38
+
39
+ Gate a section in a middleware.ts, not in a layout: a page renders before its
40
+ layouts, and a client navigation skips the layouts the browser already has.
41
+
42
+ Boundaries, the nearest one up the folders renders inside the layouts above it:
43
+ loading.ts shown while the page loads
44
+ error.ts a render that throws, given { error }
45
+ not-found.ts notFound(), or (at the root) no route matched
46
+ forbidden.ts forbidden(), a 403 unauthorized.ts unauthorized(), a 401
47
+ Throw them, or redirect('/login'), from a page, a layout or a form action.
48
+
49
+ Metadata: export const metadata = { title, description } from a page or a
50
+ layout, or export async function generateMetadata({ params }) for a dynamic one.
51
+
52
+ Look up: npx webjs source redirect, npx webjs source notFound, npx webjs source generateMetadata
53
+ Docs: https://webjs.dev/docs/routing`,
54
+ },
55
+
56
+ modules: {
57
+ need: 'how an app is laid out',
58
+ text: `Guide: modules (how a WebJs app is laid out)
59
+
60
+ One folder per feature under modules/<feature>/, and thin routes:
61
+ modules/posts/actions/create-post.server.ts 'use server', one action per file
62
+ modules/posts/queries/list-posts.server.ts server-only reads, called by pages
63
+ modules/posts/components/post-list.ts Lit components (these ship)
64
+ modules/posts/types.ts types derived from the schema
65
+ app/posts/page.ts awaits a query, renders components
66
+ db/schema.server.ts every table, one defineRelations
67
+
68
+ Each table goes in db/schema.server.ts with its row type beside it
69
+ (export type Post = typeof posts.$inferSelect), and its relations in the one
70
+ defineRelations call. modules/users/ in a fresh app is a working example of
71
+ the whole shape: read it, then replace it with the app's own features.
72
+
73
+ Next: npx webjs source progressive-enhancement (or optimistic-ui, type-safety, data-flow)
74
+ Docs: https://webjs.dev/docs/conventions`,
75
+ },
76
+
77
+ 'progressive-enhancement': {
78
+ need: 'forms that work with and without JavaScript',
79
+ text: `Guide: progressive-enhancement (forms that work before and after JavaScript)
80
+
81
+ Bind every write to a 'use server' action, never fetch() plus a click handler:
82
+ <form action=\${createPost} class="group">
83
+ <input name="title" value=\${actionData?.values?.title ?? ''}>
84
+ \${actionData?.fieldErrors?.title ? html\`<p role="alert">\${actionData.fieldErrors.title}</p>\` : nothing}
85
+ <button class="group-aria-busy:opacity-50" type="submit">Save</button>
86
+ </form>
87
+
88
+ - JavaScript off: the browser posts; a success that calls redirect() is a 303,
89
+ and a failure re-renders the page at 422 with the result on actionData.
90
+ - JavaScript on: the client router posts the same body and applies the result
91
+ in place (no reload, scroll and focus kept, the form reset on success), with
92
+ aria-busy on the form while it is in flight.
93
+ - Invalid input: return { success: false, fieldErrors, values, status: 422 }.
94
+ - Several actions on one form: <button formaction=\${deletePost}>.
95
+
96
+ Look up: npx webjs source ActionResult, npx webjs source PageProps
97
+ Docs: https://webjs.dev/docs/progressive-enhancement`,
98
+ },
99
+
100
+ 'optimistic-ui': {
101
+ need: 'show a write before the server confirms it',
102
+ text: `Guide: optimistic-ui (show a write before the server confirms it)
103
+
104
+ Inside a Lit component that owns the list, seeded by its page as a property:
105
+ pending = optimistic(this, { source: () => this.posts, update: (rows, p) => ... });
106
+ onDelete(e, id) { e.preventDefault(); this.pending.add({ id }, deletePost(new FormData(e.target))); }
107
+
108
+ - Keep the reducer pure (it runs on every render); mint a temp id in the handler.
109
+ - The entry drops when the promise settles; on success write the server's row
110
+ into the seeded property.
111
+ - Keep the form bound to the action (action=\${deletePost}) so it posts with
112
+ JavaScript off; @submit only intercepts it.
113
+ - One value (a like, a toggle): optimistic(signal, value, () => action()).
114
+ - Skip it when the result cannot be predicted from the input.
115
+
116
+ Look up: npx webjs source optimistic (a full component example)
117
+ Docs: https://webjs.dev/docs/optimistic-ui`,
118
+ },
119
+
120
+ 'type-safety': {
121
+ need: 'one set of types from the database to the page',
122
+ text: `Guide: type-safety (one set of types from the database to the page)
123
+
124
+ TypeScript must be erasable: no enum, decorator, value namespace or parameter
125
+ property. Types are stripped to whitespace and nothing is compiled.
126
+ - Rows: export type Post = typeof posts.$inferSelect beside the table;
127
+ inserts use typeof posts.$inferInsert. Never hand-write a row type.
128
+ - Actions return ActionResult<Post>, and the one function is typed for a form
129
+ binding, an RPC call from a component and a route handler alike.
130
+ - Pages: PageProps<'/posts/[id]'> types params from the route; actionData is a
131
+ failed action's result (pass its type as the second parameter).
132
+ - Components take types with import type, which is erased, so a server
133
+ module never reaches the browser.
134
+ - npx webjs types writes typed hrefs; npx webjs typecheck runs tsc.
135
+
136
+ Look up: npx webjs source ActionResult, npx webjs source PageProps
137
+ Docs: https://webjs.dev/docs/typescript`,
138
+ },
139
+
140
+ 'data-flow': {
141
+ need: 'queries, actions, RPC and route handlers',
142
+ text: `Guide: data-flow (where server code goes and how the browser reaches it)
143
+
144
+ - Reads: queries/*.server.ts with no 'use server', called from a page or a
145
+ layout, which run only on the server (as Next.js server components do).
146
+ - Drizzle 1.0's db.query takes objects, not Drizzle 0.x's eq() / asc():
147
+ db.query.posts.findMany({ where: { userId }, orderBy: { createdAt: 'desc' }, with: { author: true } })
148
+ eq(), and() and lt() go in db.select().from(posts).where(...).
149
+ - SQLite's drivers are synchronous: db.transaction((tx) => { ... }) takes a
150
+ sync callback whose queries end in .all(), .get() or .run() (never async).
151
+ - Writes: actions/*.server.ts with 'use server'. A form binds one; a component
152
+ imports and calls it, which goes over RPC (a Date or a Map survives).
153
+ - Public HTTP API: app/api/<name>/route.ts exporting GET, POST and the rest,
154
+ calling the same actions (or wrap one with route()):
155
+ // app/api/polls/[slug]/route.ts
156
+ export async function GET(_req: Request, { params }: { params: Promise<{ slug: string }> }) {
157
+ const poll = await findPoll((await params).slug);
158
+ return poll ? Response.json(poll) : Response.json({ error: 'Not found' }, { status: 404 });
159
+ }
160
+ - Components never import the database or a server-only module: data arrives
161
+ as a property from the page, or through an action call.
162
+ - After a write: revalidatePath('/posts') refreshes cached pages, redirect()
163
+ navigates, and a returned failure re-renders with its message.
164
+ - Cache a read with cache(fn, { key, ttl, tags }) and revalidateTag(tag).
165
+
166
+ Look up: npx webjs source route, npx webjs source cache, npx webjs source revalidatePath
167
+ Docs: https://webjs.dev/docs/server-actions`,
168
+ },
169
+
170
+ authentication: {
171
+ need: 'accounts, sign-in and protected pages',
172
+ text: `Guide: authentication (accounts, sign-in, protected pages)
173
+
174
+ createAuth is Auth.js's API, written once in a server-only module:
175
+ // modules/auth/auth.server.ts
176
+ export const { auth, signIn, signOut, handlers } = createAuth({
177
+ providers: [Credentials({ async authorize(c: { email: string; password: string }) {
178
+ const user = await findUserByEmail(c.email); // your own query
179
+ if (!user || !(await verifyPassword(c.password, user.passwordHash))) return null;
180
+ return { id: String(user.id), email: user.email };
181
+ } })],
182
+ });
183
+ // app/api/auth/[...path]/route.ts
184
+ export const { GET, POST } = handlers;
185
+ - Keep a password hash on users, never the password: scrypt from node:crypto
186
+ with a random salt, compared with timingSafeEqual.
187
+ - Log in: a 'use server' action awaits signIn('credentials', formData,
188
+ { redirectTo: '/' }). A wrong password throws CredentialsSignin: catch it and
189
+ return { success: false, error }. Log out: await signOut({ redirectTo: '/login' }).
190
+ - Gate pages in app/middleware.ts, not in a layout:
191
+ const PUBLIC = ['/login', '/signup', '/api/auth'];
192
+ export default async function middleware(request: Request) {
193
+ const { pathname } = new URL(request.url);
194
+ if (PUBLIC.some((p) => pathname === p || pathname.startsWith(p + '/'))) return;
195
+ if (!(await auth())?.user) return Response.redirect(new URL('/login', request.url), 303);
196
+ }
197
+ - An action a component calls directly skips app/middleware.ts: check
198
+ (await auth())?.user inside it.
199
+ - Add GitHub or Google: providers: [GitHub({ clientId, clientSecret })].
200
+ AUTH_SECRET signs the session; development and tests generate one.
201
+
202
+ Look up: npx webjs source createAuth, npx webjs source Credentials
203
+ Docs: https://webjs.dev/docs/auth`,
204
+ },
205
+
206
+ testing: {
207
+ need: 'server tests against a real database',
208
+ text: `Guide: testing (server tests against a real database)
209
+
210
+ npx webjs test runs test/**/*.test.ts (node --test, or bun test on Bun) on a
211
+ fresh migrated SQLite database, so a test never touches db/dev.db.
212
+ - Pages and forms, through the real request handler:
213
+ const app = await createRequestHandler({ appDir, dev: true });
214
+ const res = await submitForm(app.handle, '/posts/new', { title: 'Hi' });
215
+ assert.equal(res.status, 303);
216
+ - testRequest(app.handle, '/posts') for a GET or a JSON route. Carry a cookie
217
+ the app set into the next request:
218
+ const cookie = cookiesToHeader(getSetCookies(res));
219
+ await submitForm(app.handle, '/p/abc', { option: '1' }, { cookies: cookie });
220
+ loginAndGetCookies signs in with credentials and returns the same header.
221
+ - Call a query or an action directly for its logic; an action that redirects
222
+ throws, so test that one through submitForm.
223
+ - Components: npx webjs test --browser, with fixture() from @webjsdev/core/testing.
224
+
225
+ Look up: npx webjs source submitForm, npx webjs source testRequest
226
+ Docs: https://webjs.dev/docs/testing`,
227
+ },
228
+
229
+ uploads: {
230
+ need: 'attach a file to a record, store it and serve it back',
231
+ text: `Guide: uploads (attach a file to a record, store it and serve it back)
232
+
233
+ The row keeps the storage key, never the bytes, so the table gets a
234
+ fileKey: text() column beside the record's own fields.
235
+ - The form: <form action=\${attachFile}> holding <input type="file" name="file"
236
+ accept="image/png,image/jpeg,application/pdf">. A bound form gets
237
+ enctype="multipart/form-data" from the renderer (writing it is harmless).
238
+ - The 'use server' action checks the file before storing it:
239
+ const file = formData.get('file');
240
+ const fail = (msg: string) => ({ success: false as const, fieldErrors: { file: msg }, status: 422 });
241
+ if (!(file instanceof File) || file.size === 0) return fail('Choose a file.');
242
+ if (file.size > 5 * 1024 * 1024) return fail('At most 5 MB.');
243
+ if (!['image/png', 'image/jpeg', 'application/pdf'].includes(file.type)) return fail('An image or a PDF.');
244
+ const key = generateKey(file.name); // <uuid>.<ext>, safe as a path
245
+ await getFileStore().put(key, file); // streamed into .webjs/uploads
246
+ await db.update(documents).set({ fileKey: key }).where(eq(documents.id, id));
247
+ - Serving: nothing serves stored files by default, so the app adds a route:
248
+ // app/files/[key]/route.ts
249
+ export async function GET(req: Request, { params }: { params: Promise<{ key: string }> }) {
250
+ const { key } = await params;
251
+ const check = verifySignedUrl(new URL(req.url), process.env.FILE_URL_SECRET!);
252
+ if (!check.valid || check.key !== key) return new Response('Forbidden', { status: 403 });
253
+ const stored = await getFileStore().get(key);
254
+ if (!stored) return new Response('Not found', { status: 404 });
255
+ return new Response(stored.body, { headers: { 'content-type': stored.contentType,
256
+ 'x-content-type-options': 'nosniff', 'content-disposition': 'attachment' } });
257
+ }
258
+ - The link, minted in a page: base is that route's path, since the default is
259
+ the store's /uploads/<key>, which nothing serves.
260
+ signedUrl(doc.fileKey, { secret: process.env.FILE_URL_SECRET!, base: '/files/' + doc.fileKey, expiresIn: 3600 })
261
+ - file.type is what the browser claims, so keep nosniff and attachment on
262
+ anything a user sent. Set FILE_URL_SECRET in .env.
263
+
264
+ Look up: npx webjs source signedUrl, npx webjs source getFileStore, npx webjs source generateKey
265
+ Docs: https://webjs.dev/docs/file-storage`,
266
+ },
267
+ };
268
+
269
+ /**
270
+ * File convention names, as an agent looks them up, onto the guide that
271
+ * explains them. In the #1692 evals agents ran `webjs source not-found`,
272
+ * `route` and `middleware` and then read the router to learn the conventions.
273
+ * Matched case-sensitively, so a name that differs from an export only in case
274
+ * stays the export (`notfound` is the guide, `notFound` the function). None
275
+ * of these is an export: test/guides.test.mjs holds that. `route` and
276
+ * `middleware` are exports (the `route()` adapter, the middleware type), so
277
+ * they print the export with a pointer here (`conventionPointer`). The words
278
+ * for a file upload (`upload`, `attachments`) find the `uploads` guide.
279
+ * @type {Record<string, string>}
280
+ */
281
+ export const GUIDE_ALIASES = Object.fromEntries([
282
+ 'routes', 'router', 'app-router', 'file-conventions',
283
+ 'page', 'page.ts', 'layout', 'layout.ts', 'route.ts', 'route-handler', 'route-handlers',
284
+ 'middleware.ts', 'proxy.ts', 'not-found', 'not-found.ts', 'notfound', 'error', 'error.ts',
285
+ 'loading', 'loading.ts', 'forbidden.ts', 'unauthorized.ts', 'global-error', 'global-error.ts',
286
+ 'global-not-found', 'boundaries', 'dynamic-routes', 'catch-all', 'route-groups',
287
+ ].map((alias) => [alias, 'routing']).concat(
288
+ ['upload', 'file-upload', 'file-uploads', 'attachments', 'attachment'].map((alias) => [alias, 'uploads']),
289
+ ));
290
+
291
+ /** The exports whose name is also a file convention (`route.ts`, `middleware.ts`). */
292
+ const CONVENTION_EXPORTS = new Set(['route', 'middleware']);
293
+
294
+ /**
295
+ * The line an export lookup appends when the name is also a file convention,
296
+ * so `webjs source route` (the `route()` adapter) still leads to `route.ts`.
297
+ * @param {string} name
298
+ * @returns {string | null}
299
+ */
300
+ export function conventionPointer(name) {
301
+ return CONVENTION_EXPORTS.has(String(name ?? '').trim())
302
+ ? 'For the route.ts / middleware.ts file convention: npx webjs source routing'
303
+ : null;
304
+ }
305
+
306
+ /**
307
+ * One guide's text, or null when the name is not a guide. Case and spaces are
308
+ * forgiven for a guide's own name (`Optimistic UI` finds `optimistic-ui`); an
309
+ * alias (`not-found`, `middleware.ts`, `app/page.ts`) matches as written.
310
+ * @param {string} name
311
+ * @returns {string | null}
312
+ */
313
+ export function guide(name) {
314
+ const raw = String(name ?? '').trim();
315
+ const key = raw.toLowerCase().replace(/[\s_]+/g, '-');
316
+ if (Object.hasOwn(GUIDES, key)) return GUIDES[key].text;
317
+ const alias = raw.replace(/^\.?\/?app\//, '').replace(/\.(?:m?js|mts)$/, '.ts');
318
+ return Object.hasOwn(GUIDE_ALIASES, alias) ? GUIDES[GUIDE_ALIASES[alias]].text : null;
319
+ }