@launchsecure/launch-kit 0.0.4 → 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-BPR5akxH.js → index-BUih0oqR.js} +99 -64
- package/dist/chart-client/assets/index-DFslt72L.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-BCxRNp8I.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +796 -275
- package/dist/server/cli.js +939 -298
- package/dist/server/graph-mcp-entry.js +1028 -305
- package/package.json +1 -1
- package/dist/chart-client/assets/index-DhNl1aFF.css +0 -1
- package/dist/client/assets/index-nR-HgoHH.css +0 -32
- /package/dist/client/assets/{index-D9e81rsq.js → index-DCC--GO-.js} +0 -0
|
@@ -35,20 +35,41 @@ __export(chart_serve_exports, {
|
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(chart_serve_exports);
|
|
37
37
|
var import_node_http = __toESM(require("node:http"));
|
|
38
|
-
var
|
|
39
|
-
var
|
|
38
|
+
var import_node_fs11 = __toESM(require("node:fs"));
|
|
39
|
+
var import_node_path12 = __toESM(require("node:path"));
|
|
40
40
|
|
|
41
41
|
// src/server/graph/index.ts
|
|
42
|
-
var
|
|
43
|
-
var
|
|
42
|
+
var import_node_fs9 = require("node:fs");
|
|
43
|
+
var import_node_path10 = require("node:path");
|
|
44
44
|
|
|
45
|
-
// src/server/graph/
|
|
46
|
-
var
|
|
47
|
-
var
|
|
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
48
|
|
|
49
|
-
// src/server/graph/core/
|
|
49
|
+
// src/server/graph/core/config.ts
|
|
50
50
|
var import_node_fs = require("node:fs");
|
|
51
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");
|
|
52
73
|
var tsModule;
|
|
53
74
|
function getTs() {
|
|
54
75
|
if (!tsModule) {
|
|
@@ -59,8 +80,8 @@ function getTs() {
|
|
|
59
80
|
var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
60
81
|
function parseFile(absPath) {
|
|
61
82
|
const ts = getTs();
|
|
62
|
-
const content = (0,
|
|
63
|
-
const ext = (0,
|
|
83
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
84
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
64
85
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
|
|
65
86
|
const sourceFile = ts.createSourceFile(
|
|
66
87
|
absPath,
|
|
@@ -338,8 +359,8 @@ var MUTATION_METHODS = /* @__PURE__ */ new Set([
|
|
|
338
359
|
]);
|
|
339
360
|
function extractDbCalls(absPath) {
|
|
340
361
|
const ts = getTs();
|
|
341
|
-
const content = (0,
|
|
342
|
-
const ext = (0,
|
|
362
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
363
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
343
364
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
344
365
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
345
366
|
const calls = [];
|
|
@@ -368,8 +389,8 @@ function extractDbCalls(absPath) {
|
|
|
368
389
|
}
|
|
369
390
|
function extractAuthWrappers(absPath) {
|
|
370
391
|
const ts = getTs();
|
|
371
|
-
const content = (0,
|
|
372
|
-
const ext = (0,
|
|
392
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
393
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
373
394
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
374
395
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
375
396
|
const wrappers = /* @__PURE__ */ new Set();
|
|
@@ -390,12 +411,12 @@ function extractAuthWrappers(absPath) {
|
|
|
390
411
|
var RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
|
|
391
412
|
function walk(dir, exts) {
|
|
392
413
|
const results = [];
|
|
393
|
-
if (!(0,
|
|
394
|
-
for (const entry of (0,
|
|
395
|
-
const full = (0,
|
|
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);
|
|
396
417
|
if (entry.isDirectory()) {
|
|
397
418
|
results.push(...walk(full, exts));
|
|
398
|
-
} else if (exts.includes((0,
|
|
419
|
+
} else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
|
|
399
420
|
results.push(full);
|
|
400
421
|
}
|
|
401
422
|
}
|
|
@@ -403,33 +424,33 @@ function walk(dir, exts) {
|
|
|
403
424
|
}
|
|
404
425
|
function walkWithIgnore(dir, exts, ignoreDirs) {
|
|
405
426
|
const results = [];
|
|
406
|
-
if (!(0,
|
|
407
|
-
for (const entry of (0,
|
|
427
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return results;
|
|
428
|
+
for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
408
429
|
if (entry.isDirectory()) {
|
|
409
430
|
if (ignoreDirs.has(entry.name)) continue;
|
|
410
|
-
results.push(...walkWithIgnore((0,
|
|
411
|
-
} else if (exts.includes((0,
|
|
412
|
-
results.push((0,
|
|
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));
|
|
413
434
|
}
|
|
414
435
|
}
|
|
415
436
|
return results;
|
|
416
437
|
}
|
|
417
438
|
function toNodeId(srcDir, absPath) {
|
|
418
|
-
return (0,
|
|
439
|
+
return (0, import_node_path3.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
419
440
|
}
|
|
420
441
|
function resolveImport(srcDir, specifier) {
|
|
421
442
|
if (!specifier.startsWith("@/")) return null;
|
|
422
443
|
const rel = specifier.slice(2);
|
|
423
|
-
const base = (0,
|
|
424
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
425
|
-
if ((0,
|
|
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;
|
|
426
447
|
}
|
|
427
448
|
return null;
|
|
428
449
|
}
|
|
429
450
|
function resolveRelativeImport(fromFile, specifier) {
|
|
430
|
-
const base = (0,
|
|
431
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
432
|
-
if ((0,
|
|
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;
|
|
433
454
|
}
|
|
434
455
|
return null;
|
|
435
456
|
}
|
|
@@ -450,7 +471,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
|
|
|
450
471
|
const resolved = resolveRelativeImport(barrelAbsPath, re.from);
|
|
451
472
|
if (!resolved) continue;
|
|
452
473
|
if (re.isWildcard) {
|
|
453
|
-
const targetBn = (0,
|
|
474
|
+
const targetBn = (0, import_node_path3.basename)(resolved);
|
|
454
475
|
const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
|
|
455
476
|
if (targetIsBarrel) {
|
|
456
477
|
const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
|
|
@@ -477,12 +498,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
|
477
498
|
const barrels = /* @__PURE__ */ new Map();
|
|
478
499
|
const memo = /* @__PURE__ */ new Map();
|
|
479
500
|
for (const [absPath, parsed] of parsedByPath) {
|
|
480
|
-
const bn = (0,
|
|
501
|
+
const bn = (0, import_node_path3.basename)(absPath);
|
|
481
502
|
if (bn !== "index.ts" && bn !== "index.tsx") continue;
|
|
482
503
|
if (parsed.reExports.length === 0) continue;
|
|
483
504
|
const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
|
|
484
505
|
if (map.size > 0) {
|
|
485
|
-
const barrelId = (0,
|
|
506
|
+
const barrelId = (0, import_node_path3.relative)(srcDir, (0, import_node_path3.dirname)(absPath)).replace(/\\/g, "/");
|
|
486
507
|
barrels.set(barrelId, map);
|
|
487
508
|
}
|
|
488
509
|
}
|
|
@@ -541,7 +562,7 @@ function extractRoute(id) {
|
|
|
541
562
|
return route || "/";
|
|
542
563
|
}
|
|
543
564
|
function nameFromFilename(absPath) {
|
|
544
|
-
return (0,
|
|
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());
|
|
545
566
|
}
|
|
546
567
|
function resolveTemplateLiteralRoute(template, routeToNodeId) {
|
|
547
568
|
const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
@@ -622,105 +643,6 @@ function matchRouteToPage(route, routeToNodeId) {
|
|
|
622
643
|
if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
|
|
623
644
|
return null;
|
|
624
645
|
}
|
|
625
|
-
function loadApiRoutes(rootDir) {
|
|
626
|
-
const apiJsonPath = (0, import_node_path2.join)(rootDir, ".launchsecure", "graphs", "api.json");
|
|
627
|
-
if (!(0, import_node_fs2.existsSync)(apiJsonPath)) return [];
|
|
628
|
-
try {
|
|
629
|
-
const parsed = JSON.parse((0, import_node_fs2.readFileSync)(apiJsonPath, "utf-8"));
|
|
630
|
-
const routes = [];
|
|
631
|
-
for (const n of parsed.nodes ?? []) {
|
|
632
|
-
const path2 = n.path;
|
|
633
|
-
if (!path2 || typeof path2 !== "string") continue;
|
|
634
|
-
routes.push({
|
|
635
|
-
path: path2,
|
|
636
|
-
nodeId: n.id,
|
|
637
|
-
segments: path2.split("/").filter(Boolean)
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
return routes;
|
|
641
|
-
} catch {
|
|
642
|
-
return [];
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
function buildApiPathMap(routes) {
|
|
646
|
-
const map = /* @__PURE__ */ new Map();
|
|
647
|
-
for (const r of routes) {
|
|
648
|
-
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
649
|
-
}
|
|
650
|
-
return map;
|
|
651
|
-
}
|
|
652
|
-
function normalizeFetchUrl(raw) {
|
|
653
|
-
let s = raw.replace(/^`|`$/g, "");
|
|
654
|
-
const qIdx = s.indexOf("?");
|
|
655
|
-
if (qIdx >= 0) s = s.slice(0, qIdx);
|
|
656
|
-
const hIdx = s.indexOf("#");
|
|
657
|
-
if (hIdx >= 0) s = s.slice(0, hIdx);
|
|
658
|
-
let hadInterpolation = false;
|
|
659
|
-
s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
660
|
-
hadInterpolation = true;
|
|
661
|
-
const cleaned = expr.trim();
|
|
662
|
-
const last = cleaned.split(".").pop() ?? cleaned;
|
|
663
|
-
const name = last.replace(/[^\w]/g, "") || "param";
|
|
664
|
-
return ":" + name;
|
|
665
|
-
});
|
|
666
|
-
s = s.replace(/\/+/g, "/");
|
|
667
|
-
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
668
|
-
return { path: s || "/", hadInterpolation };
|
|
669
|
-
}
|
|
670
|
-
function scoreApiRouteMatch(candidate, known) {
|
|
671
|
-
if (candidate.length !== known.length) return -1;
|
|
672
|
-
let score = 0;
|
|
673
|
-
for (let i = 0; i < candidate.length; i++) {
|
|
674
|
-
const a = candidate[i];
|
|
675
|
-
const b = known[i];
|
|
676
|
-
if (a === b) {
|
|
677
|
-
score += 3;
|
|
678
|
-
continue;
|
|
679
|
-
}
|
|
680
|
-
if (a.startsWith(":") && b.startsWith(":")) {
|
|
681
|
-
score += 2;
|
|
682
|
-
continue;
|
|
683
|
-
}
|
|
684
|
-
if (a.startsWith(":") || b.startsWith(":")) {
|
|
685
|
-
score += 1;
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
return -1;
|
|
689
|
-
}
|
|
690
|
-
return score;
|
|
691
|
-
}
|
|
692
|
-
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
693
|
-
const raw = call.url;
|
|
694
|
-
if (/^(https?:)?\/\//i.test(raw)) {
|
|
695
|
-
return { kind: "external", normalizedUrl: raw };
|
|
696
|
-
}
|
|
697
|
-
if (call.isConcat) {
|
|
698
|
-
return { kind: "dynamic", normalizedUrl: raw };
|
|
699
|
-
}
|
|
700
|
-
const { path: path2, hadInterpolation } = normalizeFetchUrl(raw);
|
|
701
|
-
if (!path2.startsWith("/")) {
|
|
702
|
-
return { kind: "unresolved", normalizedUrl: path2 };
|
|
703
|
-
}
|
|
704
|
-
const segs = path2.split("/").filter(Boolean);
|
|
705
|
-
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
706
|
-
return { kind: "dynamic", normalizedUrl: path2 };
|
|
707
|
-
}
|
|
708
|
-
const exact = apiPathMap.get(path2);
|
|
709
|
-
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path2 };
|
|
710
|
-
let bestScore = -1;
|
|
711
|
-
let bestId = null;
|
|
712
|
-
for (const r of apiRoutes) {
|
|
713
|
-
const score = scoreApiRouteMatch(segs, r.segments);
|
|
714
|
-
if (score > bestScore) {
|
|
715
|
-
bestScore = score;
|
|
716
|
-
bestId = r.nodeId;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
if (bestId && bestScore > 0) {
|
|
720
|
-
return { kind: "resolved", nodeId: bestId, normalizedUrl: path2 };
|
|
721
|
-
}
|
|
722
|
-
return { kind: "unresolved", normalizedUrl: path2 };
|
|
723
|
-
}
|
|
724
646
|
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
|
|
725
647
|
const edges = [];
|
|
726
648
|
const flagged = [];
|
|
@@ -820,26 +742,26 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
820
742
|
return { edges, flagged };
|
|
821
743
|
}
|
|
822
744
|
function detect(rootDir) {
|
|
823
|
-
return (0,
|
|
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"));
|
|
824
746
|
}
|
|
825
747
|
function generate(rootDir) {
|
|
826
|
-
const srcDir = (0,
|
|
827
|
-
const appFiles = walk((0,
|
|
828
|
-
(f) => (0,
|
|
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"
|
|
829
751
|
);
|
|
830
|
-
const clientFiles = walk((0,
|
|
831
|
-
const serverFiles = walk((0,
|
|
832
|
-
(f) => (0,
|
|
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"
|
|
833
755
|
);
|
|
834
|
-
const libFiles = walk((0,
|
|
835
|
-
const configFiles = walk((0,
|
|
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"]);
|
|
836
758
|
const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
|
|
837
759
|
const parsedByPath = /* @__PURE__ */ new Map();
|
|
838
760
|
for (const absPath of allDiscovered) {
|
|
839
761
|
parsedByPath.set(absPath, parseFile(absPath));
|
|
840
762
|
}
|
|
841
763
|
const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
|
|
842
|
-
const fileSet = allDiscovered.filter((f) => !(0,
|
|
764
|
+
const fileSet = allDiscovered.filter((f) => !(0, import_node_path3.basename)(f).startsWith("index."));
|
|
843
765
|
const nodes = [];
|
|
844
766
|
const nodeIdSet = /* @__PURE__ */ new Set();
|
|
845
767
|
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
@@ -858,7 +780,6 @@ function generate(rootDir) {
|
|
|
858
780
|
}
|
|
859
781
|
const allEdges = [];
|
|
860
782
|
const allFlagged = [];
|
|
861
|
-
const crossRefs = [];
|
|
862
783
|
for (const absPath of fileSet) {
|
|
863
784
|
const sourceId = toNodeId(srcDir, absPath);
|
|
864
785
|
const parsed = parsedByPath.get(absPath);
|
|
@@ -875,66 +796,21 @@ function generate(rootDir) {
|
|
|
875
796
|
allEdges.push(...edges);
|
|
876
797
|
allFlagged.push(...flagged);
|
|
877
798
|
}
|
|
878
|
-
const
|
|
879
|
-
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
880
|
-
const includeExternalFetches = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
|
|
881
|
-
const fetchSeen = /* @__PURE__ */ new Set();
|
|
882
|
-
let fetchResolvedCount = 0;
|
|
883
|
-
let fetchDynamicCount = 0;
|
|
884
|
-
let fetchUnresolvedCount = 0;
|
|
885
|
-
let fetchExternalCount = 0;
|
|
799
|
+
const fetchCallEntries = [];
|
|
886
800
|
for (const absPath of fileSet) {
|
|
887
801
|
const sourceId = toNodeId(srcDir, absPath);
|
|
888
802
|
const parsed = parsedByPath.get(absPath);
|
|
889
803
|
if (parsed.fetchCalls.length === 0) continue;
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
type: "calls_api",
|
|
901
|
-
layer: "api"
|
|
902
|
-
});
|
|
903
|
-
fetchResolvedCount++;
|
|
904
|
-
continue;
|
|
905
|
-
}
|
|
906
|
-
if (result.kind === "dynamic") {
|
|
907
|
-
fetchDynamicCount++;
|
|
908
|
-
allFlagged.push({
|
|
909
|
-
source: sourceId,
|
|
910
|
-
target: "DYNAMIC",
|
|
911
|
-
type: "calls_api",
|
|
912
|
-
label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
|
|
913
|
-
confidence: call.isConcat ? "low" : "medium"
|
|
914
|
-
});
|
|
915
|
-
continue;
|
|
916
|
-
}
|
|
917
|
-
if (result.kind === "external") {
|
|
918
|
-
fetchExternalCount++;
|
|
919
|
-
if (!includeExternalFetches) continue;
|
|
920
|
-
allFlagged.push({
|
|
921
|
-
source: sourceId,
|
|
922
|
-
target: "EXTERNAL",
|
|
923
|
-
type: "calls_external",
|
|
924
|
-
label: `${methodTag} external fetch: ${call.url}`,
|
|
925
|
-
confidence: "high"
|
|
926
|
-
});
|
|
927
|
-
continue;
|
|
928
|
-
}
|
|
929
|
-
fetchUnresolvedCount++;
|
|
930
|
-
allFlagged.push({
|
|
931
|
-
source: sourceId,
|
|
932
|
-
target: "UNRESOLVED",
|
|
933
|
-
type: "calls_api",
|
|
934
|
-
label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
|
|
935
|
-
confidence: "medium"
|
|
936
|
-
});
|
|
937
|
-
}
|
|
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
|
+
});
|
|
938
814
|
}
|
|
939
815
|
const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
|
|
940
816
|
const IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -960,7 +836,7 @@ function generate(rootDir) {
|
|
|
960
836
|
} catch {
|
|
961
837
|
continue;
|
|
962
838
|
}
|
|
963
|
-
const externalId = (0,
|
|
839
|
+
const externalId = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
964
840
|
const edgesFromThis = [];
|
|
965
841
|
const seen = /* @__PURE__ */ new Set();
|
|
966
842
|
for (const imp of parsed.imports) {
|
|
@@ -1051,20 +927,11 @@ function generate(rootDir) {
|
|
|
1051
927
|
layer: "ui",
|
|
1052
928
|
parser: "react-nextjs-ast",
|
|
1053
929
|
...stats,
|
|
1054
|
-
api_call_detection: {
|
|
1055
|
-
includeExternalFetches,
|
|
1056
|
-
includeConcatFetches: process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1",
|
|
1057
|
-
apiRoutesLoaded: apiRoutes.length,
|
|
1058
|
-
resolved: fetchResolvedCount,
|
|
1059
|
-
dynamic: fetchDynamicCount,
|
|
1060
|
-
unresolved: fetchUnresolvedCount,
|
|
1061
|
-
external: fetchExternalCount
|
|
1062
|
-
},
|
|
1063
930
|
notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
|
|
1064
931
|
},
|
|
1065
932
|
nodes,
|
|
1066
933
|
edges: allEdges,
|
|
1067
|
-
cross_refs:
|
|
934
|
+
cross_refs: [],
|
|
1068
935
|
contradictions: [],
|
|
1069
936
|
warnings: [],
|
|
1070
937
|
flagged_edges: dedupedFlagged,
|
|
@@ -1075,7 +942,8 @@ function generate(rootDir) {
|
|
|
1075
942
|
renders: allEdges.filter((e) => e.type === "renders").length,
|
|
1076
943
|
imports: allEdges.filter((e) => e.type === "imports").length,
|
|
1077
944
|
navigates: allEdges.filter((e) => e.type === "navigates").length
|
|
1078
|
-
}
|
|
945
|
+
},
|
|
946
|
+
fetch_calls: fetchCallEntries
|
|
1079
947
|
}
|
|
1080
948
|
};
|
|
1081
949
|
}
|
|
@@ -1087,14 +955,14 @@ var reactNextjsParser = {
|
|
|
1087
955
|
};
|
|
1088
956
|
|
|
1089
957
|
// src/server/graph/parsers/api/nextjs-routes.ts
|
|
1090
|
-
var
|
|
1091
|
-
var
|
|
958
|
+
var import_node_fs4 = require("node:fs");
|
|
959
|
+
var import_node_path4 = require("node:path");
|
|
1092
960
|
var HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
1093
961
|
function walk2(dir) {
|
|
1094
962
|
const results = [];
|
|
1095
|
-
if (!(0,
|
|
1096
|
-
for (const entry of (0,
|
|
1097
|
-
const full = (0,
|
|
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);
|
|
1098
966
|
if (entry.isDirectory()) {
|
|
1099
967
|
results.push(...walk2(full));
|
|
1100
968
|
} else if (entry.name === "route.ts" || entry.name === "route.tsx") {
|
|
@@ -1104,7 +972,7 @@ function walk2(dir) {
|
|
|
1104
972
|
return results;
|
|
1105
973
|
}
|
|
1106
974
|
function filePathToRoute(apiDir, absPath) {
|
|
1107
|
-
let route = "/" + (0,
|
|
975
|
+
let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
1108
976
|
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
1109
977
|
route = route.replace(/\/+/g, "/");
|
|
1110
978
|
if (route === "/") return "/api";
|
|
@@ -1115,10 +983,10 @@ function camelToPascal(s) {
|
|
|
1115
983
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1116
984
|
}
|
|
1117
985
|
function detect2(rootDir) {
|
|
1118
|
-
return (0,
|
|
986
|
+
return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app", "api"));
|
|
1119
987
|
}
|
|
1120
988
|
function generate2(rootDir) {
|
|
1121
|
-
const apiDir = (0,
|
|
989
|
+
const apiDir = (0, import_node_path4.join)(rootDir, "src", "app", "api");
|
|
1122
990
|
const routeFiles = walk2(apiDir);
|
|
1123
991
|
const nodes = [];
|
|
1124
992
|
const edges = [];
|
|
@@ -1136,7 +1004,7 @@ function generate2(rootDir) {
|
|
|
1136
1004
|
if (HTTP_METHODS2.has(exp)) methods.push(exp);
|
|
1137
1005
|
}
|
|
1138
1006
|
const routePath = filePathToRoute(apiDir, absPath);
|
|
1139
|
-
const relPath = (0,
|
|
1007
|
+
const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1140
1008
|
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
1141
1009
|
const reads = dbCalls.filter((c) => !c.isMutation);
|
|
1142
1010
|
const mutates = mutations.length > 0;
|
|
@@ -1221,8 +1089,8 @@ var nextjsRoutesParser = {
|
|
|
1221
1089
|
};
|
|
1222
1090
|
|
|
1223
1091
|
// src/server/graph/parsers/db/prisma-schema.ts
|
|
1224
|
-
var
|
|
1225
|
-
var
|
|
1092
|
+
var import_node_fs5 = require("node:fs");
|
|
1093
|
+
var import_node_path5 = require("node:path");
|
|
1226
1094
|
function parseModels(content) {
|
|
1227
1095
|
const nodes = [];
|
|
1228
1096
|
const relations = [];
|
|
@@ -1313,11 +1181,11 @@ function parseEnums(content) {
|
|
|
1313
1181
|
return nodes;
|
|
1314
1182
|
}
|
|
1315
1183
|
function detect3(rootDir) {
|
|
1316
|
-
return (0,
|
|
1184
|
+
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
|
|
1317
1185
|
}
|
|
1318
1186
|
function generate3(rootDir) {
|
|
1319
|
-
const schemaPath = (0,
|
|
1320
|
-
const content = (0,
|
|
1187
|
+
const schemaPath = (0, import_node_path5.join)(rootDir, "prisma", "schema.prisma");
|
|
1188
|
+
const content = (0, import_node_fs5.readFileSync)(schemaPath, "utf-8");
|
|
1321
1189
|
const { nodes: modelNodes, relations } = parseModels(content);
|
|
1322
1190
|
const enumNodes = parseEnums(content);
|
|
1323
1191
|
const allNodes = [...modelNodes, ...enumNodes];
|
|
@@ -1373,33 +1241,651 @@ var prismaSchemaParser = {
|
|
|
1373
1241
|
generate: generate3
|
|
1374
1242
|
};
|
|
1375
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
|
+
|
|
1376
1811
|
// src/server/graph/core/graph-builder.ts
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
+
}
|
|
1384
1820
|
}
|
|
1385
1821
|
function generateLayer(rootDir, layer) {
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
const
|
|
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
|
+
}
|
|
1390
1846
|
return {
|
|
1391
1847
|
layer,
|
|
1392
|
-
output,
|
|
1393
|
-
nodeCount:
|
|
1394
|
-
edgeCount:
|
|
1848
|
+
output: merged,
|
|
1849
|
+
nodeCount: merged.nodes.length,
|
|
1850
|
+
edgeCount: merged.edges.length
|
|
1395
1851
|
};
|
|
1396
1852
|
}
|
|
1397
1853
|
function generateAll(rootDir) {
|
|
1398
|
-
const
|
|
1854
|
+
const config = loadConfig(rootDir);
|
|
1855
|
+
const registry = createRegistry(config, rootDir);
|
|
1856
|
+
const layerOrder = ["api", "db", "ui"];
|
|
1857
|
+
const layerOutputs = /* @__PURE__ */ new Map();
|
|
1399
1858
|
const results = [];
|
|
1400
|
-
for (const layer of
|
|
1401
|
-
const
|
|
1402
|
-
|
|
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
|
+
}
|
|
1403
1889
|
}
|
|
1404
1890
|
const byLayer = new Map(results.map((r) => [r.layer, r]));
|
|
1405
1891
|
return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
@@ -1410,23 +1896,23 @@ var GRAPHS_DIR = ".launchsecure/graphs";
|
|
|
1410
1896
|
var LAYERS = ["ui", "api", "db"];
|
|
1411
1897
|
var graphCache = /* @__PURE__ */ new Map();
|
|
1412
1898
|
function graphsDir(rootDir) {
|
|
1413
|
-
return (0,
|
|
1899
|
+
return (0, import_node_path10.join)(rootDir, GRAPHS_DIR);
|
|
1414
1900
|
}
|
|
1415
1901
|
function graphFilePath(rootDir, layer) {
|
|
1416
|
-
return (0,
|
|
1902
|
+
return (0, import_node_path10.join)(graphsDir(rootDir), `${layer}.json`);
|
|
1417
1903
|
}
|
|
1418
1904
|
function invalidateCache(filePath) {
|
|
1419
1905
|
graphCache.delete(filePath);
|
|
1420
1906
|
}
|
|
1421
1907
|
function readGraph(rootDir, layer) {
|
|
1422
1908
|
const filePath = graphFilePath(rootDir, layer);
|
|
1423
|
-
if (!(0,
|
|
1424
|
-
const stat = (0,
|
|
1909
|
+
if (!(0, import_node_fs9.existsSync)(filePath)) return null;
|
|
1910
|
+
const stat = (0, import_node_fs9.statSync)(filePath);
|
|
1425
1911
|
const cached = graphCache.get(filePath);
|
|
1426
1912
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
1427
1913
|
return cached.graph;
|
|
1428
1914
|
}
|
|
1429
|
-
const content = (0,
|
|
1915
|
+
const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
|
|
1430
1916
|
const graph = JSON.parse(content);
|
|
1431
1917
|
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
1432
1918
|
return graph;
|
|
@@ -1441,11 +1927,11 @@ function readAllGraphs(rootDir) {
|
|
|
1441
1927
|
}
|
|
1442
1928
|
function generateGraph(rootDir, layer) {
|
|
1443
1929
|
const dir = graphsDir(rootDir);
|
|
1444
|
-
(0,
|
|
1930
|
+
(0, import_node_fs9.mkdirSync)(dir, { recursive: true });
|
|
1445
1931
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
1446
1932
|
for (const result of results) {
|
|
1447
1933
|
const filePath = graphFilePath(rootDir, result.layer);
|
|
1448
|
-
(0,
|
|
1934
|
+
(0, import_node_fs9.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
1449
1935
|
invalidateCache(filePath);
|
|
1450
1936
|
}
|
|
1451
1937
|
return results;
|
|
@@ -1453,20 +1939,20 @@ function generateGraph(rootDir, layer) {
|
|
|
1453
1939
|
|
|
1454
1940
|
// src/server/lockfile.ts
|
|
1455
1941
|
var import_node_child_process = require("node:child_process");
|
|
1456
|
-
var
|
|
1942
|
+
var import_node_fs10 = require("node:fs");
|
|
1457
1943
|
var import_node_os = require("node:os");
|
|
1458
|
-
var
|
|
1944
|
+
var import_node_path11 = require("node:path");
|
|
1459
1945
|
function lockDir() {
|
|
1460
|
-
return (0,
|
|
1946
|
+
return (0, import_node_path11.join)((0, import_node_os.homedir)(), ".launchsecure");
|
|
1461
1947
|
}
|
|
1462
1948
|
function lockPath() {
|
|
1463
|
-
return (0,
|
|
1949
|
+
return (0, import_node_path11.join)(lockDir(), "launch-chart.lock");
|
|
1464
1950
|
}
|
|
1465
1951
|
function readLock() {
|
|
1466
1952
|
const p = lockPath();
|
|
1467
|
-
if (!(0,
|
|
1953
|
+
if (!(0, import_node_fs10.existsSync)(p)) return null;
|
|
1468
1954
|
try {
|
|
1469
|
-
const data = JSON.parse((0,
|
|
1955
|
+
const data = JSON.parse((0, import_node_fs10.readFileSync)(p, "utf-8"));
|
|
1470
1956
|
if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
|
|
1471
1957
|
return data;
|
|
1472
1958
|
} catch {
|
|
@@ -1502,7 +1988,7 @@ function getLiveLock() {
|
|
|
1502
1988
|
const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
|
|
1503
1989
|
if (!live) {
|
|
1504
1990
|
try {
|
|
1505
|
-
(0,
|
|
1991
|
+
(0, import_node_fs10.unlinkSync)(lockPath());
|
|
1506
1992
|
} catch {
|
|
1507
1993
|
}
|
|
1508
1994
|
return null;
|
|
@@ -1510,12 +1996,12 @@ function getLiveLock() {
|
|
|
1510
1996
|
return lock;
|
|
1511
1997
|
}
|
|
1512
1998
|
function writeLock(data) {
|
|
1513
|
-
(0,
|
|
1514
|
-
(0,
|
|
1999
|
+
(0, import_node_fs10.mkdirSync)(lockDir(), { recursive: true });
|
|
2000
|
+
(0, import_node_fs10.writeFileSync)(lockPath(), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
1515
2001
|
}
|
|
1516
2002
|
function clearLock() {
|
|
1517
2003
|
try {
|
|
1518
|
-
(0,
|
|
2004
|
+
(0, import_node_fs10.unlinkSync)(lockPath());
|
|
1519
2005
|
} catch {
|
|
1520
2006
|
}
|
|
1521
2007
|
}
|
|
@@ -1537,16 +2023,16 @@ var MIME_TYPES = {
|
|
|
1537
2023
|
function findProjectRoot(startDir) {
|
|
1538
2024
|
let dir = startDir;
|
|
1539
2025
|
for (let i = 0; i < 8; i++) {
|
|
1540
|
-
const graphsDir2 =
|
|
1541
|
-
if (
|
|
1542
|
-
const parent =
|
|
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);
|
|
1543
2029
|
if (parent === dir) break;
|
|
1544
2030
|
dir = parent;
|
|
1545
2031
|
}
|
|
1546
2032
|
dir = startDir;
|
|
1547
2033
|
for (let i = 0; i < 8; i++) {
|
|
1548
|
-
if (
|
|
1549
|
-
const parent =
|
|
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);
|
|
1550
2036
|
if (parent === dir) break;
|
|
1551
2037
|
dir = parent;
|
|
1552
2038
|
}
|
|
@@ -1598,16 +2084,16 @@ function buildMergedGraph(projectRoot) {
|
|
|
1598
2084
|
};
|
|
1599
2085
|
}
|
|
1600
2086
|
function serveStatic(res, filePath) {
|
|
1601
|
-
if (!
|
|
1602
|
-
const ext =
|
|
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();
|
|
1603
2089
|
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
1604
2090
|
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
1605
|
-
|
|
2091
|
+
import_node_fs11.default.createReadStream(filePath).pipe(res);
|
|
1606
2092
|
return true;
|
|
1607
2093
|
}
|
|
1608
2094
|
function serveIndex(res, clientDir) {
|
|
1609
|
-
const indexPath =
|
|
1610
|
-
if (!
|
|
2095
|
+
const indexPath = import_node_path12.default.join(clientDir, "index.html");
|
|
2096
|
+
if (!import_node_fs11.default.existsSync(indexPath)) {
|
|
1611
2097
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1612
2098
|
res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
|
|
1613
2099
|
return;
|
|
@@ -1615,14 +2101,14 @@ function serveIndex(res, clientDir) {
|
|
|
1615
2101
|
serveStatic(res, indexPath);
|
|
1616
2102
|
}
|
|
1617
2103
|
function tryListen(server, port) {
|
|
1618
|
-
return new Promise((
|
|
2104
|
+
return new Promise((resolve2, reject) => {
|
|
1619
2105
|
const onError = (err) => {
|
|
1620
2106
|
server.off("listening", onListening);
|
|
1621
2107
|
reject(err);
|
|
1622
2108
|
};
|
|
1623
2109
|
const onListening = () => {
|
|
1624
2110
|
server.off("error", onError);
|
|
1625
|
-
|
|
2111
|
+
resolve2(port);
|
|
1626
2112
|
};
|
|
1627
2113
|
server.once("error", onError);
|
|
1628
2114
|
server.once("listening", onListening);
|
|
@@ -1659,7 +2145,7 @@ async function startChartServer(opts = {}) {
|
|
|
1659
2145
|
}
|
|
1660
2146
|
return { port: existing.port, url: existing.url };
|
|
1661
2147
|
}
|
|
1662
|
-
const clientDir = opts.clientDir ??
|
|
2148
|
+
const clientDir = opts.clientDir ?? import_node_path12.default.join(__dirname, "..", "chart-client");
|
|
1663
2149
|
const server = import_node_http.default.createServer((req, res) => {
|
|
1664
2150
|
try {
|
|
1665
2151
|
const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
@@ -1702,8 +2188,43 @@ async function startChartServer(opts = {}) {
|
|
|
1702
2188
|
res.end(JSON.stringify({ ok: true, projectRoot }));
|
|
1703
2189
|
return;
|
|
1704
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
|
+
}
|
|
1705
2226
|
if (url2.pathname !== "/") {
|
|
1706
|
-
const staticPath =
|
|
2227
|
+
const staticPath = import_node_path12.default.join(clientDir, url2.pathname);
|
|
1707
2228
|
if (serveStatic(res, staticPath)) return;
|
|
1708
2229
|
}
|
|
1709
2230
|
serveIndex(res, clientDir);
|