sparda-mcp 0.8.1 → 0.9.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 +16 -11
- package/package.json +1 -1
- package/src/commands/ubg.js +71 -0
- package/src/index.js +6 -0
- package/src/ubg/compile.js +84 -0
- package/src/ubg/express.js +258 -0
- package/src/ubg/extract.js +607 -0
- package/src/ubg/fastapi.js +59 -0
- package/src/ubg/fastapi_extract.py +724 -0
- package/src/ubg/link.js +61 -0
- package/src/ubg/nextjs.js +152 -0
- package/src/ubg/passes/consistency-domains.js +121 -0
- package/src/ubg/passes/dead-path-elimination.js +74 -0
- package/src/ubg/passes/effect-algebra.js +66 -0
- package/src/ubg/passes/state-minimization.js +86 -0
- package/src/ubg/passes/type-propagation.js +122 -0
- package/src/ubg/pipeline.js +37 -0
- package/src/ubg/schema.js +0 -0
- package/src/ubg/serialize.js +41 -0
- package/src/ubg/sql.js +252 -0
- package/src/ubg/translate.js +300 -0
package/README.md
CHANGED
|
@@ -6,23 +6,27 @@
|
|
|
6
6
|
|
|
7
7
|
<br/>
|
|
8
8
|
|
|
9
|
-
**
|
|
9
|
+
**Compile your backend into a deterministic behavior runtime.**
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
refactor a controller, but they can't create an order, fetch a real user, or see why
|
|
13
|
-
production is failing. And giving an AI real access to your API usually means: write
|
|
14
|
-
an OpenAPI spec, build an MCP server, host it, secure it, keep it in sync with every
|
|
15
|
-
route change — and pray it never `DELETE`s the wrong row. Days of glue code, per
|
|
16
|
-
project, forever drifting.
|
|
11
|
+
For twenty years, software communicated via APIs. Then AI agents arrived. The industry's response was simply to expose more endpoints (MCP). But an agent doesn't understand your application; it only sees a list of disconnected tools. That is enough for a human, but not for an autonomous machine.
|
|
17
12
|
|
|
18
|
-
SPARDA
|
|
13
|
+
SPARDA analyzes your Express, FastAPI, or Next.js application, extracts its behavior, and compiles it into a **SPARDA Behavior Graph (SBG)**.
|
|
14
|
+
|
|
15
|
+
This graph acts as a local, deterministic runtime that powers:
|
|
16
|
+
* **MCP Server Integration**: Exposing your endpoints to AI clients dynamically.
|
|
17
|
+
* **SPARDA Twin**: An executable, offline simulation clone of your backend for safe agent testing.
|
|
18
|
+
* **SPARDA Immune**: Real-time graph-based anomaly defense, latency alerts, and auto-quarantine.
|
|
19
|
+
* **SPARDA Evolution**: Statically inferred grammar mapping and in-memory workflow optimization.
|
|
20
|
+
|
|
21
|
+
All executed 100% locally, with zero runtime dependencies and zero cloud connection.
|
|
19
22
|
|
|
20
23
|
```bash
|
|
21
|
-
npx sparda-mcp init #
|
|
22
|
-
npx sparda-mcp dev #
|
|
24
|
+
npx sparda-mcp init # Scan your app, compile the UBG, inject the SPARDA Runtime
|
|
25
|
+
npx sparda-mcp dev # Connect Claude Desktop / Cursor over stdio
|
|
23
26
|
```
|
|
24
27
|
|
|
25
|
-
No OpenAPI spec. No account. No API key. No server to host.
|
|
28
|
+
No OpenAPI spec to write. No cloud account. No API key. No server to host.
|
|
29
|
+
Exposing raw APIs to AI is the old way. SPARDA compiles the whole system's behavior.
|
|
26
30
|
|
|
27
31
|
## Quickstart
|
|
28
32
|
|
|
@@ -169,6 +173,7 @@ runtime, so the guidance never goes stale.
|
|
|
169
173
|
|
|
170
174
|
## Security posture (honest)
|
|
171
175
|
- 4 runtime dependencies, exact-pinned.
|
|
176
|
+
- **Dynamic Local Key Resolution.** The generated router contains no baked secrets. It resolves authorization keys at runtime from the `SPARDA_LOCAL_KEY` environment variable or the local gitignored `.sparda/key` file, and fails closed (503) when neither is found. For custom production or staging setups, you can override this behavior by exposing `SPARDA_LOCAL_KEY` in your environment.
|
|
172
177
|
- Local key on every router call; self-reference loop protection; 30s timeouts; 8 KB output truncation.
|
|
173
178
|
- AST-positioned injection with backup and post-injection re-parse; `npx sparda-mcp remove` leaves a clean git diff.
|
|
174
179
|
- Persistence is **value-free**: SPARDA records structure (tool names, field names, fingerprints), never your payloads.
|
package/package.json
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// commands/ubg.js — compile the codebase to its Unified Behavior Graph.
|
|
2
|
+
// The UBG is the IR every future SPARDA tool reads (time-travel debuggers,
|
|
3
|
+
// deploy provers, state-machine runtimes): compile once here, write passes
|
|
4
|
+
// there. Artifact: .sparda/ubg.json — regenerable, deterministic, never
|
|
5
|
+
// committed. `--json` prints the raw graph, `--out <file>` redirects it.
|
|
6
|
+
import { compileUBG } from '../ubg/compile.js';
|
|
7
|
+
|
|
8
|
+
export async function runUbg(opts) {
|
|
9
|
+
const { report, json, outPath } = compileUBG(opts.cwd, {
|
|
10
|
+
write: true,
|
|
11
|
+
out: opts.out,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
if (opts.json) {
|
|
15
|
+
console.log(json);
|
|
16
|
+
return { report };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { counts } = report;
|
|
20
|
+
console.log(
|
|
21
|
+
`✓ UBG compiled: ${rel(outPath, opts.cwd)} — ${counts.totalNodes} nodes, ${counts.totalEdges} edges`,
|
|
22
|
+
);
|
|
23
|
+
console.log(
|
|
24
|
+
` ${report.framework} · ${report.routes} routes · ${report.tables} SQL tables`,
|
|
25
|
+
);
|
|
26
|
+
console.log(` Nodes: ${fmtCounts(counts.nodes)}`);
|
|
27
|
+
console.log(` Edges: ${fmtCounts(counts.edges)}`);
|
|
28
|
+
|
|
29
|
+
for (const p of report.passes) {
|
|
30
|
+
if (p.pass === 'DeadPathElimination' && p.removed)
|
|
31
|
+
console.log(` ${p.pass}: ${p.removed} dead node(s) removed`);
|
|
32
|
+
else if (p.pass === 'StateMinimization' && p.merged)
|
|
33
|
+
console.log(` ${p.pass}: ${p.merged} logic block(s) merged`);
|
|
34
|
+
else if (p.pass === 'TypePropagation' && (p.resolved || p.entrypointsTyped))
|
|
35
|
+
console.log(
|
|
36
|
+
` ${p.pass}: ${p.entrypointsTyped} entrypoint return schema(s), ${p.resolved} type(s) resolved`,
|
|
37
|
+
);
|
|
38
|
+
else if (p.pass === 'EffectAlgebra' && p.classified)
|
|
39
|
+
console.log(
|
|
40
|
+
` ${p.pass}: ${p.classified} effect(s) classified, ${p.observable} observable`,
|
|
41
|
+
);
|
|
42
|
+
else if (p.pass === 'ConsistencyDomains' && Object.keys(p.domains ?? {}).length) {
|
|
43
|
+
const names = Object.keys(p.domains).sort();
|
|
44
|
+
console.log(
|
|
45
|
+
` ${p.pass}: ${names.length} domain(s) — ${names.join(', ')}${p.cycles.length ? ` (⚠ FK cycle: ${p.cycles.join(', ')})` : ''}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (report.link.inferredTables.length)
|
|
51
|
+
console.log(
|
|
52
|
+
` ⚠ tables referenced in code but absent from .sql schemas: ${report.link.inferredTables.join(', ')}`,
|
|
53
|
+
);
|
|
54
|
+
if (report.skipped.length) {
|
|
55
|
+
console.log(` · ${report.skipped.length} construct(s) out of static reach:`);
|
|
56
|
+
for (const s of report.skipped.slice(0, opts.verbose ? 100 : 5))
|
|
57
|
+
console.log(` - ${s.reason}${s.file ? ` (${s.file})` : ''}`);
|
|
58
|
+
if (!opts.verbose && report.skipped.length > 5)
|
|
59
|
+
console.log(` … ${report.skipped.length - 5} more (--verbose)`);
|
|
60
|
+
}
|
|
61
|
+
return { report };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const fmtCounts = (obj) =>
|
|
65
|
+
Object.entries(obj)
|
|
66
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
67
|
+
.map(([k, v]) => `${k} ${v}`)
|
|
68
|
+
.join(' · ');
|
|
69
|
+
|
|
70
|
+
const rel = (abs, cwd) =>
|
|
71
|
+
abs.startsWith(cwd) ? abs.slice(cwd.length + 1).replaceAll('\\', '/') : abs;
|
package/src/index.js
CHANGED
|
@@ -94,6 +94,11 @@ try {
|
|
|
94
94
|
await runEvolve(opts);
|
|
95
95
|
break;
|
|
96
96
|
}
|
|
97
|
+
case 'ubg': {
|
|
98
|
+
const { runUbg } = await import('./commands/ubg.js');
|
|
99
|
+
await runUbg(opts);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
97
102
|
default:
|
|
98
103
|
console.log(`SPARDA v${VERSION} — Turn any codebase into an MCP server.
|
|
99
104
|
|
|
@@ -110,6 +115,7 @@ Usage:
|
|
|
110
115
|
npx sparda-mcp twin The living mock (--learn from the live app, then serve the ghost)
|
|
111
116
|
npx sparda-mcp grammar Which call sequences mean something (observed + hypotheses)
|
|
112
117
|
npx sparda-mcp evolve Trial hypothesis chains against the twin; survivors become suggestions
|
|
118
|
+
npx sparda-mcp ubg Compile the codebase to its Unified Behavior Graph (.sparda/ubg.json)
|
|
113
119
|
|
|
114
120
|
Flags: --yes (skip prompts) --port <n> --quiet --verbose
|
|
115
121
|
--probe (init: also run the app to discover dynamic routes the AST missed)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// ubg/compile.js — the orchestrator: codebase in, UBG out.
|
|
2
|
+
// detect → extract (framework routes + SQL schemas) → translate → link →
|
|
3
|
+
// optimize → canonicalize. 100% local, zero network, zero LLM, deterministic:
|
|
4
|
+
// the only inputs are the bytes of the source tree, the only output is the
|
|
5
|
+
// graph plus an honest report of everything the static eye could NOT see.
|
|
6
|
+
import { detectStack } from '../detect.js';
|
|
7
|
+
import { clearModuleCache } from './extract.js';
|
|
8
|
+
import { extractExpress } from './express.js';
|
|
9
|
+
import { extractNext } from './nextjs.js';
|
|
10
|
+
import { extractFastAPI } from './fastapi.js';
|
|
11
|
+
import { parseSqlSchemas } from './sql.js';
|
|
12
|
+
import { translate } from './translate.js';
|
|
13
|
+
import { linkDataFlow } from './link.js';
|
|
14
|
+
import { optimize } from './pipeline.js';
|
|
15
|
+
import { validateGraph } from './schema.js';
|
|
16
|
+
import { serializeGraph, sourceHashOf, writeGraph } from './serialize.js';
|
|
17
|
+
|
|
18
|
+
export function compileUBG(
|
|
19
|
+
cwd,
|
|
20
|
+
{ write = true, out = null, optimizePasses = true } = {},
|
|
21
|
+
) {
|
|
22
|
+
clearModuleCache(); // each compile run parses fresh — no stale-file ghosts
|
|
23
|
+
|
|
24
|
+
const stack = detectStack(cwd);
|
|
25
|
+
const extractors = {
|
|
26
|
+
express: () => extractExpress(cwd, stack.entryFile),
|
|
27
|
+
nextjs: () => extractNext(cwd, stack.entryFile),
|
|
28
|
+
fastapi: () => extractFastAPI(cwd, stack.entryFile, stack.pythonCmd),
|
|
29
|
+
};
|
|
30
|
+
if (!extractors[stack.framework]) {
|
|
31
|
+
throw Object.assign(
|
|
32
|
+
new Error(`UBG compiler has no lowering for ${stack.framework} yet.`),
|
|
33
|
+
{ code: 'USER' },
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const extracted = extractors[stack.framework]();
|
|
37
|
+
|
|
38
|
+
const sql = parseSqlSchemas(cwd);
|
|
39
|
+
|
|
40
|
+
const graph = translate({
|
|
41
|
+
framework: stack.framework,
|
|
42
|
+
routes: extracted.routes,
|
|
43
|
+
globalMiddlewares: extracted.globalMiddlewares,
|
|
44
|
+
helpers: extracted.helpers,
|
|
45
|
+
tables: sql.tables,
|
|
46
|
+
});
|
|
47
|
+
validateGraph(graph);
|
|
48
|
+
|
|
49
|
+
const linkReport = linkDataFlow(graph);
|
|
50
|
+
validateGraph(graph);
|
|
51
|
+
|
|
52
|
+
const passReports = optimizePasses ? optimize(graph) : [];
|
|
53
|
+
|
|
54
|
+
graph.meta = {
|
|
55
|
+
framework: stack.framework,
|
|
56
|
+
entry: stack.entryFile,
|
|
57
|
+
sourceHash: sourceHashOf(cwd, [
|
|
58
|
+
...extracted.scannedFiles,
|
|
59
|
+
...sql.tables.map((t) => t.sourceFile),
|
|
60
|
+
]),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const report = {
|
|
64
|
+
framework: stack.framework,
|
|
65
|
+
entry: stack.entryFile,
|
|
66
|
+
routes: extracted.routes.length,
|
|
67
|
+
tables: sql.tables.length,
|
|
68
|
+
link: linkReport,
|
|
69
|
+
passes: passReports,
|
|
70
|
+
skipped: [...extracted.skipped, ...sql.skipped],
|
|
71
|
+
counts: countGraph(graph),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const outPath = write ? writeGraph(graph, cwd, out) : null;
|
|
75
|
+
return { graph, json: serializeGraph(graph), report, outPath };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function countGraph(graph) {
|
|
79
|
+
const nodes = {};
|
|
80
|
+
const edges = {};
|
|
81
|
+
for (const n of graph.nodes.values()) nodes[n.kind] = (nodes[n.kind] ?? 0) + 1;
|
|
82
|
+
for (const e of graph.edges) edges[e.kind] = (edges[e.kind] ?? 0) + 1;
|
|
83
|
+
return { nodes, edges, totalNodes: graph.nodes.size, totalEdges: graph.edges.length };
|
|
84
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// ubg/express.js — Express codebase → route facts for the UBG translator.
|
|
2
|
+
// Richer than parser/express.js (which only needs tool signatures): here the
|
|
3
|
+
// FULL handler chain matters — every middleware between path and handler
|
|
4
|
+
// becomes a graph node, and identifier handlers are resolved to their function
|
|
5
|
+
// bodies (same file, then relative imports) so the microscope can scan them.
|
|
6
|
+
// Depth stays bounded like the v0 parser: entry file + mounted routers.
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { parseModule, resolveRelImport } from './extract.js';
|
|
10
|
+
|
|
11
|
+
const HTTP = new Set(['get', 'post', 'put', 'patch', 'delete']);
|
|
12
|
+
|
|
13
|
+
// → { routes, globalMiddlewares, helpers, skipped, scannedFiles }
|
|
14
|
+
export function extractExpress(cwd, entryFile) {
|
|
15
|
+
const routes = [];
|
|
16
|
+
const globalMiddlewares = []; // app.use(fn) — applies to every route
|
|
17
|
+
const helpers = []; // top-level functions of scanned files (dead-path candidates)
|
|
18
|
+
const skipped = [];
|
|
19
|
+
const scannedFiles = [];
|
|
20
|
+
const visited = new Set();
|
|
21
|
+
const mounts = [];
|
|
22
|
+
|
|
23
|
+
scanFile(path.resolve(cwd, entryFile), '', 0);
|
|
24
|
+
for (const m of [...mounts]) {
|
|
25
|
+
if (m.file && fs.existsSync(m.file)) scanFile(m.file, m.prefix, 1);
|
|
26
|
+
else if (!m.file)
|
|
27
|
+
skipped.push({
|
|
28
|
+
reason: `router "${m.ident}" mounted at ${m.prefix} — source file not resolved`,
|
|
29
|
+
file: m.fromFile,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
routes.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
34
|
+
return { routes, globalMiddlewares, helpers, skipped, scannedFiles };
|
|
35
|
+
|
|
36
|
+
function scanFile(absFile, prefix, depth) {
|
|
37
|
+
const key = `${absFile}::${prefix}`;
|
|
38
|
+
if (depth > 2 || visited.has(key)) return;
|
|
39
|
+
visited.add(key);
|
|
40
|
+
|
|
41
|
+
const mod = parseModule(absFile);
|
|
42
|
+
const relFile = rel(absFile);
|
|
43
|
+
if (mod.error) {
|
|
44
|
+
skipped.push({ reason: `${mod.error} in ${relFile}`, file: relFile });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
scannedFiles.push(relFile);
|
|
48
|
+
|
|
49
|
+
// every top-level function is a potential dead-path candidate
|
|
50
|
+
for (const [name, f] of mod.functions) {
|
|
51
|
+
helpers.push({ name, sourceFile: relFile, sourceLine: f.line, fn: f.node });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const appVars = new Set();
|
|
55
|
+
const routerVars = new Set();
|
|
56
|
+
for (const node of mod.ast.program.body) collectAppVars(node, appVars, routerVars);
|
|
57
|
+
|
|
58
|
+
walkStatements(mod.ast.program.body);
|
|
59
|
+
|
|
60
|
+
function walkStatements(body) {
|
|
61
|
+
for (const stmt of body) {
|
|
62
|
+
const expr =
|
|
63
|
+
stmt.type === 'ExpressionStatement' && stmt.expression.type === 'CallExpression'
|
|
64
|
+
? stmt.expression
|
|
65
|
+
: null;
|
|
66
|
+
if (!expr) continue;
|
|
67
|
+
const callee = expr.callee;
|
|
68
|
+
if (callee.type !== 'MemberExpression' || callee.property.type !== 'Identifier')
|
|
69
|
+
continue;
|
|
70
|
+
const objName = callee.object.type === 'Identifier' ? callee.object.name : null;
|
|
71
|
+
if (!objName || (!appVars.has(objName) && !routerVars.has(objName))) continue;
|
|
72
|
+
const method = callee.property.name;
|
|
73
|
+
const args = expr.arguments;
|
|
74
|
+
|
|
75
|
+
if (method === 'use') {
|
|
76
|
+
handleUse(args, stmt);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (!HTTP.has(method)) continue;
|
|
80
|
+
|
|
81
|
+
const pathArg = args[0];
|
|
82
|
+
if (!pathArg || pathArg.type !== 'StringLiteral') {
|
|
83
|
+
skipped.push({
|
|
84
|
+
reason: `dynamic path on ${method.toUpperCase()} (non-literal first arg)`,
|
|
85
|
+
file: relFile,
|
|
86
|
+
line: expr.loc?.start.line,
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const fullPath = joinPath(prefix, pathArg.value);
|
|
91
|
+
|
|
92
|
+
const chain = [];
|
|
93
|
+
for (let i = 1; i < args.length; i++) {
|
|
94
|
+
const resolved = resolveCallable(args[i], mod, absFile, relFile);
|
|
95
|
+
if (!resolved) {
|
|
96
|
+
skipped.push({
|
|
97
|
+
reason: `unresolvable handler arg #${i} on ${method.toUpperCase()} ${fullPath}`,
|
|
98
|
+
file: relFile,
|
|
99
|
+
line: args[i].loc?.start.line,
|
|
100
|
+
});
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
resolved.role = i === args.length - 1 ? 'handler' : 'middleware';
|
|
104
|
+
chain.push(resolved);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const description = (stmt.leadingComments ?? expr.leadingComments ?? [])
|
|
108
|
+
.map((c) =>
|
|
109
|
+
c.value
|
|
110
|
+
.replace(/^\*+/gm, '')
|
|
111
|
+
.replace(/^\s*\*\s?/gm, '')
|
|
112
|
+
.trim(),
|
|
113
|
+
)
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.join(' ')
|
|
116
|
+
.slice(0, 400);
|
|
117
|
+
|
|
118
|
+
routes.push({
|
|
119
|
+
method,
|
|
120
|
+
path: fullPath,
|
|
121
|
+
sourceFile: relFile,
|
|
122
|
+
sourceLine: expr.loc?.start.line ?? 0,
|
|
123
|
+
params: pathParamsOf(fullPath),
|
|
124
|
+
chain,
|
|
125
|
+
description,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function handleUse(args, stmt) {
|
|
131
|
+
if (!args.length) return;
|
|
132
|
+
// app.use('/prefix', router) → mount for second pass
|
|
133
|
+
if (args[0].type === 'StringLiteral' && args[1]?.type === 'Identifier') {
|
|
134
|
+
mounts.push({
|
|
135
|
+
prefix: joinPath(prefix, args[0].value),
|
|
136
|
+
file: mod.imports.get(args[1].name) ?? null,
|
|
137
|
+
ident: args[1].name,
|
|
138
|
+
fromFile: relFile,
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// app.use(fn) / app.use(ident) at depth 0 → global middleware
|
|
143
|
+
if (depth === 0) {
|
|
144
|
+
for (const a of args) {
|
|
145
|
+
if (a.type === 'StringLiteral') continue;
|
|
146
|
+
// express.json() & friends: framework plumbing, not app behavior
|
|
147
|
+
if (
|
|
148
|
+
a.type === 'CallExpression' &&
|
|
149
|
+
((a.callee.type === 'MemberExpression' &&
|
|
150
|
+
a.callee.object.type === 'Identifier' &&
|
|
151
|
+
a.callee.object.name === 'express') ||
|
|
152
|
+
(a.callee.type === 'Identifier' &&
|
|
153
|
+
/^(cors|helmet|morgan|compression|cookieparser|bodyparser)$/i.test(
|
|
154
|
+
a.callee.name,
|
|
155
|
+
)))
|
|
156
|
+
)
|
|
157
|
+
continue;
|
|
158
|
+
const resolved = resolveCallable(a, mod, absFile, relFile);
|
|
159
|
+
if (resolved) {
|
|
160
|
+
resolved.role = 'middleware';
|
|
161
|
+
globalMiddlewares.push(resolved);
|
|
162
|
+
} else if (a.type !== 'CallExpression') {
|
|
163
|
+
skipped.push({
|
|
164
|
+
reason: `unresolvable app.use() argument`,
|
|
165
|
+
file: relFile,
|
|
166
|
+
line: stmt.loc?.start.line,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Identifier → function body in this module, else in the module it was
|
|
175
|
+
// imported from. Inline functions pass through as-is.
|
|
176
|
+
function resolveCallable(arg, mod, absFile, relFile) {
|
|
177
|
+
if (arg.type === 'ArrowFunctionExpression' || arg.type === 'FunctionExpression') {
|
|
178
|
+
return {
|
|
179
|
+
name: arg.id?.name ?? 'anonymous',
|
|
180
|
+
sourceFile: relFile,
|
|
181
|
+
sourceLine: arg.loc?.start.line ?? 0,
|
|
182
|
+
fn: arg,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (arg.type !== 'Identifier') return null;
|
|
186
|
+
|
|
187
|
+
const local = mod.functions.get(arg.name);
|
|
188
|
+
if (local) {
|
|
189
|
+
return {
|
|
190
|
+
name: arg.name,
|
|
191
|
+
sourceFile: relFile,
|
|
192
|
+
sourceLine: local.line,
|
|
193
|
+
fn: local.node,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const importedFrom = mod.imports.get(arg.name);
|
|
197
|
+
if (importedFrom) {
|
|
198
|
+
const target = parseModule(importedFrom);
|
|
199
|
+
const fn = target.functions.get(arg.name) ?? target.functions.get('default');
|
|
200
|
+
if (fn) {
|
|
201
|
+
const fromRel = rel(importedFrom);
|
|
202
|
+
if (!scannedFiles.includes(fromRel)) {
|
|
203
|
+
scannedFiles.push(fromRel);
|
|
204
|
+
for (const [name, f] of target.functions)
|
|
205
|
+
helpers.push({
|
|
206
|
+
name,
|
|
207
|
+
sourceFile: fromRel,
|
|
208
|
+
sourceLine: f.line,
|
|
209
|
+
fn: f.node,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return { name: arg.name, sourceFile: fromRel, sourceLine: fn.line, fn: fn.node };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// known but bodyless — node exists, microscope stays blind
|
|
216
|
+
return {
|
|
217
|
+
name: arg.name,
|
|
218
|
+
sourceFile: relFile,
|
|
219
|
+
sourceLine: arg.loc?.start.line ?? 0,
|
|
220
|
+
fn: null,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function rel(abs) {
|
|
225
|
+
return path.relative(cwd, abs).split(path.sep).join('/');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function collectAppVars(node, appVars, routerVars) {
|
|
230
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration)
|
|
231
|
+
return collectAppVars(node.declaration, appVars, routerVars);
|
|
232
|
+
if (node.type !== 'VariableDeclaration') return;
|
|
233
|
+
for (const d of node.declarations) {
|
|
234
|
+
if (d.id?.type !== 'Identifier' || d.init?.type !== 'CallExpression') continue;
|
|
235
|
+
const callee = d.init.callee;
|
|
236
|
+
if (callee.type === 'Identifier' && callee.name === 'express') appVars.add(d.id.name);
|
|
237
|
+
if (callee.type === 'Identifier' && callee.name === 'Router')
|
|
238
|
+
routerVars.add(d.id.name);
|
|
239
|
+
if (callee.type === 'MemberExpression' && callee.property?.name === 'Router')
|
|
240
|
+
routerVars.add(d.id.name);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function pathParamsOf(fullPath) {
|
|
245
|
+
return [...fullPath.matchAll(/:(\w+)/g)].map((m) => ({
|
|
246
|
+
name: m[1],
|
|
247
|
+
in: 'path',
|
|
248
|
+
type: 'string',
|
|
249
|
+
required: true,
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function joinPath(prefix, p) {
|
|
254
|
+
const joined = `${prefix ?? ''}${p === '/' && prefix ? '' : p}`.replace(/\/{2,}/g, '/');
|
|
255
|
+
return joined.startsWith('/') ? joined : `/${joined}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export { resolveRelImport };
|