@launchsecure/launch-kit 0.0.3 → 0.0.5
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/dist/chart-client/assets/index-BUih0oqR.js +358 -0
- package/dist/chart-client/assets/index-DFslt72L.css +1 -0
- package/dist/chart-client/index.html +21 -0
- package/dist/client/assets/index-BCxRNp8I.css +32 -0
- package/dist/client/assets/{index-CcHIoRl6.js → index-DCC--GO-.js} +68 -63
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +2285 -0
- package/dist/server/cli.js +1222 -120
- package/dist/server/graph-mcp-entry.js +1879 -300
- package/package.json +7 -3
- package/dist/client/assets/index-C8GAsRGO.css +0 -32
|
@@ -0,0 +1,2285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/server/chart-serve.ts
|
|
31
|
+
var chart_serve_exports = {};
|
|
32
|
+
__export(chart_serve_exports, {
|
|
33
|
+
runServeCli: () => runServeCli,
|
|
34
|
+
startChartServer: () => startChartServer
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(chart_serve_exports);
|
|
37
|
+
var import_node_http = __toESM(require("node:http"));
|
|
38
|
+
var import_node_fs11 = __toESM(require("node:fs"));
|
|
39
|
+
var import_node_path12 = __toESM(require("node:path"));
|
|
40
|
+
|
|
41
|
+
// src/server/graph/index.ts
|
|
42
|
+
var import_node_fs9 = require("node:fs");
|
|
43
|
+
var import_node_path10 = require("node:path");
|
|
44
|
+
|
|
45
|
+
// src/server/graph/core/graph-builder.ts
|
|
46
|
+
var import_node_fs8 = require("node:fs");
|
|
47
|
+
var import_node_path9 = require("node:path");
|
|
48
|
+
|
|
49
|
+
// src/server/graph/core/config.ts
|
|
50
|
+
var import_node_fs = require("node:fs");
|
|
51
|
+
var import_node_path = require("node:path");
|
|
52
|
+
var CONFIG_FILENAME = ".launchchart.json";
|
|
53
|
+
function loadConfig(rootDir) {
|
|
54
|
+
const configPath = (0, import_node_path.join)(rootDir, CONFIG_FILENAME);
|
|
55
|
+
if (!(0, import_node_fs.existsSync)(configPath)) return {};
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
|
|
58
|
+
} catch {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/server/graph/core/parser-registry.ts
|
|
64
|
+
var import_node_path8 = require("node:path");
|
|
65
|
+
|
|
66
|
+
// src/server/graph/parsers/ui/react-nextjs.ts
|
|
67
|
+
var import_node_fs3 = require("node:fs");
|
|
68
|
+
var import_node_path3 = require("node:path");
|
|
69
|
+
|
|
70
|
+
// src/server/graph/core/ast-helpers.ts
|
|
71
|
+
var import_node_fs2 = require("node:fs");
|
|
72
|
+
var import_node_path2 = require("node:path");
|
|
73
|
+
var tsModule;
|
|
74
|
+
function getTs() {
|
|
75
|
+
if (!tsModule) {
|
|
76
|
+
tsModule = require("typescript");
|
|
77
|
+
}
|
|
78
|
+
return tsModule;
|
|
79
|
+
}
|
|
80
|
+
var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
81
|
+
function parseFile(absPath) {
|
|
82
|
+
const ts = getTs();
|
|
83
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
84
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
85
|
+
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
|
|
86
|
+
const sourceFile = ts.createSourceFile(
|
|
87
|
+
absPath,
|
|
88
|
+
content,
|
|
89
|
+
ts.ScriptTarget.Latest,
|
|
90
|
+
/* setParentNodes */
|
|
91
|
+
true,
|
|
92
|
+
scriptKind
|
|
93
|
+
);
|
|
94
|
+
const exportsSet = /* @__PURE__ */ new Set();
|
|
95
|
+
const exportsOrdered = [];
|
|
96
|
+
let defaultName = null;
|
|
97
|
+
let firstValueExport = null;
|
|
98
|
+
let firstTypeExport = null;
|
|
99
|
+
const imports = [];
|
|
100
|
+
const reExports = [];
|
|
101
|
+
const jsxElements = /* @__PURE__ */ new Set();
|
|
102
|
+
const navigations = [];
|
|
103
|
+
const fetchCalls = [];
|
|
104
|
+
const includeConcat = process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1";
|
|
105
|
+
function addExport(name2, kind) {
|
|
106
|
+
if (!exportsSet.has(name2)) {
|
|
107
|
+
exportsSet.add(name2);
|
|
108
|
+
exportsOrdered.push(name2);
|
|
109
|
+
}
|
|
110
|
+
if (kind === "default") defaultName = name2;
|
|
111
|
+
else if (kind === "value" && !firstValueExport) firstValueExport = name2;
|
|
112
|
+
else if (kind === "type" && !firstTypeExport) firstTypeExport = name2;
|
|
113
|
+
}
|
|
114
|
+
function hasModifier(node, kind) {
|
|
115
|
+
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
|
|
116
|
+
return modifiers?.some((m) => m.kind === kind) ?? false;
|
|
117
|
+
}
|
|
118
|
+
function extractTargetFromExpression(expr) {
|
|
119
|
+
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
|
|
120
|
+
return { target: expr.text, isTemplate: false };
|
|
121
|
+
}
|
|
122
|
+
if (ts.isTemplateExpression(expr)) {
|
|
123
|
+
return { target: expr.getText(sourceFile), isTemplate: true };
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
function looksLikeUrl(s) {
|
|
128
|
+
return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
|
|
129
|
+
}
|
|
130
|
+
function templateStartsWithSlash(expr) {
|
|
131
|
+
const head = expr.head.text;
|
|
132
|
+
return head.startsWith("/");
|
|
133
|
+
}
|
|
134
|
+
function extractUrlFromFetchArg(arg) {
|
|
135
|
+
if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
|
|
136
|
+
if (!looksLikeUrl(arg.text)) return null;
|
|
137
|
+
return { url: arg.text, isTemplate: false };
|
|
138
|
+
}
|
|
139
|
+
if (ts.isTemplateExpression(arg)) {
|
|
140
|
+
if (!templateStartsWithSlash(arg)) return null;
|
|
141
|
+
return { url: arg.getText(sourceFile), isTemplate: true };
|
|
142
|
+
}
|
|
143
|
+
if (includeConcat && ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
144
|
+
let leftmost = arg;
|
|
145
|
+
while (ts.isBinaryExpression(leftmost) && leftmost.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
146
|
+
leftmost = leftmost.left;
|
|
147
|
+
}
|
|
148
|
+
if ((ts.isStringLiteral(leftmost) || ts.isNoSubstitutionTemplateLiteral(leftmost)) && leftmost.text.startsWith("/")) {
|
|
149
|
+
return { url: arg.getText(sourceFile), isTemplate: false, isConcat: true };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
function visit(node) {
|
|
155
|
+
if (ts.isImportDeclaration(node)) {
|
|
156
|
+
const moduleSpec = node.moduleSpecifier;
|
|
157
|
+
if (ts.isStringLiteral(moduleSpec)) {
|
|
158
|
+
const specifier = moduleSpec.text;
|
|
159
|
+
const clause = node.importClause;
|
|
160
|
+
const isTypeOnly = !!clause?.isTypeOnly;
|
|
161
|
+
const names = [];
|
|
162
|
+
const typeNames = /* @__PURE__ */ new Set();
|
|
163
|
+
if (clause) {
|
|
164
|
+
if (clause.name) names.push(clause.name.text);
|
|
165
|
+
const nb = clause.namedBindings;
|
|
166
|
+
if (nb && ts.isNamedImports(nb)) {
|
|
167
|
+
for (const el of nb.elements) {
|
|
168
|
+
names.push(el.name.text);
|
|
169
|
+
if (el.isTypeOnly) typeNames.add(el.name.text);
|
|
170
|
+
}
|
|
171
|
+
} else if (nb && ts.isNamespaceImport(nb)) {
|
|
172
|
+
names.push(nb.name.text);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (names.length > 0 || isTypeOnly) {
|
|
176
|
+
imports.push({ names, specifier, isTypeOnly, typeNames });
|
|
177
|
+
} else if (!clause) {
|
|
178
|
+
imports.push({ names: [], specifier, isTypeOnly: false, typeNames: /* @__PURE__ */ new Set() });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (ts.isExportDeclaration(node)) {
|
|
183
|
+
const fromSpec = node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : null;
|
|
184
|
+
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
185
|
+
for (const el of node.exportClause.elements) {
|
|
186
|
+
const exportedName = el.name.text;
|
|
187
|
+
addExport(exportedName, "value");
|
|
188
|
+
if (fromSpec) {
|
|
189
|
+
reExports.push({ name: exportedName, from: fromSpec });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} else if (!node.exportClause && fromSpec) {
|
|
193
|
+
reExports.push({ name: "*", from: fromSpec, isWildcard: true });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
197
|
+
const arg = node.arguments[0];
|
|
198
|
+
if (arg && ts.isStringLiteral(arg)) {
|
|
199
|
+
imports.push({
|
|
200
|
+
names: [],
|
|
201
|
+
specifier: arg.text,
|
|
202
|
+
isTypeOnly: false,
|
|
203
|
+
typeNames: /* @__PURE__ */ new Set()
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (ts.isExportAssignment(node) && !node.isExportEquals) {
|
|
208
|
+
if (ts.isIdentifier(node.expression)) {
|
|
209
|
+
addExport(node.expression.text, "default");
|
|
210
|
+
} else {
|
|
211
|
+
if (!defaultName) defaultName = "default";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (ts.isFunctionDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
|
|
215
|
+
const isDefault = hasModifier(node, ts.SyntaxKind.DefaultKeyword);
|
|
216
|
+
if (node.name) addExport(node.name.text, isDefault ? "default" : "value");
|
|
217
|
+
}
|
|
218
|
+
if (ts.isVariableStatement(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
|
|
219
|
+
for (const decl of node.declarationList.declarations) {
|
|
220
|
+
if (ts.isIdentifier(decl.name)) {
|
|
221
|
+
addExport(decl.name.text, "value");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (ts.isClassDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
|
|
226
|
+
const isDefault = hasModifier(node, ts.SyntaxKind.DefaultKeyword);
|
|
227
|
+
if (node.name) addExport(node.name.text, isDefault ? "default" : "value");
|
|
228
|
+
}
|
|
229
|
+
if (ts.isTypeAliasDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
|
|
230
|
+
addExport(node.name.text, "type");
|
|
231
|
+
}
|
|
232
|
+
if (ts.isInterfaceDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
|
|
233
|
+
addExport(node.name.text, "type");
|
|
234
|
+
}
|
|
235
|
+
if (ts.isEnumDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
|
|
236
|
+
addExport(node.name.text, "value");
|
|
237
|
+
}
|
|
238
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
239
|
+
const tagName = node.tagName;
|
|
240
|
+
if (ts.isIdentifier(tagName) && /^[A-Z]/.test(tagName.text)) {
|
|
241
|
+
jsxElements.add(tagName.text);
|
|
242
|
+
} else if (ts.isPropertyAccessExpression(tagName)) {
|
|
243
|
+
let root = tagName;
|
|
244
|
+
while (ts.isPropertyAccessExpression(root)) root = root.expression;
|
|
245
|
+
if (ts.isIdentifier(root) && /^[A-Z]/.test(root.text)) {
|
|
246
|
+
jsxElements.add(root.text);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (ts.isCallExpression(node)) {
|
|
251
|
+
const expr = node.expression;
|
|
252
|
+
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && expr.expression.text === "router" && (expr.name.text === "push" || expr.name.text === "replace")) {
|
|
253
|
+
const arg = node.arguments[0];
|
|
254
|
+
if (arg) {
|
|
255
|
+
const parsed = extractTargetFromExpression(arg);
|
|
256
|
+
if (parsed) {
|
|
257
|
+
navigations.push({
|
|
258
|
+
kind: expr.name.text === "push" ? "router-push" : "router-replace",
|
|
259
|
+
target: parsed.target,
|
|
260
|
+
isTemplate: parsed.isTemplate
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (ts.isCallExpression(node) && node.arguments.length > 0) {
|
|
267
|
+
const expr = node.expression;
|
|
268
|
+
const firstArg = node.arguments[0];
|
|
269
|
+
if (ts.isIdentifier(expr) && expr.text === "fetch") {
|
|
270
|
+
const extracted = extractUrlFromFetchArg(firstArg);
|
|
271
|
+
if (extracted) {
|
|
272
|
+
fetchCalls.push({
|
|
273
|
+
url: extracted.url,
|
|
274
|
+
isTemplate: extracted.isTemplate,
|
|
275
|
+
...extracted.isConcat ? { isConcat: true } : {},
|
|
276
|
+
kind: "fetch"
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) {
|
|
281
|
+
const methodName = expr.name.text;
|
|
282
|
+
if (HTTP_METHODS.has(methodName)) {
|
|
283
|
+
const extracted = extractUrlFromFetchArg(firstArg);
|
|
284
|
+
if (extracted) {
|
|
285
|
+
fetchCalls.push({
|
|
286
|
+
method: methodName.toUpperCase(),
|
|
287
|
+
url: extracted.url,
|
|
288
|
+
isTemplate: extracted.isTemplate,
|
|
289
|
+
...extracted.isConcat ? { isConcat: true } : {},
|
|
290
|
+
kind: "client-method"
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
297
|
+
const tagName = node.tagName;
|
|
298
|
+
if (ts.isIdentifier(tagName) && tagName.text === "Link") {
|
|
299
|
+
for (const attr of node.attributes.properties) {
|
|
300
|
+
if (ts.isJsxAttribute(attr) && attr.name.getText(sourceFile) === "href" && attr.initializer) {
|
|
301
|
+
const init = attr.initializer;
|
|
302
|
+
if (ts.isStringLiteral(init)) {
|
|
303
|
+
navigations.push({ kind: "link-href", target: init.text, isTemplate: false });
|
|
304
|
+
} else if (ts.isJsxExpression(init) && init.expression) {
|
|
305
|
+
const parsed = extractTargetFromExpression(init.expression);
|
|
306
|
+
if (parsed) {
|
|
307
|
+
navigations.push({ kind: "link-href", target: parsed.target, isTemplate: parsed.isTemplate });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
315
|
+
const left = node.left;
|
|
316
|
+
if (ts.isPropertyAccessExpression(left) && ts.isPropertyAccessExpression(left.expression) && ts.isIdentifier(left.expression.expression) && left.expression.expression.text === "window" && left.expression.name.text === "location" && left.name.text === "href") {
|
|
317
|
+
const parsed = extractTargetFromExpression(node.right);
|
|
318
|
+
if (parsed) {
|
|
319
|
+
navigations.push({ kind: "window-location", target: parsed.target, isTemplate: parsed.isTemplate });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (ts.isCallExpression(node)) {
|
|
324
|
+
const expr = node.expression;
|
|
325
|
+
if (ts.isPropertyAccessExpression(expr) && ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.expression) && expr.expression.expression.text === "window" && expr.expression.name.text === "location" && (expr.name.text === "assign" || expr.name.text === "replace")) {
|
|
326
|
+
const arg = node.arguments[0];
|
|
327
|
+
if (arg) {
|
|
328
|
+
const parsed = extractTargetFromExpression(arg);
|
|
329
|
+
if (parsed) {
|
|
330
|
+
navigations.push({ kind: "window-location", target: parsed.target, isTemplate: parsed.isTemplate });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
ts.forEachChild(node, visit);
|
|
336
|
+
}
|
|
337
|
+
visit(sourceFile);
|
|
338
|
+
const name = defaultName ?? firstValueExport ?? firstTypeExport ?? "";
|
|
339
|
+
return {
|
|
340
|
+
name,
|
|
341
|
+
exports: exportsOrdered,
|
|
342
|
+
imports,
|
|
343
|
+
reExports,
|
|
344
|
+
jsxElements,
|
|
345
|
+
navigations,
|
|
346
|
+
fetchCalls
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
var MUTATION_METHODS = /* @__PURE__ */ new Set([
|
|
350
|
+
"create",
|
|
351
|
+
"createMany",
|
|
352
|
+
"createManyAndReturn",
|
|
353
|
+
"update",
|
|
354
|
+
"updateMany",
|
|
355
|
+
"updateManyAndReturn",
|
|
356
|
+
"upsert",
|
|
357
|
+
"delete",
|
|
358
|
+
"deleteMany"
|
|
359
|
+
]);
|
|
360
|
+
function extractDbCalls(absPath) {
|
|
361
|
+
const ts = getTs();
|
|
362
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
363
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
364
|
+
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
365
|
+
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
366
|
+
const calls = [];
|
|
367
|
+
const seen = /* @__PURE__ */ new Set();
|
|
368
|
+
function visit(node) {
|
|
369
|
+
if (ts.isCallExpression(node)) {
|
|
370
|
+
const expr = node.expression;
|
|
371
|
+
if (ts.isPropertyAccessExpression(expr) && ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.expression) && expr.expression.expression.text === "db") {
|
|
372
|
+
const model = expr.expression.name.text;
|
|
373
|
+
const method = expr.name.text;
|
|
374
|
+
const key = `${model}.${method}`;
|
|
375
|
+
if (!seen.has(key)) {
|
|
376
|
+
seen.add(key);
|
|
377
|
+
calls.push({
|
|
378
|
+
model,
|
|
379
|
+
method,
|
|
380
|
+
isMutation: MUTATION_METHODS.has(method)
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
ts.forEachChild(node, visit);
|
|
386
|
+
}
|
|
387
|
+
visit(sourceFile);
|
|
388
|
+
return calls;
|
|
389
|
+
}
|
|
390
|
+
function extractAuthWrappers(absPath) {
|
|
391
|
+
const ts = getTs();
|
|
392
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
393
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
394
|
+
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
395
|
+
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
396
|
+
const wrappers = /* @__PURE__ */ new Set();
|
|
397
|
+
const AUTH_WRAPPERS = /* @__PURE__ */ new Set(["withAuth", "withPermission", "withRole", "requireAuth"]);
|
|
398
|
+
function visit(node) {
|
|
399
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
400
|
+
if (AUTH_WRAPPERS.has(node.expression.text)) {
|
|
401
|
+
wrappers.add(node.expression.text);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
ts.forEachChild(node, visit);
|
|
405
|
+
}
|
|
406
|
+
visit(sourceFile);
|
|
407
|
+
return wrappers;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/server/graph/parsers/ui/react-nextjs.ts
|
|
411
|
+
var RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
|
|
412
|
+
function walk(dir, exts) {
|
|
413
|
+
const results = [];
|
|
414
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return results;
|
|
415
|
+
for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
416
|
+
const full = (0, import_node_path3.join)(dir, entry.name);
|
|
417
|
+
if (entry.isDirectory()) {
|
|
418
|
+
results.push(...walk(full, exts));
|
|
419
|
+
} else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
|
|
420
|
+
results.push(full);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return results;
|
|
424
|
+
}
|
|
425
|
+
function walkWithIgnore(dir, exts, ignoreDirs) {
|
|
426
|
+
const results = [];
|
|
427
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return results;
|
|
428
|
+
for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
429
|
+
if (entry.isDirectory()) {
|
|
430
|
+
if (ignoreDirs.has(entry.name)) continue;
|
|
431
|
+
results.push(...walkWithIgnore((0, import_node_path3.join)(dir, entry.name), exts, ignoreDirs));
|
|
432
|
+
} else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
|
|
433
|
+
results.push((0, import_node_path3.join)(dir, entry.name));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return results;
|
|
437
|
+
}
|
|
438
|
+
function toNodeId(srcDir, absPath) {
|
|
439
|
+
return (0, import_node_path3.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
440
|
+
}
|
|
441
|
+
function resolveImport(srcDir, specifier) {
|
|
442
|
+
if (!specifier.startsWith("@/")) return null;
|
|
443
|
+
const rel = specifier.slice(2);
|
|
444
|
+
const base = (0, import_node_path3.join)(srcDir, rel);
|
|
445
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path3.join)(base, "index.ts"), (0, import_node_path3.join)(base, "index.tsx")]) {
|
|
446
|
+
if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
function resolveRelativeImport(fromFile, specifier) {
|
|
451
|
+
const base = (0, import_node_path3.join)((0, import_node_path3.dirname)(fromFile), specifier);
|
|
452
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path3.join)(base, "index.ts"), (0, import_node_path3.join)(base, "index.tsx")]) {
|
|
453
|
+
if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
|
|
458
|
+
const cached = memo.get(barrelAbsPath);
|
|
459
|
+
if (cached) return cached;
|
|
460
|
+
if (visiting.has(barrelAbsPath)) return /* @__PURE__ */ new Map();
|
|
461
|
+
visiting.add(barrelAbsPath);
|
|
462
|
+
const parsed = parsedByPath.get(barrelAbsPath);
|
|
463
|
+
const map = /* @__PURE__ */ new Map();
|
|
464
|
+
if (!parsed) {
|
|
465
|
+
visiting.delete(barrelAbsPath);
|
|
466
|
+
memo.set(barrelAbsPath, map);
|
|
467
|
+
return map;
|
|
468
|
+
}
|
|
469
|
+
for (const re of parsed.reExports) {
|
|
470
|
+
if (!re.from.startsWith(".")) continue;
|
|
471
|
+
const resolved = resolveRelativeImport(barrelAbsPath, re.from);
|
|
472
|
+
if (!resolved) continue;
|
|
473
|
+
if (re.isWildcard) {
|
|
474
|
+
const targetBn = (0, import_node_path3.basename)(resolved);
|
|
475
|
+
const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
|
|
476
|
+
if (targetIsBarrel) {
|
|
477
|
+
const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
|
|
478
|
+
for (const [name, target] of nested) {
|
|
479
|
+
if (!map.has(name)) map.set(name, target);
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
const targetParsed = parsedByPath.get(resolved);
|
|
483
|
+
if (targetParsed) {
|
|
484
|
+
for (const exp of targetParsed.exports) {
|
|
485
|
+
if (!map.has(exp)) map.set(exp, resolved);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
if (!map.has(re.name)) map.set(re.name, resolved);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
visiting.delete(barrelAbsPath);
|
|
494
|
+
memo.set(barrelAbsPath, map);
|
|
495
|
+
return map;
|
|
496
|
+
}
|
|
497
|
+
function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
498
|
+
const barrels = /* @__PURE__ */ new Map();
|
|
499
|
+
const memo = /* @__PURE__ */ new Map();
|
|
500
|
+
for (const [absPath, parsed] of parsedByPath) {
|
|
501
|
+
const bn = (0, import_node_path3.basename)(absPath);
|
|
502
|
+
if (bn !== "index.ts" && bn !== "index.tsx") continue;
|
|
503
|
+
if (parsed.reExports.length === 0) continue;
|
|
504
|
+
const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
|
|
505
|
+
if (map.size > 0) {
|
|
506
|
+
const barrelId = (0, import_node_path3.relative)(srcDir, (0, import_node_path3.dirname)(absPath)).replace(/\\/g, "/");
|
|
507
|
+
barrels.set(barrelId, map);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return barrels;
|
|
511
|
+
}
|
|
512
|
+
function classifyType(id) {
|
|
513
|
+
if (id.endsWith("/page.tsx")) return "page";
|
|
514
|
+
if (id.endsWith("/layout.tsx")) return "layout";
|
|
515
|
+
if (id.startsWith("client/components/ui/")) return "ui";
|
|
516
|
+
if (id.startsWith("client/components/")) return "component";
|
|
517
|
+
if (id.startsWith("client/hooks/")) return "hook";
|
|
518
|
+
if (/client\/lib\/.*-context\./.test(id)) return "context";
|
|
519
|
+
if (id.startsWith("client/lib/")) return id.includes("config") ? "config" : "util";
|
|
520
|
+
if (id.startsWith("client/api/")) return "util";
|
|
521
|
+
if (id.startsWith("server/mcp/")) return "mcp-tool";
|
|
522
|
+
if (id.startsWith("server/lib/")) return "lib";
|
|
523
|
+
if (id.startsWith("server/")) return "lib";
|
|
524
|
+
if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
|
|
525
|
+
return "component";
|
|
526
|
+
}
|
|
527
|
+
function classifyModule(id) {
|
|
528
|
+
if (/app\/\(auth\)\//.test(id)) return "auth";
|
|
529
|
+
if (/app\/\(admin\)\//.test(id)) return "admin";
|
|
530
|
+
if (/app\/\(settings\)\//.test(id)) return "settings";
|
|
531
|
+
if (/app\/\(app\)\/\[orgSlug\]\/\(project-pages\)\//.test(id)) return "project";
|
|
532
|
+
if (/app\/\(app\)\/\[orgSlug\]\/\(org-pages\)\//.test(id)) return "org";
|
|
533
|
+
if (/app\/\(app\)\/\[orgSlug\]\//.test(id)) return "org";
|
|
534
|
+
if (id.startsWith("app/integrations/")) return "integrations";
|
|
535
|
+
if (id.startsWith("app/docs/")) return "admin";
|
|
536
|
+
if (id.startsWith("client/components/ui/")) return "shared-ui";
|
|
537
|
+
if (id.startsWith("client/components/layout/") || /client\/lib\/navigation/.test(id)) return "layout";
|
|
538
|
+
if (/client\/components\/auth\//.test(id) || /client\/lib\/auth-/.test(id) || /client\/lib\/github-oauth/.test(id) || /client\/lib\/permission-service/.test(id) || /client\/hooks\/use-permissions/.test(id)) return "auth";
|
|
539
|
+
if (/client\/components\/prd-/.test(id) || /client\/hooks\/use-admin/.test(id)) return "admin";
|
|
540
|
+
if (/client\/components\/org-/.test(id) || /client\/hooks\/use-org-/.test(id) || /client\/hooks\/use-provider-def/.test(id)) return "org";
|
|
541
|
+
if (/client\/components\/project/.test(id) || /client\/hooks\/use-project-/.test(id) || /client\/hooks\/use-pipeline/.test(id) || /client\/hooks\/use-databases/.test(id) || /client\/hooks\/use-provider-env/.test(id) || /client\/hooks\/use-role-assign/.test(id) || /client\/components\/pipeline/.test(id) || /client\/components\/deployments/.test(id)) return "project";
|
|
542
|
+
if (/client\/hooks\/use-(profile|sessions|organizations|notification)/.test(id)) return "settings";
|
|
543
|
+
if (id.startsWith("server/auth/")) return "auth";
|
|
544
|
+
if (id.startsWith("server/mcp/")) return "mcp";
|
|
545
|
+
if (id.startsWith("server/lib/")) return "server-lib";
|
|
546
|
+
if (id.startsWith("server/middleware")) return "middleware";
|
|
547
|
+
if (id.startsWith("server/services/")) return "services";
|
|
548
|
+
if (id.startsWith("server/db")) return "db";
|
|
549
|
+
if (id.startsWith("server/errors")) return "errors";
|
|
550
|
+
if (id.startsWith("server/")) return "server-lib";
|
|
551
|
+
if (id.startsWith("config/")) return "config";
|
|
552
|
+
if (id.startsWith("lib/")) return "lib";
|
|
553
|
+
return "root";
|
|
554
|
+
}
|
|
555
|
+
function extractRoute(id) {
|
|
556
|
+
if (!id.endsWith("/page.tsx")) return null;
|
|
557
|
+
let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
|
|
558
|
+
route = route.replace(/\/\([^)]+\)/g, "");
|
|
559
|
+
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
560
|
+
route = route.replace(/\/+/g, "/");
|
|
561
|
+
if (!route.startsWith("/")) route = "/" + route;
|
|
562
|
+
return route || "/";
|
|
563
|
+
}
|
|
564
|
+
function nameFromFilename(absPath) {
|
|
565
|
+
return (0, import_node_path3.basename)(absPath, (0, import_node_path3.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
|
|
566
|
+
}
|
|
567
|
+
function resolveTemplateLiteralRoute(template, routeToNodeId) {
|
|
568
|
+
const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
569
|
+
const cleaned = expr.trim();
|
|
570
|
+
if (cleaned.includes(".")) {
|
|
571
|
+
const parts = cleaned.split(".");
|
|
572
|
+
const last = parts[parts.length - 1];
|
|
573
|
+
const secondLast = parts.length > 1 ? parts[parts.length - 2] : "";
|
|
574
|
+
if (last === "slug" && secondLast === "project") return ":projectSlug";
|
|
575
|
+
if (last === "slug") return ":projectSlug";
|
|
576
|
+
if (last === "id" && /cred/i.test(secondLast)) return ":credentialId";
|
|
577
|
+
if (last === "id" && /run/i.test(secondLast)) return ":runId";
|
|
578
|
+
if (last === "sha") return ":commitSha";
|
|
579
|
+
if (last === "id") return ":id";
|
|
580
|
+
return `:${last}`;
|
|
581
|
+
}
|
|
582
|
+
if (/orgSlug/i.test(cleaned)) return ":orgSlug";
|
|
583
|
+
if (/projectSlug/i.test(cleaned)) return ":projectSlug";
|
|
584
|
+
if (/runId/i.test(cleaned)) return ":runId";
|
|
585
|
+
if (/credentialId/i.test(cleaned)) return ":credentialId";
|
|
586
|
+
if (/commitSha/i.test(cleaned)) return ":commitSha";
|
|
587
|
+
if (/token/i.test(cleaned)) return ":token";
|
|
588
|
+
return `:${cleaned}`;
|
|
589
|
+
});
|
|
590
|
+
const normalized = parameterized.replace(/\/+/g, "/").replace(/\/$/, "") || "/";
|
|
591
|
+
if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
|
|
592
|
+
let bestScore = -1;
|
|
593
|
+
let bestId = null;
|
|
594
|
+
for (const [route, nodeId] of routeToNodeId) {
|
|
595
|
+
const score = routeMatchScore(normalized, route);
|
|
596
|
+
if (score > bestScore) {
|
|
597
|
+
bestScore = score;
|
|
598
|
+
bestId = nodeId;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return bestScore > 0 ? bestId : null;
|
|
602
|
+
}
|
|
603
|
+
function routeMatchScore(candidate, known) {
|
|
604
|
+
const segsA = candidate.split("/");
|
|
605
|
+
const segsB = known.split("/");
|
|
606
|
+
if (segsA.length !== segsB.length) return -1;
|
|
607
|
+
let score = 0;
|
|
608
|
+
for (let i = 0; i < segsA.length; i++) {
|
|
609
|
+
const a = segsA[i], b = segsB[i];
|
|
610
|
+
if (a === b) {
|
|
611
|
+
score += 3;
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if (a.startsWith(":") && b.startsWith(":")) {
|
|
615
|
+
score += 2;
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
if (a.startsWith(":") || b.startsWith(":")) {
|
|
619
|
+
score += 0;
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
return -1;
|
|
623
|
+
}
|
|
624
|
+
return score;
|
|
625
|
+
}
|
|
626
|
+
function templateToRoute(template) {
|
|
627
|
+
return template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
628
|
+
const cleaned = expr.trim();
|
|
629
|
+
if (cleaned.includes(".")) {
|
|
630
|
+
const parts = cleaned.split(".");
|
|
631
|
+
const last = parts[parts.length - 1];
|
|
632
|
+
const secondLast = parts.length > 1 ? parts[parts.length - 2] : "";
|
|
633
|
+
if (last === "slug" && /project/i.test(secondLast)) return ":projectSlug";
|
|
634
|
+
if (last === "slug") return ":slug";
|
|
635
|
+
if (last === "sha") return ":commitSha";
|
|
636
|
+
return `:${last}`;
|
|
637
|
+
}
|
|
638
|
+
return `:${cleaned}`;
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
function matchRouteToPage(route, routeToNodeId) {
|
|
642
|
+
const normalized = route.replace(/\/$/, "") || "/";
|
|
643
|
+
if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
|
|
647
|
+
const edges = [];
|
|
648
|
+
const flagged = [];
|
|
649
|
+
const seen = /* @__PURE__ */ new Set();
|
|
650
|
+
function addEdge(target, type, label) {
|
|
651
|
+
const key = `${sourceId}\u2192${target}\u2192${type}`;
|
|
652
|
+
if (seen.has(key)) return;
|
|
653
|
+
seen.add(key);
|
|
654
|
+
const edge = { source: sourceId, target, type };
|
|
655
|
+
if (label) edge.label = label;
|
|
656
|
+
edges.push(edge);
|
|
657
|
+
}
|
|
658
|
+
function edgeTypeFor(targetId, isTypeOnlyImport, importedNames) {
|
|
659
|
+
if (isTypeOnlyImport) return "imports";
|
|
660
|
+
const targetType = nodeTypeMap.get(targetId);
|
|
661
|
+
if (targetType && RENDER_TYPES.has(targetType)) {
|
|
662
|
+
const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
|
|
663
|
+
if (anyRendered) return "renders";
|
|
664
|
+
}
|
|
665
|
+
return "imports";
|
|
666
|
+
}
|
|
667
|
+
for (const imp of parsed.imports) {
|
|
668
|
+
const { names, specifier, isTypeOnly, typeNames } = imp;
|
|
669
|
+
if (specifier.startsWith("@/")) {
|
|
670
|
+
const relToSrc = specifier.slice(2);
|
|
671
|
+
const barrelMap = barrelMaps.get(relToSrc);
|
|
672
|
+
if (barrelMap && names.length > 0) {
|
|
673
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
674
|
+
for (const name of names) {
|
|
675
|
+
const targetAbs = barrelMap.get(name);
|
|
676
|
+
if (targetAbs) {
|
|
677
|
+
const targetId = toNodeId(srcDir, targetAbs);
|
|
678
|
+
if (nodeIdSet.has(targetId)) {
|
|
679
|
+
if (!byTarget.has(targetId)) byTarget.set(targetId, []);
|
|
680
|
+
byTarget.get(targetId).push(name);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
for (const [targetId, targetNames] of byTarget) {
|
|
685
|
+
const allType = isTypeOnly || targetNames.every((n) => typeNames.has(n));
|
|
686
|
+
addEdge(targetId, edgeTypeFor(targetId, allType, targetNames));
|
|
687
|
+
}
|
|
688
|
+
} else {
|
|
689
|
+
const resolved = resolveImport(srcDir, specifier);
|
|
690
|
+
if (resolved) {
|
|
691
|
+
const targetId = toNodeId(srcDir, resolved);
|
|
692
|
+
if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
|
|
693
|
+
addEdge(targetId, edgeTypeFor(targetId, isTypeOnly, names));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
} else if (specifier.startsWith(".")) {
|
|
698
|
+
const resolved = resolveRelativeImport(absPath, specifier);
|
|
699
|
+
if (resolved) {
|
|
700
|
+
const targetId = toNodeId(srcDir, resolved);
|
|
701
|
+
if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
|
|
702
|
+
addEdge(targetId, edgeTypeFor(targetId, isTypeOnly, names));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
for (const nav of parsed.navigations) {
|
|
708
|
+
if (nav.kind === "window-location") {
|
|
709
|
+
flagged.push({
|
|
710
|
+
source: sourceId,
|
|
711
|
+
target: "EXTERNAL",
|
|
712
|
+
type: "navigates",
|
|
713
|
+
label: `window.location to ${nav.target}`,
|
|
714
|
+
confidence: "high"
|
|
715
|
+
});
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
if (!nav.isTemplate) {
|
|
719
|
+
const targetId = matchRouteToPage(nav.target, routeToNodeId);
|
|
720
|
+
if (targetId && targetId !== sourceId) {
|
|
721
|
+
const label = nav.kind === "link-href" ? `Link to ${nav.target}` : `router.${nav.kind === "router-push" ? "push" : "replace"}('${nav.target}')`;
|
|
722
|
+
addEdge(targetId, "navigates", label);
|
|
723
|
+
}
|
|
724
|
+
} else {
|
|
725
|
+
const template = nav.target.replace(/^`|`$/g, "");
|
|
726
|
+
if (!template.includes("${")) continue;
|
|
727
|
+
const targetId = resolveTemplateLiteralRoute(template, routeToNodeId);
|
|
728
|
+
if (targetId && targetId !== sourceId) {
|
|
729
|
+
const label = nav.kind === "link-href" ? `Link to ${templateToRoute(template)}` : `router.${nav.kind === "router-push" ? "push" : "replace"}('${templateToRoute(template)}')`;
|
|
730
|
+
addEdge(targetId, "navigates", label);
|
|
731
|
+
} else {
|
|
732
|
+
flagged.push({
|
|
733
|
+
source: sourceId,
|
|
734
|
+
target: "DYNAMIC",
|
|
735
|
+
type: "navigates",
|
|
736
|
+
label: nav.kind === "link-href" ? `Link with template: \`${template}\`` : `router.${nav.kind === "router-push" ? "push" : "replace"} with template: \`${template}\``,
|
|
737
|
+
confidence: "medium"
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return { edges, flagged };
|
|
743
|
+
}
|
|
744
|
+
function detect(rootDir) {
|
|
745
|
+
return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "src", "app")) && (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.ts")) || (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.js")) || (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.mjs"));
|
|
746
|
+
}
|
|
747
|
+
function generate(rootDir) {
|
|
748
|
+
const srcDir = (0, import_node_path3.join)(rootDir, "src");
|
|
749
|
+
const appFiles = walk((0, import_node_path3.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
|
|
750
|
+
(f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
|
|
751
|
+
);
|
|
752
|
+
const clientFiles = walk((0, import_node_path3.join)(srcDir, "client"), [".tsx", ".ts"]);
|
|
753
|
+
const serverFiles = walk((0, import_node_path3.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
|
|
754
|
+
(f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
|
|
755
|
+
);
|
|
756
|
+
const libFiles = walk((0, import_node_path3.join)(srcDir, "lib"), [".ts", ".tsx"]);
|
|
757
|
+
const configFiles = walk((0, import_node_path3.join)(srcDir, "config"), [".ts", ".tsx"]);
|
|
758
|
+
const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
|
|
759
|
+
const parsedByPath = /* @__PURE__ */ new Map();
|
|
760
|
+
for (const absPath of allDiscovered) {
|
|
761
|
+
parsedByPath.set(absPath, parseFile(absPath));
|
|
762
|
+
}
|
|
763
|
+
const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
|
|
764
|
+
const fileSet = allDiscovered.filter((f) => !(0, import_node_path3.basename)(f).startsWith("index."));
|
|
765
|
+
const nodes = [];
|
|
766
|
+
const nodeIdSet = /* @__PURE__ */ new Set();
|
|
767
|
+
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
768
|
+
const routeToNodeId = /* @__PURE__ */ new Map();
|
|
769
|
+
for (const absPath of fileSet) {
|
|
770
|
+
const id = toNodeId(srcDir, absPath);
|
|
771
|
+
const type = classifyType(id);
|
|
772
|
+
const parsed = parsedByPath.get(absPath);
|
|
773
|
+
const name = parsed.name || nameFromFilename(absPath);
|
|
774
|
+
const route = extractRoute(id);
|
|
775
|
+
const module_ = classifyModule(id);
|
|
776
|
+
nodes.push({ id, type, name, route, module: module_, exports: parsed.exports });
|
|
777
|
+
nodeIdSet.add(id);
|
|
778
|
+
nodeTypeMap.set(id, type);
|
|
779
|
+
if (route) routeToNodeId.set(route, id);
|
|
780
|
+
}
|
|
781
|
+
const allEdges = [];
|
|
782
|
+
const allFlagged = [];
|
|
783
|
+
for (const absPath of fileSet) {
|
|
784
|
+
const sourceId = toNodeId(srcDir, absPath);
|
|
785
|
+
const parsed = parsedByPath.get(absPath);
|
|
786
|
+
const { edges, flagged } = extractEdges(
|
|
787
|
+
srcDir,
|
|
788
|
+
absPath,
|
|
789
|
+
sourceId,
|
|
790
|
+
parsed,
|
|
791
|
+
nodeIdSet,
|
|
792
|
+
nodeTypeMap,
|
|
793
|
+
barrelMaps,
|
|
794
|
+
routeToNodeId
|
|
795
|
+
);
|
|
796
|
+
allEdges.push(...edges);
|
|
797
|
+
allFlagged.push(...flagged);
|
|
798
|
+
}
|
|
799
|
+
const fetchCallEntries = [];
|
|
800
|
+
for (const absPath of fileSet) {
|
|
801
|
+
const sourceId = toNodeId(srcDir, absPath);
|
|
802
|
+
const parsed = parsedByPath.get(absPath);
|
|
803
|
+
if (parsed.fetchCalls.length === 0) continue;
|
|
804
|
+
fetchCallEntries.push({
|
|
805
|
+
nodeId: sourceId,
|
|
806
|
+
calls: parsed.fetchCalls.map((c) => ({
|
|
807
|
+
url: c.url,
|
|
808
|
+
method: c.method,
|
|
809
|
+
isTemplate: c.isTemplate,
|
|
810
|
+
isConcat: c.isConcat,
|
|
811
|
+
kind: c.kind
|
|
812
|
+
}))
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
|
|
816
|
+
const IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
817
|
+
"node_modules",
|
|
818
|
+
".next",
|
|
819
|
+
"dist",
|
|
820
|
+
".launchsecure",
|
|
821
|
+
".git",
|
|
822
|
+
"src",
|
|
823
|
+
"coverage",
|
|
824
|
+
".turbo",
|
|
825
|
+
"build",
|
|
826
|
+
"out",
|
|
827
|
+
".vercel"
|
|
828
|
+
]);
|
|
829
|
+
const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS);
|
|
830
|
+
for (const absPath of externalCandidates) {
|
|
831
|
+
const normalized = absPath.replace(/\\/g, "/");
|
|
832
|
+
if (externalScanned.has(normalized)) continue;
|
|
833
|
+
let parsed;
|
|
834
|
+
try {
|
|
835
|
+
parsed = parseFile(absPath);
|
|
836
|
+
} catch {
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
const externalId = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
840
|
+
const edgesFromThis = [];
|
|
841
|
+
const seen = /* @__PURE__ */ new Set();
|
|
842
|
+
for (const imp of parsed.imports) {
|
|
843
|
+
const { specifier, isTypeOnly, names } = imp;
|
|
844
|
+
let resolved = null;
|
|
845
|
+
if (specifier.startsWith("@/")) {
|
|
846
|
+
const relToSrc = specifier.slice(2);
|
|
847
|
+
const barrelMap = barrelMaps.get(relToSrc);
|
|
848
|
+
if (barrelMap && names.length > 0) {
|
|
849
|
+
for (const name of names) {
|
|
850
|
+
const targetAbs = barrelMap.get(name);
|
|
851
|
+
if (!targetAbs) continue;
|
|
852
|
+
const targetId2 = toNodeId(srcDir, targetAbs);
|
|
853
|
+
if (!nodeIdSet.has(targetId2)) continue;
|
|
854
|
+
const key2 = `${externalId}\u2192${targetId2}`;
|
|
855
|
+
if (seen.has(key2)) continue;
|
|
856
|
+
seen.add(key2);
|
|
857
|
+
edgesFromThis.push({ source: externalId, target: targetId2, type: "imports" });
|
|
858
|
+
}
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
resolved = resolveImport(srcDir, specifier);
|
|
862
|
+
} else if (specifier.startsWith(".")) {
|
|
863
|
+
resolved = resolveRelativeImport(absPath, specifier);
|
|
864
|
+
}
|
|
865
|
+
if (!resolved) continue;
|
|
866
|
+
const targetId = toNodeId(srcDir, resolved);
|
|
867
|
+
if (!nodeIdSet.has(targetId)) continue;
|
|
868
|
+
if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
|
|
869
|
+
const key = `${externalId}\u2192${targetId}\u2192${isTypeOnly ? "type" : "value"}`;
|
|
870
|
+
if (seen.has(key)) continue;
|
|
871
|
+
seen.add(key);
|
|
872
|
+
edgesFromThis.push({ source: externalId, target: targetId, type: "imports" });
|
|
873
|
+
}
|
|
874
|
+
if (edgesFromThis.length === 0) continue;
|
|
875
|
+
nodes.push({
|
|
876
|
+
id: externalId,
|
|
877
|
+
type: "external",
|
|
878
|
+
name: parsed.name || nameFromFilename(absPath),
|
|
879
|
+
route: null,
|
|
880
|
+
module: "external",
|
|
881
|
+
exports: parsed.exports
|
|
882
|
+
});
|
|
883
|
+
nodeIdSet.add(externalId);
|
|
884
|
+
nodeTypeMap.set(externalId, "external");
|
|
885
|
+
allEdges.push(...edgesFromThis);
|
|
886
|
+
}
|
|
887
|
+
const flaggedSet = /* @__PURE__ */ new Set();
|
|
888
|
+
const dedupedFlagged = allFlagged.filter((f) => {
|
|
889
|
+
const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
|
|
890
|
+
if (flaggedSet.has(key)) return false;
|
|
891
|
+
flaggedSet.add(key);
|
|
892
|
+
return true;
|
|
893
|
+
});
|
|
894
|
+
const typePriority = {
|
|
895
|
+
layout: 0,
|
|
896
|
+
page: 1,
|
|
897
|
+
component: 2,
|
|
898
|
+
ui: 3,
|
|
899
|
+
context: 4,
|
|
900
|
+
config: 5,
|
|
901
|
+
util: 6,
|
|
902
|
+
hook: 7,
|
|
903
|
+
lib: 8
|
|
904
|
+
};
|
|
905
|
+
nodes.sort((a, b) => (typePriority[a.type] ?? 99) - (typePriority[b.type] ?? 99) || a.id.localeCompare(b.id));
|
|
906
|
+
allEdges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
907
|
+
const byType = (t) => nodes.filter((n) => n.type === t).length;
|
|
908
|
+
const stats = {
|
|
909
|
+
total_pages: byType("page"),
|
|
910
|
+
total_layouts: byType("layout"),
|
|
911
|
+
total_components: byType("component"),
|
|
912
|
+
total_ui: byType("ui"),
|
|
913
|
+
total_hooks: byType("hook"),
|
|
914
|
+
total_contexts: byType("context"),
|
|
915
|
+
total_configs: byType("config"),
|
|
916
|
+
total_utils: byType("util"),
|
|
917
|
+
total_libs: byType("lib"),
|
|
918
|
+
total_external: byType("external"),
|
|
919
|
+
total_edges: allEdges.length,
|
|
920
|
+
total_flagged: dedupedFlagged.length
|
|
921
|
+
};
|
|
922
|
+
return {
|
|
923
|
+
metadata: {
|
|
924
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
925
|
+
scope: "main-app-only",
|
|
926
|
+
app_root: "src/",
|
|
927
|
+
layer: "ui",
|
|
928
|
+
parser: "react-nextjs-ast",
|
|
929
|
+
...stats,
|
|
930
|
+
notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
|
|
931
|
+
},
|
|
932
|
+
nodes,
|
|
933
|
+
edges: allEdges,
|
|
934
|
+
cross_refs: [],
|
|
935
|
+
contradictions: [],
|
|
936
|
+
warnings: [],
|
|
937
|
+
flagged_edges: dedupedFlagged,
|
|
938
|
+
patterns: {
|
|
939
|
+
total_nodes: nodes.length,
|
|
940
|
+
by_type: stats,
|
|
941
|
+
by_edge_type: {
|
|
942
|
+
renders: allEdges.filter((e) => e.type === "renders").length,
|
|
943
|
+
imports: allEdges.filter((e) => e.type === "imports").length,
|
|
944
|
+
navigates: allEdges.filter((e) => e.type === "navigates").length
|
|
945
|
+
},
|
|
946
|
+
fetch_calls: fetchCallEntries
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
var reactNextjsParser = {
|
|
951
|
+
id: "react-nextjs",
|
|
952
|
+
layer: "ui",
|
|
953
|
+
detect,
|
|
954
|
+
generate
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
// src/server/graph/parsers/api/nextjs-routes.ts
|
|
958
|
+
var import_node_fs4 = require("node:fs");
|
|
959
|
+
var import_node_path4 = require("node:path");
|
|
960
|
+
var HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
961
|
+
function walk2(dir) {
|
|
962
|
+
const results = [];
|
|
963
|
+
if (!(0, import_node_fs4.existsSync)(dir)) return results;
|
|
964
|
+
for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
965
|
+
const full = (0, import_node_path4.join)(dir, entry.name);
|
|
966
|
+
if (entry.isDirectory()) {
|
|
967
|
+
results.push(...walk2(full));
|
|
968
|
+
} else if (entry.name === "route.ts" || entry.name === "route.tsx") {
|
|
969
|
+
results.push(full);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return results;
|
|
973
|
+
}
|
|
974
|
+
function filePathToRoute(apiDir, absPath) {
|
|
975
|
+
let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
976
|
+
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
977
|
+
route = route.replace(/\/+/g, "/");
|
|
978
|
+
if (route === "/") return "/api";
|
|
979
|
+
return "/api" + route;
|
|
980
|
+
}
|
|
981
|
+
function camelToPascal(s) {
|
|
982
|
+
if (!s) return s;
|
|
983
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
984
|
+
}
|
|
985
|
+
function detect2(rootDir) {
|
|
986
|
+
return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app", "api"));
|
|
987
|
+
}
|
|
988
|
+
function generate2(rootDir) {
|
|
989
|
+
const apiDir = (0, import_node_path4.join)(rootDir, "src", "app", "api");
|
|
990
|
+
const routeFiles = walk2(apiDir);
|
|
991
|
+
const nodes = [];
|
|
992
|
+
const edges = [];
|
|
993
|
+
const crossRefs = [];
|
|
994
|
+
const mutatorCount = {};
|
|
995
|
+
const authUsage = {};
|
|
996
|
+
let endpointsWithAuth = 0;
|
|
997
|
+
let endpointsWithDbAccess = 0;
|
|
998
|
+
for (const absPath of routeFiles) {
|
|
999
|
+
const parsed = parseFile(absPath);
|
|
1000
|
+
const dbCalls = extractDbCalls(absPath);
|
|
1001
|
+
const authWrappers = extractAuthWrappers(absPath);
|
|
1002
|
+
const methods = [];
|
|
1003
|
+
for (const exp of parsed.exports) {
|
|
1004
|
+
if (HTTP_METHODS2.has(exp)) methods.push(exp);
|
|
1005
|
+
}
|
|
1006
|
+
const routePath = filePathToRoute(apiDir, absPath);
|
|
1007
|
+
const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1008
|
+
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
1009
|
+
const reads = dbCalls.filter((c) => !c.isMutation);
|
|
1010
|
+
const mutates = mutations.length > 0;
|
|
1011
|
+
if (mutates) {
|
|
1012
|
+
for (const m of mutations) {
|
|
1013
|
+
mutatorCount[m.method] = (mutatorCount[m.method] ?? 0) + 1;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (dbCalls.length > 0) endpointsWithDbAccess++;
|
|
1017
|
+
const authStrategy = [];
|
|
1018
|
+
for (const w of authWrappers) {
|
|
1019
|
+
authStrategy.push(w);
|
|
1020
|
+
authUsage[w] = (authUsage[w] ?? 0) + 1;
|
|
1021
|
+
}
|
|
1022
|
+
if (authStrategy.length > 0) endpointsWithAuth++;
|
|
1023
|
+
nodes.push({
|
|
1024
|
+
id: relPath,
|
|
1025
|
+
type: "endpoint",
|
|
1026
|
+
name: routePath,
|
|
1027
|
+
path: routePath,
|
|
1028
|
+
methods,
|
|
1029
|
+
handler: relPath,
|
|
1030
|
+
// Behavioral classification from handler body (AST-derived).
|
|
1031
|
+
mutates,
|
|
1032
|
+
auth: authStrategy.length > 0 ? authStrategy : ["public"],
|
|
1033
|
+
db_models: [...new Set(dbCalls.map((c) => c.model))],
|
|
1034
|
+
db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))]
|
|
1035
|
+
});
|
|
1036
|
+
const seenModels = /* @__PURE__ */ new Set();
|
|
1037
|
+
for (const call of dbCalls) {
|
|
1038
|
+
if (seenModels.has(call.model)) continue;
|
|
1039
|
+
seenModels.add(call.model);
|
|
1040
|
+
crossRefs.push({
|
|
1041
|
+
source: relPath,
|
|
1042
|
+
target: camelToPascal(call.model),
|
|
1043
|
+
type: call.isMutation ? "mutates" : "reads",
|
|
1044
|
+
layer: "db"
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
nodes.sort((a, b) => a.path.localeCompare(b.path));
|
|
1049
|
+
crossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1050
|
+
const mutatorNodes = nodes.filter((n) => n.mutates).length;
|
|
1051
|
+
const readOnlyNodes = nodes.filter((n) => !n.mutates).length;
|
|
1052
|
+
return {
|
|
1053
|
+
metadata: {
|
|
1054
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1055
|
+
scope: "main-app-only",
|
|
1056
|
+
stack: "nextjs-app-router",
|
|
1057
|
+
layer: "api",
|
|
1058
|
+
parser: "nextjs-routes-ast",
|
|
1059
|
+
total_endpoints: nodes.length,
|
|
1060
|
+
total_methods: nodes.reduce((sum, n) => sum + n.methods.length, 0),
|
|
1061
|
+
endpoints_with_auth: endpointsWithAuth,
|
|
1062
|
+
endpoints_with_db_access: endpointsWithDbAccess,
|
|
1063
|
+
mutator_endpoints: mutatorNodes,
|
|
1064
|
+
read_only_endpoints: readOnlyNodes
|
|
1065
|
+
},
|
|
1066
|
+
nodes,
|
|
1067
|
+
edges,
|
|
1068
|
+
cross_refs: crossRefs,
|
|
1069
|
+
contradictions: [],
|
|
1070
|
+
warnings: [],
|
|
1071
|
+
flagged_edges: [],
|
|
1072
|
+
patterns: {
|
|
1073
|
+
total_endpoints: nodes.length,
|
|
1074
|
+
methods_breakdown: [...HTTP_METHODS2].reduce((acc, m) => {
|
|
1075
|
+
acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
|
|
1076
|
+
return acc;
|
|
1077
|
+
}, {}),
|
|
1078
|
+
auth_strategies: authUsage,
|
|
1079
|
+
mutation_operations: mutatorCount,
|
|
1080
|
+
mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
var nextjsRoutesParser = {
|
|
1085
|
+
id: "nextjs-routes",
|
|
1086
|
+
layer: "api",
|
|
1087
|
+
detect: detect2,
|
|
1088
|
+
generate: generate2
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
// src/server/graph/parsers/db/prisma-schema.ts
|
|
1092
|
+
var import_node_fs5 = require("node:fs");
|
|
1093
|
+
var import_node_path5 = require("node:path");
|
|
1094
|
+
function parseModels(content) {
|
|
1095
|
+
const nodes = [];
|
|
1096
|
+
const relations = [];
|
|
1097
|
+
const modelRe = /model\s+(\w+)\s*\{([^}]+)\}/g;
|
|
1098
|
+
let m;
|
|
1099
|
+
while ((m = modelRe.exec(content)) !== null) {
|
|
1100
|
+
const modelName = m[1];
|
|
1101
|
+
const body = m[2];
|
|
1102
|
+
const columns = [];
|
|
1103
|
+
const lines = body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("@@"));
|
|
1104
|
+
for (const line of lines) {
|
|
1105
|
+
const fieldMatch = line.match(/^(\w+)\s+(\S+)(.*)/);
|
|
1106
|
+
if (!fieldMatch) continue;
|
|
1107
|
+
const fieldName = fieldMatch[1];
|
|
1108
|
+
const fieldType = fieldMatch[2];
|
|
1109
|
+
const rest = fieldMatch[3] ?? "";
|
|
1110
|
+
const commentMatch = rest.match(/\/\/\s*(.+)$/);
|
|
1111
|
+
const comment = commentMatch ? commentMatch[1].trim() : null;
|
|
1112
|
+
const restNoComment = commentMatch ? rest.slice(0, commentMatch.index).trim() : rest;
|
|
1113
|
+
const isPrimary = restNoComment.includes("@id");
|
|
1114
|
+
const isUnique = restNoComment.includes("@unique");
|
|
1115
|
+
const isNullable = fieldType.endsWith("?");
|
|
1116
|
+
const baseType = fieldType.replace(/[?\[\]]/g, "");
|
|
1117
|
+
const defaultMatch = restNoComment.match(/@default\(([^)]+)\)/);
|
|
1118
|
+
const defaultVal = defaultMatch ? defaultMatch[1] : null;
|
|
1119
|
+
const relationMatch = restNoComment.match(/@relation\(([^)]*)\)/);
|
|
1120
|
+
const isRelationField = !!relationMatch;
|
|
1121
|
+
columns.push({
|
|
1122
|
+
name: fieldName,
|
|
1123
|
+
type: fieldType,
|
|
1124
|
+
primary: isPrimary,
|
|
1125
|
+
unique: isUnique,
|
|
1126
|
+
nullable: isNullable,
|
|
1127
|
+
default: defaultVal,
|
|
1128
|
+
isRelation: isRelationField,
|
|
1129
|
+
comment
|
|
1130
|
+
});
|
|
1131
|
+
if (relationMatch) {
|
|
1132
|
+
const relArgs = relationMatch[1];
|
|
1133
|
+
const fieldsMatch = relArgs.match(/fields:\s*\[([^\]]+)\]/);
|
|
1134
|
+
const refsMatch = relArgs.match(/references:\s*\[([^\]]+)\]/);
|
|
1135
|
+
const onDeleteMatch = relArgs.match(/onDelete:\s*(\w+)/);
|
|
1136
|
+
if (fieldsMatch && refsMatch) {
|
|
1137
|
+
const fk = fieldsMatch[1].trim();
|
|
1138
|
+
relations.push({
|
|
1139
|
+
source: modelName,
|
|
1140
|
+
target: baseType,
|
|
1141
|
+
type: "belongs_to",
|
|
1142
|
+
fk,
|
|
1143
|
+
onDelete: onDeleteMatch ? onDeleteMatch[1] : null
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (fieldType.endsWith("[]") && !relationMatch) {
|
|
1148
|
+
relations.push({
|
|
1149
|
+
source: modelName,
|
|
1150
|
+
target: baseType,
|
|
1151
|
+
type: "has_many",
|
|
1152
|
+
fk: null,
|
|
1153
|
+
onDelete: null
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
nodes.push({
|
|
1158
|
+
id: modelName,
|
|
1159
|
+
type: "table",
|
|
1160
|
+
name: modelName,
|
|
1161
|
+
columns: columns.filter((c) => !c.isRelation || c.primary)
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
return { nodes, relations };
|
|
1165
|
+
}
|
|
1166
|
+
function parseEnums(content) {
|
|
1167
|
+
const nodes = [];
|
|
1168
|
+
const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
|
|
1169
|
+
let m;
|
|
1170
|
+
while ((m = enumRe.exec(content)) !== null) {
|
|
1171
|
+
const enumName = m[1];
|
|
1172
|
+
const body = m[2];
|
|
1173
|
+
const values = body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
|
|
1174
|
+
nodes.push({
|
|
1175
|
+
id: enumName,
|
|
1176
|
+
type: "enum",
|
|
1177
|
+
name: enumName,
|
|
1178
|
+
values
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
return nodes;
|
|
1182
|
+
}
|
|
1183
|
+
function detect3(rootDir) {
|
|
1184
|
+
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
|
|
1185
|
+
}
|
|
1186
|
+
function generate3(rootDir) {
|
|
1187
|
+
const schemaPath = (0, import_node_path5.join)(rootDir, "prisma", "schema.prisma");
|
|
1188
|
+
const content = (0, import_node_fs5.readFileSync)(schemaPath, "utf-8");
|
|
1189
|
+
const { nodes: modelNodes, relations } = parseModels(content);
|
|
1190
|
+
const enumNodes = parseEnums(content);
|
|
1191
|
+
const allNodes = [...modelNodes, ...enumNodes];
|
|
1192
|
+
const edges = relations.map((r) => ({
|
|
1193
|
+
source: r.source,
|
|
1194
|
+
target: r.target,
|
|
1195
|
+
type: r.type,
|
|
1196
|
+
fk: r.fk,
|
|
1197
|
+
onDelete: r.onDelete
|
|
1198
|
+
}));
|
|
1199
|
+
allNodes.sort((a, b) => {
|
|
1200
|
+
if (a.type !== b.type) return a.type === "table" ? -1 : 1;
|
|
1201
|
+
return a.name.localeCompare(b.name);
|
|
1202
|
+
});
|
|
1203
|
+
edges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1204
|
+
return {
|
|
1205
|
+
metadata: {
|
|
1206
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1207
|
+
scope: "prisma-schema",
|
|
1208
|
+
source: "prisma/schema.prisma",
|
|
1209
|
+
provider: "postgresql",
|
|
1210
|
+
layer: "db",
|
|
1211
|
+
total_models: modelNodes.length,
|
|
1212
|
+
total_enums: enumNodes.length,
|
|
1213
|
+
total_relations: edges.length
|
|
1214
|
+
},
|
|
1215
|
+
nodes: allNodes,
|
|
1216
|
+
edges,
|
|
1217
|
+
cross_refs: [],
|
|
1218
|
+
contradictions: [],
|
|
1219
|
+
warnings: [
|
|
1220
|
+
{
|
|
1221
|
+
type: "schema_file_only",
|
|
1222
|
+
detail: "Live DB introspection not yet implemented. Graph derived from prisma/schema.prisma."
|
|
1223
|
+
}
|
|
1224
|
+
],
|
|
1225
|
+
flagged_edges: [],
|
|
1226
|
+
patterns: {
|
|
1227
|
+
total_tables: modelNodes.length,
|
|
1228
|
+
total_enums: enumNodes.length,
|
|
1229
|
+
total_relations: edges.length,
|
|
1230
|
+
relation_types: {
|
|
1231
|
+
belongs_to: edges.filter((e) => e.type === "belongs_to").length,
|
|
1232
|
+
has_many: edges.filter((e) => e.type === "has_many").length
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
var prismaSchemaParser = {
|
|
1238
|
+
id: "prisma-schema",
|
|
1239
|
+
layer: "db",
|
|
1240
|
+
detect: detect3,
|
|
1241
|
+
generate: generate3
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
// src/server/graph/core/api-route-matching.ts
|
|
1245
|
+
function loadApiRoutesFromOutput(apiOutput) {
|
|
1246
|
+
const routes = [];
|
|
1247
|
+
for (const n of apiOutput.nodes) {
|
|
1248
|
+
const path2 = n.path;
|
|
1249
|
+
if (!path2 || typeof path2 !== "string") continue;
|
|
1250
|
+
routes.push({
|
|
1251
|
+
path: path2,
|
|
1252
|
+
nodeId: n.id,
|
|
1253
|
+
segments: path2.split("/").filter(Boolean)
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
return routes;
|
|
1257
|
+
}
|
|
1258
|
+
function buildApiPathMap(routes) {
|
|
1259
|
+
const map = /* @__PURE__ */ new Map();
|
|
1260
|
+
for (const r of routes) {
|
|
1261
|
+
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
1262
|
+
}
|
|
1263
|
+
return map;
|
|
1264
|
+
}
|
|
1265
|
+
function normalizeFetchUrl(raw) {
|
|
1266
|
+
let s = raw.replace(/^`|`$/g, "");
|
|
1267
|
+
const qIdx = s.indexOf("?");
|
|
1268
|
+
if (qIdx >= 0) s = s.slice(0, qIdx);
|
|
1269
|
+
const hIdx = s.indexOf("#");
|
|
1270
|
+
if (hIdx >= 0) s = s.slice(0, hIdx);
|
|
1271
|
+
let hadInterpolation = false;
|
|
1272
|
+
s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
1273
|
+
hadInterpolation = true;
|
|
1274
|
+
const cleaned = expr.trim();
|
|
1275
|
+
const last = cleaned.split(".").pop() ?? cleaned;
|
|
1276
|
+
const name = last.replace(/[^\w]/g, "") || "param";
|
|
1277
|
+
return ":" + name;
|
|
1278
|
+
});
|
|
1279
|
+
s = s.replace(/\/+/g, "/");
|
|
1280
|
+
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
1281
|
+
return { path: s || "/", hadInterpolation };
|
|
1282
|
+
}
|
|
1283
|
+
function scoreApiRouteMatch(candidate, known) {
|
|
1284
|
+
if (candidate.length !== known.length) return -1;
|
|
1285
|
+
let score = 0;
|
|
1286
|
+
for (let i = 0; i < candidate.length; i++) {
|
|
1287
|
+
const a = candidate[i];
|
|
1288
|
+
const b = known[i];
|
|
1289
|
+
if (a === b) {
|
|
1290
|
+
score += 3;
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
if (a.startsWith(":") && b.startsWith(":")) {
|
|
1294
|
+
score += 2;
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
if (a.startsWith(":") || b.startsWith(":")) {
|
|
1298
|
+
score += 1;
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
return -1;
|
|
1302
|
+
}
|
|
1303
|
+
return score;
|
|
1304
|
+
}
|
|
1305
|
+
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
1306
|
+
const raw = call.url;
|
|
1307
|
+
if (/^(https?:)?\/\//i.test(raw)) {
|
|
1308
|
+
return { kind: "external", normalizedUrl: raw };
|
|
1309
|
+
}
|
|
1310
|
+
if (call.isConcat) {
|
|
1311
|
+
return { kind: "dynamic", normalizedUrl: raw };
|
|
1312
|
+
}
|
|
1313
|
+
const { path: path2, hadInterpolation } = normalizeFetchUrl(raw);
|
|
1314
|
+
if (!path2.startsWith("/")) {
|
|
1315
|
+
return { kind: "unresolved", normalizedUrl: path2 };
|
|
1316
|
+
}
|
|
1317
|
+
const segs = path2.split("/").filter(Boolean);
|
|
1318
|
+
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
1319
|
+
return { kind: "dynamic", normalizedUrl: path2 };
|
|
1320
|
+
}
|
|
1321
|
+
const exact = apiPathMap.get(path2);
|
|
1322
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path2 };
|
|
1323
|
+
let bestScore = -1;
|
|
1324
|
+
let bestId = null;
|
|
1325
|
+
for (const r of apiRoutes) {
|
|
1326
|
+
const score = scoreApiRouteMatch(segs, r.segments);
|
|
1327
|
+
if (score > bestScore) {
|
|
1328
|
+
bestScore = score;
|
|
1329
|
+
bestId = r.nodeId;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
if (bestId && bestScore > 0) {
|
|
1333
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path2 };
|
|
1334
|
+
}
|
|
1335
|
+
return { kind: "unresolved", normalizedUrl: path2 };
|
|
1336
|
+
}
|
|
1337
|
+
function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
|
|
1338
|
+
const { path: path2, hadInterpolation } = normalizeFetchUrl(urlPath);
|
|
1339
|
+
if (!path2.startsWith("/")) {
|
|
1340
|
+
return { kind: "unresolved", normalizedUrl: path2 };
|
|
1341
|
+
}
|
|
1342
|
+
const segs = path2.split("/").filter(Boolean);
|
|
1343
|
+
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
1344
|
+
return { kind: "dynamic", normalizedUrl: path2 };
|
|
1345
|
+
}
|
|
1346
|
+
const exact = apiPathMap.get(path2);
|
|
1347
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path2 };
|
|
1348
|
+
let bestScore = -1;
|
|
1349
|
+
let bestId = null;
|
|
1350
|
+
for (const r of apiRoutes) {
|
|
1351
|
+
const score = scoreApiRouteMatch(segs, r.segments);
|
|
1352
|
+
if (score > bestScore) {
|
|
1353
|
+
bestScore = score;
|
|
1354
|
+
bestId = r.nodeId;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
if (bestId && bestScore > 0) {
|
|
1358
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path2 };
|
|
1359
|
+
}
|
|
1360
|
+
return { kind: "unresolved", normalizedUrl: path2 };
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// src/server/graph/parsers/crosslayer/fetch-resolver.ts
|
|
1364
|
+
var fetchResolverParser = {
|
|
1365
|
+
id: "fetch-resolver",
|
|
1366
|
+
layer: "crosslayer",
|
|
1367
|
+
detect(_rootDir) {
|
|
1368
|
+
return true;
|
|
1369
|
+
},
|
|
1370
|
+
generate(_rootDir, layerOutputs) {
|
|
1371
|
+
const uiOutput = layerOutputs.get("ui");
|
|
1372
|
+
const apiOutput = layerOutputs.get("api");
|
|
1373
|
+
if (!uiOutput || !apiOutput) {
|
|
1374
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1375
|
+
}
|
|
1376
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1377
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1378
|
+
const fetchCallEntries = uiOutput.patterns?.fetch_calls ?? [];
|
|
1379
|
+
if (fetchCallEntries.length === 0) {
|
|
1380
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1381
|
+
}
|
|
1382
|
+
const includeExternal = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
|
|
1383
|
+
const crossRefs = [];
|
|
1384
|
+
const flaggedEdges = [];
|
|
1385
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1386
|
+
let resolvedCount = 0;
|
|
1387
|
+
let dynamicCount = 0;
|
|
1388
|
+
let unresolvedCount = 0;
|
|
1389
|
+
let externalCount = 0;
|
|
1390
|
+
for (const entry of fetchCallEntries) {
|
|
1391
|
+
for (const call of entry.calls) {
|
|
1392
|
+
const result = resolveFetchCall(call, apiPathMap, apiRoutes);
|
|
1393
|
+
const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
|
|
1394
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
1395
|
+
const key = `${entry.nodeId}\u2192${result.nodeId}\u2192calls_api`;
|
|
1396
|
+
if (seen.has(key)) continue;
|
|
1397
|
+
seen.add(key);
|
|
1398
|
+
crossRefs.push({
|
|
1399
|
+
source: entry.nodeId,
|
|
1400
|
+
target: result.nodeId,
|
|
1401
|
+
type: "calls_api",
|
|
1402
|
+
layer: "api"
|
|
1403
|
+
});
|
|
1404
|
+
resolvedCount++;
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
if (result.kind === "dynamic") {
|
|
1408
|
+
dynamicCount++;
|
|
1409
|
+
flaggedEdges.push({
|
|
1410
|
+
source: entry.nodeId,
|
|
1411
|
+
target: "DYNAMIC",
|
|
1412
|
+
type: "calls_api",
|
|
1413
|
+
label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
|
|
1414
|
+
confidence: call.isConcat ? "low" : "medium"
|
|
1415
|
+
});
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
if (result.kind === "external") {
|
|
1419
|
+
externalCount++;
|
|
1420
|
+
if (!includeExternal) continue;
|
|
1421
|
+
flaggedEdges.push({
|
|
1422
|
+
source: entry.nodeId,
|
|
1423
|
+
target: "EXTERNAL",
|
|
1424
|
+
type: "calls_external",
|
|
1425
|
+
label: `${methodTag} external fetch: ${call.url}`,
|
|
1426
|
+
confidence: "high"
|
|
1427
|
+
});
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
unresolvedCount++;
|
|
1431
|
+
flaggedEdges.push({
|
|
1432
|
+
source: entry.nodeId,
|
|
1433
|
+
target: "UNRESOLVED",
|
|
1434
|
+
type: "calls_api",
|
|
1435
|
+
label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
|
|
1436
|
+
confidence: "medium"
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return {
|
|
1441
|
+
cross_refs: crossRefs,
|
|
1442
|
+
flagged_edges: flaggedEdges,
|
|
1443
|
+
warnings: [],
|
|
1444
|
+
patterns: {
|
|
1445
|
+
api_call_detection: {
|
|
1446
|
+
resolved: resolvedCount,
|
|
1447
|
+
dynamic: dynamicCount,
|
|
1448
|
+
unresolved: unresolvedCount,
|
|
1449
|
+
external: externalCount
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
// src/server/graph/parsers/crosslayer/api-annotations.ts
|
|
1457
|
+
var import_node_fs6 = require("node:fs");
|
|
1458
|
+
var import_node_path6 = require("node:path");
|
|
1459
|
+
var API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
|
|
1460
|
+
function walk3(dir, exts) {
|
|
1461
|
+
if (!(0, import_node_fs6.existsSync)(dir)) return [];
|
|
1462
|
+
const results = [];
|
|
1463
|
+
for (const entry of (0, import_node_fs6.readdirSync)(dir, { withFileTypes: true })) {
|
|
1464
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1465
|
+
const full = (0, import_node_path6.join)(dir, entry.name);
|
|
1466
|
+
if (entry.isDirectory()) {
|
|
1467
|
+
results.push(...walk3(full, exts));
|
|
1468
|
+
} else if (exts.includes((0, import_node_path6.extname)(entry.name))) {
|
|
1469
|
+
results.push(full);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
return results;
|
|
1473
|
+
}
|
|
1474
|
+
function toNodeId2(srcDir, absPath) {
|
|
1475
|
+
return (0, import_node_path6.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
1476
|
+
}
|
|
1477
|
+
var apiAnnotationsParser = {
|
|
1478
|
+
id: "api-annotations",
|
|
1479
|
+
layer: "crosslayer",
|
|
1480
|
+
detect(rootDir) {
|
|
1481
|
+
return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "src"));
|
|
1482
|
+
},
|
|
1483
|
+
generate(rootDir, layerOutputs) {
|
|
1484
|
+
const apiOutput = layerOutputs.get("api");
|
|
1485
|
+
if (!apiOutput) {
|
|
1486
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1487
|
+
}
|
|
1488
|
+
const uiOutput = layerOutputs.get("ui");
|
|
1489
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
1490
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1491
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1492
|
+
const srcDir = (0, import_node_path6.join)(rootDir, "src");
|
|
1493
|
+
const files = walk3(srcDir, [".ts", ".tsx"]);
|
|
1494
|
+
const crossRefs = [];
|
|
1495
|
+
const flaggedEdges = [];
|
|
1496
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1497
|
+
for (const absPath of files) {
|
|
1498
|
+
const content = (0, import_node_fs6.readFileSync)(absPath, "utf-8");
|
|
1499
|
+
const sourceId = toNodeId2(srcDir, absPath);
|
|
1500
|
+
if (!uiNodeIds.has(sourceId)) continue;
|
|
1501
|
+
let match;
|
|
1502
|
+
API_ANNOTATION_RE.lastIndex = 0;
|
|
1503
|
+
while ((match = API_ANNOTATION_RE.exec(content)) !== null) {
|
|
1504
|
+
const method = match[1];
|
|
1505
|
+
const urlPath = match[2];
|
|
1506
|
+
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
1507
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
1508
|
+
const key = `${sourceId}|${result.nodeId}|calls_api`;
|
|
1509
|
+
if (seen.has(key)) continue;
|
|
1510
|
+
seen.add(key);
|
|
1511
|
+
crossRefs.push({
|
|
1512
|
+
source: sourceId,
|
|
1513
|
+
target: result.nodeId,
|
|
1514
|
+
type: "calls_api",
|
|
1515
|
+
layer: "api"
|
|
1516
|
+
});
|
|
1517
|
+
} else {
|
|
1518
|
+
flaggedEdges.push({
|
|
1519
|
+
source: sourceId,
|
|
1520
|
+
target: "UNRESOLVED",
|
|
1521
|
+
type: "annotation_unresolved",
|
|
1522
|
+
label: `@api ${method} ${urlPath} \u2014 no matching API route found`,
|
|
1523
|
+
confidence: "high"
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
return {
|
|
1529
|
+
cross_refs: crossRefs,
|
|
1530
|
+
flagged_edges: flaggedEdges,
|
|
1531
|
+
warnings: [],
|
|
1532
|
+
patterns: {
|
|
1533
|
+
annotations_found: crossRefs.length + flaggedEdges.length,
|
|
1534
|
+
annotations_resolved: crossRefs.length,
|
|
1535
|
+
annotations_unresolved: flaggedEdges.length
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1541
|
+
// src/server/graph/parsers/crosslayer/url-literal-scanner.ts
|
|
1542
|
+
var import_node_fs7 = require("node:fs");
|
|
1543
|
+
var import_node_path7 = require("node:path");
|
|
1544
|
+
var URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
|
|
1545
|
+
function walk4(dir, exts) {
|
|
1546
|
+
if (!(0, import_node_fs7.existsSync)(dir)) return [];
|
|
1547
|
+
const results = [];
|
|
1548
|
+
for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
|
|
1549
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1550
|
+
const full = (0, import_node_path7.join)(dir, entry.name);
|
|
1551
|
+
if (entry.isDirectory()) {
|
|
1552
|
+
results.push(...walk4(full, exts));
|
|
1553
|
+
} else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
|
|
1554
|
+
results.push(full);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return results;
|
|
1558
|
+
}
|
|
1559
|
+
function toNodeId3(srcDir, absPath) {
|
|
1560
|
+
return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
1561
|
+
}
|
|
1562
|
+
var urlLiteralScannerParser = {
|
|
1563
|
+
id: "url-literal-scanner",
|
|
1564
|
+
layer: "crosslayer",
|
|
1565
|
+
detect(rootDir) {
|
|
1566
|
+
return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
|
|
1567
|
+
},
|
|
1568
|
+
generate(rootDir, layerOutputs) {
|
|
1569
|
+
const apiOutput = layerOutputs.get("api");
|
|
1570
|
+
if (!apiOutput) {
|
|
1571
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1572
|
+
}
|
|
1573
|
+
const uiOutput = layerOutputs.get("ui");
|
|
1574
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
1575
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1576
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1577
|
+
const srcDir = (0, import_node_path7.join)(rootDir, "src");
|
|
1578
|
+
const clientDir = (0, import_node_path7.join)(srcDir, "client");
|
|
1579
|
+
const appDir = (0, import_node_path7.join)(srcDir, "app");
|
|
1580
|
+
const files = [
|
|
1581
|
+
...walk4(clientDir, [".ts", ".tsx"]),
|
|
1582
|
+
...walk4(appDir, [".ts", ".tsx"])
|
|
1583
|
+
];
|
|
1584
|
+
const crossRefs = [];
|
|
1585
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1586
|
+
for (const absPath of files) {
|
|
1587
|
+
const sourceId = toNodeId3(srcDir, absPath);
|
|
1588
|
+
if (!uiNodeIds.has(sourceId)) continue;
|
|
1589
|
+
const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
|
|
1590
|
+
let match;
|
|
1591
|
+
URL_LITERAL_RE.lastIndex = 0;
|
|
1592
|
+
while ((match = URL_LITERAL_RE.exec(content)) !== null) {
|
|
1593
|
+
const urlPath = match[1];
|
|
1594
|
+
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
1595
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
1596
|
+
const key = `${sourceId}|${result.nodeId}|references_api`;
|
|
1597
|
+
if (seen.has(key)) continue;
|
|
1598
|
+
seen.add(key);
|
|
1599
|
+
crossRefs.push({
|
|
1600
|
+
source: sourceId,
|
|
1601
|
+
target: result.nodeId,
|
|
1602
|
+
type: "references_api",
|
|
1603
|
+
layer: "api"
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return {
|
|
1609
|
+
cross_refs: crossRefs,
|
|
1610
|
+
flagged_edges: [],
|
|
1611
|
+
warnings: [],
|
|
1612
|
+
patterns: {
|
|
1613
|
+
url_literals_resolved: crossRefs.length
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1618
|
+
|
|
1619
|
+
// src/server/graph/core/parser-registry.ts
|
|
1620
|
+
var ParserRegistry = class {
|
|
1621
|
+
constructor() {
|
|
1622
|
+
this.parsers = /* @__PURE__ */ new Map();
|
|
1623
|
+
this.ids = /* @__PURE__ */ new Set();
|
|
1624
|
+
}
|
|
1625
|
+
register(parser) {
|
|
1626
|
+
if (this.ids.has(parser.id)) {
|
|
1627
|
+
throw new Error(`Duplicate parser id: ${parser.id}`);
|
|
1628
|
+
}
|
|
1629
|
+
this.ids.add(parser.id);
|
|
1630
|
+
const list = this.parsers.get(parser.layer) ?? [];
|
|
1631
|
+
list.push(parser);
|
|
1632
|
+
this.parsers.set(parser.layer, list);
|
|
1633
|
+
}
|
|
1634
|
+
getParsers(layer) {
|
|
1635
|
+
return this.parsers.get(layer) ?? [];
|
|
1636
|
+
}
|
|
1637
|
+
getCrossLayerParsers() {
|
|
1638
|
+
return this.parsers.get("crosslayer") ?? [];
|
|
1639
|
+
}
|
|
1640
|
+
getAll() {
|
|
1641
|
+
const all = [];
|
|
1642
|
+
for (const list of this.parsers.values()) all.push(...list);
|
|
1643
|
+
return all;
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
function registerBuiltins(registry, disabled) {
|
|
1647
|
+
const builtins = [
|
|
1648
|
+
reactNextjsParser,
|
|
1649
|
+
nextjsRoutesParser,
|
|
1650
|
+
prismaSchemaParser,
|
|
1651
|
+
fetchResolverParser,
|
|
1652
|
+
apiAnnotationsParser,
|
|
1653
|
+
urlLiteralScannerParser
|
|
1654
|
+
];
|
|
1655
|
+
for (const parser of builtins) {
|
|
1656
|
+
if (disabled.has(parser.id)) continue;
|
|
1657
|
+
registry.register(parser);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
1661
|
+
for (const entry of config.parsers?.custom ?? []) {
|
|
1662
|
+
try {
|
|
1663
|
+
const absPath = (0, import_node_path8.resolve)(rootDir, entry.path);
|
|
1664
|
+
const mod = require(absPath);
|
|
1665
|
+
const parser = "default" in mod ? mod.default : mod;
|
|
1666
|
+
if (disabled.has(parser.id)) continue;
|
|
1667
|
+
if (parser.layer !== entry.layer) {
|
|
1668
|
+
process.stderr.write(
|
|
1669
|
+
`[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
|
|
1670
|
+
`
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
registry.register(parser);
|
|
1674
|
+
} catch (err) {
|
|
1675
|
+
process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err}
|
|
1676
|
+
`);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
function createRegistry(config, rootDir) {
|
|
1681
|
+
const registry = new ParserRegistry();
|
|
1682
|
+
const disabled = new Set(config.parsers?.disabled ?? []);
|
|
1683
|
+
registerBuiltins(registry, disabled);
|
|
1684
|
+
loadCustomParsers(registry, config, rootDir, disabled);
|
|
1685
|
+
return registry;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// src/server/graph/core/merge.ts
|
|
1689
|
+
function mergeGraphOutputs(outputs, layer) {
|
|
1690
|
+
if (outputs.length === 0) {
|
|
1691
|
+
return {
|
|
1692
|
+
metadata: { generated: (/* @__PURE__ */ new Date()).toISOString(), scope: "", layer },
|
|
1693
|
+
nodes: [],
|
|
1694
|
+
edges: [],
|
|
1695
|
+
cross_refs: [],
|
|
1696
|
+
contradictions: [],
|
|
1697
|
+
warnings: [],
|
|
1698
|
+
flagged_edges: []
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
if (outputs.length === 1) return outputs[0];
|
|
1702
|
+
const seenNodes = /* @__PURE__ */ new Set();
|
|
1703
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
1704
|
+
const seenCrossRefs = /* @__PURE__ */ new Set();
|
|
1705
|
+
const mergedNodes = [];
|
|
1706
|
+
const mergedEdges = [];
|
|
1707
|
+
const mergedCrossRefs = [];
|
|
1708
|
+
const mergedContradictions = [];
|
|
1709
|
+
const mergedWarnings = [];
|
|
1710
|
+
const mergedFlagged = [];
|
|
1711
|
+
const parserIds = [];
|
|
1712
|
+
for (const output of outputs) {
|
|
1713
|
+
if (output.metadata.parser) {
|
|
1714
|
+
parserIds.push(String(output.metadata.parser));
|
|
1715
|
+
}
|
|
1716
|
+
for (const node of output.nodes) {
|
|
1717
|
+
if (seenNodes.has(node.id)) {
|
|
1718
|
+
mergedWarnings.push({
|
|
1719
|
+
type: "merge_conflict",
|
|
1720
|
+
detail: `Node "${node.id}" produced by multiple parsers; keeping first`
|
|
1721
|
+
});
|
|
1722
|
+
continue;
|
|
1723
|
+
}
|
|
1724
|
+
seenNodes.add(node.id);
|
|
1725
|
+
mergedNodes.push(node);
|
|
1726
|
+
}
|
|
1727
|
+
for (const edge of output.edges) {
|
|
1728
|
+
const key = `${edge.source}|${edge.target}|${edge.type}`;
|
|
1729
|
+
if (seenEdges.has(key)) continue;
|
|
1730
|
+
seenEdges.add(key);
|
|
1731
|
+
mergedEdges.push(edge);
|
|
1732
|
+
}
|
|
1733
|
+
for (const ref of output.cross_refs) {
|
|
1734
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
1735
|
+
if (seenCrossRefs.has(key)) continue;
|
|
1736
|
+
seenCrossRefs.add(key);
|
|
1737
|
+
mergedCrossRefs.push(ref);
|
|
1738
|
+
}
|
|
1739
|
+
mergedContradictions.push(...output.contradictions);
|
|
1740
|
+
mergedWarnings.push(...output.warnings);
|
|
1741
|
+
mergedFlagged.push(...output.flagged_edges);
|
|
1742
|
+
}
|
|
1743
|
+
const metadata = {
|
|
1744
|
+
...outputs[0].metadata,
|
|
1745
|
+
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1746
|
+
parsers: parserIds
|
|
1747
|
+
};
|
|
1748
|
+
return {
|
|
1749
|
+
metadata,
|
|
1750
|
+
nodes: mergedNodes,
|
|
1751
|
+
edges: mergedEdges,
|
|
1752
|
+
cross_refs: mergedCrossRefs,
|
|
1753
|
+
contradictions: mergedContradictions,
|
|
1754
|
+
warnings: mergedWarnings,
|
|
1755
|
+
flagged_edges: mergedFlagged,
|
|
1756
|
+
patterns: outputs[0].patterns
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
function dedupCrossRefs(refs) {
|
|
1760
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1761
|
+
const result = [];
|
|
1762
|
+
for (const ref of refs) {
|
|
1763
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
1764
|
+
if (seen.has(key)) continue;
|
|
1765
|
+
seen.add(key);
|
|
1766
|
+
result.push(ref);
|
|
1767
|
+
}
|
|
1768
|
+
return result;
|
|
1769
|
+
}
|
|
1770
|
+
function applyCrossLayerResults(uiOutput, results, primaryId) {
|
|
1771
|
+
const allCrossRefs = [...uiOutput.cross_refs];
|
|
1772
|
+
const allFlagged = [...uiOutput.flagged_edges];
|
|
1773
|
+
const allWarnings = [...uiOutput.warnings];
|
|
1774
|
+
const primaryResult = results.find((r) => r.parserId === primaryId);
|
|
1775
|
+
const secondaryResults = results.filter((r) => r.parserId !== primaryId);
|
|
1776
|
+
if (primaryResult) {
|
|
1777
|
+
allCrossRefs.push(...primaryResult.output.cross_refs);
|
|
1778
|
+
allFlagged.push(...primaryResult.output.flagged_edges);
|
|
1779
|
+
allWarnings.push(...primaryResult.output.warnings);
|
|
1780
|
+
}
|
|
1781
|
+
const primarySet = new Set(
|
|
1782
|
+
(primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
|
|
1783
|
+
);
|
|
1784
|
+
for (const sec of secondaryResults) {
|
|
1785
|
+
for (const ref of sec.output.cross_refs) {
|
|
1786
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
1787
|
+
if (primarySet.has(key)) {
|
|
1788
|
+
allCrossRefs.push(ref);
|
|
1789
|
+
} else {
|
|
1790
|
+
allFlagged.push({
|
|
1791
|
+
source: ref.source,
|
|
1792
|
+
target: ref.target,
|
|
1793
|
+
type: "out_of_pattern",
|
|
1794
|
+
label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
|
|
1795
|
+
confidence: "medium"
|
|
1796
|
+
});
|
|
1797
|
+
allCrossRefs.push(ref);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
allFlagged.push(...sec.output.flagged_edges);
|
|
1801
|
+
allWarnings.push(...sec.output.warnings);
|
|
1802
|
+
}
|
|
1803
|
+
return {
|
|
1804
|
+
...uiOutput,
|
|
1805
|
+
cross_refs: dedupCrossRefs(allCrossRefs),
|
|
1806
|
+
flagged_edges: allFlagged,
|
|
1807
|
+
warnings: allWarnings
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// src/server/graph/core/graph-builder.ts
|
|
1812
|
+
function readGraphFromDisk(rootDir, layer) {
|
|
1813
|
+
const filePath = (0, import_node_path9.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
1814
|
+
if (!(0, import_node_fs8.existsSync)(filePath)) return null;
|
|
1815
|
+
try {
|
|
1816
|
+
return JSON.parse((0, import_node_fs8.readFileSync)(filePath, "utf-8"));
|
|
1817
|
+
} catch {
|
|
1818
|
+
return null;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
function generateLayer(rootDir, layer) {
|
|
1822
|
+
const config = loadConfig(rootDir);
|
|
1823
|
+
const registry = createRegistry(config, rootDir);
|
|
1824
|
+
const parsers = registry.getParsers(layer);
|
|
1825
|
+
const outputs = [];
|
|
1826
|
+
for (const parser of parsers) {
|
|
1827
|
+
if (!parser.detect(rootDir)) continue;
|
|
1828
|
+
outputs.push(parser.generate(rootDir));
|
|
1829
|
+
}
|
|
1830
|
+
if (outputs.length === 0) return null;
|
|
1831
|
+
let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
1832
|
+
if (layer === "ui") {
|
|
1833
|
+
const layerOutputs = /* @__PURE__ */ new Map();
|
|
1834
|
+
layerOutputs.set("ui", merged);
|
|
1835
|
+
for (const otherLayer of ["api", "db"]) {
|
|
1836
|
+
const existing = readGraphFromDisk(rootDir, otherLayer);
|
|
1837
|
+
if (existing) layerOutputs.set(otherLayer, existing);
|
|
1838
|
+
}
|
|
1839
|
+
const crossParsers = registry.getCrossLayerParsers();
|
|
1840
|
+
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
1841
|
+
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
1842
|
+
if (crossResults.length > 0) {
|
|
1843
|
+
merged = applyCrossLayerResults(merged, crossResults, primaryId);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
return {
|
|
1847
|
+
layer,
|
|
1848
|
+
output: merged,
|
|
1849
|
+
nodeCount: merged.nodes.length,
|
|
1850
|
+
edgeCount: merged.edges.length
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
function generateAll(rootDir) {
|
|
1854
|
+
const config = loadConfig(rootDir);
|
|
1855
|
+
const registry = createRegistry(config, rootDir);
|
|
1856
|
+
const layerOrder = ["api", "db", "ui"];
|
|
1857
|
+
const layerOutputs = /* @__PURE__ */ new Map();
|
|
1858
|
+
const results = [];
|
|
1859
|
+
for (const layer of layerOrder) {
|
|
1860
|
+
const parsers = registry.getParsers(layer);
|
|
1861
|
+
const outputs = [];
|
|
1862
|
+
for (const parser of parsers) {
|
|
1863
|
+
if (!parser.detect(rootDir)) continue;
|
|
1864
|
+
outputs.push(parser.generate(rootDir));
|
|
1865
|
+
}
|
|
1866
|
+
if (outputs.length === 0) continue;
|
|
1867
|
+
const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
1868
|
+
layerOutputs.set(layer, merged);
|
|
1869
|
+
results.push({
|
|
1870
|
+
layer,
|
|
1871
|
+
output: merged,
|
|
1872
|
+
nodeCount: merged.nodes.length,
|
|
1873
|
+
edgeCount: merged.edges.length
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
const crossParsers = registry.getCrossLayerParsers();
|
|
1877
|
+
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
1878
|
+
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
1879
|
+
if (crossResults.length > 0 && layerOutputs.has("ui")) {
|
|
1880
|
+
const uiOutput = layerOutputs.get("ui");
|
|
1881
|
+
const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
|
|
1882
|
+
layerOutputs.set("ui", merged);
|
|
1883
|
+
const uiResult = results.find((r) => r.layer === "ui");
|
|
1884
|
+
if (uiResult) {
|
|
1885
|
+
uiResult.output = merged;
|
|
1886
|
+
uiResult.nodeCount = merged.nodes.length;
|
|
1887
|
+
uiResult.edgeCount = merged.edges.length;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
const byLayer = new Map(results.map((r) => [r.layer, r]));
|
|
1891
|
+
return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// src/server/graph/index.ts
|
|
1895
|
+
var GRAPHS_DIR = ".launchsecure/graphs";
|
|
1896
|
+
var LAYERS = ["ui", "api", "db"];
|
|
1897
|
+
var graphCache = /* @__PURE__ */ new Map();
|
|
1898
|
+
function graphsDir(rootDir) {
|
|
1899
|
+
return (0, import_node_path10.join)(rootDir, GRAPHS_DIR);
|
|
1900
|
+
}
|
|
1901
|
+
function graphFilePath(rootDir, layer) {
|
|
1902
|
+
return (0, import_node_path10.join)(graphsDir(rootDir), `${layer}.json`);
|
|
1903
|
+
}
|
|
1904
|
+
function invalidateCache(filePath) {
|
|
1905
|
+
graphCache.delete(filePath);
|
|
1906
|
+
}
|
|
1907
|
+
function readGraph(rootDir, layer) {
|
|
1908
|
+
const filePath = graphFilePath(rootDir, layer);
|
|
1909
|
+
if (!(0, import_node_fs9.existsSync)(filePath)) return null;
|
|
1910
|
+
const stat = (0, import_node_fs9.statSync)(filePath);
|
|
1911
|
+
const cached = graphCache.get(filePath);
|
|
1912
|
+
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
1913
|
+
return cached.graph;
|
|
1914
|
+
}
|
|
1915
|
+
const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
|
|
1916
|
+
const graph = JSON.parse(content);
|
|
1917
|
+
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
1918
|
+
return graph;
|
|
1919
|
+
}
|
|
1920
|
+
function readAllGraphs(rootDir) {
|
|
1921
|
+
const result = {};
|
|
1922
|
+
for (const layer of LAYERS) {
|
|
1923
|
+
const graph = readGraph(rootDir, layer);
|
|
1924
|
+
if (graph) result[layer] = graph;
|
|
1925
|
+
}
|
|
1926
|
+
return result;
|
|
1927
|
+
}
|
|
1928
|
+
function generateGraph(rootDir, layer) {
|
|
1929
|
+
const dir = graphsDir(rootDir);
|
|
1930
|
+
(0, import_node_fs9.mkdirSync)(dir, { recursive: true });
|
|
1931
|
+
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
1932
|
+
for (const result of results) {
|
|
1933
|
+
const filePath = graphFilePath(rootDir, result.layer);
|
|
1934
|
+
(0, import_node_fs9.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
1935
|
+
invalidateCache(filePath);
|
|
1936
|
+
}
|
|
1937
|
+
return results;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// src/server/lockfile.ts
|
|
1941
|
+
var import_node_child_process = require("node:child_process");
|
|
1942
|
+
var import_node_fs10 = require("node:fs");
|
|
1943
|
+
var import_node_os = require("node:os");
|
|
1944
|
+
var import_node_path11 = require("node:path");
|
|
1945
|
+
function lockDir() {
|
|
1946
|
+
return (0, import_node_path11.join)((0, import_node_os.homedir)(), ".launchsecure");
|
|
1947
|
+
}
|
|
1948
|
+
function lockPath() {
|
|
1949
|
+
return (0, import_node_path11.join)(lockDir(), "launch-chart.lock");
|
|
1950
|
+
}
|
|
1951
|
+
function readLock() {
|
|
1952
|
+
const p = lockPath();
|
|
1953
|
+
if (!(0, import_node_fs10.existsSync)(p)) return null;
|
|
1954
|
+
try {
|
|
1955
|
+
const data = JSON.parse((0, import_node_fs10.readFileSync)(p, "utf-8"));
|
|
1956
|
+
if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
|
|
1957
|
+
return data;
|
|
1958
|
+
} catch {
|
|
1959
|
+
return null;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
function isPidAlive(pid) {
|
|
1963
|
+
try {
|
|
1964
|
+
process.kill(pid, 0);
|
|
1965
|
+
return true;
|
|
1966
|
+
} catch {
|
|
1967
|
+
return false;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
function getListenerPid(port) {
|
|
1971
|
+
try {
|
|
1972
|
+
const out = (0, import_node_child_process.execFileSync)("lsof", ["-nP", "-iTCP:" + port, "-sTCP:LISTEN", "-t"], {
|
|
1973
|
+
encoding: "utf-8",
|
|
1974
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1975
|
+
timeout: 500
|
|
1976
|
+
}).trim();
|
|
1977
|
+
if (!out) return null;
|
|
1978
|
+
const pid = parseInt(out.split("\n")[0], 10);
|
|
1979
|
+
return Number.isFinite(pid) ? pid : null;
|
|
1980
|
+
} catch {
|
|
1981
|
+
return null;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
function getLiveLock() {
|
|
1985
|
+
const lock = readLock();
|
|
1986
|
+
if (!lock) return null;
|
|
1987
|
+
const listenerPid = getListenerPid(lock.port);
|
|
1988
|
+
const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
|
|
1989
|
+
if (!live) {
|
|
1990
|
+
try {
|
|
1991
|
+
(0, import_node_fs10.unlinkSync)(lockPath());
|
|
1992
|
+
} catch {
|
|
1993
|
+
}
|
|
1994
|
+
return null;
|
|
1995
|
+
}
|
|
1996
|
+
return lock;
|
|
1997
|
+
}
|
|
1998
|
+
function writeLock(data) {
|
|
1999
|
+
(0, import_node_fs10.mkdirSync)(lockDir(), { recursive: true });
|
|
2000
|
+
(0, import_node_fs10.writeFileSync)(lockPath(), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
2001
|
+
}
|
|
2002
|
+
function clearLock() {
|
|
2003
|
+
try {
|
|
2004
|
+
(0, import_node_fs10.unlinkSync)(lockPath());
|
|
2005
|
+
} catch {
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// src/server/chart-serve.ts
|
|
2010
|
+
var DEFAULT_PORT = 52819;
|
|
2011
|
+
var MAX_PORT_SCAN = 20;
|
|
2012
|
+
var MIME_TYPES = {
|
|
2013
|
+
".html": "text/html; charset=utf-8",
|
|
2014
|
+
".js": "application/javascript; charset=utf-8",
|
|
2015
|
+
".css": "text/css; charset=utf-8",
|
|
2016
|
+
".json": "application/json; charset=utf-8",
|
|
2017
|
+
".png": "image/png",
|
|
2018
|
+
".svg": "image/svg+xml",
|
|
2019
|
+
".ico": "image/x-icon",
|
|
2020
|
+
".woff": "font/woff",
|
|
2021
|
+
".woff2": "font/woff2"
|
|
2022
|
+
};
|
|
2023
|
+
function findProjectRoot(startDir) {
|
|
2024
|
+
let dir = startDir;
|
|
2025
|
+
for (let i = 0; i < 8; i++) {
|
|
2026
|
+
const graphsDir2 = import_node_path12.default.join(dir, ".launchsecure", "graphs");
|
|
2027
|
+
if (import_node_fs11.default.existsSync(import_node_path12.default.join(graphsDir2, "ui.json")) || import_node_fs11.default.existsSync(import_node_path12.default.join(graphsDir2, "api.json")) || import_node_fs11.default.existsSync(import_node_path12.default.join(graphsDir2, "db.json"))) return dir;
|
|
2028
|
+
const parent = import_node_path12.default.dirname(dir);
|
|
2029
|
+
if (parent === dir) break;
|
|
2030
|
+
dir = parent;
|
|
2031
|
+
}
|
|
2032
|
+
dir = startDir;
|
|
2033
|
+
for (let i = 0; i < 8; i++) {
|
|
2034
|
+
if (import_node_fs11.default.existsSync(import_node_path12.default.join(dir, ".git"))) return dir;
|
|
2035
|
+
const parent = import_node_path12.default.dirname(dir);
|
|
2036
|
+
if (parent === dir) break;
|
|
2037
|
+
dir = parent;
|
|
2038
|
+
}
|
|
2039
|
+
return startDir;
|
|
2040
|
+
}
|
|
2041
|
+
function buildMergedGraph(projectRoot) {
|
|
2042
|
+
let graphs = readAllGraphs(projectRoot);
|
|
2043
|
+
if (!graphs.ui && !graphs.api && !graphs.db) {
|
|
2044
|
+
generateGraph(projectRoot);
|
|
2045
|
+
graphs = readAllGraphs(projectRoot);
|
|
2046
|
+
}
|
|
2047
|
+
const nodes = [];
|
|
2048
|
+
const rawLinks = [];
|
|
2049
|
+
const LAYERS2 = ["ui", "api", "db"];
|
|
2050
|
+
for (const layer of LAYERS2) {
|
|
2051
|
+
const g = graphs[layer];
|
|
2052
|
+
if (!g) continue;
|
|
2053
|
+
for (const n of g.nodes) {
|
|
2054
|
+
nodes.push({
|
|
2055
|
+
id: `${layer}:${n.id}`,
|
|
2056
|
+
name: n.name,
|
|
2057
|
+
type: n.type,
|
|
2058
|
+
layer,
|
|
2059
|
+
module: n.module ?? null,
|
|
2060
|
+
path: n.path ?? n.id
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
for (const e of g.edges) {
|
|
2064
|
+
rawLinks.push({ source: `${layer}:${e.source}`, target: `${layer}:${e.target}`, type: e.type, layer, cross: false });
|
|
2065
|
+
}
|
|
2066
|
+
for (const c of g.cross_refs ?? []) {
|
|
2067
|
+
rawLinks.push({ source: `${layer}:${c.source}`, target: `${c.layer}:${c.target}`, type: c.type, layer, cross: true });
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
2071
|
+
const links = rawLinks.filter((l) => nodeIds.has(l.source) && nodeIds.has(l.target));
|
|
2072
|
+
return {
|
|
2073
|
+
nodes,
|
|
2074
|
+
links,
|
|
2075
|
+
stats: {
|
|
2076
|
+
nodes: nodes.length,
|
|
2077
|
+
links: links.length,
|
|
2078
|
+
byLayer: {
|
|
2079
|
+
ui: graphs.ui ? graphs.ui.nodes.length : 0,
|
|
2080
|
+
api: graphs.api ? graphs.api.nodes.length : 0,
|
|
2081
|
+
db: graphs.db ? graphs.db.nodes.length : 0
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
function serveStatic(res, filePath) {
|
|
2087
|
+
if (!import_node_fs11.default.existsSync(filePath) || !import_node_fs11.default.statSync(filePath).isFile()) return false;
|
|
2088
|
+
const ext = import_node_path12.default.extname(filePath).toLowerCase();
|
|
2089
|
+
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
2090
|
+
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
2091
|
+
import_node_fs11.default.createReadStream(filePath).pipe(res);
|
|
2092
|
+
return true;
|
|
2093
|
+
}
|
|
2094
|
+
function serveIndex(res, clientDir) {
|
|
2095
|
+
const indexPath = import_node_path12.default.join(clientDir, "index.html");
|
|
2096
|
+
if (!import_node_fs11.default.existsSync(indexPath)) {
|
|
2097
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
2098
|
+
res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
serveStatic(res, indexPath);
|
|
2102
|
+
}
|
|
2103
|
+
function tryListen(server, port) {
|
|
2104
|
+
return new Promise((resolve2, reject) => {
|
|
2105
|
+
const onError = (err) => {
|
|
2106
|
+
server.off("listening", onListening);
|
|
2107
|
+
reject(err);
|
|
2108
|
+
};
|
|
2109
|
+
const onListening = () => {
|
|
2110
|
+
server.off("error", onError);
|
|
2111
|
+
resolve2(port);
|
|
2112
|
+
};
|
|
2113
|
+
server.once("error", onError);
|
|
2114
|
+
server.once("listening", onListening);
|
|
2115
|
+
server.listen(port, "127.0.0.1");
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
async function bindWithFallback(server, startPort) {
|
|
2119
|
+
let lastErr = null;
|
|
2120
|
+
for (let i = 0; i < MAX_PORT_SCAN; i++) {
|
|
2121
|
+
const port = startPort + i;
|
|
2122
|
+
try {
|
|
2123
|
+
return await tryListen(server, port);
|
|
2124
|
+
} catch (err) {
|
|
2125
|
+
const code = err.code;
|
|
2126
|
+
if (code === "EADDRINUSE") {
|
|
2127
|
+
lastErr = err;
|
|
2128
|
+
continue;
|
|
2129
|
+
}
|
|
2130
|
+
throw err;
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
throw lastErr ?? new Error("Failed to bind any port");
|
|
2134
|
+
}
|
|
2135
|
+
async function startChartServer(opts = {}) {
|
|
2136
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2137
|
+
const projectRoot = findProjectRoot(cwd);
|
|
2138
|
+
const existing = getLiveLock();
|
|
2139
|
+
if (existing) {
|
|
2140
|
+
if (!opts.quiet) {
|
|
2141
|
+
process.stderr.write(
|
|
2142
|
+
`[launch-chart] already running (pid ${existing.pid}) at ${existing.url}
|
|
2143
|
+
`
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
return { port: existing.port, url: existing.url };
|
|
2147
|
+
}
|
|
2148
|
+
const clientDir = opts.clientDir ?? import_node_path12.default.join(__dirname, "..", "chart-client");
|
|
2149
|
+
const server = import_node_http.default.createServer((req, res) => {
|
|
2150
|
+
try {
|
|
2151
|
+
const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
2152
|
+
if (req.method === "GET" && url2.pathname === "/api/project-graph") {
|
|
2153
|
+
const regenerate = url2.searchParams.get("regenerate") === "1";
|
|
2154
|
+
if (regenerate) generateGraph(projectRoot);
|
|
2155
|
+
const merged = buildMergedGraph(projectRoot);
|
|
2156
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2157
|
+
res.end(JSON.stringify({
|
|
2158
|
+
...merged,
|
|
2159
|
+
debug: { cwd, projectRoot }
|
|
2160
|
+
}));
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
|
|
2164
|
+
const graphs = readAllGraphs(projectRoot);
|
|
2165
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2166
|
+
res.end(JSON.stringify({ ui: graphs.ui ?? null, api: graphs.api ?? null, db: graphs.db ?? null }));
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
|
|
2170
|
+
try {
|
|
2171
|
+
generateGraph(projectRoot);
|
|
2172
|
+
const graphs = readAllGraphs(projectRoot);
|
|
2173
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2174
|
+
res.end(JSON.stringify({
|
|
2175
|
+
ok: true,
|
|
2176
|
+
ui: graphs.ui ?? null,
|
|
2177
|
+
api: graphs.api ?? null,
|
|
2178
|
+
db: graphs.db ?? null
|
|
2179
|
+
}));
|
|
2180
|
+
} catch (err) {
|
|
2181
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2182
|
+
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
2183
|
+
}
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
if (req.method === "GET" && url2.pathname === "/api/health") {
|
|
2187
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2188
|
+
res.end(JSON.stringify({ ok: true, projectRoot }));
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
if (req.method === "GET" && url2.pathname === "/api/parser-config") {
|
|
2192
|
+
const config = loadConfig(projectRoot);
|
|
2193
|
+
const detection = [
|
|
2194
|
+
{ id: "react-nextjs", layer: "ui", label: "React + Next.js", detected: reactNextjsParser.detect(projectRoot) },
|
|
2195
|
+
{ id: "nextjs-routes", layer: "api", label: "Next.js API Routes", detected: nextjsRoutesParser.detect(projectRoot) },
|
|
2196
|
+
{ id: "prisma-schema", layer: "db", label: "Prisma Schema", detected: prismaSchemaParser.detect(projectRoot) }
|
|
2197
|
+
];
|
|
2198
|
+
const crosslayerParsers = [
|
|
2199
|
+
{ id: "fetch-resolver", label: "Fetch / api.method() calls" },
|
|
2200
|
+
{ id: "api-annotations", label: "@api annotations" },
|
|
2201
|
+
{ id: "url-literal-scanner", label: "/api/... URL literals" }
|
|
2202
|
+
];
|
|
2203
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2204
|
+
res.end(JSON.stringify({ config, detection, crosslayerParsers }));
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
if (req.method === "POST" && url2.pathname === "/api/parser-config") {
|
|
2208
|
+
let body = "";
|
|
2209
|
+
req.on("data", (chunk) => {
|
|
2210
|
+
body += chunk.toString();
|
|
2211
|
+
});
|
|
2212
|
+
req.on("end", () => {
|
|
2213
|
+
try {
|
|
2214
|
+
const newConfig = JSON.parse(body);
|
|
2215
|
+
const configPath = import_node_path12.default.join(projectRoot, ".launchchart.json");
|
|
2216
|
+
import_node_fs11.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
|
|
2217
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2218
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2219
|
+
} catch (err) {
|
|
2220
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2221
|
+
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
2222
|
+
}
|
|
2223
|
+
});
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
if (url2.pathname !== "/") {
|
|
2227
|
+
const staticPath = import_node_path12.default.join(clientDir, url2.pathname);
|
|
2228
|
+
if (serveStatic(res, staticPath)) return;
|
|
2229
|
+
}
|
|
2230
|
+
serveIndex(res, clientDir);
|
|
2231
|
+
} catch (err) {
|
|
2232
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2233
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
2234
|
+
}
|
|
2235
|
+
});
|
|
2236
|
+
const port = await bindWithFallback(server, opts.port ?? DEFAULT_PORT);
|
|
2237
|
+
const url = `http://localhost:${port}`;
|
|
2238
|
+
writeLock({
|
|
2239
|
+
pid: process.pid,
|
|
2240
|
+
port,
|
|
2241
|
+
cwd,
|
|
2242
|
+
url,
|
|
2243
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2244
|
+
});
|
|
2245
|
+
const cleanup = () => {
|
|
2246
|
+
clearLock();
|
|
2247
|
+
server.close();
|
|
2248
|
+
};
|
|
2249
|
+
process.once("SIGINT", () => {
|
|
2250
|
+
cleanup();
|
|
2251
|
+
process.exit(0);
|
|
2252
|
+
});
|
|
2253
|
+
process.once("SIGTERM", () => {
|
|
2254
|
+
cleanup();
|
|
2255
|
+
process.exit(0);
|
|
2256
|
+
});
|
|
2257
|
+
process.once("exit", cleanup);
|
|
2258
|
+
if (!opts.quiet) {
|
|
2259
|
+
process.stderr.write(`[launch-chart] serving ${url}
|
|
2260
|
+
`);
|
|
2261
|
+
process.stderr.write(`[launch-chart] project root: ${projectRoot}
|
|
2262
|
+
`);
|
|
2263
|
+
}
|
|
2264
|
+
return { port, url };
|
|
2265
|
+
}
|
|
2266
|
+
function runServeCli(argv) {
|
|
2267
|
+
let port;
|
|
2268
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2269
|
+
if (argv[i] === "--port" && argv[i + 1]) {
|
|
2270
|
+
port = parseInt(argv[++i], 10);
|
|
2271
|
+
} else if (argv[i].startsWith("--port=")) {
|
|
2272
|
+
port = parseInt(argv[i].slice("--port=".length), 10);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
startChartServer({ port }).catch((err) => {
|
|
2276
|
+
process.stderr.write(`[launch-chart] failed to start: ${err}
|
|
2277
|
+
`);
|
|
2278
|
+
process.exit(1);
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2282
|
+
0 && (module.exports = {
|
|
2283
|
+
runServeCli,
|
|
2284
|
+
startChartServer
|
|
2285
|
+
});
|