archsync 1.0.0 → 1.0.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 +67 -0
- package/dist/archsync.cjs +2 -0
- package/package.json +8 -4
- package/bin/cli.js +0 -91
- package/src/__tests__/e2e-workflow.test.js +0 -66
- package/src/__tests__/hashEngine.test.js +0 -109
- package/src/__tests__/impact.test.js +0 -137
- package/src/__tests__/parsers.test.js +0 -496
- package/src/__tests__/scan-pipeline.test.js +0 -332
- package/src/__tests__/schemaBuilder.test.js +0 -145
- package/src/__tests__/workspace.test.js +0 -178
- package/src/commands/backup.js +0 -54
- package/src/commands/connect.js +0 -129
- package/src/commands/diff.js +0 -228
- package/src/commands/export.js +0 -125
- package/src/commands/impactReport.js +0 -50
- package/src/commands/import.js +0 -126
- package/src/commands/init.js +0 -80
- package/src/commands/login.js +0 -116
- package/src/commands/plugin.js +0 -28
- package/src/commands/push.js +0 -194
- package/src/commands/register.js +0 -127
- package/src/commands/scan.js +0 -498
- package/src/commands/serve.js +0 -133
- package/src/commands/setup.js +0 -233
- package/src/commands/status.js +0 -56
- package/src/commands/validate.js +0 -245
- package/src/commands/watch.js +0 -70
- package/src/core/credentialStore.js +0 -76
- package/src/core/hashEngine.js +0 -34
- package/src/core/impactEngine.js +0 -192
- package/src/core/monorepoDetector.js +0 -41
- package/src/core/pluginManager.js +0 -40
- package/src/core/relationshipEngine.js +0 -917
- package/src/core/requestSigning.js +0 -16
- package/src/core/schemaBuilder.js +0 -230
- package/src/core/schemaDeduplicator.js +0 -54
- package/src/core/supabaseClient.js +0 -68
- package/src/core/workspaceDetector.js +0 -113
- package/src/parsers/astParser.js +0 -274
- package/src/parsers/configParser.js +0 -49
- package/src/parsers/dependencyGraph.js +0 -31
- package/src/parsers/flutterParser.js +0 -98
- package/src/parsers/goParser.js +0 -99
- package/src/parsers/index.js +0 -211
- package/src/parsers/javaParser.js +0 -89
- package/src/parsers/nodeParser.js +0 -429
- package/src/parsers/pythonParser.js +0 -109
- package/src/parsers/reactParser.js +0 -368
- package/src/parsers/smartComment.js +0 -144
package/src/parsers/astParser.js
DELETED
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import * as babelParser from '@babel/parser';
|
|
4
|
-
import _traverse from '@babel/traverse';
|
|
5
|
-
|
|
6
|
-
// Handle ESM default import compatibility
|
|
7
|
-
const traverse = _traverse.default || _traverse;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* AST-based deep parser for JavaScript/TypeScript files.
|
|
11
|
-
* Uses @babel/parser + @babel/traverse for accurate extraction.
|
|
12
|
-
* Extracts: classes, functions, routes, models, API calls, decorators.
|
|
13
|
-
*/
|
|
14
|
-
export function parseWithAST(filePath) {
|
|
15
|
-
const source = fs.readFileSync(filePath, 'utf-8');
|
|
16
|
-
const ext = path.extname(filePath);
|
|
17
|
-
const entities = [];
|
|
18
|
-
const relations = [];
|
|
19
|
-
const fileName = path.basename(filePath, ext);
|
|
20
|
-
const dirName = path.basename(path.dirname(filePath));
|
|
21
|
-
|
|
22
|
-
// Determine system
|
|
23
|
-
let system = 'backend';
|
|
24
|
-
if (/src\/(app|mobile|native)/i.test(filePath)) system = 'app';
|
|
25
|
-
else if (/src\/(web|frontend|client|pages|components)/i.test(filePath)) system = 'web';
|
|
26
|
-
else if (/admin/i.test(filePath)) system = 'admin';
|
|
27
|
-
|
|
28
|
-
const plugins = ['jsx', 'classProperties', 'decorators-legacy', 'optionalChaining', 'nullishCoalescingOperator'];
|
|
29
|
-
if (ext === '.ts' || ext === '.tsx') plugins.push('typescript');
|
|
30
|
-
|
|
31
|
-
let ast;
|
|
32
|
-
try {
|
|
33
|
-
ast = babelParser.parse(source, {
|
|
34
|
-
sourceType: 'module',
|
|
35
|
-
plugins,
|
|
36
|
-
errorRecovery: true,
|
|
37
|
-
});
|
|
38
|
-
} catch (err) {
|
|
39
|
-
// Fallback: file can't be parsed
|
|
40
|
-
return { filePath, entities: [], relations: [], error: err.message };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const importMap = new Map(); // local name → source path
|
|
44
|
-
|
|
45
|
-
traverse(ast, {
|
|
46
|
-
// ─── Track imports ──────────────────────────────────────
|
|
47
|
-
ImportDeclaration(nodePath) {
|
|
48
|
-
const source = nodePath.node.source.value;
|
|
49
|
-
for (const spec of nodePath.node.specifiers) {
|
|
50
|
-
importMap.set(spec.local.name, source);
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
|
|
54
|
-
// ─── Classes ────────────────────────────────────────────
|
|
55
|
-
ClassDeclaration(nodePath) {
|
|
56
|
-
const name = nodePath.node.id?.name;
|
|
57
|
-
if (!name) return;
|
|
58
|
-
|
|
59
|
-
const parent = nodePath.node.superClass?.name || null;
|
|
60
|
-
let entityType = inferEntityType(name, filePath);
|
|
61
|
-
|
|
62
|
-
// Check decorators (NestJS)
|
|
63
|
-
const decorators = nodePath.node.decorators || [];
|
|
64
|
-
for (const dec of decorators) {
|
|
65
|
-
const decName = dec.expression?.callee?.name || dec.expression?.name;
|
|
66
|
-
if (decName === 'Controller') entityType = 'controller';
|
|
67
|
-
if (decName === 'Injectable') entityType = 'service';
|
|
68
|
-
if (decName === 'Module') entityType = 'middleware';
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Extract methods
|
|
72
|
-
const methods = [];
|
|
73
|
-
for (const member of nodePath.node.body.body) {
|
|
74
|
-
if (member.type === 'ClassMethod' && member.key?.name) {
|
|
75
|
-
const methodDecorators = member.decorators || [];
|
|
76
|
-
const httpMethod = methodDecorators.find(d =>
|
|
77
|
-
['Get', 'Post', 'Put', 'Patch', 'Delete'].includes(d.expression?.callee?.name)
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
if (httpMethod) {
|
|
81
|
-
const method = httpMethod.expression.callee.name.toUpperCase();
|
|
82
|
-
const routePath = httpMethod.expression.arguments?.[0]?.value || '/';
|
|
83
|
-
entities.push({
|
|
84
|
-
entityType: 'route',
|
|
85
|
-
name: `${method} ${routePath}`,
|
|
86
|
-
system: 'backend',
|
|
87
|
-
data: { method, path: routePath, handler: member.key.name },
|
|
88
|
-
metadata: { sourceFile: filePath, line: member.loc?.start?.line },
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
methods.push({
|
|
93
|
-
name: member.key.name,
|
|
94
|
-
params: member.params.map(p => p.name || p.left?.name || 'arg').filter(Boolean),
|
|
95
|
-
isAsync: member.async,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
entities.push({
|
|
101
|
-
entityType,
|
|
102
|
-
name,
|
|
103
|
-
system,
|
|
104
|
-
data: { parentClass: parent, methods },
|
|
105
|
-
metadata: { sourceFile: filePath, line: nodePath.node.loc?.start?.line },
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
if (parent) {
|
|
109
|
-
relations.push({
|
|
110
|
-
source: name, target: parent,
|
|
111
|
-
sourceType: entityType, targetType: 'unknown',
|
|
112
|
-
relation: 'extends',
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
|
|
117
|
-
// ─── Arrow/Function Exports ─────────────────────────────
|
|
118
|
-
ExportNamedDeclaration(nodePath) {
|
|
119
|
-
const decl = nodePath.node.declaration;
|
|
120
|
-
if (!decl) return;
|
|
121
|
-
|
|
122
|
-
if (decl.type === 'FunctionDeclaration' && decl.id?.name) {
|
|
123
|
-
const name = decl.id.name;
|
|
124
|
-
entities.push({
|
|
125
|
-
entityType: inferEntityType(name, filePath),
|
|
126
|
-
name,
|
|
127
|
-
system,
|
|
128
|
-
data: {
|
|
129
|
-
params: decl.params.map(p => p.name || p.left?.name || 'arg').filter(Boolean),
|
|
130
|
-
isAsync: decl.async,
|
|
131
|
-
},
|
|
132
|
-
metadata: { sourceFile: filePath, line: decl.loc?.start?.line },
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (decl.type === 'VariableDeclaration') {
|
|
137
|
-
for (const declarator of decl.declarations) {
|
|
138
|
-
if (declarator.id?.name && declarator.init) {
|
|
139
|
-
const name = declarator.id.name;
|
|
140
|
-
// Skip Router vars
|
|
141
|
-
if (name === 'router' || name === 'Router') continue;
|
|
142
|
-
|
|
143
|
-
// Infer type from file path + name
|
|
144
|
-
let entityType = inferEntityType(name, filePath);
|
|
145
|
-
|
|
146
|
-
// Arrow function exports in controller files → controller entities
|
|
147
|
-
if (declarator.init.type === 'ArrowFunctionExpression' || declarator.init.type === 'FunctionExpression') {
|
|
148
|
-
// Check if params suggest it's a request handler (req, res)
|
|
149
|
-
const params = declarator.init.params?.map(p => p.name || p.left?.name || '').filter(Boolean) || [];
|
|
150
|
-
const isHandler = params.some(p => /^(req|request|ctx)$/i.test(p));
|
|
151
|
-
if (isHandler && /controller/i.test(filePath)) {
|
|
152
|
-
entityType = 'controller';
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Check if it's a React component (starts with uppercase, returns JSX)
|
|
157
|
-
if (/^[A-Z]/.test(name) && entityType === 'service') {
|
|
158
|
-
entityType = 'screen';
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
entities.push({
|
|
162
|
-
entityType,
|
|
163
|
-
name,
|
|
164
|
-
system,
|
|
165
|
-
data: { componentType: 'functional' },
|
|
166
|
-
metadata: { sourceFile: filePath, line: declarator.loc?.start?.line },
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
},
|
|
172
|
-
|
|
173
|
-
// ─── Express Route Calls ────────────────────────────────
|
|
174
|
-
CallExpression(nodePath) {
|
|
175
|
-
const callee = nodePath.node.callee;
|
|
176
|
-
|
|
177
|
-
// app.get('/path', handler) or router.post('/path', handler)
|
|
178
|
-
if (callee.type === 'MemberExpression') {
|
|
179
|
-
const method = callee.property?.name;
|
|
180
|
-
const obj = callee.object?.name;
|
|
181
|
-
|
|
182
|
-
if (['get', 'post', 'put', 'patch', 'delete', 'all'].includes(method) &&
|
|
183
|
-
['app', 'router', 'server'].includes(obj)) {
|
|
184
|
-
const args = nodePath.node.arguments;
|
|
185
|
-
if (args[0]?.type === 'StringLiteral') {
|
|
186
|
-
entities.push({
|
|
187
|
-
entityType: 'route',
|
|
188
|
-
name: `${method.toUpperCase()} ${args[0].value}`,
|
|
189
|
-
system: 'backend',
|
|
190
|
-
data: { method: method.toUpperCase(), path: args[0].value },
|
|
191
|
-
metadata: { sourceFile: filePath, line: nodePath.node.loc?.start?.line },
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// fetch/axios API calls
|
|
197
|
-
if (['fetch'].includes(obj) || (method && ['get', 'post', 'put', 'delete'].includes(method) && obj === 'axios')) {
|
|
198
|
-
const args = nodePath.node.arguments;
|
|
199
|
-
if (args[0]?.type === 'StringLiteral') {
|
|
200
|
-
entities.push({
|
|
201
|
-
entityType: 'api',
|
|
202
|
-
name: `${(method || 'GET').toUpperCase()} ${args[0].value}`,
|
|
203
|
-
system,
|
|
204
|
-
data: { method: (method || 'GET').toUpperCase(), path: args[0].value, scope: 'frontend-ref' },
|
|
205
|
-
metadata: { sourceFile: filePath, line: nodePath.node.loc?.start?.line },
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// mongoose.model('Name', schema)
|
|
212
|
-
if (callee.type === 'MemberExpression' &&
|
|
213
|
-
callee.property?.name === 'model' &&
|
|
214
|
-
['mongoose', 'sequelize'].includes(callee.object?.name)) {
|
|
215
|
-
const args = nodePath.node.arguments;
|
|
216
|
-
if (args[0]?.type === 'StringLiteral') {
|
|
217
|
-
entities.push({
|
|
218
|
-
entityType: 'model',
|
|
219
|
-
name: args[0].value,
|
|
220
|
-
system: 'backend',
|
|
221
|
-
data: { orm: callee.object.name },
|
|
222
|
-
metadata: { sourceFile: filePath, line: nodePath.node.loc?.start?.line },
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Firestore .collection('name')
|
|
228
|
-
if (callee.type === 'MemberExpression' &&
|
|
229
|
-
callee.property?.name === 'collection') {
|
|
230
|
-
const args = nodePath.node.arguments;
|
|
231
|
-
if (args[0]?.type === 'StringLiteral') {
|
|
232
|
-
const collName = args[0].value;
|
|
233
|
-
entities.push({
|
|
234
|
-
entityType: 'database',
|
|
235
|
-
name: `collection:${collName}`,
|
|
236
|
-
system: 'backend',
|
|
237
|
-
data: { collection: collName, dbType: 'firestore' },
|
|
238
|
-
metadata: { sourceFile: filePath, line: nodePath.node.loc?.start?.line },
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
// ─── Infer relations from imports ─────────────────────────
|
|
246
|
-
for (const [localName, sourcePath] of importMap) {
|
|
247
|
-
if (sourcePath.startsWith('.') && entities.length > 0) {
|
|
248
|
-
const importedName = path.basename(sourcePath).replace(/\.[^.]+$/, '');
|
|
249
|
-
relations.push({
|
|
250
|
-
source: entities[0].name,
|
|
251
|
-
target: importedName,
|
|
252
|
-
sourceType: entities[0].entityType,
|
|
253
|
-
targetType: 'unknown',
|
|
254
|
-
relation: 'uses',
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return { filePath, entities, relations };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Infer entity type from naming conventions
|
|
263
|
-
function inferEntityType(name, filePath) {
|
|
264
|
-
const combined = `${name} ${filePath}`.toLowerCase();
|
|
265
|
-
if (/controller/.test(combined)) return 'controller';
|
|
266
|
-
if (/middleware/.test(combined)) return 'middleware';
|
|
267
|
-
if (/service/.test(combined)) return 'service';
|
|
268
|
-
if (/model|schema|entity/.test(combined)) return 'model';
|
|
269
|
-
if (/worker|job|queue/.test(combined)) return 'worker';
|
|
270
|
-
if (/route/.test(combined)) return 'route';
|
|
271
|
-
if (/screen|page|view|layout/.test(combined)) return 'screen';
|
|
272
|
-
if (/widget|component/.test(combined)) return 'widget';
|
|
273
|
-
return 'service';
|
|
274
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
|
|
3
|
-
function parsePackageJson(content, filePath) {
|
|
4
|
-
try {
|
|
5
|
-
const pkg = JSON.parse(content);
|
|
6
|
-
return {
|
|
7
|
-
entities: [{
|
|
8
|
-
id: `${filePath}::package.json`,
|
|
9
|
-
name: pkg.name || 'package',
|
|
10
|
-
type: 'config',
|
|
11
|
-
filePath,
|
|
12
|
-
attributes: { scriptsCount: Object.keys(pkg.scripts || {}).length, depsCount: Object.keys(pkg.dependencies || {}).length }
|
|
13
|
-
}],
|
|
14
|
-
relations: []
|
|
15
|
-
};
|
|
16
|
-
} catch { return { entities: [], relations: [] }; }
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function parseDockerCompose(content, filePath) {
|
|
20
|
-
const entities = [];
|
|
21
|
-
const serviceRegex = /^ ([a-zA-Z0-9_-]+):\s*$/gm;
|
|
22
|
-
let m;
|
|
23
|
-
while ((m = serviceRegex.exec(content)) !== null) {
|
|
24
|
-
if (!['version', 'services', 'networks', 'volumes'].includes(m[1])) {
|
|
25
|
-
entities.push({ id: `${filePath}::${m[1]}`, name: m[1], type: 'infrastructure', filePath, attributes: {} });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return { entities, relations: [] };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function parsePrismaSchema(content, filePath) {
|
|
32
|
-
const entities = [];
|
|
33
|
-
const modelRegex = /^model\s+(\w+)\s*\{/gm;
|
|
34
|
-
let m;
|
|
35
|
-
while ((m = modelRegex.exec(content)) !== null) {
|
|
36
|
-
entities.push({ id: `${filePath}::${m[1]}`, name: m[1], type: 'model', filePath, attributes: {} });
|
|
37
|
-
}
|
|
38
|
-
return { entities, relations: [] };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function parse(content, filePath) {
|
|
42
|
-
const base = path.basename(filePath);
|
|
43
|
-
if (base === 'package.json') return parsePackageJson(content, filePath);
|
|
44
|
-
if (base === 'docker-compose.yml' || base === 'docker-compose.yaml') return parseDockerCompose(content, filePath);
|
|
45
|
-
if (base.endsWith('.prisma')) return parsePrismaSchema(content, filePath);
|
|
46
|
-
return { entities: [], relations: [] };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export { parse, parsePackageJson, parseDockerCompose, parsePrismaSchema };
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
|
|
3
|
-
function buildDependencyGraph(packageJsonContent, sourceFiles = []) {
|
|
4
|
-
let pkg;
|
|
5
|
-
try { pkg = JSON.parse(packageJsonContent); } catch { return { entities: [], relations: [] }; }
|
|
6
|
-
|
|
7
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
8
|
-
const entities = Object.keys(allDeps).map(name => ({
|
|
9
|
-
id: `dep::${name}`,
|
|
10
|
-
name,
|
|
11
|
-
type: 'dependency',
|
|
12
|
-
filePath: 'package.json',
|
|
13
|
-
attributes: { version: allDeps[name], isDev: name in (pkg.devDependencies || {}) }
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
const relations = [];
|
|
17
|
-
for (const { content, filePath } of sourceFiles) {
|
|
18
|
-
const importRegex = /(?:import|require)\s*(?:\(?\s*['"]([^'"]+)['"]|.*from\s+['"]([^'"]+)['"])/g;
|
|
19
|
-
let m;
|
|
20
|
-
while ((m = importRegex.exec(content)) !== null) {
|
|
21
|
-
const dep = m[1] || m[2];
|
|
22
|
-
if (dep && !dep.startsWith('.') && allDeps[dep]) {
|
|
23
|
-
relations.push({ from: filePath, to: `dep::${dep}`, type: 'depends_on' });
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return { entities, relations };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export { buildDependencyGraph };
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Flutter / Dart parser — extracts entities from .dart files.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const CLASS_REGEX = /class\s+(\w+)\s*(?:extends\s+(\w+))?\s*(?:with\s+([\w,\s]+))?\s*(?:implements\s+([\w,\s]+))?\s*\{/g;
|
|
9
|
-
const STATEFUL_REGEX = /class\s+(\w+)\s+extends\s+State<(\w+)>/g;
|
|
10
|
-
const ROUTE_REGEX = /['"`]\/([^'"`]+)['"`]\s*:\s*(?:\(.*?\)\s*=>)?\s*(\w+)/g;
|
|
11
|
-
const NAV_REGEX = /Navigator\.\w+\s*\(\s*[^,]*?['"`]\/([^'"`]+)['"`]/g;
|
|
12
|
-
// Matches http.get('…'), dio.post('…'), and the idiomatic
|
|
13
|
-
// http.get(Uri.parse('…')) / Uri.https('host', '/path') forms.
|
|
14
|
-
const HTTP_REGEX = /(?:http|dio|client|_client|_dio)\s*\.\s*(get|post|put|patch|delete)\s*\(\s*(?:Uri\.parse\s*\(\s*)?['"`]([^'"`]+)['"`]/gi;
|
|
15
|
-
const PROVIDER_REGEX = /(?:ChangeNotifier|Provider|Bloc|Cubit)\s*(?:<[^>]+>)?\s*/g;
|
|
16
|
-
|
|
17
|
-
export function parseFlutterFile(filePath) {
|
|
18
|
-
const source = fs.readFileSync(filePath, 'utf-8');
|
|
19
|
-
const entities = [];
|
|
20
|
-
const relations = [];
|
|
21
|
-
const fileName = path.basename(filePath, '.dart');
|
|
22
|
-
|
|
23
|
-
let system = 'app';
|
|
24
|
-
if (/admin/i.test(filePath)) system = 'admin';
|
|
25
|
-
else if (/web/i.test(filePath)) system = 'web';
|
|
26
|
-
|
|
27
|
-
let match;
|
|
28
|
-
|
|
29
|
-
// ─── Classes → Screens / Widgets / Models ─────────────────
|
|
30
|
-
CLASS_REGEX.lastIndex = 0;
|
|
31
|
-
while ((match = CLASS_REGEX.exec(source)) !== null) {
|
|
32
|
-
const className = match[1];
|
|
33
|
-
const parent = match[2] || '';
|
|
34
|
-
|
|
35
|
-
// Skip State classes (handled by StatefulWidget)
|
|
36
|
-
if (parent.startsWith('State<')) continue;
|
|
37
|
-
|
|
38
|
-
let entityType = 'widget';
|
|
39
|
-
if (/Screen|Page|View|Layout/i.test(className) || /StatelessWidget|StatefulWidget/.test(parent)) {
|
|
40
|
-
entityType = /Screen|Page|View/i.test(className) ? 'screen' : 'widget';
|
|
41
|
-
} else if (/Model|Entity|DTO/i.test(className)) {
|
|
42
|
-
entityType = 'model';
|
|
43
|
-
} else if (/Controller|Bloc|Cubit|Provider|Notifier/i.test(className) || /ChangeNotifier|Bloc|Cubit/.test(parent)) {
|
|
44
|
-
entityType = 'controller';
|
|
45
|
-
} else if (/Service|Repository|Api|Client/i.test(className)) {
|
|
46
|
-
entityType = 'service';
|
|
47
|
-
} else if (/Middleware/i.test(className)) {
|
|
48
|
-
entityType = 'middleware';
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
entities.push({
|
|
52
|
-
entityType,
|
|
53
|
-
name: className,
|
|
54
|
-
system,
|
|
55
|
-
data: { parentClass: parent || null },
|
|
56
|
-
metadata: { sourceFile: filePath },
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
if (parent && parent !== 'StatelessWidget' && parent !== 'StatefulWidget') {
|
|
60
|
-
relations.push({
|
|
61
|
-
source: className, target: parent,
|
|
62
|
-
sourceType: entityType, targetType: 'unknown',
|
|
63
|
-
relation: 'extends',
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ─── HTTP Calls ───────────────────────────────────────────
|
|
69
|
-
HTTP_REGEX.lastIndex = 0;
|
|
70
|
-
while ((match = HTTP_REGEX.exec(source)) !== null) {
|
|
71
|
-
// Only the pathname is comparable to backend routes — drop the origin.
|
|
72
|
-
const apiPath = match[2].replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]+/i, '') || '/';
|
|
73
|
-
entities.push({
|
|
74
|
-
entityType: 'api',
|
|
75
|
-
name: `${match[1].toUpperCase()} ${apiPath}`,
|
|
76
|
-
system,
|
|
77
|
-
data: { method: match[1].toUpperCase(), path: apiPath, scope: 'frontend-ref' },
|
|
78
|
-
metadata: { sourceFile: filePath },
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// ─── Navigation Relations ─────────────────────────────────
|
|
83
|
-
NAV_REGEX.lastIndex = 0;
|
|
84
|
-
while ((match = NAV_REGEX.exec(source)) !== null) {
|
|
85
|
-
const route = match[1];
|
|
86
|
-
if (entities.length > 0) {
|
|
87
|
-
relations.push({
|
|
88
|
-
source: entities[0].name,
|
|
89
|
-
target: route,
|
|
90
|
-
sourceType: entities[0].entityType,
|
|
91
|
-
targetType: 'screen',
|
|
92
|
-
relation: 'navigates',
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return { filePath, entities, relations };
|
|
98
|
-
}
|
package/src/parsers/goParser.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Go parser — extracts backend entities from Go source files.
|
|
3
|
-
* Detects: structs (controller/service/model), HTTP handlers,
|
|
4
|
-
* Gin routes, and Echo routes.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// type UserHandler struct {
|
|
8
|
-
const STRUCT_REGEX = /^type\s+(\w+)\s+struct\s*\{/gm;
|
|
9
|
-
|
|
10
|
-
// func (h *SomeHandler) MethodName(w http.ResponseWriter, r *http.Request)
|
|
11
|
-
const HTTP_HANDLER_REGEX = /^func\s+\(\w+\s+\*?(\w+)\)\s+(\w+)\s*\(\s*\w+\s+http\.ResponseWriter\s*,\s*\w+\s+\*http\.Request\s*\)/gm;
|
|
12
|
-
|
|
13
|
-
// Gin routes: r.GET("/path", handler) / r.POST("/path", handler) etc.
|
|
14
|
-
const GIN_ROUTE_REGEX = /\b\w+\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(\s*"([^"]+)"\s*,\s*(\w+(?:\.\w+)?)\s*\)/g;
|
|
15
|
-
|
|
16
|
-
// Echo routes: e.GET("/path", handler)
|
|
17
|
-
const ECHO_ROUTE_REGEX = /\be\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(\s*"([^"]+)"\s*,\s*(\w+(?:\.\w+)?)\s*\)/g;
|
|
18
|
-
|
|
19
|
-
function classifyStruct(name) {
|
|
20
|
-
if (/Handler$/.test(name)) return 'controller';
|
|
21
|
-
if (/Service$/.test(name)) return 'service';
|
|
22
|
-
return 'model';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function parse(content, filePath) {
|
|
26
|
-
const entities = [];
|
|
27
|
-
const relations = [];
|
|
28
|
-
let m;
|
|
29
|
-
|
|
30
|
-
// ─── Structs ──────────────────────────────────────────────────
|
|
31
|
-
STRUCT_REGEX.lastIndex = 0;
|
|
32
|
-
while ((m = STRUCT_REGEX.exec(content)) !== null) {
|
|
33
|
-
const name = m[1];
|
|
34
|
-
entities.push({
|
|
35
|
-
id: `${filePath}::${name}`,
|
|
36
|
-
name,
|
|
37
|
-
type: classifyStruct(name),
|
|
38
|
-
filePath,
|
|
39
|
-
attributes: {},
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ─── HTTP handler methods ─────────────────────────────────────
|
|
44
|
-
HTTP_HANDLER_REGEX.lastIndex = 0;
|
|
45
|
-
while ((m = HTTP_HANDLER_REGEX.exec(content)) !== null) {
|
|
46
|
-
const receiver = m[1];
|
|
47
|
-
const methodName = m[2];
|
|
48
|
-
const name = `${receiver}.${methodName}`;
|
|
49
|
-
if (!entities.find(e => e.id === `${filePath}::${name}`)) {
|
|
50
|
-
entities.push({
|
|
51
|
-
id: `${filePath}::${name}`,
|
|
52
|
-
name,
|
|
53
|
-
type: 'api',
|
|
54
|
-
filePath,
|
|
55
|
-
attributes: { receiver, method: methodName },
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ─── Gin routes ───────────────────────────────────────────────
|
|
61
|
-
GIN_ROUTE_REGEX.lastIndex = 0;
|
|
62
|
-
while ((m = GIN_ROUTE_REGEX.exec(content)) !== null) {
|
|
63
|
-
const httpMethod = m[1].toUpperCase();
|
|
64
|
-
const routePath = m[2];
|
|
65
|
-
const handler = m[3];
|
|
66
|
-
const name = `${httpMethod} ${routePath}`;
|
|
67
|
-
if (!entities.find(e => e.id === `${filePath}::${name}`)) {
|
|
68
|
-
entities.push({
|
|
69
|
-
id: `${filePath}::${name}`,
|
|
70
|
-
name,
|
|
71
|
-
type: 'api',
|
|
72
|
-
filePath,
|
|
73
|
-
attributes: { method: httpMethod, path: routePath, handler, framework: 'gin' },
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ─── Echo routes ──────────────────────────────────────────────
|
|
79
|
-
ECHO_ROUTE_REGEX.lastIndex = 0;
|
|
80
|
-
while ((m = ECHO_ROUTE_REGEX.exec(content)) !== null) {
|
|
81
|
-
const httpMethod = m[1].toUpperCase();
|
|
82
|
-
const routePath = m[2];
|
|
83
|
-
const handler = m[3];
|
|
84
|
-
const name = `${httpMethod} ${routePath}`;
|
|
85
|
-
if (!entities.find(e => e.id === `${filePath}::${name}`)) {
|
|
86
|
-
entities.push({
|
|
87
|
-
id: `${filePath}::${name}`,
|
|
88
|
-
name,
|
|
89
|
-
type: 'api',
|
|
90
|
-
filePath,
|
|
91
|
-
attributes: { method: httpMethod, path: routePath, handler, framework: 'echo' },
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return { entities, relations };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export { parse };
|