carto-md 1.0.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/.cartoignore +16 -0
- package/package.json +35 -0
- package/src/agents/formatter.js +150 -0
- package/src/agents/merger.js +70 -0
- package/src/cli/index.js +43 -0
- package/src/cli/init.js +80 -0
- package/src/cli/sync.js +27 -0
- package/src/cli/watch.js +52 -0
- package/src/detector/files.js +92 -0
- package/src/detector/framework.js +117 -0
- package/src/extractors/dbtables.js +80 -0
- package/src/extractors/envvars.js +40 -0
- package/src/extractors/filemap.js +36 -0
- package/src/extractors/frontend.js +43 -0
- package/src/extractors/functions.js +69 -0
- package/src/extractors/languages/html.js +15 -0
- package/src/extractors/languages/javascript.js +376 -0
- package/src/extractors/languages/prisma.js +80 -0
- package/src/extractors/languages/python.js +26 -0
- package/src/extractors/languages/typescript.js +235 -0
- package/src/extractors/loader.js +67 -0
- package/src/extractors/models.js +40 -0
- package/src/extractors/routes.js +66 -0
- package/src/security/ignore.js +66 -0
- package/src/sync.js +180 -0
- package/src/watcher/watch.js +46 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extractDBTables(content) → Array<{ tableName, modelName }>
|
|
3
|
+
*
|
|
4
|
+
* Supports two ORM patterns:
|
|
5
|
+
* 1. SQLAlchemy: class Foo(Base): __tablename__ = 'foo'
|
|
6
|
+
* 2. Django ORM: class Foo(models.Model): class Meta: db_table = 'foo'
|
|
7
|
+
* (falls back to snake_case of class name if no db_table)
|
|
8
|
+
*/
|
|
9
|
+
function extractDBTables(content) {
|
|
10
|
+
const tables = [];
|
|
11
|
+
const lines = content.split('\n');
|
|
12
|
+
|
|
13
|
+
// Pattern 1 — SQLAlchemy
|
|
14
|
+
const sqlaClassPattern = /^class\s+(\w+)\s*\(.*Base.*\)\s*:/gm;
|
|
15
|
+
let classMatch;
|
|
16
|
+
while ((classMatch = sqlaClassPattern.exec(content)) !== null) {
|
|
17
|
+
const modelName = classMatch[1];
|
|
18
|
+
const classEnd = classMatch.index + classMatch[0].length;
|
|
19
|
+
|
|
20
|
+
// Find line index of this class
|
|
21
|
+
const textBefore = content.substring(0, classEnd);
|
|
22
|
+
const lineIndex = textBefore.split('\n').length - 1;
|
|
23
|
+
|
|
24
|
+
// Look ahead up to 10 lines for __tablename__
|
|
25
|
+
let found = false;
|
|
26
|
+
for (let j = lineIndex + 1; j < Math.min(lineIndex + 11, lines.length); j++) {
|
|
27
|
+
const tnMatch = lines[j].match(/^\s+__tablename__\s*=\s*['"]([^'"]+)['"]/);
|
|
28
|
+
if (tnMatch) {
|
|
29
|
+
tables.push({ tableName: tnMatch[1], modelName });
|
|
30
|
+
found = true;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
// Stop if we hit another class definition
|
|
34
|
+
if (/^class\s/.test(lines[j])) break;
|
|
35
|
+
}
|
|
36
|
+
// If not found, skip — not a table-mapped model
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Pattern 2 — Django ORM
|
|
40
|
+
const djangoClassPattern = /^class\s+(\w+)\s*\(.*models\.Model.*\)\s*:/gm;
|
|
41
|
+
while ((classMatch = djangoClassPattern.exec(content)) !== null) {
|
|
42
|
+
const modelName = classMatch[1];
|
|
43
|
+
const classEnd = classMatch.index + classMatch[0].length;
|
|
44
|
+
|
|
45
|
+
const textBefore = content.substring(0, classEnd);
|
|
46
|
+
const lineIndex = textBefore.split('\n').length - 1;
|
|
47
|
+
|
|
48
|
+
// Look ahead up to 15 lines for db_table
|
|
49
|
+
let found = false;
|
|
50
|
+
for (let j = lineIndex + 1; j < Math.min(lineIndex + 16, lines.length); j++) {
|
|
51
|
+
const dtMatch = lines[j].match(/db_table\s*=\s*['"]([^'"]+)['"]/);
|
|
52
|
+
if (dtMatch) {
|
|
53
|
+
tables.push({ tableName: dtMatch[1], modelName });
|
|
54
|
+
found = true;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
if (/^class\s/.test(lines[j])) break;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Django default: snake_case of class name
|
|
61
|
+
if (!found) {
|
|
62
|
+
tables.push({ tableName: toSnakeCase(modelName), modelName });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return tables;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Convert PascalCase to snake_case.
|
|
71
|
+
* MyModel → my_model
|
|
72
|
+
*/
|
|
73
|
+
function toSnakeCase(str) {
|
|
74
|
+
return str
|
|
75
|
+
.replace(/([A-Z])/g, '_$1')
|
|
76
|
+
.toLowerCase()
|
|
77
|
+
.replace(/^_/, '');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { extractDBTables };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extractEnvVars(content) → Array<string>
|
|
3
|
+
*
|
|
4
|
+
* Extracts environment variable NAMES ONLY. Never values. Never defaults.
|
|
5
|
+
* Returns deduplicated, alphabetically sorted array.
|
|
6
|
+
*
|
|
7
|
+
* Supported patterns (Python only):
|
|
8
|
+
* os.getenv('VAR')
|
|
9
|
+
* os.getenv("VAR")
|
|
10
|
+
* os.environ.get('VAR')
|
|
11
|
+
* os.environ.get("VAR")
|
|
12
|
+
* os.environ['VAR']
|
|
13
|
+
* os.environ["VAR"]
|
|
14
|
+
*/
|
|
15
|
+
function extractEnvVars(content) {
|
|
16
|
+
const vars = new Set();
|
|
17
|
+
|
|
18
|
+
// os.getenv('VAR') or os.getenv("VAR")
|
|
19
|
+
const getenvPattern = /os\.getenv\s*\(\s*['"]([^'"]+)['"]/g;
|
|
20
|
+
let match;
|
|
21
|
+
while ((match = getenvPattern.exec(content)) !== null) {
|
|
22
|
+
vars.add(match[1]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// os.environ.get('VAR') or os.environ.get("VAR")
|
|
26
|
+
const environGetPattern = /os\.environ\.get\s*\(\s*['"]([^'"]+)['"]/g;
|
|
27
|
+
while ((match = environGetPattern.exec(content)) !== null) {
|
|
28
|
+
vars.add(match[1]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// os.environ['VAR'] or os.environ["VAR"]
|
|
32
|
+
const environBracketPattern = /os\.environ\s*\[\s*['"]([^'"]+)['"]\s*\]/g;
|
|
33
|
+
while ((match = environBracketPattern.exec(content)) !== null) {
|
|
34
|
+
vars.add(match[1]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return [...vars].sort();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { extractEnvVars };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* inferResponsibility(filename, functionCount, routeCount) → string
|
|
5
|
+
*
|
|
6
|
+
* Returns a short description of what a file does.
|
|
7
|
+
* If nothing matches confidently → return "—".
|
|
8
|
+
*/
|
|
9
|
+
function inferResponsibility(filename, functionCount, routeCount) {
|
|
10
|
+
const base = path.basename(filename).toLowerCase();
|
|
11
|
+
|
|
12
|
+
// Skip __init__.py entirely
|
|
13
|
+
if (base === '__init__.py') return null;
|
|
14
|
+
|
|
15
|
+
// Check in order, return first match
|
|
16
|
+
if (routeCount > 0) return `API routes (${routeCount} routes)`;
|
|
17
|
+
if (base.includes('collect')) return 'Data collection';
|
|
18
|
+
if (base.includes('rule') || base.includes('check')) return `Rule checks (${functionCount} functions)`;
|
|
19
|
+
if (base.includes('score') || base.includes('scoring')) return 'Scoring logic';
|
|
20
|
+
if (base.includes('llm') || base.includes('ai') || base.includes('ml')) return 'AI/LLM integration';
|
|
21
|
+
if (base.includes('github')) return 'GitHub integration';
|
|
22
|
+
if (base.includes('storage')) return 'Storage operations';
|
|
23
|
+
if (base.includes('drift')) return 'Drift detection';
|
|
24
|
+
if (base.includes('auth')) return 'Authentication';
|
|
25
|
+
if (base.includes('database') || base.includes('db')) return 'Database operations';
|
|
26
|
+
if (base.includes('model')) return 'Data models';
|
|
27
|
+
if (base.includes('test') || base.includes('spec')) return 'Tests';
|
|
28
|
+
if (base.includes('util') || base.includes('helper')) return 'Utilities';
|
|
29
|
+
if (base.includes('config') || base.includes('setting')) return 'Configuration';
|
|
30
|
+
if (base.includes('middleware')) return 'Middleware';
|
|
31
|
+
if (functionCount > 0) return `${functionCount} functions`;
|
|
32
|
+
|
|
33
|
+
return '\u2014';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { inferResponsibility };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts fetch() calls and sessionStorage usage from HTML/JS content.
|
|
3
|
+
* Strips newlines before fetch matching to handle multiline fetch options.
|
|
4
|
+
* Also detects dynamic fetch calls where the URL is a variable.
|
|
5
|
+
*/
|
|
6
|
+
function extractFrontend(content) {
|
|
7
|
+
const fetches = [];
|
|
8
|
+
const storageKeys = [];
|
|
9
|
+
|
|
10
|
+
// Strip newlines so [^}]* can cross what were originally separate lines
|
|
11
|
+
const singleLineContent = content.replace(/\n/g, ' ');
|
|
12
|
+
|
|
13
|
+
const fetchPattern = /fetch\s*\(\s*[`"']([^`"']+)[`"']\s*(?:,\s*\{[^}]*method\s*:\s*["'](\w+)["'][^}]*\})?/g;
|
|
14
|
+
let match;
|
|
15
|
+
while ((match = fetchPattern.exec(singleLineContent)) !== null) {
|
|
16
|
+
fetches.push({
|
|
17
|
+
url: match[1],
|
|
18
|
+
method: match[2] ? match[2].toUpperCase() : 'GET'
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Detect dynamic fetch calls — fetch(variable, ...) where variable is a bare identifier
|
|
23
|
+
const dynamicFetchPattern = /fetch\s*\(\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[,)]/g;
|
|
24
|
+
const reserved = new Set(['true', 'false', 'null', 'undefined']);
|
|
25
|
+
while ((match = dynamicFetchPattern.exec(singleLineContent)) !== null) {
|
|
26
|
+
if (!reserved.has(match[1])) {
|
|
27
|
+
fetches.push({ url: '[dynamic]', method: '[dynamic]' });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// sessionStorage — original content is fine, these are single-line
|
|
32
|
+
const storagePattern = /sessionStorage\.(getItem|setItem)\s*\(\s*["']([^"']+)["']/g;
|
|
33
|
+
while ((match = storagePattern.exec(content)) !== null) {
|
|
34
|
+
storageKeys.push({
|
|
35
|
+
operation: match[1],
|
|
36
|
+
key: match[2]
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { fetches, storageKeys };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { extractFrontend };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extractFunctions(content, filename) → Array<{ name, params, returnType }>
|
|
3
|
+
*
|
|
4
|
+
* Handles both single-line and multiline function signatures.
|
|
5
|
+
* Only extracts TOP-LEVEL functions (lines starting at column 0).
|
|
6
|
+
* Skips dunder methods (__name__).
|
|
7
|
+
* Includes private functions (_name).
|
|
8
|
+
*/
|
|
9
|
+
function extractFunctions(content, filename) {
|
|
10
|
+
const functions = [];
|
|
11
|
+
const lines = content.split('\n');
|
|
12
|
+
|
|
13
|
+
// Step 1: Collapse multiline signatures
|
|
14
|
+
const collapsed = [];
|
|
15
|
+
for (let i = 0; i < lines.length; i++) {
|
|
16
|
+
const line = lines[i];
|
|
17
|
+
// Only match top-level function defs (column 0)
|
|
18
|
+
if (/^(async\s+)?def\s+\w+\s*\(/.test(line)) {
|
|
19
|
+
let combined = line;
|
|
20
|
+
// If no closing paren, keep appending (up to 10 lines safety limit)
|
|
21
|
+
let safety = 0;
|
|
22
|
+
while (!combined.includes(')') && safety < 10 && i + 1 < lines.length) {
|
|
23
|
+
i++;
|
|
24
|
+
safety++;
|
|
25
|
+
combined += ' ' + lines[i].trim();
|
|
26
|
+
}
|
|
27
|
+
collapsed.push(combined);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Step 2: Extract from collapsed lines
|
|
32
|
+
const defPattern = /^(async\s+)?def\s+(\w+)\s*\(([^)]*)\)\s*(?:->\s*(.+?))?\s*:/;
|
|
33
|
+
|
|
34
|
+
for (const line of collapsed) {
|
|
35
|
+
const match = line.match(defPattern);
|
|
36
|
+
if (!match) continue;
|
|
37
|
+
|
|
38
|
+
const name = match[2];
|
|
39
|
+
|
|
40
|
+
// Skip dunder methods
|
|
41
|
+
if (name.startsWith('__')) continue;
|
|
42
|
+
|
|
43
|
+
const rawParams = match[3];
|
|
44
|
+
const returnType = match[4] ? match[4].trim() : '\u2014';
|
|
45
|
+
|
|
46
|
+
// Step 3: Clean params
|
|
47
|
+
const skipParams = new Set(['self', '*args', '**kwargs', '*', '']);
|
|
48
|
+
const params = rawParams
|
|
49
|
+
.split(',')
|
|
50
|
+
.map(p => {
|
|
51
|
+
// Strip type annotation (everything after first ":")
|
|
52
|
+
let cleaned = p.split(':')[0];
|
|
53
|
+
// Strip default value (everything after first "=")
|
|
54
|
+
cleaned = cleaned.split('=')[0];
|
|
55
|
+
return cleaned.trim();
|
|
56
|
+
})
|
|
57
|
+
.filter(p => !skipParams.has(p));
|
|
58
|
+
|
|
59
|
+
functions.push({
|
|
60
|
+
name,
|
|
61
|
+
params: params.length > 0 ? params.join(', ') : '\u2014',
|
|
62
|
+
returnType
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return functions;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { extractFunctions };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const { extractFrontend } = require('../frontend');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
name: 'html',
|
|
5
|
+
extensions: ['.html'],
|
|
6
|
+
extract(content, filename) {
|
|
7
|
+
try {
|
|
8
|
+
const { fetches, storageKeys } = extractFrontend(content);
|
|
9
|
+
return { routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches, storageKeys };
|
|
10
|
+
} catch (err) {
|
|
11
|
+
console.warn(`[CARTO] html plugin error on ${filename}: ${err.message}`);
|
|
12
|
+
return { routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [] };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
const parser = require('@babel/parser');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const PARSE_OPTIONS = {
|
|
5
|
+
sourceType: 'module',
|
|
6
|
+
allowImportExportEverywhere: true,
|
|
7
|
+
allowReturnOutsideFunction: true,
|
|
8
|
+
plugins: ['jsx', 'optionalChaining', 'nullishCoalescingOperator', 'classProperties', 'decorators-legacy']
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Known Express-like router object names
|
|
12
|
+
const ROUTER_NAMES = new Set(['app', 'router', 'server', 'api']);
|
|
13
|
+
// HTTP methods
|
|
14
|
+
const HTTP_METHODS = new Set(['get', 'post', 'put', 'delete', 'patch']);
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
name: 'javascript',
|
|
18
|
+
extensions: ['.js', '.jsx', '.mjs', '.cjs'],
|
|
19
|
+
extract(content, filename) {
|
|
20
|
+
let ast;
|
|
21
|
+
try {
|
|
22
|
+
ast = parser.parse(content, PARSE_OPTIONS);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.warn(`[CARTO] JS parse failed on ${filename}: ${err.message} — skipping`);
|
|
25
|
+
return { routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [] };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
routes: extractExpressRoutes(ast, filename),
|
|
30
|
+
models: [],
|
|
31
|
+
functions: extractJSFunctions(ast),
|
|
32
|
+
envVars: extractProcessEnv(ast),
|
|
33
|
+
dbTables: [],
|
|
34
|
+
fetches: extractJSFetches(ast),
|
|
35
|
+
storageKeys: [],
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Expose internals for typescript.js to reuse
|
|
40
|
+
_extractExpressRoutes: extractExpressRoutes,
|
|
41
|
+
_extractProcessEnv: extractProcessEnv,
|
|
42
|
+
_extractJSFetches: extractJSFetches,
|
|
43
|
+
_extractJSFunctions: extractJSFunctions,
|
|
44
|
+
_PARSE_OPTIONS: PARSE_OPTIONS,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// AST traversal helper
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
function walk(node, visitor) {
|
|
52
|
+
if (!node || typeof node !== 'object') return;
|
|
53
|
+
if (Array.isArray(node)) {
|
|
54
|
+
for (const child of node) walk(child, visitor);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (node.type) {
|
|
58
|
+
visitor(node);
|
|
59
|
+
}
|
|
60
|
+
for (const key of Object.keys(node)) {
|
|
61
|
+
if (key === 'leadingComments' || key === 'trailingComments' || key === 'innerComments') continue;
|
|
62
|
+
const child = node[key];
|
|
63
|
+
if (child && typeof child === 'object') {
|
|
64
|
+
walk(child, visitor);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Express route extraction
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
function extractExpressRoutes(ast, filename) {
|
|
74
|
+
const routes = [];
|
|
75
|
+
|
|
76
|
+
// Check for Next.js route file pattern
|
|
77
|
+
const isNextRoute = isNextJSRouteFile(filename);
|
|
78
|
+
|
|
79
|
+
if (isNextRoute) {
|
|
80
|
+
const nextRoutes = extractNextJSRoutes(ast, filename);
|
|
81
|
+
routes.push(...nextRoutes);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
walk(ast, (node) => {
|
|
85
|
+
if (node.type !== 'CallExpression') return;
|
|
86
|
+
if (!node.callee || node.callee.type !== 'MemberExpression') return;
|
|
87
|
+
|
|
88
|
+
const obj = node.callee.object;
|
|
89
|
+
const prop = node.callee.property;
|
|
90
|
+
|
|
91
|
+
// Check: app.get(...), router.post(...), etc.
|
|
92
|
+
if (!obj || !prop) return;
|
|
93
|
+
const objName = obj.name || (obj.property && obj.property.name);
|
|
94
|
+
const methodName = prop.name || (prop.value);
|
|
95
|
+
|
|
96
|
+
if (!objName || !ROUTER_NAMES.has(objName)) return;
|
|
97
|
+
if (!methodName || !HTTP_METHODS.has(methodName)) return;
|
|
98
|
+
|
|
99
|
+
// First argument should be the path
|
|
100
|
+
const args = node.arguments;
|
|
101
|
+
if (!args || args.length === 0) return;
|
|
102
|
+
|
|
103
|
+
let routePath;
|
|
104
|
+
if (args[0].type === 'StringLiteral') {
|
|
105
|
+
routePath = args[0].value;
|
|
106
|
+
} else if (args[0].type === 'TemplateLiteral') {
|
|
107
|
+
routePath = '[dynamic]';
|
|
108
|
+
} else {
|
|
109
|
+
return; // Can't determine path — skip
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Handler name: last argument
|
|
113
|
+
const lastArg = args[args.length - 1];
|
|
114
|
+
let handler = '[anonymous]';
|
|
115
|
+
if (lastArg.type === 'Identifier') {
|
|
116
|
+
handler = lastArg.name;
|
|
117
|
+
} else if (lastArg.type === 'FunctionExpression' && lastArg.id) {
|
|
118
|
+
handler = lastArg.id.name;
|
|
119
|
+
} else if (lastArg.type === 'ArrowFunctionExpression') {
|
|
120
|
+
// Arrow functions are anonymous, but check if assigned to a variable
|
|
121
|
+
handler = '[anonymous]';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
routes.push({
|
|
125
|
+
method: methodName.toUpperCase(),
|
|
126
|
+
path: routePath,
|
|
127
|
+
functionName: handler
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return routes;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isNextJSRouteFile(filename) {
|
|
135
|
+
const base = path.basename(filename, path.extname(filename));
|
|
136
|
+
return (base === 'route' || base === 'Route');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function extractNextJSRoutes(ast, filename) {
|
|
140
|
+
const routes = [];
|
|
141
|
+
const httpExports = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']);
|
|
142
|
+
|
|
143
|
+
// Infer path from filename: app/api/users/route.js → /api/users
|
|
144
|
+
// We only have the basename, so we can't infer the full path
|
|
145
|
+
// This would need the full file path — for now use the filename
|
|
146
|
+
const routePath = '[inferred]';
|
|
147
|
+
|
|
148
|
+
walk(ast, (node) => {
|
|
149
|
+
// export async function GET(req) { ... }
|
|
150
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
151
|
+
const decl = node.declaration;
|
|
152
|
+
if (decl.type === 'FunctionDeclaration' && decl.id && httpExports.has(decl.id.name)) {
|
|
153
|
+
routes.push({
|
|
154
|
+
method: decl.id.name,
|
|
155
|
+
path: routePath,
|
|
156
|
+
functionName: decl.id.name
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// export const GET = async (req) => { ... }
|
|
160
|
+
if (decl.type === 'VariableDeclaration') {
|
|
161
|
+
for (const declarator of decl.declarations) {
|
|
162
|
+
if (declarator.id && declarator.id.name && httpExports.has(declarator.id.name)) {
|
|
163
|
+
routes.push({
|
|
164
|
+
method: declarator.id.name,
|
|
165
|
+
path: routePath,
|
|
166
|
+
functionName: declarator.id.name
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return routes;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// JS function extraction
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
function extractJSFunctions(ast) {
|
|
182
|
+
const functions = [];
|
|
183
|
+
|
|
184
|
+
if (!ast.program || !ast.program.body) return functions;
|
|
185
|
+
|
|
186
|
+
for (const node of ast.program.body) {
|
|
187
|
+
// 1. FunctionDeclaration at top level
|
|
188
|
+
if (node.type === 'FunctionDeclaration' && node.id) {
|
|
189
|
+
const name = node.id.name;
|
|
190
|
+
if (shouldSkipJSFunction(name, node.params)) continue;
|
|
191
|
+
functions.push({
|
|
192
|
+
name,
|
|
193
|
+
params: extractParams(node.params),
|
|
194
|
+
returnType: '\u2014'
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 2. VariableDeclaration at top level with arrow/function expression
|
|
199
|
+
if (node.type === 'VariableDeclaration') {
|
|
200
|
+
for (const decl of node.declarations) {
|
|
201
|
+
if (!decl.id || !decl.id.name) continue;
|
|
202
|
+
if (!decl.init) continue;
|
|
203
|
+
if (decl.init.type === 'ArrowFunctionExpression' || decl.init.type === 'FunctionExpression') {
|
|
204
|
+
const name = decl.id.name;
|
|
205
|
+
if (shouldSkipJSFunction(name, decl.init.params)) continue;
|
|
206
|
+
functions.push({
|
|
207
|
+
name,
|
|
208
|
+
params: extractParams(decl.init.params),
|
|
209
|
+
returnType: '\u2014'
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 3. ExportDefaultDeclaration wrapping a function
|
|
216
|
+
if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
|
|
217
|
+
const decl = node.declaration;
|
|
218
|
+
if (decl.type === 'FunctionDeclaration' && decl.id) {
|
|
219
|
+
const name = decl.id.name;
|
|
220
|
+
if (!shouldSkipJSFunction(name, decl.params)) {
|
|
221
|
+
functions.push({
|
|
222
|
+
name,
|
|
223
|
+
params: extractParams(decl.params),
|
|
224
|
+
returnType: '\u2014'
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 4. ExportNamedDeclaration wrapping a function or variable
|
|
231
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
232
|
+
const decl = node.declaration;
|
|
233
|
+
if (decl.type === 'FunctionDeclaration' && decl.id) {
|
|
234
|
+
const name = decl.id.name;
|
|
235
|
+
if (!shouldSkipJSFunction(name, decl.params)) {
|
|
236
|
+
functions.push({
|
|
237
|
+
name,
|
|
238
|
+
params: extractParams(decl.params),
|
|
239
|
+
returnType: '\u2014'
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (decl.type === 'VariableDeclaration') {
|
|
244
|
+
for (const vDecl of decl.declarations) {
|
|
245
|
+
if (!vDecl.id || !vDecl.id.name || !vDecl.init) continue;
|
|
246
|
+
if (vDecl.init.type === 'ArrowFunctionExpression' || vDecl.init.type === 'FunctionExpression') {
|
|
247
|
+
const name = vDecl.id.name;
|
|
248
|
+
if (!shouldSkipJSFunction(name, vDecl.init.params)) {
|
|
249
|
+
functions.push({
|
|
250
|
+
name,
|
|
251
|
+
params: extractParams(vDecl.init.params),
|
|
252
|
+
returnType: '\u2014'
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return functions;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function shouldSkipJSFunction(name, params) {
|
|
265
|
+
// Skip _ prefixed functions only if they have 0 params (likely private utility)
|
|
266
|
+
if (name.startsWith('_') && (!params || params.length === 0)) return true;
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function extractParams(params) {
|
|
271
|
+
if (!params || params.length === 0) return '\u2014';
|
|
272
|
+
const names = [];
|
|
273
|
+
for (const p of params) {
|
|
274
|
+
if (p.type === 'Identifier') {
|
|
275
|
+
names.push(p.name);
|
|
276
|
+
} else if (p.type === 'AssignmentPattern' && p.left && p.left.type === 'Identifier') {
|
|
277
|
+
names.push(p.left.name);
|
|
278
|
+
} else if (p.type === 'RestElement' && p.argument && p.argument.type === 'Identifier') {
|
|
279
|
+
// ...args — skip like Python's *args
|
|
280
|
+
continue;
|
|
281
|
+
} else if (p.type === 'ObjectPattern') {
|
|
282
|
+
names.push('{...}');
|
|
283
|
+
} else if (p.type === 'ArrayPattern') {
|
|
284
|
+
names.push('[...]');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return names.length > 0 ? names.join(', ') : '\u2014';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// process.env extraction
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
function extractProcessEnv(ast) {
|
|
295
|
+
const vars = new Set();
|
|
296
|
+
|
|
297
|
+
walk(ast, (node) => {
|
|
298
|
+
if (node.type !== 'MemberExpression') return;
|
|
299
|
+
|
|
300
|
+
// Pattern 1: process.env.VAR_NAME
|
|
301
|
+
if (
|
|
302
|
+
node.object &&
|
|
303
|
+
node.object.type === 'MemberExpression' &&
|
|
304
|
+
node.object.object &&
|
|
305
|
+
node.object.object.name === 'process' &&
|
|
306
|
+
node.object.property &&
|
|
307
|
+
node.object.property.name === 'env' &&
|
|
308
|
+
node.property
|
|
309
|
+
) {
|
|
310
|
+
if (!node.computed && node.property.name) {
|
|
311
|
+
vars.add(node.property.name);
|
|
312
|
+
}
|
|
313
|
+
// Pattern 2: process.env['VAR_NAME']
|
|
314
|
+
if (node.computed && node.property.type === 'StringLiteral') {
|
|
315
|
+
vars.add(node.property.value);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return [...vars].sort();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// fetch() extraction
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
function extractJSFetches(ast) {
|
|
328
|
+
const fetches = [];
|
|
329
|
+
|
|
330
|
+
walk(ast, (node) => {
|
|
331
|
+
if (node.type !== 'CallExpression') return;
|
|
332
|
+
|
|
333
|
+
// fetch(...) or something.fetch(...)
|
|
334
|
+
const isFetch =
|
|
335
|
+
(node.callee.type === 'Identifier' && node.callee.name === 'fetch') ||
|
|
336
|
+
(node.callee.type === 'MemberExpression' && node.callee.property && node.callee.property.name === 'fetch');
|
|
337
|
+
|
|
338
|
+
if (!isFetch) return;
|
|
339
|
+
if (!node.arguments || node.arguments.length === 0) return;
|
|
340
|
+
|
|
341
|
+
const firstArg = node.arguments[0];
|
|
342
|
+
let url = '[dynamic]';
|
|
343
|
+
let method = '[dynamic]';
|
|
344
|
+
|
|
345
|
+
if (firstArg.type === 'StringLiteral') {
|
|
346
|
+
url = firstArg.value;
|
|
347
|
+
method = 'GET'; // default
|
|
348
|
+
} else if (firstArg.type === 'TemplateLiteral') {
|
|
349
|
+
url = '[dynamic]';
|
|
350
|
+
method = '[dynamic]';
|
|
351
|
+
} else if (firstArg.type === 'Identifier') {
|
|
352
|
+
url = '[dynamic]';
|
|
353
|
+
method = '[dynamic]';
|
|
354
|
+
} else {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Check for method in second argument (options object)
|
|
359
|
+
if (node.arguments.length > 1 && node.arguments[1].type === 'ObjectExpression') {
|
|
360
|
+
for (const prop of node.arguments[1].properties) {
|
|
361
|
+
if (
|
|
362
|
+
prop.key &&
|
|
363
|
+
(prop.key.name === 'method' || prop.key.value === 'method') &&
|
|
364
|
+
prop.value &&
|
|
365
|
+
prop.value.type === 'StringLiteral'
|
|
366
|
+
) {
|
|
367
|
+
method = prop.value.value.toUpperCase();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
fetches.push({ url, method });
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
return fetches;
|
|
376
|
+
}
|