@vayo-hq/ast 0.1.1-beta.0
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 +62 -0
- package/dist/index.d.ts +240 -0
- package/dist/index.js +1167 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @vayo-hq/ast
|
|
2
|
+
|
|
3
|
+
Vayo's static analysis pass — the part of `vayo scan` that reads your
|
|
4
|
+
Express app's source without needing any traffic first.
|
|
5
|
+
|
|
6
|
+
Given a bootstrapped Express app (`export default app` or `export const
|
|
7
|
+
app`), `scanProject(rootDir, config)` uses `express-list-endpoints` +
|
|
8
|
+
`ts-morph` to recover, per route: the middleware chain, an auth-required
|
|
9
|
+
guess (configurable middleware-name patterns), scopes (configurable
|
|
10
|
+
scope-check function names), a folder/group guess from your route file
|
|
11
|
+
layout, and — when your project uses Zod or a plain Mongoose model — a
|
|
12
|
+
best-effort request body schema, all without executing a single request.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { scanProject, type VayoConfig } from "@vayo-hq/ast";
|
|
16
|
+
|
|
17
|
+
const config: VayoConfig = { appEntryPath: "./src/app.js" };
|
|
18
|
+
const { routes } = await scanProject(process.cwd(), config);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Optional JSDoc tags
|
|
22
|
+
|
|
23
|
+
A route's leading comment can carry explicit tags — recognized only inside
|
|
24
|
+
a comment that also has a bare `@vayo` sentinel line, so an unrelated TODO
|
|
25
|
+
or workaround comment is never misread as a declaration:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
/**
|
|
29
|
+
* Fetch a single order by ID.
|
|
30
|
+
* @vayo
|
|
31
|
+
* @group Orders
|
|
32
|
+
* @deprecated
|
|
33
|
+
* @response 200 OrderSchema
|
|
34
|
+
* @example 404 {"message": "Order not found"}
|
|
35
|
+
* @description
|
|
36
|
+
* Longer, multi-line explanation of this endpoint — the counterpart to
|
|
37
|
+
* the one-line summary above.
|
|
38
|
+
*/
|
|
39
|
+
router.get("/orders/:id", getOrder);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **`@group <name>`** (nested: `@group Admin/Users`) wins over both the
|
|
43
|
+
folder-layout guess and the URL-segment fallback, and locks the
|
|
44
|
+
endpoint's folder placement against being dragged elsewhere in the UI.
|
|
45
|
+
- **`@deprecated`** marks the endpoint deprecated independent of its API
|
|
46
|
+
version's own lifecycle, and locks it against being un-deprecated in
|
|
47
|
+
the UI.
|
|
48
|
+
- **`@response <status> <SchemaName>`** points at an existing Zod schema
|
|
49
|
+
to use for that status code's response shape.
|
|
50
|
+
- **`@example <status> <JSON>`** provides a literal example response
|
|
51
|
+
value for a status code.
|
|
52
|
+
- **`@description`** fills in a longer, multi-line description separate
|
|
53
|
+
from the one-line `summary` every route already gets for free.
|
|
54
|
+
|
|
55
|
+
Most people never call this directly —
|
|
56
|
+
[`vayo`](https://www.npmjs.com/package/vayo)'s `vayo scan` command
|
|
57
|
+
is the intended entry point. This package exists standalone for anyone
|
|
58
|
+
building custom tooling around the same static pass.
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { Project, type CallExpression } from "ts-morph";
|
|
2
|
+
import type { JSONSchema } from "@vayo-hq/types";
|
|
3
|
+
export declare const DEFAULT_AUTH_MIDDLEWARE_PATTERNS: string[];
|
|
4
|
+
export declare const DEFAULT_SCOPE_CHECK_PATTERNS: string[];
|
|
5
|
+
/** Body-validation middleware names to recognize a Zod schema argument
|
|
6
|
+
* against (docs/04-capture-engine.md Step 2 #3) — configurable the same way
|
|
7
|
+
* auth/scope patterns are, since every team names this differently. */
|
|
8
|
+
export declare const DEFAULT_VALIDATION_MIDDLEWARE_PATTERNS: string[];
|
|
9
|
+
export interface VayoConfig {
|
|
10
|
+
appEntryPath: string;
|
|
11
|
+
authMiddlewarePatterns?: string[];
|
|
12
|
+
scopeCheckPatterns?: string[];
|
|
13
|
+
validationMiddlewarePatterns?: string[];
|
|
14
|
+
redact?: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface StaticRouteResult {
|
|
17
|
+
method: string;
|
|
18
|
+
pathTemplate: string;
|
|
19
|
+
middlewareChain: string[];
|
|
20
|
+
authRequiredGuess: boolean;
|
|
21
|
+
scopes: string[];
|
|
22
|
+
group: string;
|
|
23
|
+
/** "declared" when an explicit `@group <name>` tag was found in the
|
|
24
|
+
* route's leading comment (swagger-jsdoc's own convention), "inferred"
|
|
25
|
+
* when `group` instead came from the `routes/` file-layout convention or
|
|
26
|
+
* the URL-segment fallback (docs/04-capture-engine.md Step 2 #4). Lets
|
|
27
|
+
* the UI treat a `"declared"` grouping as authoritative — e.g. refusing
|
|
28
|
+
* to let a drag-and-drop move it to a different sidebar folder, since
|
|
29
|
+
* that would silently diverge from what the code itself says. */
|
|
30
|
+
groupSource: "declared" | "inferred";
|
|
31
|
+
summary: string | null;
|
|
32
|
+
/** Longer, potentially multi-line/multi-paragraph explanation from an
|
|
33
|
+
* explicit `@description` tag — OpenAPI's own standard Operation Object
|
|
34
|
+
* distinguishes this from `summary` (a short one-liner); see
|
|
35
|
+
* `extractDescription`. `null` when absent, same as `summary`. */
|
|
36
|
+
description: string | null;
|
|
37
|
+
/** True when an explicit bare `@deprecated` tag was found in the route's
|
|
38
|
+
* leading comment (the same OpenAPI/swagger-jsdoc convention `deprecated:
|
|
39
|
+
* true` on an Operation Object mirrors) — docs/04-capture-engine.md Step
|
|
40
|
+
* 2 #4a. Always `false` when absent; there's no inferred/guessed
|
|
41
|
+
* "probably deprecated" signal the way `group` has multiple fallbacks. */
|
|
42
|
+
deprecated: boolean;
|
|
43
|
+
/** A Zod- or Mongoose-derived request body shape, when one could be
|
|
44
|
+
* traced statically — see `findRequestSchemaForRoute`/
|
|
45
|
+
* `findMongooseRequestSchemaForRoute` below. `null` (not an empty
|
|
46
|
+
* object) when nothing was found, so a merge never mistakes "found
|
|
47
|
+
* nothing" for "this endpoint genuinely takes no body." */
|
|
48
|
+
requestSchema: JSONSchema | null;
|
|
49
|
+
/** "declared" when `requestSchema` came from a Zod schema the code
|
|
50
|
+
* itself validates against, "inferred" when it came from a Mongoose
|
|
51
|
+
* model's schema instead (docs/03-data-model.md) — `null` exactly when
|
|
52
|
+
* `requestSchema` is. */
|
|
53
|
+
requestSchemaSource: "declared" | "inferred" | null;
|
|
54
|
+
/** Response body shape(s) declared via one or more `@response <status>
|
|
55
|
+
* <SchemaName>` tags in the route's leading comment, keyed by status code
|
|
56
|
+
* (docs/04-capture-engine.md Step 2 #4b) — see `extractDeclaredResponseSchemas`
|
|
57
|
+
* below. Empty when none were found; unlike `requestSchema` there's no
|
|
58
|
+
* "inferred" convention for a response shape (no Mongoose-model
|
|
59
|
+
* equivalent for "what does this endpoint send back"), so a declared
|
|
60
|
+
* response schema is always high-confidence when present at all. */
|
|
61
|
+
declaredResponseSchemas: Record<string, JSONSchema>;
|
|
62
|
+
/** Literal example response value(s) declared via one or more `@example
|
|
63
|
+
* <status> <JSON>` tags, keyed by status code — see
|
|
64
|
+
* `extractDeclaredExamples` below. Empty when none were found. */
|
|
65
|
+
declaredExamples: Record<string, unknown>;
|
|
66
|
+
}
|
|
67
|
+
export interface StaticScanResult {
|
|
68
|
+
routes: StaticRouteResult[];
|
|
69
|
+
}
|
|
70
|
+
/** Folder/mount-path convention (docs/04-capture-engine.md Step 2 #4): a
|
|
71
|
+
* route registered from a file under `routes/orders/*.ts` -> "Orders", and
|
|
72
|
+
* one under `routes/admin/users/*.ts` -> "Admin/Users" — every directory
|
|
73
|
+
* segment between `routes/` and the file itself becomes one level of the
|
|
74
|
+
* "/"-separated group path, so `@vayo-hq/db-mongo`'s `autoOrganizeFolders` can
|
|
75
|
+
* turn a nested route-file layout into real nested sidebar folders instead
|
|
76
|
+
* of flattening it to one level. Falls back to the first meaningful path
|
|
77
|
+
* segment (never nested — a URL's own segments aren't a reliable
|
|
78
|
+
* organizational signal the way a deliberate file layout is) when the entry
|
|
79
|
+
* file isn't organized that way at all (e.g. a single flat app.ts, as in
|
|
80
|
+
* the demo app). */
|
|
81
|
+
export declare function inferGroup(pathTemplate: string, sourceFilePath: string): string;
|
|
82
|
+
/** True when `registrationPath` (a literal string found at a route
|
|
83
|
+
* registration site, e.g. `router.get("/:id", ...)`) resolves to
|
|
84
|
+
* `runtimePath` (the fully-mounted path `express-list-endpoints` reports,
|
|
85
|
+
* e.g. `/api/admin/products/:id`). Exact equality covers routes registered
|
|
86
|
+
* directly on `app` with their full path repeated (today's flat demo-app
|
|
87
|
+
* style); segment-suffix equality covers `express.Router()` composition
|
|
88
|
+
* mounted via `app.use(prefix, router)` at any nesting depth — the router's
|
|
89
|
+
* own registration only ever sees its path relative to wherever it gets
|
|
90
|
+
* mounted, but `express-list-endpoints` resolves the live app's actual
|
|
91
|
+
* router stack at runtime, so the runtime path is always the source of
|
|
92
|
+
* truth here; only the static side needs to tolerate the prefix it can't
|
|
93
|
+
* see. Segment-based (not raw substring) so `/id` can never accidentally
|
|
94
|
+
* suffix-match inside `/userid`. */
|
|
95
|
+
export declare function pathSegmentsMatch(registrationPath: string, runtimePath: string): boolean;
|
|
96
|
+
/** Joins an `express.Router()`'s own relative registration path onto the
|
|
97
|
+
* literal prefix it's mounted at (`app.use(prefix, router)`) — mirrors how
|
|
98
|
+
* Express itself resolves it, including the router-root special case
|
|
99
|
+
* (`router.get("/", ...)` mounted at `prefix` resolves to exactly `prefix`,
|
|
100
|
+
* not `prefix + "/"`). Empty `prefix` (a registration found directly on
|
|
101
|
+
* `app`, never resolved through a mount call) is a no-op, so today's flat
|
|
102
|
+
* style keeps matching by plain equality with zero special-casing. */
|
|
103
|
+
export declare function joinMountedPath(prefix: string, relativePath: string): string;
|
|
104
|
+
/** Scans every file in the project for `X.use("/prefix", router)` calls and
|
|
105
|
+
* maps each mounted router's *source file* to the literal prefix it's
|
|
106
|
+
* mounted at — the piece `express-list-endpoints` can't give us (it only
|
|
107
|
+
* reports the live app's fully-*resolved* runtime paths, not which static
|
|
108
|
+
* file each route came from). One level of mounting only (a router
|
|
109
|
+
* mounted directly on `app` or on another router) — sufficient for the
|
|
110
|
+
* "one router per domain, all mounted in one place" convention this is
|
|
111
|
+
* built around; deeper indirection just falls through to the
|
|
112
|
+
* `pathSegmentsMatch` fallback below. */
|
|
113
|
+
export declare function buildMountPrefixMap(project: Project): Map<string, string>;
|
|
114
|
+
/** Extracts the middleware chain from a static route registration's own
|
|
115
|
+
* arguments (e.g. `router.post("/", requireAuth, requireRole("admin"),
|
|
116
|
+
* handler)` -> `["requireAuth", "requireRole"]`), in registration order.
|
|
117
|
+
*
|
|
118
|
+
* Needed because `express-list-endpoints` (7.x) merges endpoints that share
|
|
119
|
+
* a literal path across different HTTP methods by concatenating their
|
|
120
|
+
* `methods` arrays, but silently keeps only the *first-registered* method's
|
|
121
|
+
* own `middlewares` array for the merged entry (see its `addEndpoints`) — a
|
|
122
|
+
* public `GET "/"` registered before a protected `POST "/"` on the same
|
|
123
|
+
* path makes the protected POST inherit GET's *empty* middleware chain.
|
|
124
|
+
* Reading it statically, per registration, sidesteps that upstream bug —
|
|
125
|
+
* this is the actual mechanism the Flowmap tab and `authRequiredGuess`
|
|
126
|
+
* below depend on being correct per method, not just per path.
|
|
127
|
+
*
|
|
128
|
+
* The first argument (the path string) and the last (the handler, always
|
|
129
|
+
* anonymous in every convention this cares about) are dropped; everything
|
|
130
|
+
* between is either a plain identifier (`requireAuth`) or a middleware
|
|
131
|
+
* factory call (`requireRole("admin")`, reported by its callee name). */
|
|
132
|
+
export declare function extractMiddlewareNames(call: CallExpression): string[];
|
|
133
|
+
/** Finds a request-body Zod schema statically associated with a route
|
|
134
|
+
* registration, trying two conventions in order: (1) a validation-
|
|
135
|
+
* middleware call among the route's own arguments (`validateBody(Schema)`,
|
|
136
|
+
* `zValidator("json", Schema)` — the schema is whichever argument actually
|
|
137
|
+
* resolves, checked last-to-first since the schema is usually the final
|
|
138
|
+
* arg), then (2) `Schema.parse(req.body)`/`.safeParse(...)` called directly
|
|
139
|
+
* inside the handler body. Returns null — never throws, never guesses —
|
|
140
|
+
* when neither convention matches. */
|
|
141
|
+
export declare function findRequestSchemaForRoute(call: CallExpression, validationPatterns: string[]): JSONSchema | null;
|
|
142
|
+
/** Finds a request-body schema inferred from a Mongoose model, when the
|
|
143
|
+
* Zod path above found nothing — see the two conventions documented
|
|
144
|
+
* above. Never throws, never guesses; null when neither matches. */
|
|
145
|
+
export declare function findMongooseRequestSchemaForRoute(call: CallExpression): JSONSchema | null;
|
|
146
|
+
/** JSDoc/leading-comment above a route registration statement, if any —
|
|
147
|
+
* higher fidelity than nothing, never required (docs/04-capture-engine.md
|
|
148
|
+
* Step 2 #6). Absent for the demo app on purpose: the M1 done-when bar
|
|
149
|
+
* requires zero annotations in demo-app's own code. Strips out any
|
|
150
|
+
* `@vayo`/`@group`/`@deprecated` tag lines (see `hasVayoDocSentinel`/
|
|
151
|
+
* `extractExplicitGroup`/`extractDeprecated` below) — same
|
|
152
|
+
* description-vs-structured-tags split swagger-jsdoc itself uses, so a tag
|
|
153
|
+
* doesn't show up twice (once as this summary, once as its own structured
|
|
154
|
+
* field). */
|
|
155
|
+
export declare function extractSummary(call: CallExpression): string | null;
|
|
156
|
+
/** Explicit `@description` tag in the same leading comment `extractSummary`
|
|
157
|
+
* reads — OpenAPI's own standard Operation Object distinguishes a short
|
|
158
|
+
* `summary` (one-liner) from a longer `description` (can be multiple
|
|
159
|
+
* paragraphs, markdown-supported); Vayo's zero-annotation `summary`
|
|
160
|
+
* extraction only ever produces the former. Unlike `@group`/`@deprecated`/
|
|
161
|
+
* `@response`/`@example`, the tag's own text can continue across multiple
|
|
162
|
+
* lines — everything from right after `@description` on its own line
|
|
163
|
+
* through the line before the next recognized `@`-tag (or the comment's
|
|
164
|
+
* end) is joined together, e.g.:
|
|
165
|
+
* ```
|
|
166
|
+
* * @description
|
|
167
|
+
* * Returns the full order record including line items.
|
|
168
|
+
* * Use `include=customer` to also embed customer info.
|
|
169
|
+
* ```
|
|
170
|
+
* Only recognized inside a comment carrying the `@vayo` sentinel
|
|
171
|
+
* (`hasVayoDocSentinel`) — same reasoning as every other structured tag
|
|
172
|
+
* here: an incidental "see the @description field" sentence elsewhere
|
|
173
|
+
* shouldn't silently become this endpoint's documented description. */
|
|
174
|
+
export declare function extractDescription(call: CallExpression): string | null;
|
|
175
|
+
/** Explicit `@group <name>` tag in the same leading comment `extractSummary`
|
|
176
|
+
* reads — swagger-jsdoc's own convention (e.g. `@group Orders - order
|
|
177
|
+
* management`) for declaring a route's documentation grouping directly in
|
|
178
|
+
* code, rather than leaving it to the `routes/` file-layout convention or a
|
|
179
|
+
* guessed URL segment (docs/04-capture-engine.md Step 2 #4). A trailing
|
|
180
|
+
* `- description` some tools also allow after the name is trimmed off,
|
|
181
|
+
* since that's redundant with `extractSummary`'s own result. Nested groups
|
|
182
|
+
* follow the same "/"-separated convention `inferGroup` uses, e.g.
|
|
183
|
+
* `@group Admin/Users`. Only recognized inside a comment carrying the
|
|
184
|
+
* `@vayo` sentinel (`hasVayoDocSentinel`) — otherwise this text is treated
|
|
185
|
+
* as ordinary prose, never a declaration. */
|
|
186
|
+
export declare function extractExplicitGroup(call: CallExpression): string | null;
|
|
187
|
+
/** Bare `@deprecated` tag in the same leading comment `extractSummary`
|
|
188
|
+
* reads — the OpenAPI/swagger-jsdoc convention for marking a specific
|
|
189
|
+
* route deprecated in code, independent of the whole API *version*'s own
|
|
190
|
+
* lifecycle (`ApiVersionDoc.status`, `07-api-versioning.md`): a route can
|
|
191
|
+
* be deprecated while the version it belongs to is still fully active.
|
|
192
|
+
* Unlike `@group`, this tag carries no value of its own — its mere
|
|
193
|
+
* presence is the signal, matching how `@deprecated` is used bare in both
|
|
194
|
+
* conventions, so the whole line must be nothing else (a stray "the
|
|
195
|
+
* @deprecated tag needs cleanup" TODO elsewhere in a legitimately
|
|
196
|
+
* `@vayo`-tagged block still shouldn't flip this on). Only recognized
|
|
197
|
+
* inside a comment carrying the `@vayo` sentinel — see
|
|
198
|
+
* `extractExplicitGroup`'s own comment for why that gate exists at all. */
|
|
199
|
+
export declare function extractDeprecated(call: CallExpression): boolean;
|
|
200
|
+
/** Explicit `@response <status> <SchemaName>` tag(s) in the same leading
|
|
201
|
+
* comment `extractSummary` reads — declares a route's response body shape
|
|
202
|
+
* for a given status code by pointing at an existing Zod schema identifier
|
|
203
|
+
* (`findSchemaByName` above), the mirror of how `findRequestSchemaForRoute`
|
|
204
|
+
* traces a *request* body from a Zod schema — just declared explicitly
|
|
205
|
+
* rather than found at a fixed call-site convention, since there's no
|
|
206
|
+
* equivalent "always the last argument to X" shape for a response the way
|
|
207
|
+
* there is for request-validation middleware. One line per status code,
|
|
208
|
+
* e.g. `@response 200 OrderSchema` / `@response 404 ErrorSchema` — multiple
|
|
209
|
+
* lines declare multiple statuses. A name that doesn't resolve to a real
|
|
210
|
+
* Zod schema anywhere in scope is silently skipped for that line, never
|
|
211
|
+
* guessed. Only recognized inside a comment carrying the `@vayo` sentinel —
|
|
212
|
+
* see `extractExplicitGroup`'s comment for why that gate exists at all. */
|
|
213
|
+
export declare function extractDeclaredResponseSchemas(call: CallExpression): Record<string, JSONSchema>;
|
|
214
|
+
/** Explicit `@example <status> <JSON>` tag(s) in the same leading comment —
|
|
215
|
+
* a literal example response value for a given status code, declared
|
|
216
|
+
* directly in code rather than captured from real traffic or pinned by a
|
|
217
|
+
* team member from Try It Now (docs/03-data-model.md `vayo_examples`).
|
|
218
|
+
* One line per status code, e.g. `@example 200 {"id":"abc","total":42}` —
|
|
219
|
+
* single-line JSON only (no multi-line objects), to keep parsing this out
|
|
220
|
+
* of a free-text comment unambiguous. Invalid JSON on a matching line is
|
|
221
|
+
* silently skipped for that line, never thrown — same "never guess" bar
|
|
222
|
+
* every other tag here holds itself to. Only recognized inside a comment
|
|
223
|
+
* carrying the `@vayo` sentinel. */
|
|
224
|
+
export declare function extractDeclaredExamples(call: CallExpression): Record<string, unknown>;
|
|
225
|
+
/**
|
|
226
|
+
* Runs the static pass against a bootstrapped instance of the user's app.
|
|
227
|
+
*
|
|
228
|
+
* `config.appEntryPath` (resolved relative to `rootDir`) must be a module
|
|
229
|
+
* loadable via dynamic `import()` that exports a already-configured Express
|
|
230
|
+
* app — either `export default app` or `export const app`. It should be the
|
|
231
|
+
* user's plain app (no Vayo middleware mounted), so express-list-endpoints
|
|
232
|
+
* reports only the user's own routes/middleware — see apps/demo-app for the
|
|
233
|
+
* create-app-vs-start-server split this implies.
|
|
234
|
+
*
|
|
235
|
+
* Async (unlike the synchronous signature sketched in
|
|
236
|
+
* docs/08-packages-and-repo-structure.md) because bootstrapping a live app
|
|
237
|
+
* instance is inherently a dynamic import — there's no way to do that
|
|
238
|
+
* synchronously in an ESM-resolution world.
|
|
239
|
+
*/
|
|
240
|
+
export declare function scanProject(rootDir: string, config: VayoConfig): Promise<StaticScanResult>;
|