@webjsdev/mcp 0.1.8 → 0.1.10
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 +6 -2
- package/resources/AGENTS.md +6 -3
- package/resources/SKILL.md +1 -0
- package/resources/references/ui-kit.md +68 -0
- package/src/mcp.js +80 -46
- package/src/routes-report.js +90 -0
package/README.md
CHANGED
|
@@ -35,6 +35,10 @@ subcommand) delegates to this same server, so both routes run identical code.
|
|
|
35
35
|
and `prompts` (the recipes as guided workflows).
|
|
36
36
|
- **`source` tool**: reads the framework's own no-build source from
|
|
37
37
|
`node_modules/@webjsdev/*/src` (read-only, traversal-guarded).
|
|
38
|
+
- **`ui` tool**: the `@webjsdev/ui` kit inventory (no args) or one component's
|
|
39
|
+
helper signatures, paste-ready structural example, a11y header, and deps (pass
|
|
40
|
+
`name`). Kit-scoped (not `appDir`-scoped); shares one projector with
|
|
41
|
+
`webjsui view`.
|
|
38
42
|
|
|
39
43
|
The docs corpus is bundled into the package at `prepack`, so `npx @webjsdev/mcp`
|
|
40
44
|
is self-contained; in the monorepo it falls back to the live repo-root docs.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The webjs Model Context Protocol server: live app introspection (routes / actions / components / check) plus the framework knowledge layer (docs, recipes, source) for AI coding agents",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"./check-report": {
|
|
14
14
|
"default": "./src/check-report.js"
|
|
15
15
|
},
|
|
16
|
+
"./routes-report": {
|
|
17
|
+
"default": "./src/routes-report.js"
|
|
18
|
+
},
|
|
16
19
|
"./package.json": "./package.json"
|
|
17
20
|
},
|
|
18
21
|
"files": [
|
|
@@ -22,7 +25,8 @@
|
|
|
22
25
|
"resources"
|
|
23
26
|
],
|
|
24
27
|
"dependencies": {
|
|
25
|
-
"@webjsdev/server": "^0.8.0"
|
|
28
|
+
"@webjsdev/server": "^0.8.0",
|
|
29
|
+
"@webjsdev/ui": "^0.3.9"
|
|
26
30
|
},
|
|
27
31
|
"publishConfig": {
|
|
28
32
|
"access": "public"
|
package/resources/AGENTS.md
CHANGED
|
@@ -133,7 +133,7 @@ Self-check: `page.ts` / `layout.ts` should NOT appear in the network tab or the
|
|
|
133
133
|
|
|
134
134
|
## Framework source: where to find it
|
|
135
135
|
|
|
136
|
-
Plain JS with JSDoc lives in `node_modules/@webjsdev/` (`core/`, `server/`, `cli/`, `mcp/`, `intellisense/`, `ui/`); what you read is what runs. Starting points: SSR `@webjsdev/server/src/ssr.js`, client hydration `@webjsdev/core/src/render-client.js`, client router `@webjsdev/core/src/router-client.js`, convention rules `@webjsdev/server/src/check.js`. For UI debugging use the Playwright MCP server; for live introspection the scaffold wires the read-only `@webjsdev/mcp` server (`npx @webjsdev/mcp`, also reachable as `webjs mcp`): `list_routes`, `list_actions`, `list_components`, `check`, plus a knowledge layer (docs / recipes / framework source).
|
|
136
|
+
Plain JS with JSDoc lives in `node_modules/@webjsdev/` (`core/`, `server/`, `cli/`, `mcp/`, `intellisense/`, `ui/`); what you read is what runs. Starting points: SSR `@webjsdev/server/src/ssr.js`, client hydration `@webjsdev/core/src/render-client.js`, client router `@webjsdev/core/src/router-client.js`, convention rules `@webjsdev/server/src/check.js`. For UI debugging use the Playwright MCP server; for live introspection the scaffold wires the read-only `@webjsdev/mcp` server (`npx @webjsdev/mcp`, also reachable as `webjs mcp`): `list_routes`, `list_actions`, `list_components`, `check`, `ui` (the `@webjsdev/ui` kit inventory + a component's helpers / paste-ready example / a11y header), plus a knowledge layer (docs / recipes / framework source).
|
|
137
137
|
|
|
138
138
|
---
|
|
139
139
|
|
|
@@ -462,9 +462,12 @@ webjs dev [--port N] [--no-hot] # dev server with live reload (node --watch o
|
|
|
462
462
|
webjs start [--port N] # prod server; source IS the runtime, plain HTTP/1.1 (reverse-proxy for TLS + HTTP/2). Runs webjs.start.before first (#550)
|
|
463
463
|
webjs test [--server] [--browser] [--watch]
|
|
464
464
|
webjs check [--rules] [--json] # correctness validator (report-only, no autofix); --json for an agent loop
|
|
465
|
-
webjs
|
|
466
|
-
webjs
|
|
465
|
+
webjs routes [--json] [--table] [--no-headers] # print the route table (path / owner file / methods, #975). Default tree; --json is byte-identical to the MCP list_routes tool; --no-headers drops the --table header for piping
|
|
466
|
+
webjs mcp # read-only MCP: routes, actions (RPC hashes), components, check, ui kit
|
|
467
|
+
webjs doctor [--json] [--strict] # project-health checklist (incl. a framework-resolve check that warns when @webjsdev/core can't be resolved from the app dir, the fresh-worktree-without-node_modules trap #954; a page/layout elision advisory); non-zero exit on a hard fail. --json emits `{ results, summary }` (results is the DoctorResult[], each carrying a stable code); --strict also fails the exit on warnings (#975)
|
|
467
468
|
webjs types # generate .webjs/routes.d.ts (typed Route union + per-route params, #258)
|
|
469
|
+
webjs version # print the installed @webjsdev/cli version (also: webjs --version / -v, #975)
|
|
470
|
+
webjs help [command] # full usage banner, or per-command usage + Options + Examples (e.g. webjs help routes, #975). Flag forms: webjs --help / -h (banner), webjs <command> --help / -h (that command). typecheck/db/ui --help forward to their wrapped tool; an unknown topic exits 1
|
|
468
471
|
webjs typecheck [tsc args...] # the project's own tsc --noEmit
|
|
469
472
|
webjs create <name> [--template api|saas]
|
|
470
473
|
webjs db <generate|migrate|push|studio|seed> # wraps drizzle-kit (+ runs db/seed.server.ts)
|
package/resources/SKILL.md
CHANGED
|
@@ -44,6 +44,7 @@ Classify the task first, then load the smallest useful reference set. Each refer
|
|
|
44
44
|
| Tailwind, light-DOM tag-prefix rule, tokens, fixed headers, no-reflow layout | `references/styling.md` |
|
|
45
45
|
| Client router, prefetch, frames, view transitions, Suspense streaming | `references/client-router-and-streaming.md` |
|
|
46
46
|
| Optimistic UI for a user-facing mutation | `references/optimistic-ui.md` |
|
|
47
|
+
| The `@webjsdev/ui` component kit (a `components.json` is present): class helpers, tokens, `add` / `view`, the MCP `ui` tool | `references/ui-kit.md` |
|
|
47
48
|
| TypeScript at runtime, erasable syntax, full-stack types | `references/typescript.md` |
|
|
48
49
|
| Unit, browser, e2e tests, the `handle()` harness, Bun parity | `references/testing.md` |
|
|
49
50
|
| Auth, caching, env vars, rate limit, file storage, the `webjs` config block | `references/built-ins.md` |
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# The `@webjsdev/ui` component kit
|
|
2
|
+
|
|
3
|
+
Load this when the app has a `components.json` (it uses `@webjsdev/ui`, the
|
|
4
|
+
shadcn-style kit for WebJs). The source is copied into your repo (`components/ui/`),
|
|
5
|
+
so you own and edit it. Two tiers:
|
|
6
|
+
|
|
7
|
+
- **Tier 1, class helpers (23 components).** Pure functions returning Tailwind
|
|
8
|
+
class strings (`buttonClass({ variant })`, `cardClass()`), composed with
|
|
9
|
+
whatever native element you write. Reach for these instead of expanding
|
|
10
|
+
Tailwind by hand: the call site is a fraction of the tokens and the class list
|
|
11
|
+
cannot drift.
|
|
12
|
+
- **Tier 2, stateful custom elements (9 components).** `<ui-dialog>`, `<ui-tabs>`,
|
|
13
|
+
`<ui-dropdown-menu>`, and friends own their ARIA (focus trap, roving tabindex,
|
|
14
|
+
`aria-controls` / `inert`, live regions). Write the tag and the accessible
|
|
15
|
+
behaviour comes with it. Do NOT hand-roll these; the wiring is easy to get
|
|
16
|
+
subtly wrong.
|
|
17
|
+
|
|
18
|
+
## The workflow: query for the structure, do not guess it
|
|
19
|
+
|
|
20
|
+
`add` copies a Tier-1 component's class helpers plus a lean header (what each
|
|
21
|
+
helper is, the accessibility obligations) and a one-line pointer. It does NOT
|
|
22
|
+
copy the worked structural example, because that example is guidance you consume
|
|
23
|
+
once while composing, not code that should sit in your repo. Get the full
|
|
24
|
+
paste-ready structure on demand:
|
|
25
|
+
|
|
26
|
+
- **MCP `ui` tool** (preferred when available): call `ui` with no args for the
|
|
27
|
+
kit inventory (each component's tier, helper signatures, npm deps); pass
|
|
28
|
+
`{ name: "accordion" }` for one component's helper signatures, the paste-ready
|
|
29
|
+
structural example, the accessibility header, and deps.
|
|
30
|
+
- **CLI**: `webjs ui list` (inventory), `webjs ui view <name>` (the projected
|
|
31
|
+
view plus the full source). Same data as the MCP tool (one shared projector).
|
|
32
|
+
|
|
33
|
+
So the loop is: `add` the component, then query `ui <name>` (MCP) or
|
|
34
|
+
`webjs ui view <name>` for the accessible structure, paste it, and fill it in.
|
|
35
|
+
|
|
36
|
+
## Setup and resolution
|
|
37
|
+
|
|
38
|
+
- `webjs ui init` writes `components.json`, `lib/utils.ts`, and the CSS design
|
|
39
|
+
tokens the helpers render against (`--background`, `--foreground`,
|
|
40
|
+
`--destructive`, ...). It HARD-FAILS if the tokens cannot be written, so a
|
|
41
|
+
clean exit means the kit is styled. `add` self-heals the tokens if they go
|
|
42
|
+
missing.
|
|
43
|
+
- Resolution is LOCAL-FIRST: `init` / `add` / `list` / `view` read the registry
|
|
44
|
+
that ships inside the installed `@webjsdev/ui`, with no network. This pins you
|
|
45
|
+
to the installed version; run `webjs ui diff` to see where your local copies
|
|
46
|
+
drift from the upstream (that command alone compares against the live registry).
|
|
47
|
+
|
|
48
|
+
## Inventory (run `webjs ui list` or the MCP `ui` tool for the authoritative, current set)
|
|
49
|
+
|
|
50
|
+
**Tier 1 (class helpers):** accordion, alert, aspect-ratio, avatar, badge,
|
|
51
|
+
breadcrumb, button, card, checkbox, collapsible, input, kbd, label,
|
|
52
|
+
native-select, pagination, popover, progress, radio-group, separator, skeleton,
|
|
53
|
+
switch, table, textarea.
|
|
54
|
+
|
|
55
|
+
**Tier 2 (custom elements, own their ARIA):** alert-dialog, dialog,
|
|
56
|
+
dropdown-menu, hover-card, sonner, tabs, tooltip, plus toggle and toggle-group
|
|
57
|
+
(these two register an element AND export a `*Class` helper).
|
|
58
|
+
|
|
59
|
+
## Idioms
|
|
60
|
+
|
|
61
|
+
- A helper is a function, so compose it: `class=${buttonClass({ variant: 'outline' })}`.
|
|
62
|
+
The unquoted `${...}` is a normal `html` attribute hole.
|
|
63
|
+
- Tier-1 helpers assume the design tokens exist; if a component paints unstyled,
|
|
64
|
+
the tokens are missing (re-run `webjs ui init` or let `add` self-heal them).
|
|
65
|
+
- Custom elements are display-only-safe at SSR and hydrate in the browser, the
|
|
66
|
+
standard WebJs component model (`references/components.md`).
|
|
67
|
+
|
|
68
|
+
Full per-package reference lives in the installed `@webjsdev/ui/AGENTS.md`.
|
package/src/mcp.js
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
getPrompt,
|
|
34
34
|
} from './mcp-docs.js';
|
|
35
35
|
import { resolveFrameworkRoots, runSourceTool } from './mcp-source.js';
|
|
36
|
+
import { projectRoutes } from './routes-report.js';
|
|
36
37
|
|
|
37
38
|
const PROTOCOL_VERSION = '2024-11-05';
|
|
38
39
|
|
|
@@ -80,6 +81,18 @@ const DOCS_SCHEMA = {
|
|
|
80
81
|
required: [],
|
|
81
82
|
};
|
|
82
83
|
|
|
84
|
+
/** `ui` lists the @webjsdev/ui kit, or details one component by `name`. */
|
|
85
|
+
const UI_SCHEMA = {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
name: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'A component name (e.g. button, accordion, dialog). Omit for the full kit inventory.',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
required: [],
|
|
94
|
+
};
|
|
95
|
+
|
|
83
96
|
/** `source` reads the framework source: a `path` to read, a `query` to grep, or a `package` to list. */
|
|
84
97
|
const SOURCE_SCHEMA = {
|
|
85
98
|
type: 'object',
|
|
@@ -126,6 +139,12 @@ const TOOL_DEFS = [
|
|
|
126
139
|
'Read the FRAMEWORK authored source (webjs is buildless: node_modules/@webjsdev/*/src is the JSDoc source, run directly server-side; only the core browser bundle is built into dist/, which this skips). Pass `path` to read a file (e.g. server/src/ssr.js), `query` to grep the @webjsdev/* src trees, or no args to list the packages + entry points. Use when the docs do not answer something. Read-only.',
|
|
127
140
|
inputSchema: SOURCE_SCHEMA,
|
|
128
141
|
},
|
|
142
|
+
{
|
|
143
|
+
name: 'ui',
|
|
144
|
+
description:
|
|
145
|
+
'The @webjsdev/ui component kit (shadcn-style, copied into your repo). No args returns the inventory (each component: tier, helper signatures, npm deps); pass `name` for one component: helper signatures, the paste-ready structural example, the a11y/description header, and deps. Use before hand-writing UI so you reach for a helper (e.g. buttonClass()) and the accessible structure instead of expanding Tailwind blind. Reads the kit, not your app. Read-only.',
|
|
146
|
+
inputSchema: UI_SCHEMA,
|
|
147
|
+
},
|
|
129
148
|
{
|
|
130
149
|
name: 'list_routes',
|
|
131
150
|
description:
|
|
@@ -274,22 +293,6 @@ export function extractActionConfig(src) {
|
|
|
274
293
|
};
|
|
275
294
|
}
|
|
276
295
|
|
|
277
|
-
/**
|
|
278
|
-
* The literal URL path for a page/api directory: `blog/[slug]` -> `/blog/[slug]`,
|
|
279
|
-
* the root `.` -> `/`. Route groups `(group)` and `_private` segments drop, the
|
|
280
|
-
* same normalization `buildRouteTable` uses for matching.
|
|
281
|
-
*
|
|
282
|
-
* @param {string} routeDir POSIX-style, `.` for the app root.
|
|
283
|
-
* @returns {string}
|
|
284
|
-
*/
|
|
285
|
-
function routePathFromDir(routeDir) {
|
|
286
|
-
if (!routeDir || routeDir === '.') return '/';
|
|
287
|
-
const segs = routeDir
|
|
288
|
-
.split('/')
|
|
289
|
-
.filter((s) => !(s.startsWith('(') && s.endsWith(')')) && !s.startsWith('_'));
|
|
290
|
-
return segs.length ? '/' + segs.join('/') : '/';
|
|
291
|
-
}
|
|
292
|
-
|
|
293
296
|
/**
|
|
294
297
|
* The tool runners. Each is async, takes `(appDir)`, and returns a plain
|
|
295
298
|
* JSON-serialisable projection of an existing server data function. All are
|
|
@@ -311,34 +314,10 @@ export function makeToolRunners(deps) {
|
|
|
311
314
|
return {
|
|
312
315
|
async list_routes(appDir) {
|
|
313
316
|
const table = await buildRouteTable(appDir);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
path: routePathFromDir(r.routeDir),
|
|
319
|
-
file: relative(appDir, r.file),
|
|
320
|
-
};
|
|
321
|
-
if (r.paramNames && r.paramNames.length) {
|
|
322
|
-
out.dynamic = true;
|
|
323
|
-
out.params = r.paramNames;
|
|
324
|
-
}
|
|
325
|
-
return out;
|
|
326
|
-
}),
|
|
327
|
-
);
|
|
328
|
-
const apis = await Promise.all(
|
|
329
|
-
table.apis.map(async (r) => {
|
|
330
|
-
let methods = [];
|
|
331
|
-
try {
|
|
332
|
-
methods = extractRouteMethods(await readFile(r.file, 'utf8'));
|
|
333
|
-
} catch {}
|
|
334
|
-
return {
|
|
335
|
-
path: routePathFromDir(r.routeDir),
|
|
336
|
-
file: relative(appDir, r.file),
|
|
337
|
-
methods,
|
|
338
|
-
};
|
|
339
|
-
}),
|
|
340
|
-
);
|
|
341
|
-
return { pages, apis };
|
|
317
|
+
// The projection lives in `routes-report.js` so `list_routes` and the CLI
|
|
318
|
+
// `webjs routes --json` stay byte-identical (#975), the same split as
|
|
319
|
+
// `check-report.js` for the `check` tool.
|
|
320
|
+
return projectRoutes(table, { appDir, readFile, extractRouteMethods });
|
|
342
321
|
},
|
|
343
322
|
|
|
344
323
|
async list_actions(appDir) {
|
|
@@ -397,6 +376,50 @@ export function makeToolRunners(deps) {
|
|
|
397
376
|
};
|
|
398
377
|
}
|
|
399
378
|
|
|
379
|
+
/**
|
|
380
|
+
* Run the `ui` tool: the @webjsdev/ui kit inventory (no args) or one component's
|
|
381
|
+
* projection (helper signatures + paste-ready example + a11y/description header
|
|
382
|
+
* + deps). Projects the SHARED `@webjsdev/ui/registry/extract` leaf, the same
|
|
383
|
+
* one `webjsui view` renders, so the CLI and MCP cannot drift (#983, the #979
|
|
384
|
+
* shared-projector pattern). `uiDeps` is injected so tests need no real package.
|
|
385
|
+
*
|
|
386
|
+
* @param {{ uiInventory: Function, uiComponent: Function }} uiDeps
|
|
387
|
+
* @param {{ name?: string }} args
|
|
388
|
+
*/
|
|
389
|
+
/**
|
|
390
|
+
* Resolve the `ui` tool's deps from the shared `@webjsdev/ui/registry/extract`
|
|
391
|
+
* leaf, GUARDED (#983): if `@webjsdev/ui` is absent or too old to carry the
|
|
392
|
+
* `./registry/extract` subpath (a cross-package version skew), only the `ui`
|
|
393
|
+
* tool degrades (its deps throw a clear "unavailable" error, surfaced as an
|
|
394
|
+
* isError tool result) while the rest of the server keeps working. An unguarded
|
|
395
|
+
* top-level import would instead reject during bootstrap and sink the whole
|
|
396
|
+
* server. `importer` is injected so the failure path is unit-testable.
|
|
397
|
+
*
|
|
398
|
+
* @param {() => Promise<{ uiInventory: Function, uiComponent: Function }>} importer
|
|
399
|
+
* @returns {Promise<{ uiInventory: Function, uiComponent: Function }>}
|
|
400
|
+
*/
|
|
401
|
+
export async function loadUiDeps(importer) {
|
|
402
|
+
try {
|
|
403
|
+
const ui = await importer();
|
|
404
|
+
return { uiInventory: ui.uiInventory, uiComponent: ui.uiComponent };
|
|
405
|
+
} catch {
|
|
406
|
+
const unavailable = () => {
|
|
407
|
+
throw new Error('@webjsdev/ui is not available (install/upgrade @webjsdev/ui to use the ui tool)');
|
|
408
|
+
};
|
|
409
|
+
return { uiInventory: unavailable, uiComponent: unavailable };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function runUiTool(uiDeps, args) {
|
|
414
|
+
const name = typeof args.name === 'string' && args.name ? args.name : null;
|
|
415
|
+
if (!name) return { inventory: uiDeps.uiInventory() };
|
|
416
|
+
const component = uiDeps.uiComponent(name);
|
|
417
|
+
if (!component) {
|
|
418
|
+
throw new Error(`Unknown component "${String(name)}". Call ui with no args for the inventory.`);
|
|
419
|
+
}
|
|
420
|
+
return component;
|
|
421
|
+
}
|
|
422
|
+
|
|
400
423
|
/**
|
|
401
424
|
* A JSON-RPC 2.0 result frame.
|
|
402
425
|
* @param {string|number|null} id
|
|
@@ -493,6 +516,13 @@ export async function runMcpServer(opts) {
|
|
|
493
516
|
};
|
|
494
517
|
}
|
|
495
518
|
|
|
519
|
+
// The `ui` tool (#983): the @webjsdev/ui kit inventory / per-component
|
|
520
|
+
// projection, read from the shared `@webjsdev/ui/registry/extract` leaf.
|
|
521
|
+
// Injectable for tests; otherwise resolved (guarded) from the installed
|
|
522
|
+
// package. See {@link loadUiDeps} for why the import is guarded.
|
|
523
|
+
let uiDeps = opts.uiDeps;
|
|
524
|
+
if (!uiDeps) uiDeps = await loadUiDeps(() => import('@webjsdev/ui/registry/extract'));
|
|
525
|
+
|
|
496
526
|
/** Write one JSON-RPC frame as a single line to stdout. */
|
|
497
527
|
const send = (frame) => {
|
|
498
528
|
stdout.write(JSON.stringify(frame) + '\n');
|
|
@@ -565,7 +595,9 @@ export async function runMcpServer(opts) {
|
|
|
565
595
|
const args = params.arguments || {};
|
|
566
596
|
// The knowledge tools route to the docs / source layer; they return text.
|
|
567
597
|
const isKnowledgeTool = name === 'init' || name === 'docs' || name === 'source';
|
|
568
|
-
|
|
598
|
+
// `ui` is kit-scoped (not app-scoped), so it is dispatched separately.
|
|
599
|
+
const isUiTool = name === 'ui';
|
|
600
|
+
if (!isKnowledgeTool && !isUiTool && !runners[name]) {
|
|
569
601
|
return rpcError(id, -32602, `Unknown tool: ${String(name)}`);
|
|
570
602
|
}
|
|
571
603
|
const appDir = typeof args.appDir === 'string' && args.appDir ? args.appDir : cwd;
|
|
@@ -576,7 +608,9 @@ export async function runMcpServer(opts) {
|
|
|
576
608
|
: name === 'docs'
|
|
577
609
|
? await searchDocs(docsDeps, args)
|
|
578
610
|
: await runSourceTool(sourceDeps, args)
|
|
579
|
-
:
|
|
611
|
+
: isUiTool
|
|
612
|
+
? runUiTool(uiDeps, args)
|
|
613
|
+
: await runners[name](appDir);
|
|
580
614
|
// Knowledge tools return a markdown string; introspection tools return
|
|
581
615
|
// a JSON-serialisable object.
|
|
582
616
|
const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared JSON projector for the app route table (#975).
|
|
3
|
+
*
|
|
4
|
+
* `webjs routes --json` and the `webjs mcp` server's `list_routes` tool BOTH
|
|
5
|
+
* return the identical shape, so the projection lives here once (the same
|
|
6
|
+
* pattern as `check-report.js` for `check --json` / the MCP `check` tool). The
|
|
7
|
+
* input is the raw `RouteTable` from `buildRouteTable(appDir)` (the ONE route
|
|
8
|
+
* walker, reused, never re-implemented); the output is a stable, agent-friendly
|
|
9
|
+
* `{ pages, apis }` shape.
|
|
10
|
+
*
|
|
11
|
+
* This module is a LEAF: it imports only `node:path` and receives the two
|
|
12
|
+
* effectful dependencies it needs (`readFile` to read a `route.{js,ts}` file's
|
|
13
|
+
* source, `extractRouteMethods` to lexically pull its HTTP-verb exports) as
|
|
14
|
+
* injected arguments, the same dependency-injection style the MCP tool runners
|
|
15
|
+
* already use. That keeps it free of any `@webjsdev/mcp` main-entry import (so
|
|
16
|
+
* the CLI can pull just this projector without loading the MCP server) and free
|
|
17
|
+
* of a cycle with `mcp.js`.
|
|
18
|
+
*
|
|
19
|
+
* @module routes-report
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { relative } from 'node:path';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The literal URL path for a page/api directory: `blog/[slug]` -> `/blog/[slug]`,
|
|
26
|
+
* the root `.` -> `/`. Route groups `(group)` and `_private` segments drop, the
|
|
27
|
+
* same normalization `buildRouteTable` uses for matching.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} routeDir POSIX-style, `.` for the app root.
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
export function routePathFromDir(routeDir) {
|
|
33
|
+
if (!routeDir || routeDir === '.') return '/';
|
|
34
|
+
const segs = routeDir
|
|
35
|
+
.split('/')
|
|
36
|
+
.filter((s) => !(s.startsWith('(') && s.endsWith(')')) && !s.startsWith('_'));
|
|
37
|
+
return segs.length ? '/' + segs.join('/') : '/';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {{ path: string, file: string, dynamic?: boolean, params?: string[] }} PageRouteReport
|
|
42
|
+
* @typedef {{ path: string, file: string, methods: string[] }} ApiRouteReport
|
|
43
|
+
* @typedef {{ pages: PageRouteReport[], apis: ApiRouteReport[] }} RoutesReport
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Project a `RouteTable` into the structured `{ pages, apis }` report shared by
|
|
48
|
+
* `webjs routes --json` and the MCP `list_routes` tool. Pure over its inputs
|
|
49
|
+
* (given the same table + deps it produces the same output); the only I/O is
|
|
50
|
+
* the injected `readFile` reading each `route.{js,ts}` source to extract its
|
|
51
|
+
* methods. A `route.{js,ts}` that cannot be read degrades to an empty method
|
|
52
|
+
* list rather than throwing, so one unreadable file never sinks the whole
|
|
53
|
+
* report.
|
|
54
|
+
*
|
|
55
|
+
* @param {{ pages: any[], apis: any[] }} table the `buildRouteTable(appDir)` result
|
|
56
|
+
* @param {{
|
|
57
|
+
* appDir: string,
|
|
58
|
+
* readFile: (path: string, enc: string) => Promise<string>,
|
|
59
|
+
* extractRouteMethods: (src: string) => string[],
|
|
60
|
+
* }} deps
|
|
61
|
+
* @returns {Promise<RoutesReport>}
|
|
62
|
+
*/
|
|
63
|
+
export async function projectRoutes(table, { appDir, readFile, extractRouteMethods }) {
|
|
64
|
+
const pages = table.pages.map((r) => {
|
|
65
|
+
/** @type {PageRouteReport} */
|
|
66
|
+
const out = {
|
|
67
|
+
path: routePathFromDir(r.routeDir),
|
|
68
|
+
file: relative(appDir, r.file),
|
|
69
|
+
};
|
|
70
|
+
if (r.paramNames && r.paramNames.length) {
|
|
71
|
+
out.dynamic = true;
|
|
72
|
+
out.params = r.paramNames;
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
});
|
|
76
|
+
const apis = await Promise.all(
|
|
77
|
+
table.apis.map(async (r) => {
|
|
78
|
+
let methods = [];
|
|
79
|
+
try {
|
|
80
|
+
methods = extractRouteMethods(await readFile(r.file, 'utf8'));
|
|
81
|
+
} catch {}
|
|
82
|
+
return {
|
|
83
|
+
path: routePathFromDir(r.routeDir),
|
|
84
|
+
file: relative(appDir, r.file),
|
|
85
|
+
methods,
|
|
86
|
+
};
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
return { pages, apis };
|
|
90
|
+
}
|