backend-skeleton 1.7.0 → 1.7.1
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 -3
- package/package.json +1 -1
- package/scanners/adapters/javascript-express.mjs +133 -83
package/README.md
CHANGED
|
@@ -602,9 +602,10 @@ string for anything missing.
|
|
|
602
602
|
(entities come from `@Entity`/`@PrimaryGeneratedColumn`); no operation extraction — plain Express
|
|
603
603
|
has no operationId concept, so pass `--openapi-file` for a contract. See
|
|
604
604
|
`D-typescript-express-provider` in `DECISIONS.md`.
|
|
605
|
-
- `javascript-express` — plain-JavaScript ESM
|
|
606
|
-
including `serverless-http`/Lambda deployments. **Scanner only** — routes
|
|
607
|
-
paths are resolved through a full mount-graph walk
|
|
605
|
+
- `javascript-express` — plain-JavaScript Express, both ESM and CommonJS, with **no ORM** (raw
|
|
606
|
+
`mysql2`/`mariadb`), including `serverless-http`/Lambda deployments. **Scanner only** — routes
|
|
607
|
+
and their real absolute paths are resolved through a full mount-graph walk (including direct
|
|
608
|
+
CommonJS `require()` mounts and `module.exports`), but every capability is honestly `false`:
|
|
608
609
|
there is no codegen provider, because raw SQL string literals carry no trustworthy
|
|
609
610
|
table/primary-key/column-allow-list metadata. See `D-javascript-express-adapter` in `DECISIONS.md`
|
|
610
611
|
for the measured reasoning.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backend-skeleton",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic gate layer for AI-assisted backend changes -- blocks brownfield collisions and contract/handle drift via disk-hash checks before code ships. Scaffolding codegen included (Java/Spring, Python/FastAPI, TypeScript/Express).",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// G6 (D-javascript-express-adapter): the fourth first-class scanner adapter -- plain-JavaScript
|
|
2
|
-
// ESM
|
|
2
|
+
// Express (ESM or CommonJS), with NO ORM and NO TypeScript anywhere. Sibling of
|
|
3
|
+
// `typescript-express.mjs` (G5),
|
|
3
4
|
// not a generalization of it: they share the low-level Express primitives (`_express-shared.mjs`)
|
|
4
5
|
// and deliberately do NOT share endpoint/mount-tree extraction, because a plain-JS app's routing
|
|
5
6
|
// is written differently in three ways that each break G5's own regexes (see below).
|
|
@@ -9,7 +10,7 @@
|
|
|
9
10
|
// `typescript-express`'s detect() greps `-g '*.ts'` only, so a repo with zero `.ts` files fell all
|
|
10
11
|
// the way through to the low-confidence `generic-grep` fallback.
|
|
11
12
|
//
|
|
12
|
-
//
|
|
13
|
+
// FOUR real divergences from G5, each grounded in what plain-JS Express code actually looks like,
|
|
13
14
|
// not anticipated defensively:
|
|
14
15
|
// 1. `import express from 'express'; const r = express.Router()` is the dominant plain-JS idiom.
|
|
15
16
|
// G5's detect() requires a NAMED `import { Router } from 'express'`, which a repo using only
|
|
@@ -22,6 +23,10 @@
|
|
|
22
23
|
// application to a locally-declared Router (`app.use('/api', route)`) -- no import involved,
|
|
23
24
|
// so G5's file-to-file edge model cannot represent it and would silently drop `/api` from
|
|
24
25
|
// every route below it. Mount-tree nodes here are (file, variable) pairs, not files.
|
|
26
|
+
// 4. Express predates Node ESM by years, so real applications commonly use
|
|
27
|
+
// `require('express').Router()`, direct `router.use('/api', require('./api'))` mounts, and
|
|
28
|
+
// `module.exports = router`. The CommonJS path is first-class rather than falling through to
|
|
29
|
+
// generic-grep; `rtfeldman/node-express-realworld-example-app` exposed this gap with 19 routes.
|
|
25
30
|
//
|
|
26
31
|
// **`codegen.handles` is false, and that is the whole shipped scope.** There is no
|
|
27
32
|
// `handles/providers/javascript-express/`. See D-javascript-express-adapter's EXCLUDED section in
|
|
@@ -46,55 +51,63 @@ import {
|
|
|
46
51
|
expressDiagnostics,
|
|
47
52
|
} from './_express-shared.mjs';
|
|
48
53
|
|
|
49
|
-
// detect()'s ripgrep candidate filter
|
|
50
|
-
// import statement: rg matches line by line, so a clause spread over several
|
|
51
|
-
// by a fuller pattern.
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
+
// detect()'s ripgrep candidate filter. The ESM half is deliberately just the `from 'express'`
|
|
55
|
+
// tail, not a whole import statement: rg matches line by line, so a clause spread over several
|
|
56
|
+
// lines would be missed by a fuller pattern. The CommonJS half recognizes the module load itself,
|
|
57
|
+
// whether it is assigned (`const express = require(...)`) or immediately dereferenced
|
|
58
|
+
// (`require('express').Router()`). This is only a cheap pre-filter; the masked re-read plus the
|
|
59
|
+
// file's real Node module kind is the gate, so a false positive here costs nothing.
|
|
60
|
+
const EXPRESS_MODULE_SRC = "(?:from\\s*['\"]express['\"]|require\\s*\\(\\s*['\"]express['\"]\\s*\\))";
|
|
54
61
|
const FROM_EXPRESS_RE = /\bfrom\s*['"]express['"]/g;
|
|
62
|
+
const REQUIRE_EXPRESS_RE = /\brequire\s*\(\s*['"]express['"]\s*\)/g;
|
|
55
63
|
|
|
56
64
|
// The exact shapes an express import clause may legally take: `express`, `{ Router }`,
|
|
57
65
|
// `express, { Router }`. Anything else is REFUSED rather than parsed optimistically.
|
|
58
66
|
const IMPORT_CLAUSE_RE = /^(?:([\w$]+))?(?:\s*,\s*)?(?:\{([^}]*)\})?$/;
|
|
59
67
|
|
|
60
|
-
// Node's
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
function
|
|
65
|
-
|
|
68
|
+
// Node's own top-level module-kind rule for the package root this adapter detected. `.mjs` and
|
|
69
|
+
// `.cjs` are unconditional; `.js` follows package.json's `type`. This keeps an `import` written in
|
|
70
|
+
// a non-module `.js` file from being treated as live ESM while still supporting mixed packages
|
|
71
|
+
// containing both explicit extensions.
|
|
72
|
+
function moduleKindFor(file, packageType) {
|
|
73
|
+
if (path.extname(file) === '.mjs') return 'esm';
|
|
74
|
+
if (path.extname(file) === '.cjs') return 'commonjs';
|
|
75
|
+
return packageType === 'module' ? 'esm' : 'commonjs';
|
|
66
76
|
}
|
|
67
77
|
|
|
68
78
|
function extensionSuffixes(globs) {
|
|
69
|
-
return globs.map((g) => g.replace(/^\*/, '')); // ['*.js','*.mjs'] ->
|
|
79
|
+
return globs.map((g) => g.replace(/^\*/, '')); // ['*.js','*.mjs','*.cjs'] -> suffixes
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
// Two independent signals required, the same combined bar java-spring ("build file AND src
|
|
73
83
|
// layout"), python-fastapi ("dependency declared AND source-confirmed") and typescript-express all
|
|
74
|
-
// use: (a) a package.json declares express, (b) at least one
|
|
75
|
-
// express and calls `Router()` /
|
|
84
|
+
// use: (a) a package.json declares express, (b) at least one JavaScript source file under it both
|
|
85
|
+
// loads express in the syntax valid for that file's Node module kind and calls `Router()` /
|
|
86
|
+
// `<something>.Router()`. Walks the whole repo for candidate
|
|
76
87
|
// package.json files (not just repoRoot) for the same monorepo reason python-fastapi does.
|
|
77
88
|
export function detectJavaScriptExpressRoot(repoRoot) {
|
|
78
89
|
for (const pkgFile of listCandidatePackageFiles(repoRoot)) {
|
|
79
90
|
if (!declaresExpress(pkgFile)) continue;
|
|
80
91
|
const projectRoot = path.dirname(pkgFile);
|
|
81
|
-
const
|
|
92
|
+
const packageType = readPackageJson(pkgFile)?.type ?? 'commonjs';
|
|
93
|
+
const globs = ['*.js', '*.mjs', '*.cjs'];
|
|
82
94
|
// rg is a cheap candidate filter over raw bytes and can match inside a comment; the real
|
|
83
95
|
// gate is the masked re-read below, which is why detection needs both the import AND a
|
|
84
96
|
// Router() call to be genuine code.
|
|
85
|
-
const sourceFiles = rgFilesMatching(
|
|
97
|
+
const sourceFiles = rgFilesMatching(EXPRESS_MODULE_SRC, globs, projectRoot);
|
|
86
98
|
// `\bRouter\s*\(` matches BOTH `Router(...)` and `express.Router(...)` -- there is a word
|
|
87
99
|
// boundary between `.` and `R`, and none inside `makeRouter(`. Not `\(\s*\)`: an options
|
|
88
100
|
// object (`Router({ mergeParams: true })`) is ordinary Express and must still detect.
|
|
89
101
|
const callsRouter = sourceFiles.some((f) => {
|
|
90
102
|
try {
|
|
91
103
|
const masked = maskJsComments(fs.readFileSync(f, 'utf8'));
|
|
92
|
-
|
|
104
|
+
const kind = moduleKindFor(f, packageType);
|
|
105
|
+
return expressBindings(masked, kind) !== null && /\bRouter\s*\(/.test(masked);
|
|
93
106
|
} catch {
|
|
94
107
|
return false;
|
|
95
108
|
}
|
|
96
109
|
});
|
|
97
|
-
if (callsRouter) return { projectRoot, globs };
|
|
110
|
+
if (callsRouter) return { projectRoot, globs, packageType };
|
|
98
111
|
}
|
|
99
112
|
return null;
|
|
100
113
|
}
|
|
@@ -103,9 +116,10 @@ function listSourceFiles(projectRoot, globs) {
|
|
|
103
116
|
return listRgFiles(projectRoot, globs);
|
|
104
117
|
}
|
|
105
118
|
|
|
106
|
-
// What THIS file named its express bindings. `import express, { Router } from 'express'`
|
|
107
|
-
//
|
|
108
|
-
//
|
|
119
|
+
// What THIS file named its express bindings. `import express, { Router } from 'express'` and
|
|
120
|
+
// `const express = require('express')` both yield defaultNames containing `express`; destructuring
|
|
121
|
+
// `Router` sets hasNamedRouter, and `require('express').Router()` sets hasDirectRouter. Returns
|
|
122
|
+
// null when the file does not load express in the syntax valid for its Node module kind.
|
|
109
123
|
//
|
|
110
124
|
// Anchors on `from 'express'` and scans BACKWARD to the nearest `import` keyword, rather than
|
|
111
125
|
// matching a whole `import ... from 'express'` statement forward. A forward
|
|
@@ -115,25 +129,37 @@ function listSourceFiles(projectRoot, globs) {
|
|
|
115
129
|
// express`; and a clause spread over several lines. The backward scan handles both, and the
|
|
116
130
|
// strict IMPORT_CLAUSE_RE shape check means an unparseable clause is REFUSED (skipped), never
|
|
117
131
|
// parsed optimistically into a wrong binding name.
|
|
118
|
-
function expressBindings(text) {
|
|
119
|
-
|
|
132
|
+
function expressBindings(text, moduleKind) {
|
|
133
|
+
const defaultNames = new Set();
|
|
120
134
|
let hasNamedRouter = false;
|
|
135
|
+
let hasDirectRouter = false;
|
|
121
136
|
let found = false;
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
if (moduleKind === 'esm') {
|
|
138
|
+
for (const m of text.matchAll(FROM_EXPRESS_RE)) {
|
|
139
|
+
const before = text.slice(0, m.index);
|
|
140
|
+
const importIdx = before.lastIndexOf('import');
|
|
141
|
+
if (importIdx === -1) continue; // e.g. `export * from 'express'` -- not a binding
|
|
142
|
+
const clause = before.slice(importIdx + 'import'.length).replace(/\s+/g, ' ').trim();
|
|
143
|
+
const parsed = clause.match(IMPORT_CLAUSE_RE);
|
|
144
|
+
if (!parsed) continue;
|
|
145
|
+
found = true;
|
|
146
|
+
if (parsed[1]) defaultNames.add(parsed[1]);
|
|
147
|
+
// `Router as R` aliasing is deliberately NOT resolved -- a documented, narrow
|
|
148
|
+
// limitation, not a silent guess at which local name means Router.
|
|
149
|
+
if (parsed[2] && parsed[2].split(',').some((s) => s.trim() === 'Router')) hasNamedRouter = true;
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
for (const m of text.matchAll(REQUIRE_EXPRESS_RE)) {
|
|
153
|
+
found = true;
|
|
154
|
+
const beforeLine = text.slice(Math.max(text.lastIndexOf('\n', m.index) + 1, 0), m.index);
|
|
155
|
+
const defaultMatch = beforeLine.match(/([\w$]+)\s*=\s*$/);
|
|
156
|
+
if (defaultMatch) defaultNames.add(defaultMatch[1]);
|
|
157
|
+
if (/\{\s*Router\s*\}\s*=\s*$/.test(beforeLine)) hasNamedRouter = true;
|
|
158
|
+
const after = text.slice(m.index + m[0].length);
|
|
159
|
+
if (/^\s*\.\s*Router\s*\(/.test(after)) hasDirectRouter = true;
|
|
160
|
+
}
|
|
135
161
|
}
|
|
136
|
-
return found ? {
|
|
162
|
+
return found ? { defaultNames, hasNamedRouter, hasDirectRouter } : null;
|
|
137
163
|
}
|
|
138
164
|
|
|
139
165
|
// Every locally-declared mountable value in this file, with what it is. Both kinds matter: an
|
|
@@ -150,7 +176,7 @@ function declaredMountables(text, bindings) {
|
|
|
150
176
|
// completely ordinary Express, and requiring `()` dropped the declaration entirely -- which,
|
|
151
177
|
// here, means the file yields no routes at all rather than merely losing an option. Matching
|
|
152
178
|
// the opening paren is sufficient to identify the variable; the argument list is never read.
|
|
153
|
-
const routerDeclRe = /\b(?:const|let|var)\s+([\w$]+)\s*=\s*(?:[\w$]+\s*\.\s*)?Router\s*\(/g;
|
|
179
|
+
const routerDeclRe = /\b(?:const|let|var)\s+([\w$]+)\s*=\s*((?:[\w$]+\s*\.\s*)?Router|require\s*\(\s*['"]express['"]\s*\)\s*\.\s*Router)\s*\(/g;
|
|
154
180
|
for (const m of text.matchAll(routerDeclRe)) {
|
|
155
181
|
// A bare `Router()` only counts when Router is genuinely imported from express; a
|
|
156
182
|
// `<name>.Router()` member call always counts (that IS the default-import idiom).
|
|
@@ -158,11 +184,15 @@ function declaredMountables(text, bindings) {
|
|
|
158
184
|
// past the member call, so it classified EVERY `express.Router()` as a bare call -- which,
|
|
159
185
|
// with no named `Router` import in the file, dropped the declaration entirely and collapsed
|
|
160
186
|
// the whole mount graph. Found by running the real fixture, not by review.
|
|
161
|
-
const
|
|
162
|
-
|
|
187
|
+
const initializer = m[2];
|
|
188
|
+
const memberMatch = initializer.match(/^([\w$]+)\s*\.\s*Router$/);
|
|
189
|
+
const isBoundMemberCall = memberMatch && bindings.defaultNames.has(memberMatch[1]);
|
|
190
|
+
const isDirectRequireCall = /^require\b/.test(initializer) && bindings.hasDirectRouter;
|
|
191
|
+
const isBareCall = initializer === 'Router' && bindings.hasNamedRouter;
|
|
192
|
+
if (isBoundMemberCall || isDirectRequireCall || isBareCall) mountables.set(m[1], 'router');
|
|
163
193
|
}
|
|
164
|
-
|
|
165
|
-
const appDeclRe = new RegExp(`\\b(?:const|let|var)\\s+([\\w$]+)\\s*=\\s*${
|
|
194
|
+
for (const defaultName of bindings.defaultNames) {
|
|
195
|
+
const appDeclRe = new RegExp(`\\b(?:const|let|var)\\s+([\\w$]+)\\s*=\\s*${defaultName}\\s*\\(\\s*\\)`, 'g');
|
|
166
196
|
for (const m of text.matchAll(appDeclRe)) mountables.set(m[1], 'app');
|
|
167
197
|
}
|
|
168
198
|
return mountables;
|
|
@@ -179,7 +209,11 @@ function nodeKey(file, varName) {
|
|
|
179
209
|
|
|
180
210
|
// LOCAL endpoints only (verb/path/handler/line), with an EMPTY prefix -- exactly like G5, because
|
|
181
211
|
// no path prefix is ever visible at an Express route-registration call site. The mount-tree walk in
|
|
182
|
-
// scanJavaScriptExpress() joins the real prefix chain afterward.
|
|
212
|
+
// scanJavaScriptExpress() joins the real prefix chain afterward. Inline handlers are real route
|
|
213
|
+
// declarations too; they carry method:null rather than a fabricated name, matching the existing
|
|
214
|
+
// typescript-express contract.
|
|
215
|
+
const INLINE_HANDLER_RE = /^(?:async\s+)?(?:\([^)]*\)|[$\w]+)\s*(?::[^=]*)?=>|^(?:async\s+)?function\b/;
|
|
216
|
+
|
|
183
217
|
function extractEndpoints(text, mountableNames) {
|
|
184
218
|
if (mountableNames.length === 0) return [];
|
|
185
219
|
const re = new RegExp(`\\b(${alternationOf(mountableNames)})\\.(${VERBS.join('|')})\\s*\\(`, 'gi');
|
|
@@ -196,22 +230,19 @@ function extractEndpoints(text, mountableNames) {
|
|
|
196
230
|
|
|
197
231
|
const args = splitTopLevelArgs(argsText);
|
|
198
232
|
const lastArg = args[args.length - 1]?.trim();
|
|
199
|
-
// A bare identifier only -- an inline arrow-function handler has no name to correlate to a
|
|
200
|
-
// controller file, so it's skipped rather than guessed at (same discipline as G5's and
|
|
201
|
-
// FastAPI's own "no path literal -> skip").
|
|
202
233
|
const handlerMatch = lastArg?.match(/^([\w$]+)$/);
|
|
203
|
-
|
|
234
|
+
const isInlineHandler = !handlerMatch && lastArg && INLINE_HANDLER_RE.test(lastArg);
|
|
235
|
+
if (!handlerMatch && !isInlineHandler) continue;
|
|
204
236
|
|
|
205
|
-
endpoints.push({ varName, verb, path: pathMatch[1], operationId: null, method: handlerMatch[1], line: lineNumberAt(text, m.index) });
|
|
237
|
+
endpoints.push({ varName, verb, path: pathMatch[1], operationId: null, method: handlerMatch ? handlerMatch[1] : null, line: lineNumberAt(text, m.index) });
|
|
206
238
|
}
|
|
207
239
|
return endpoints;
|
|
208
240
|
}
|
|
209
241
|
|
|
210
|
-
// Resolves a relative ESM
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
function resolveEsmImport(fromFile, specifier, suffixes) {
|
|
242
|
+
// Resolves a relative ESM import or CommonJS require. Explicit files are accepted first; the
|
|
243
|
+
// extension/index probes cover ordinary CommonJS (`require('./routes')`) and the extensionless ESM
|
|
244
|
+
// people write through bundlers. Never guesses beyond the adapter's three JavaScript suffixes.
|
|
245
|
+
function resolveRelativeModule(fromFile, specifier, suffixes) {
|
|
215
246
|
if (!specifier.startsWith('.')) return null; // only relative specifiers resolve mount edges
|
|
216
247
|
const base = path.resolve(path.dirname(fromFile), specifier);
|
|
217
248
|
const candidates = [base];
|
|
@@ -234,11 +265,12 @@ function resolveEsmImport(fromFile, specifier, suffixes) {
|
|
|
234
265
|
// export-prefixed declaration (`export const router = Router();`, ordinary and common) previously
|
|
235
266
|
// had no path to being recognized as this file's "the" exported mountable at all.
|
|
236
267
|
//
|
|
237
|
-
//
|
|
238
|
-
// { router as r }`
|
|
239
|
-
// `Router as R` import-aliasing decision
|
|
240
|
-
// limitation, not a silent guess at which local name an alias refers to.
|
|
268
|
+
// ESM's three real hand-off forms plus CommonJS's direct `module.exports = router`. ESM aliasing
|
|
269
|
+
// (`export { router as r }`) is deliberately NOT resolved, same restraint as this file's own
|
|
270
|
+
// `Router as R` import-aliasing decision: a documented, narrow limitation, not a silent guess.
|
|
241
271
|
function exportedMountableName(text, mountables) {
|
|
272
|
+
const commonJsMatch = text.match(/\bmodule\s*\.\s*exports\s*=\s*([\w$]+)\s*;?/);
|
|
273
|
+
if (commonJsMatch && mountables.has(commonJsMatch[1])) return commonJsMatch[1];
|
|
242
274
|
const defaultMatch = text.match(/export\s+default\s+([\w$]+)\s*;?/);
|
|
243
275
|
if (defaultMatch && mountables.has(defaultMatch[1])) return defaultMatch[1];
|
|
244
276
|
const namedMatch = text.match(/export\s*\{\s*([\w$]+)\s*\}/);
|
|
@@ -248,12 +280,12 @@ function exportedMountableName(text, mountables) {
|
|
|
248
280
|
return null;
|
|
249
281
|
}
|
|
250
282
|
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
// is deliberately not resolved, same restraint as exportedMountableName's own aliasing decision
|
|
254
|
-
// above -- a bare, unaliased single-name clause only, matching this file's existing default-import
|
|
255
|
-
// regex's own narrow scope (never a general multi-specifier import-clause parser).
|
|
283
|
+
// ESM default/named imports OR a CommonJS `const target = require('./relative')`. Alias forms stay
|
|
284
|
+
// deliberately unresolved; this is only the source lookup for an already-observed mount target.
|
|
256
285
|
function importSourceFor(text, target) {
|
|
286
|
+
const requireRe = new RegExp(`(?:\\b(?:const|let|var)\\s+|[,;]\\s*)${target}\\s*=\\s*require\\s*\\(\\s*["']([^"']+)["']\\s*\\)`);
|
|
287
|
+
const requireMatch = text.match(requireRe);
|
|
288
|
+
if (requireMatch) return requireMatch[1];
|
|
257
289
|
const defaultImportRe = new RegExp(`import\\s+${target}\\s*(?:,\\s*\\{[^}]*\\})?\\s*from\\s*["']([^"']+)["']`);
|
|
258
290
|
const defaultMatch = text.match(defaultImportRe);
|
|
259
291
|
if (defaultMatch) return defaultMatch[1];
|
|
@@ -262,12 +294,13 @@ function importSourceFor(text, target) {
|
|
|
262
294
|
return namedMatch ? namedMatch[1] : null;
|
|
263
295
|
}
|
|
264
296
|
|
|
265
|
-
// Builds the mount graph over (file, variable) nodes. Two edge kinds,
|
|
266
|
-
// `X.use('/literal', Y)`
|
|
297
|
+
// Builds the mount graph over (file, variable) nodes. Two edge kinds, from either
|
|
298
|
+
// `X.use('/literal', Y)`, `X.use('/literal', require('./y'))`, or `X.use(require('./y'))`:
|
|
267
299
|
// - INTRA-FILE: Y is another mountable declared in this same file (`app.use('/api', route)`)
|
|
268
300
|
// - CROSS-FILE: Y is imported from a RELATIVE specifier whose file default-exports a mountable
|
|
269
|
-
// A computed/dynamic mount (`route.use(prefix, buildRouter())`)
|
|
270
|
-
//
|
|
301
|
+
// A computed/dynamic mount (`route.use(prefix, buildRouter())`) or a bare/package specifier is
|
|
302
|
+
// skipped, never guessed. A one-argument identifier remains middleware and is skipped; only a
|
|
303
|
+
// one-argument relative `require()` is unambiguously a CommonJS sub-router hand-off.
|
|
271
304
|
function buildMountEdges(files, fileInfo, suffixes) {
|
|
272
305
|
const edges = []; // { from: nodeKey, to: nodeKey, prefix }
|
|
273
306
|
for (const file of files) {
|
|
@@ -281,24 +314,40 @@ function buildMountEdges(files, fileInfo, suffixes) {
|
|
|
281
314
|
const closeIdx = matchBalancedParens(info.text, openIdx);
|
|
282
315
|
if (closeIdx === -1) continue;
|
|
283
316
|
const args = splitTopLevelArgs(info.text.slice(openIdx + 1, closeIdx));
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
317
|
+
let prefix;
|
|
318
|
+
let targetExpression;
|
|
319
|
+
if (args.length === 2) {
|
|
320
|
+
const pathMatch = args[0].match(STRING_LITERAL_RE);
|
|
321
|
+
if (!pathMatch) continue;
|
|
322
|
+
prefix = pathMatch[1];
|
|
323
|
+
targetExpression = args[1].trim();
|
|
324
|
+
} else if (args.length === 1) {
|
|
325
|
+
prefix = '';
|
|
326
|
+
targetExpression = args[0].trim();
|
|
327
|
+
} else {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const identMatch = targetExpression.match(/^([\w$]+)$/);
|
|
332
|
+
const directRequireMatch = targetExpression.match(/^require\s*\(\s*["']([^"']+)["']\s*\)$/);
|
|
333
|
+
if (!identMatch && !directRequireMatch) continue;
|
|
334
|
+
const target = identMatch?.[1] ?? null;
|
|
289
335
|
|
|
290
|
-
if (info.mountables.has(target)) {
|
|
291
|
-
edges.push({ from: nodeKey(file, fromVar), to: nodeKey(file, target), prefix
|
|
336
|
+
if (target && info.mountables.has(target)) {
|
|
337
|
+
edges.push({ from: nodeKey(file, fromVar), to: nodeKey(file, target), prefix });
|
|
292
338
|
continue;
|
|
293
339
|
}
|
|
294
|
-
|
|
340
|
+
// A one-argument identifier is ordinary middleware, not a router mount. Reaching this
|
|
341
|
+
// branch means it was not a local mountable; only a real import/require binding can turn
|
|
342
|
+
// it into a cross-file edge.
|
|
343
|
+
const importSource = directRequireMatch?.[1] ?? (target ? importSourceFor(info.text, target) : null);
|
|
295
344
|
if (!importSource) continue;
|
|
296
|
-
const toFile =
|
|
345
|
+
const toFile = resolveRelativeModule(file, importSource, suffixes);
|
|
297
346
|
if (!toFile || !fileInfo.has(toFile)) continue;
|
|
298
347
|
const toInfo = fileInfo.get(toFile);
|
|
299
348
|
const toVar = exportedMountableName(toInfo.text, toInfo.mountables);
|
|
300
349
|
if (!toVar) continue;
|
|
301
|
-
edges.push({ from: nodeKey(file, fromVar), to: nodeKey(toFile, toVar), prefix
|
|
350
|
+
edges.push({ from: nodeKey(file, fromVar), to: nodeKey(toFile, toVar), prefix });
|
|
302
351
|
}
|
|
303
352
|
}
|
|
304
353
|
return edges;
|
|
@@ -331,7 +380,8 @@ function moduleNameFor(file) {
|
|
|
331
380
|
}
|
|
332
381
|
|
|
333
382
|
const API_SURFACE_SOURCE = 'route paths are resolved by walking the Express mount graph over (file, router-variable) ' +
|
|
334
|
-
'nodes --
|
|
383
|
+
'nodes -- ESM imports and CommonJS require()/module.exports are both supported; cross-file ' +
|
|
384
|
+
'`use(\'/literal\', importedRouter)` / `use(\'/literal\', require(\'./router\'))` edges (RELATIVE specifiers only) and intra-file ' +
|
|
335
385
|
'`app.use(\'/literal\', localRouter)` edges, where a global prefix usually lives. A computed/dynamic mount is ' +
|
|
336
386
|
'skipped, never guessed. Plain Express has no operationId concept at all, so they are never statically ' +
|
|
337
387
|
'derivable here. This adapter also reports NO persistence entities: the target stack calls a raw SQL driver ' +
|
|
@@ -340,10 +390,10 @@ const API_SURFACE_SOURCE = 'route paths are resolved by walking the Express moun
|
|
|
340
390
|
'--openapi-file <path> --path-prefix <prefix>` for trustworthy operation identity, if this app has one.';
|
|
341
391
|
|
|
342
392
|
export function scanJavaScriptExpress(repoRoot, detection) {
|
|
343
|
-
const { projectRoot, globs } = detection;
|
|
393
|
+
const { projectRoot, globs, packageType = 'commonjs' } = detection;
|
|
344
394
|
const suffixes = extensionSuffixes(globs);
|
|
345
395
|
// Normalized to absolute up front: `rg --files` echoes back paths in whatever style its `dir`
|
|
346
|
-
// argument used, but `
|
|
396
|
+
// argument used, but `resolveRelativeModule()` builds candidates with `path.resolve()`, which is
|
|
347
397
|
// ALWAYS absolute. With a relative repoRoot the two never compare equal, every cross-file mount
|
|
348
398
|
// edge is silently dropped, and every route loses its prefix while still looking successfully
|
|
349
399
|
// scanned. Real callers happen to pass an absolute repoRoot today (`git rev-parse
|
|
@@ -360,7 +410,7 @@ export function scanJavaScriptExpress(repoRoot, detection) {
|
|
|
360
410
|
// routing can never be mistaken for routing. String literals survive intact, so every path
|
|
361
411
|
// value is still read from the real source. See maskJsComments in _express-shared.mjs.
|
|
362
412
|
const text = maskJsComments(fs.readFileSync(file, 'utf8'));
|
|
363
|
-
const bindings = expressBindings(text);
|
|
413
|
+
const bindings = expressBindings(text, moduleKindFor(file, packageType));
|
|
364
414
|
fileInfo.set(file, { text, bindings, mountables: bindings ? declaredMountables(text, bindings) : new Map() });
|
|
365
415
|
}
|
|
366
416
|
const edges = buildMountEdges(files, fileInfo, suffixes);
|
|
@@ -413,7 +463,7 @@ export function scanJavaScriptExpress(repoRoot, detection) {
|
|
|
413
463
|
export const adapter = {
|
|
414
464
|
contract: 'sbf.adapter/2',
|
|
415
465
|
id: 'javascript-express',
|
|
416
|
-
title: 'JavaScript / Express (ESM, no ORM)',
|
|
466
|
+
title: 'JavaScript / Express (ESM/CommonJS, no ORM)',
|
|
417
467
|
specificity: 80,
|
|
418
468
|
// high, matching python-fastapi's own G2 shipping state: confidence describes trust in what the
|
|
419
469
|
// scan REPORTS (routes and their real absolute paths, resolved through a genuine mount-graph
|