@launchsecure/launch-kit 0.0.13 → 0.0.15
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-BpQPtTuo.js +441 -0
- package/dist/chart-client/assets/index-CbZ13AXL.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-BCYw64M7.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +2069 -499
- package/dist/server/cli.js +5151 -3319
- package/dist/server/graph-mcp-entry.js +2600 -601
- package/package.json +1 -1
- package/dist/chart-client/assets/index-DIPKWwEJ.js +0 -404
- package/dist/chart-client/assets/index-DjnSR-Hf.css +0 -1
- package/dist/client/assets/index-BIpeUMYR.css +0 -32
- /package/dist/client/assets/{index-qzK1AD2G.js → index-3ENenBk-.js} +0 -0
|
@@ -54,46 +54,19 @@ var init_config = __esm({
|
|
|
54
54
|
}
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
// src/server/chart-serve.ts
|
|
58
|
-
var chart_serve_exports = {};
|
|
59
|
-
__export(chart_serve_exports, {
|
|
60
|
-
runServeCli: () => runServeCli,
|
|
61
|
-
startChartServer: () => startChartServer
|
|
62
|
-
});
|
|
63
|
-
module.exports = __toCommonJS(chart_serve_exports);
|
|
64
|
-
var import_node_http = __toESM(require("node:http"));
|
|
65
|
-
var import_node_fs13 = __toESM(require("node:fs"));
|
|
66
|
-
var import_node_path15 = __toESM(require("node:path"));
|
|
67
|
-
|
|
68
|
-
// src/server/graph/index.ts
|
|
69
|
-
var import_node_fs11 = require("node:fs");
|
|
70
|
-
var import_node_path13 = require("node:path");
|
|
71
|
-
|
|
72
|
-
// src/server/graph/core/graph-builder.ts
|
|
73
|
-
var import_node_fs8 = require("node:fs");
|
|
74
|
-
var import_node_path9 = require("node:path");
|
|
75
|
-
init_config();
|
|
76
|
-
|
|
77
|
-
// src/server/graph/core/parser-registry.ts
|
|
78
|
-
var import_node_path8 = require("node:path");
|
|
79
|
-
|
|
80
|
-
// src/server/graph/parsers/ui/react-nextjs.ts
|
|
81
|
-
var import_node_fs3 = require("node:fs");
|
|
82
|
-
var import_node_path3 = require("node:path");
|
|
83
|
-
|
|
84
57
|
// src/server/graph/core/ts-extractor.ts
|
|
85
|
-
var
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
58
|
+
var ts_extractor_exports = {};
|
|
59
|
+
__export(ts_extractor_exports, {
|
|
60
|
+
classifyFile: () => classifyFile,
|
|
61
|
+
createQuery: () => createQuery,
|
|
62
|
+
extractAuthWrappersTS: () => extractAuthWrappersTS,
|
|
63
|
+
extractDbCallsTS: () => extractDbCallsTS,
|
|
64
|
+
extractDeep: () => extractDeep,
|
|
65
|
+
initTreeSitter: () => initTreeSitter,
|
|
66
|
+
parseCodeTS: () => parseCodeTS,
|
|
67
|
+
parseFileTS: () => parseFileTS,
|
|
68
|
+
setExtractorConfig: () => setExtractorConfig
|
|
69
|
+
});
|
|
97
70
|
async function initTreeSitter() {
|
|
98
71
|
if (initialized) return;
|
|
99
72
|
if (initPromise) return initPromise;
|
|
@@ -117,31 +90,25 @@ function getQuery(name) {
|
|
|
117
90
|
ensureInit();
|
|
118
91
|
const cached = queryCache.get(name);
|
|
119
92
|
if (cached) return cached;
|
|
120
|
-
const scmPath = (0,
|
|
121
|
-
const scm = (0,
|
|
93
|
+
const scmPath = (0, import_node_path3.join)(queriesDir, `${name}.scm`);
|
|
94
|
+
const scm = (0, import_node_fs3.readFileSync)(scmPath, "utf-8");
|
|
122
95
|
const query = tsxLanguage.query(scm);
|
|
123
96
|
queryCache.set(name, query);
|
|
124
97
|
return query;
|
|
125
98
|
}
|
|
126
99
|
function parseSource(absPath) {
|
|
127
100
|
ensureInit();
|
|
128
|
-
const content = (0,
|
|
101
|
+
const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
|
|
129
102
|
return parserInstance.parse(content);
|
|
130
103
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
"delete",
|
|
140
|
-
"deleteMany"
|
|
141
|
-
];
|
|
142
|
-
var DB_IDENTIFIERS_FALLBACK = ["db", "prisma"];
|
|
143
|
-
var extraDbIdentifiers = [];
|
|
144
|
-
var extraMutationMethods = [];
|
|
104
|
+
function parseCodeTS(code) {
|
|
105
|
+
ensureInit();
|
|
106
|
+
return parserInstance.parse(code);
|
|
107
|
+
}
|
|
108
|
+
function createQuery(pattern) {
|
|
109
|
+
ensureInit();
|
|
110
|
+
return tsxLanguage.query(pattern);
|
|
111
|
+
}
|
|
145
112
|
function setExtractorConfig(config) {
|
|
146
113
|
extraDbIdentifiers = config.dbIdentifiers ?? [];
|
|
147
114
|
extraMutationMethods = config.mutationMethods ?? [];
|
|
@@ -596,16 +563,115 @@ function extractDeep(absPath) {
|
|
|
596
563
|
}
|
|
597
564
|
return { elements, stateVars, conditions, variables, responses, params };
|
|
598
565
|
}
|
|
566
|
+
var import_node_fs3, import_node_path3, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods;
|
|
567
|
+
var init_ts_extractor = __esm({
|
|
568
|
+
"src/server/graph/core/ts-extractor.ts"() {
|
|
569
|
+
"use strict";
|
|
570
|
+
import_node_fs3 = require("node:fs");
|
|
571
|
+
import_node_path3 = require("node:path");
|
|
572
|
+
initialized = false;
|
|
573
|
+
queriesDir = (() => {
|
|
574
|
+
const srcPath = (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "..", "queries");
|
|
575
|
+
if (require("fs").existsSync(srcPath)) return srcPath;
|
|
576
|
+
return (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "graph", "queries");
|
|
577
|
+
})();
|
|
578
|
+
queryCache = /* @__PURE__ */ new Map();
|
|
579
|
+
PRISMA_MUTATION_METHODS_BUILTIN = [
|
|
580
|
+
"create",
|
|
581
|
+
"createMany",
|
|
582
|
+
"createManyAndReturn",
|
|
583
|
+
"update",
|
|
584
|
+
"updateMany",
|
|
585
|
+
"updateManyAndReturn",
|
|
586
|
+
"upsert",
|
|
587
|
+
"delete",
|
|
588
|
+
"deleteMany"
|
|
589
|
+
];
|
|
590
|
+
DB_IDENTIFIERS_FALLBACK = ["db", "prisma"];
|
|
591
|
+
extraDbIdentifiers = [];
|
|
592
|
+
extraMutationMethods = [];
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// src/server/chart-serve.ts
|
|
597
|
+
var chart_serve_exports = {};
|
|
598
|
+
__export(chart_serve_exports, {
|
|
599
|
+
runServeCli: () => runServeCli,
|
|
600
|
+
startChartServer: () => startChartServer
|
|
601
|
+
});
|
|
602
|
+
module.exports = __toCommonJS(chart_serve_exports);
|
|
603
|
+
var import_node_http = __toESM(require("node:http"));
|
|
604
|
+
var import_node_fs17 = __toESM(require("node:fs"));
|
|
605
|
+
var import_node_path19 = __toESM(require("node:path"));
|
|
606
|
+
|
|
607
|
+
// src/server/graph/index.ts
|
|
608
|
+
var import_node_fs14 = require("node:fs");
|
|
609
|
+
var import_node_path16 = require("node:path");
|
|
610
|
+
|
|
611
|
+
// src/server/graph/core/graph-builder.ts
|
|
612
|
+
var import_node_fs11 = require("node:fs");
|
|
613
|
+
var import_node_path12 = require("node:path");
|
|
614
|
+
init_config();
|
|
615
|
+
|
|
616
|
+
// src/server/graph/core/parser-registry.ts
|
|
617
|
+
var import_node_path11 = require("node:path");
|
|
618
|
+
|
|
619
|
+
// src/server/graph/parsers/ts/typescript-project.ts
|
|
620
|
+
var import_node_fs4 = require("node:fs");
|
|
621
|
+
var import_node_path4 = require("node:path");
|
|
622
|
+
init_config();
|
|
623
|
+
|
|
624
|
+
// src/server/graph/core/resolve-paths.ts
|
|
625
|
+
var import_node_fs2 = require("node:fs");
|
|
626
|
+
var import_node_path2 = require("node:path");
|
|
627
|
+
function detectDbDir(rootDir, config) {
|
|
628
|
+
if (config.paths?.dbDir) return (0, import_node_path2.join)(rootDir, config.paths.dbDir);
|
|
629
|
+
const prismaDir = (0, import_node_path2.join)(rootDir, "prisma");
|
|
630
|
+
if ((0, import_node_fs2.existsSync)(prismaDir)) return prismaDir;
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
function resolveProjectPaths(rootDir, config) {
|
|
634
|
+
const dbDir = detectDbDir(rootDir, config);
|
|
635
|
+
if (config.paths?.appDir) {
|
|
636
|
+
const appDir = (0, import_node_path2.join)(rootDir, config.paths.appDir);
|
|
637
|
+
const srcDir = config.paths.srcDir ? (0, import_node_path2.join)(rootDir, config.paths.srcDir) : (0, import_node_path2.dirname)(appDir);
|
|
638
|
+
return { srcDir, appDir, apiDir: (0, import_node_path2.join)(appDir, "api"), dbDir };
|
|
639
|
+
}
|
|
640
|
+
const srcApp = (0, import_node_path2.join)(rootDir, "src", "app");
|
|
641
|
+
if ((0, import_node_fs2.existsSync)(srcApp)) {
|
|
642
|
+
return { srcDir: (0, import_node_path2.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path2.join)(srcApp, "api"), dbDir };
|
|
643
|
+
}
|
|
644
|
+
const rootApp = (0, import_node_path2.join)(rootDir, "app");
|
|
645
|
+
if ((0, import_node_fs2.existsSync)(rootApp)) {
|
|
646
|
+
return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path2.join)(rootApp, "api"), dbDir };
|
|
647
|
+
}
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
599
650
|
|
|
600
|
-
// src/server/graph/parsers/
|
|
651
|
+
// src/server/graph/parsers/ts/typescript-project.ts
|
|
652
|
+
init_ts_extractor();
|
|
653
|
+
var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
654
|
+
var CLASSIFICATION_TO_LAYER = {
|
|
655
|
+
endpoint: "api",
|
|
656
|
+
page: "ui",
|
|
657
|
+
layout: "ui",
|
|
658
|
+
component: "ui",
|
|
659
|
+
ui: "ui",
|
|
660
|
+
hook: "ui",
|
|
661
|
+
context: "ui",
|
|
662
|
+
config: "ui",
|
|
663
|
+
lib: "ui",
|
|
664
|
+
"mcp-tool": "ui",
|
|
665
|
+
external: "ui"
|
|
666
|
+
};
|
|
601
667
|
function walk(dir, exts) {
|
|
602
668
|
const results = [];
|
|
603
|
-
if (!(0,
|
|
604
|
-
for (const entry of (0,
|
|
605
|
-
const full = (0,
|
|
669
|
+
if (!(0, import_node_fs4.existsSync)(dir)) return results;
|
|
670
|
+
for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
671
|
+
const full = (0, import_node_path4.join)(dir, entry.name);
|
|
606
672
|
if (entry.isDirectory()) {
|
|
607
673
|
results.push(...walk(full, exts));
|
|
608
|
-
} else if (exts.includes((0,
|
|
674
|
+
} else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
|
|
609
675
|
results.push(full);
|
|
610
676
|
}
|
|
611
677
|
}
|
|
@@ -613,33 +679,33 @@ function walk(dir, exts) {
|
|
|
613
679
|
}
|
|
614
680
|
function walkWithIgnore(dir, exts, ignoreDirs) {
|
|
615
681
|
const results = [];
|
|
616
|
-
if (!(0,
|
|
617
|
-
for (const entry of (0,
|
|
682
|
+
if (!(0, import_node_fs4.existsSync)(dir)) return results;
|
|
683
|
+
for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
618
684
|
if (entry.isDirectory()) {
|
|
619
685
|
if (ignoreDirs.has(entry.name)) continue;
|
|
620
|
-
results.push(...walkWithIgnore((0,
|
|
621
|
-
} else if (exts.includes((0,
|
|
622
|
-
results.push((0,
|
|
686
|
+
results.push(...walkWithIgnore((0, import_node_path4.join)(dir, entry.name), exts, ignoreDirs));
|
|
687
|
+
} else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
|
|
688
|
+
results.push((0, import_node_path4.join)(dir, entry.name));
|
|
623
689
|
}
|
|
624
690
|
}
|
|
625
691
|
return results;
|
|
626
692
|
}
|
|
627
693
|
function toNodeId(srcDir, absPath) {
|
|
628
|
-
return (0,
|
|
694
|
+
return (0, import_node_path4.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
629
695
|
}
|
|
630
696
|
function resolveImport(srcDir, specifier) {
|
|
631
697
|
if (!specifier.startsWith("@/")) return null;
|
|
632
698
|
const rel = specifier.slice(2);
|
|
633
|
-
const base = (0,
|
|
634
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
635
|
-
if ((0,
|
|
699
|
+
const base = (0, import_node_path4.join)(srcDir, rel);
|
|
700
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
|
|
701
|
+
if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
|
|
636
702
|
}
|
|
637
703
|
return null;
|
|
638
704
|
}
|
|
639
705
|
function resolveRelativeImport(fromFile, specifier) {
|
|
640
|
-
const base = (0,
|
|
641
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
642
|
-
if ((0,
|
|
706
|
+
const base = (0, import_node_path4.join)((0, import_node_path4.dirname)(fromFile), specifier);
|
|
707
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
|
|
708
|
+
if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
|
|
643
709
|
}
|
|
644
710
|
return null;
|
|
645
711
|
}
|
|
@@ -660,7 +726,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
|
|
|
660
726
|
const resolved = resolveRelativeImport(barrelAbsPath, re.from);
|
|
661
727
|
if (!resolved) continue;
|
|
662
728
|
if (re.isWildcard) {
|
|
663
|
-
const targetBn = (0,
|
|
729
|
+
const targetBn = (0, import_node_path4.basename)(resolved);
|
|
664
730
|
const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
|
|
665
731
|
if (targetIsBarrel) {
|
|
666
732
|
const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
|
|
@@ -687,12 +753,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
|
687
753
|
const barrels = /* @__PURE__ */ new Map();
|
|
688
754
|
const memo = /* @__PURE__ */ new Map();
|
|
689
755
|
for (const [absPath, parsed] of parsedByPath) {
|
|
690
|
-
const bn = (0,
|
|
756
|
+
const bn = (0, import_node_path4.basename)(absPath);
|
|
691
757
|
if (bn !== "index.ts" && bn !== "index.tsx") continue;
|
|
692
758
|
if (parsed.reExports.length === 0) continue;
|
|
693
759
|
const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
|
|
694
760
|
if (map.size > 0) {
|
|
695
|
-
const barrelId = (0,
|
|
761
|
+
const barrelId = (0, import_node_path4.relative)(srcDir, (0, import_node_path4.dirname)(absPath)).replace(/\\/g, "/");
|
|
696
762
|
barrels.set(barrelId, map);
|
|
697
763
|
}
|
|
698
764
|
}
|
|
@@ -713,7 +779,18 @@ function extractRoute(id) {
|
|
|
713
779
|
return route || "/";
|
|
714
780
|
}
|
|
715
781
|
function nameFromFilename(absPath) {
|
|
716
|
-
return (0,
|
|
782
|
+
return (0, import_node_path4.basename)(absPath, (0, import_node_path4.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
|
|
783
|
+
}
|
|
784
|
+
function filePathToApiRoute(apiDir, absPath) {
|
|
785
|
+
let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
786
|
+
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
787
|
+
route = route.replace(/\/+/g, "/");
|
|
788
|
+
if (route === "/") return "/api";
|
|
789
|
+
return "/api" + route;
|
|
790
|
+
}
|
|
791
|
+
function camelToPascal(s) {
|
|
792
|
+
if (!s) return s;
|
|
793
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
717
794
|
}
|
|
718
795
|
function resolveTemplateLiteralRoute(template, routeToNodeId) {
|
|
719
796
|
const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
@@ -794,7 +871,7 @@ function matchRouteToPage(route, routeToNodeId) {
|
|
|
794
871
|
if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
|
|
795
872
|
return null;
|
|
796
873
|
}
|
|
797
|
-
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet,
|
|
874
|
+
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps, routeToNodeId) {
|
|
798
875
|
const edges = [];
|
|
799
876
|
const flagged = [];
|
|
800
877
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -806,7 +883,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
806
883
|
if (label) edge.label = label;
|
|
807
884
|
edges.push(edge);
|
|
808
885
|
}
|
|
809
|
-
function edgeTypeFor(
|
|
886
|
+
function edgeTypeFor(isTypeOnlyImport, importedNames) {
|
|
810
887
|
if (isTypeOnlyImport) return "imports";
|
|
811
888
|
const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
|
|
812
889
|
if (anyRendered) return "renders";
|
|
@@ -831,14 +908,14 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
831
908
|
}
|
|
832
909
|
for (const [targetId, targetNames] of byTarget) {
|
|
833
910
|
const allType = isTypeOnly || targetNames.every((n) => typeNames.has(n));
|
|
834
|
-
addEdge(targetId, edgeTypeFor(
|
|
911
|
+
addEdge(targetId, edgeTypeFor(allType, targetNames));
|
|
835
912
|
}
|
|
836
913
|
} else {
|
|
837
914
|
const resolved = resolveImport(srcDir, specifier);
|
|
838
915
|
if (resolved) {
|
|
839
916
|
const targetId = toNodeId(srcDir, resolved);
|
|
840
917
|
if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
|
|
841
|
-
addEdge(targetId, edgeTypeFor(
|
|
918
|
+
addEdge(targetId, edgeTypeFor(isTypeOnly, names));
|
|
842
919
|
}
|
|
843
920
|
}
|
|
844
921
|
}
|
|
@@ -847,7 +924,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
847
924
|
if (resolved) {
|
|
848
925
|
const targetId = toNodeId(srcDir, resolved);
|
|
849
926
|
if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
|
|
850
|
-
addEdge(targetId, edgeTypeFor(
|
|
927
|
+
addEdge(targetId, edgeTypeFor(isTypeOnly, names));
|
|
851
928
|
}
|
|
852
929
|
}
|
|
853
930
|
}
|
|
@@ -889,74 +966,113 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
889
966
|
}
|
|
890
967
|
return { edges, flagged };
|
|
891
968
|
}
|
|
969
|
+
function hasNextConfig(rootDir) {
|
|
970
|
+
return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.ts")) || (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.js")) || (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.mjs"));
|
|
971
|
+
}
|
|
892
972
|
function detect(rootDir) {
|
|
893
|
-
|
|
973
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
974
|
+
return paths !== null && hasNextConfig(rootDir);
|
|
894
975
|
}
|
|
895
976
|
function generate(rootDir) {
|
|
896
|
-
const
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
const
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
);
|
|
904
|
-
const
|
|
905
|
-
const configFiles = walk((0, import_node_path3.join)(srcDir, "config"), [".ts", ".tsx"]);
|
|
977
|
+
const config = loadConfig(rootDir);
|
|
978
|
+
const paths = resolveProjectPaths(rootDir, config);
|
|
979
|
+
const srcDir = paths.srcDir;
|
|
980
|
+
const apiDir = paths.apiDir;
|
|
981
|
+
const appFiles = walk(paths.appDir, [".tsx", ".ts"]);
|
|
982
|
+
const clientFiles = walk((0, import_node_path4.join)(srcDir, "client"), [".tsx", ".ts"]);
|
|
983
|
+
const serverFiles = walk((0, import_node_path4.join)(srcDir, "server"), [".ts", ".tsx"]);
|
|
984
|
+
const libFiles = walk((0, import_node_path4.join)(srcDir, "lib"), [".ts", ".tsx"]);
|
|
985
|
+
const configFiles = walk((0, import_node_path4.join)(srcDir, "config"), [".ts", ".tsx"]);
|
|
906
986
|
const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
|
|
907
987
|
const parsedByPath = /* @__PURE__ */ new Map();
|
|
908
988
|
for (const absPath of allDiscovered) {
|
|
909
989
|
parsedByPath.set(absPath, parseFileTS(absPath));
|
|
910
990
|
}
|
|
911
991
|
const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
|
|
912
|
-
const
|
|
913
|
-
const
|
|
992
|
+
const uiNodes = [];
|
|
993
|
+
const apiNodes = [];
|
|
914
994
|
const nodeIdSet = /* @__PURE__ */ new Set();
|
|
915
|
-
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
916
995
|
const routeToNodeId = /* @__PURE__ */ new Map();
|
|
996
|
+
const fileSet = allDiscovered.filter((f) => !(0, import_node_path4.basename)(f).startsWith("index."));
|
|
917
997
|
for (const absPath of fileSet) {
|
|
918
998
|
const id = toNodeId(srcDir, absPath);
|
|
919
999
|
const type = classifyType(absPath, id);
|
|
1000
|
+
if (type === "test" || type === "story") continue;
|
|
920
1001
|
const parsed = parsedByPath.get(absPath);
|
|
921
1002
|
const name = parsed.name || nameFromFilename(absPath);
|
|
922
|
-
const
|
|
923
|
-
const deep = extractDeep(absPath);
|
|
924
|
-
nodes.push({
|
|
925
|
-
id,
|
|
926
|
-
type,
|
|
927
|
-
name,
|
|
928
|
-
route,
|
|
929
|
-
exports: parsed.exports,
|
|
930
|
-
elements: deep.elements,
|
|
931
|
-
stateVars: deep.stateVars,
|
|
932
|
-
conditions: deep.conditions,
|
|
933
|
-
variables: deep.variables
|
|
934
|
-
});
|
|
1003
|
+
const layer = CLASSIFICATION_TO_LAYER[type] ?? "ui";
|
|
935
1004
|
nodeIdSet.add(id);
|
|
936
|
-
|
|
937
|
-
|
|
1005
|
+
if (layer === "api") {
|
|
1006
|
+
const methods = [];
|
|
1007
|
+
for (const exp of parsed.exports) {
|
|
1008
|
+
if (HTTP_METHODS.has(exp)) methods.push(exp);
|
|
1009
|
+
}
|
|
1010
|
+
const dbCalls = extractDbCallsTS(absPath);
|
|
1011
|
+
const authWrappers = extractAuthWrappersTS(absPath);
|
|
1012
|
+
const deep = extractDeep(absPath);
|
|
1013
|
+
const routePath = (0, import_node_fs4.existsSync)(apiDir) ? filePathToApiRoute(apiDir, absPath) : `/api/${id.replace(/\/route\.tsx?$/, "")}`;
|
|
1014
|
+
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
1015
|
+
const mutates = mutations.length > 0;
|
|
1016
|
+
const authStrategy = [...authWrappers];
|
|
1017
|
+
apiNodes.push({
|
|
1018
|
+
id,
|
|
1019
|
+
type: "endpoint",
|
|
1020
|
+
name: routePath,
|
|
1021
|
+
layer: "api",
|
|
1022
|
+
path: routePath,
|
|
1023
|
+
methods,
|
|
1024
|
+
handler: id,
|
|
1025
|
+
mutates,
|
|
1026
|
+
auth: authStrategy.length > 0 ? authStrategy : ["public"],
|
|
1027
|
+
db_models: [...new Set(dbCalls.map((c) => c.model))],
|
|
1028
|
+
db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
|
|
1029
|
+
conditions: deep.conditions,
|
|
1030
|
+
variables: deep.variables,
|
|
1031
|
+
responses: deep.responses,
|
|
1032
|
+
params: deep.params,
|
|
1033
|
+
_dbCalls: dbCalls
|
|
1034
|
+
// temp: used for cross-ref building below
|
|
1035
|
+
});
|
|
1036
|
+
} else {
|
|
1037
|
+
const route = extractRoute(id);
|
|
1038
|
+
const deep = extractDeep(absPath);
|
|
1039
|
+
uiNodes.push({
|
|
1040
|
+
id,
|
|
1041
|
+
type,
|
|
1042
|
+
name,
|
|
1043
|
+
layer: "ui",
|
|
1044
|
+
route,
|
|
1045
|
+
exports: parsed.exports,
|
|
1046
|
+
elements: deep.elements,
|
|
1047
|
+
stateVars: deep.stateVars,
|
|
1048
|
+
conditions: deep.conditions,
|
|
1049
|
+
variables: deep.variables
|
|
1050
|
+
});
|
|
1051
|
+
if (route) routeToNodeId.set(route, id);
|
|
1052
|
+
}
|
|
938
1053
|
}
|
|
939
|
-
const
|
|
940
|
-
const
|
|
1054
|
+
const uiEdges = [];
|
|
1055
|
+
const uiFlagged = [];
|
|
941
1056
|
for (const absPath of fileSet) {
|
|
942
|
-
const
|
|
1057
|
+
const id = toNodeId(srcDir, absPath);
|
|
1058
|
+
if (!nodeIdSet.has(id)) continue;
|
|
943
1059
|
const parsed = parsedByPath.get(absPath);
|
|
944
1060
|
const { edges, flagged } = extractEdges(
|
|
945
1061
|
srcDir,
|
|
946
1062
|
absPath,
|
|
947
|
-
|
|
1063
|
+
id,
|
|
948
1064
|
parsed,
|
|
949
1065
|
nodeIdSet,
|
|
950
|
-
nodeTypeMap,
|
|
951
1066
|
barrelMaps,
|
|
952
1067
|
routeToNodeId
|
|
953
1068
|
);
|
|
954
|
-
|
|
955
|
-
|
|
1069
|
+
uiEdges.push(...edges);
|
|
1070
|
+
uiFlagged.push(...flagged);
|
|
956
1071
|
}
|
|
957
1072
|
const fetchCallEntries = [];
|
|
958
1073
|
for (const absPath of fileSet) {
|
|
959
1074
|
const sourceId = toNodeId(srcDir, absPath);
|
|
1075
|
+
if (!nodeIdSet.has(sourceId)) continue;
|
|
960
1076
|
const parsed = parsedByPath.get(absPath);
|
|
961
1077
|
if (parsed.fetchCalls.length === 0) continue;
|
|
962
1078
|
fetchCallEntries.push({
|
|
@@ -994,11 +1110,11 @@ function generate(rootDir) {
|
|
|
994
1110
|
} catch {
|
|
995
1111
|
continue;
|
|
996
1112
|
}
|
|
997
|
-
const externalId = (0,
|
|
1113
|
+
const externalId = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
998
1114
|
const edgesFromThis = [];
|
|
999
1115
|
const seen = /* @__PURE__ */ new Set();
|
|
1000
1116
|
for (const imp of parsed.imports) {
|
|
1001
|
-
const { specifier,
|
|
1117
|
+
const { specifier, names } = imp;
|
|
1002
1118
|
let resolved = null;
|
|
1003
1119
|
if (specifier.startsWith("@/")) {
|
|
1004
1120
|
const relToSrc = specifier.slice(2);
|
|
@@ -1024,25 +1140,52 @@ function generate(rootDir) {
|
|
|
1024
1140
|
const targetId = toNodeId(srcDir, resolved);
|
|
1025
1141
|
if (!nodeIdSet.has(targetId)) continue;
|
|
1026
1142
|
if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
|
|
1027
|
-
const key = `${externalId}\u2192${targetId}
|
|
1143
|
+
const key = `${externalId}\u2192${targetId}`;
|
|
1028
1144
|
if (seen.has(key)) continue;
|
|
1029
1145
|
seen.add(key);
|
|
1030
1146
|
edgesFromThis.push({ source: externalId, target: targetId, type: "imports" });
|
|
1031
1147
|
}
|
|
1032
1148
|
if (edgesFromThis.length === 0) continue;
|
|
1033
|
-
|
|
1149
|
+
uiNodes.push({
|
|
1034
1150
|
id: externalId,
|
|
1035
1151
|
type: "external",
|
|
1036
1152
|
name: parsed.name || nameFromFilename(absPath),
|
|
1153
|
+
layer: "ui",
|
|
1037
1154
|
route: null,
|
|
1038
1155
|
exports: parsed.exports
|
|
1039
1156
|
});
|
|
1040
1157
|
nodeIdSet.add(externalId);
|
|
1041
|
-
|
|
1042
|
-
|
|
1158
|
+
uiEdges.push(...edgesFromThis);
|
|
1159
|
+
}
|
|
1160
|
+
const apiCrossRefs = [];
|
|
1161
|
+
for (const node of apiNodes) {
|
|
1162
|
+
const dbCalls = node._dbCalls;
|
|
1163
|
+
if (!dbCalls) continue;
|
|
1164
|
+
const seenModels = /* @__PURE__ */ new Set();
|
|
1165
|
+
for (const call of dbCalls) {
|
|
1166
|
+
if (seenModels.has(call.model)) continue;
|
|
1167
|
+
seenModels.add(call.model);
|
|
1168
|
+
apiCrossRefs.push({
|
|
1169
|
+
source: node.id,
|
|
1170
|
+
target: camelToPascal(call.model),
|
|
1171
|
+
type: call.isMutation ? "mutates" : "reads",
|
|
1172
|
+
layer: "db"
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
delete node._dbCalls;
|
|
1176
|
+
}
|
|
1177
|
+
const apiNodeIds = new Set(apiNodes.map((n) => n.id));
|
|
1178
|
+
const apiEdges = [];
|
|
1179
|
+
const uiOnlyEdges = [];
|
|
1180
|
+
for (const edge of uiEdges) {
|
|
1181
|
+
if (apiNodeIds.has(edge.source)) {
|
|
1182
|
+
apiEdges.push(edge);
|
|
1183
|
+
} else {
|
|
1184
|
+
uiOnlyEdges.push(edge);
|
|
1185
|
+
}
|
|
1043
1186
|
}
|
|
1044
1187
|
const flaggedSet = /* @__PURE__ */ new Set();
|
|
1045
|
-
const dedupedFlagged =
|
|
1188
|
+
const dedupedFlagged = uiFlagged.filter((f) => {
|
|
1046
1189
|
const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
|
|
1047
1190
|
if (flaggedSet.has(key)) return false;
|
|
1048
1191
|
flaggedSet.add(key);
|
|
@@ -1059,10 +1202,12 @@ function generate(rootDir) {
|
|
|
1059
1202
|
hook: 7,
|
|
1060
1203
|
lib: 8
|
|
1061
1204
|
};
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1205
|
+
uiNodes.sort((a, b) => (typePriority[a.type] ?? 99) - (typePriority[b.type] ?? 99) || a.id.localeCompare(b.id));
|
|
1206
|
+
uiOnlyEdges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1207
|
+
apiNodes.sort((a, b) => (a.path ?? "").localeCompare(b.path ?? ""));
|
|
1208
|
+
apiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1209
|
+
const byType = (t) => uiNodes.filter((n) => n.type === t).length;
|
|
1210
|
+
const uiStats = {
|
|
1066
1211
|
total_pages: byType("page"),
|
|
1067
1212
|
total_layouts: byType("layout"),
|
|
1068
1213
|
total_components: byType("component"),
|
|
@@ -1073,182 +1218,99 @@ function generate(rootDir) {
|
|
|
1073
1218
|
total_utils: byType("util"),
|
|
1074
1219
|
total_libs: byType("lib"),
|
|
1075
1220
|
total_external: byType("external"),
|
|
1076
|
-
total_edges:
|
|
1221
|
+
total_edges: uiOnlyEdges.length,
|
|
1077
1222
|
total_flagged: dedupedFlagged.length
|
|
1078
1223
|
};
|
|
1079
|
-
|
|
1224
|
+
const stripLayer = (nodes) => nodes.map(({ layer: _, ...rest }) => rest);
|
|
1225
|
+
const result = /* @__PURE__ */ new Map();
|
|
1226
|
+
result.set("ui", {
|
|
1080
1227
|
metadata: {
|
|
1081
1228
|
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1082
1229
|
scope: "main-app-only",
|
|
1083
1230
|
app_root: "src/",
|
|
1084
1231
|
layer: "ui",
|
|
1085
|
-
parser: "
|
|
1086
|
-
...
|
|
1232
|
+
parser: "typescript-project",
|
|
1233
|
+
...uiStats,
|
|
1087
1234
|
notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
|
|
1088
1235
|
},
|
|
1089
|
-
nodes,
|
|
1090
|
-
edges:
|
|
1236
|
+
nodes: stripLayer(uiNodes),
|
|
1237
|
+
edges: uiOnlyEdges,
|
|
1091
1238
|
cross_refs: [],
|
|
1092
1239
|
contradictions: [],
|
|
1093
1240
|
warnings: [],
|
|
1094
1241
|
flagged_edges: dedupedFlagged,
|
|
1095
1242
|
patterns: {
|
|
1096
|
-
total_nodes:
|
|
1097
|
-
by_type:
|
|
1243
|
+
total_nodes: uiNodes.length,
|
|
1244
|
+
by_type: uiStats,
|
|
1098
1245
|
by_edge_type: {
|
|
1099
|
-
renders:
|
|
1100
|
-
imports:
|
|
1101
|
-
navigates:
|
|
1246
|
+
renders: uiOnlyEdges.filter((e) => e.type === "renders").length,
|
|
1247
|
+
imports: uiOnlyEdges.filter((e) => e.type === "imports").length,
|
|
1248
|
+
navigates: uiOnlyEdges.filter((e) => e.type === "navigates").length
|
|
1102
1249
|
},
|
|
1103
1250
|
fetch_calls: fetchCallEntries
|
|
1104
1251
|
}
|
|
1105
|
-
};
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
const results = [];
|
|
1120
|
-
if (!(0, import_node_fs4.existsSync)(dir)) return results;
|
|
1121
|
-
for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
1122
|
-
const full = (0, import_node_path4.join)(dir, entry.name);
|
|
1123
|
-
if (entry.isDirectory()) {
|
|
1124
|
-
results.push(...walk2(full));
|
|
1125
|
-
} else if (entry.name === "route.ts" || entry.name === "route.tsx") {
|
|
1126
|
-
results.push(full);
|
|
1252
|
+
});
|
|
1253
|
+
if (apiNodes.length > 0) {
|
|
1254
|
+
const mutatorNodes = apiNodes.filter((n) => n.mutates).length;
|
|
1255
|
+
const readOnlyNodes = apiNodes.filter((n) => !n.mutates).length;
|
|
1256
|
+
const authUsage = {};
|
|
1257
|
+
let endpointsWithAuth = 0;
|
|
1258
|
+
for (const n of apiNodes) {
|
|
1259
|
+
const auth = n.auth;
|
|
1260
|
+
if (auth.length > 0 && auth[0] !== "public") {
|
|
1261
|
+
endpointsWithAuth++;
|
|
1262
|
+
for (const w of auth) {
|
|
1263
|
+
authUsage[w] = (authUsage[w] ?? 0) + 1;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1127
1266
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
if (route === "/") return "/api";
|
|
1136
|
-
return "/api" + route;
|
|
1137
|
-
}
|
|
1138
|
-
function camelToPascal(s) {
|
|
1139
|
-
if (!s) return s;
|
|
1140
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1141
|
-
}
|
|
1142
|
-
function detect2(rootDir) {
|
|
1143
|
-
return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app", "api"));
|
|
1144
|
-
}
|
|
1145
|
-
function generate2(rootDir) {
|
|
1146
|
-
const apiDir = (0, import_node_path4.join)(rootDir, "src", "app", "api");
|
|
1147
|
-
const routeFiles = walk2(apiDir);
|
|
1148
|
-
const nodes = [];
|
|
1149
|
-
const edges = [];
|
|
1150
|
-
const crossRefs = [];
|
|
1151
|
-
const mutatorCount = {};
|
|
1152
|
-
const authUsage = {};
|
|
1153
|
-
let endpointsWithAuth = 0;
|
|
1154
|
-
let endpointsWithDbAccess = 0;
|
|
1155
|
-
for (const absPath of routeFiles) {
|
|
1156
|
-
const parsed = parseFileTS(absPath);
|
|
1157
|
-
const dbCalls = extractDbCallsTS(absPath);
|
|
1158
|
-
const authWrappers = extractAuthWrappersTS(absPath);
|
|
1159
|
-
const methods = [];
|
|
1160
|
-
for (const exp of parsed.exports) {
|
|
1161
|
-
if (HTTP_METHODS.has(exp)) methods.push(exp);
|
|
1162
|
-
}
|
|
1163
|
-
const routePath = filePathToRoute(apiDir, absPath);
|
|
1164
|
-
const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1165
|
-
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
1166
|
-
const reads = dbCalls.filter((c) => !c.isMutation);
|
|
1167
|
-
const mutates = mutations.length > 0;
|
|
1168
|
-
if (mutates) {
|
|
1169
|
-
for (const m of mutations) {
|
|
1170
|
-
mutatorCount[m.method] = (mutatorCount[m.method] ?? 0) + 1;
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
if (dbCalls.length > 0) endpointsWithDbAccess++;
|
|
1174
|
-
const authStrategy = [];
|
|
1175
|
-
for (const w of authWrappers) {
|
|
1176
|
-
authStrategy.push(w);
|
|
1177
|
-
authUsage[w] = (authUsage[w] ?? 0) + 1;
|
|
1178
|
-
}
|
|
1179
|
-
if (authStrategy.length > 0) endpointsWithAuth++;
|
|
1180
|
-
const deep = extractDeep(absPath);
|
|
1181
|
-
nodes.push({
|
|
1182
|
-
id: relPath,
|
|
1183
|
-
type: "endpoint",
|
|
1184
|
-
name: routePath,
|
|
1185
|
-
path: routePath,
|
|
1186
|
-
methods,
|
|
1187
|
-
handler: relPath,
|
|
1188
|
-
// Behavioral classification from handler body (AST-derived).
|
|
1189
|
-
mutates,
|
|
1190
|
-
auth: authStrategy.length > 0 ? authStrategy : ["public"],
|
|
1191
|
-
db_models: [...new Set(dbCalls.map((c) => c.model))],
|
|
1192
|
-
db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
|
|
1193
|
-
// Deep extraction fields
|
|
1194
|
-
conditions: deep.conditions,
|
|
1195
|
-
variables: deep.variables,
|
|
1196
|
-
responses: deep.responses,
|
|
1197
|
-
params: deep.params
|
|
1198
|
-
});
|
|
1199
|
-
const seenModels = /* @__PURE__ */ new Set();
|
|
1200
|
-
for (const call of dbCalls) {
|
|
1201
|
-
if (seenModels.has(call.model)) continue;
|
|
1202
|
-
seenModels.add(call.model);
|
|
1203
|
-
crossRefs.push({
|
|
1204
|
-
source: relPath,
|
|
1205
|
-
target: camelToPascal(call.model),
|
|
1206
|
-
type: call.isMutation ? "mutates" : "reads",
|
|
1207
|
-
layer: "db"
|
|
1208
|
-
});
|
|
1267
|
+
const mutatorCount = {};
|
|
1268
|
+
for (const n of apiNodes) {
|
|
1269
|
+
const ops = n.db_operations;
|
|
1270
|
+
for (const op of ops) {
|
|
1271
|
+
const method = op.split(".")[1];
|
|
1272
|
+
if (method) mutatorCount[method] = (mutatorCount[method] ?? 0) + 1;
|
|
1273
|
+
}
|
|
1209
1274
|
}
|
|
1275
|
+
result.set("api", {
|
|
1276
|
+
metadata: {
|
|
1277
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1278
|
+
scope: "main-app-only",
|
|
1279
|
+
stack: "nextjs-app-router",
|
|
1280
|
+
layer: "api",
|
|
1281
|
+
parser: "typescript-project",
|
|
1282
|
+
total_endpoints: apiNodes.length,
|
|
1283
|
+
total_methods: apiNodes.reduce((sum, n) => sum + n.methods.length, 0),
|
|
1284
|
+
endpoints_with_auth: endpointsWithAuth,
|
|
1285
|
+
endpoints_with_db_access: apiNodes.filter((n) => n.db_models.length > 0).length,
|
|
1286
|
+
mutator_endpoints: mutatorNodes,
|
|
1287
|
+
read_only_endpoints: readOnlyNodes
|
|
1288
|
+
},
|
|
1289
|
+
nodes: stripLayer(apiNodes),
|
|
1290
|
+
edges: apiEdges,
|
|
1291
|
+
cross_refs: apiCrossRefs,
|
|
1292
|
+
contradictions: [],
|
|
1293
|
+
warnings: [],
|
|
1294
|
+
flagged_edges: [],
|
|
1295
|
+
patterns: {
|
|
1296
|
+
total_endpoints: apiNodes.length,
|
|
1297
|
+
methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
|
|
1298
|
+
acc[m] = apiNodes.filter((n) => n.methods.includes(m)).length;
|
|
1299
|
+
return acc;
|
|
1300
|
+
}, {}),
|
|
1301
|
+
auth_strategies: authUsage,
|
|
1302
|
+
mutation_operations: mutatorCount,
|
|
1303
|
+
mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1210
1306
|
}
|
|
1211
|
-
|
|
1212
|
-
crossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1213
|
-
const mutatorNodes = nodes.filter((n) => n.mutates).length;
|
|
1214
|
-
const readOnlyNodes = nodes.filter((n) => !n.mutates).length;
|
|
1215
|
-
return {
|
|
1216
|
-
metadata: {
|
|
1217
|
-
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1218
|
-
scope: "main-app-only",
|
|
1219
|
-
stack: "nextjs-app-router",
|
|
1220
|
-
layer: "api",
|
|
1221
|
-
parser: "nextjs-routes-ast",
|
|
1222
|
-
total_endpoints: nodes.length,
|
|
1223
|
-
total_methods: nodes.reduce((sum, n) => sum + n.methods.length, 0),
|
|
1224
|
-
endpoints_with_auth: endpointsWithAuth,
|
|
1225
|
-
endpoints_with_db_access: endpointsWithDbAccess,
|
|
1226
|
-
mutator_endpoints: mutatorNodes,
|
|
1227
|
-
read_only_endpoints: readOnlyNodes
|
|
1228
|
-
},
|
|
1229
|
-
nodes,
|
|
1230
|
-
edges,
|
|
1231
|
-
cross_refs: crossRefs,
|
|
1232
|
-
contradictions: [],
|
|
1233
|
-
warnings: [],
|
|
1234
|
-
flagged_edges: [],
|
|
1235
|
-
patterns: {
|
|
1236
|
-
total_endpoints: nodes.length,
|
|
1237
|
-
methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
|
|
1238
|
-
acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
|
|
1239
|
-
return acc;
|
|
1240
|
-
}, {}),
|
|
1241
|
-
auth_strategies: authUsage,
|
|
1242
|
-
mutation_operations: mutatorCount,
|
|
1243
|
-
mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
|
|
1244
|
-
}
|
|
1245
|
-
};
|
|
1307
|
+
return result;
|
|
1246
1308
|
}
|
|
1247
|
-
var
|
|
1248
|
-
id: "
|
|
1249
|
-
|
|
1250
|
-
detect
|
|
1251
|
-
generate
|
|
1309
|
+
var typescriptProjectParser = {
|
|
1310
|
+
id: "typescript-project",
|
|
1311
|
+
layers: ["ui", "api"],
|
|
1312
|
+
detect,
|
|
1313
|
+
generate
|
|
1252
1314
|
};
|
|
1253
1315
|
|
|
1254
1316
|
// src/server/graph/parsers/db/prisma-schema.ts
|
|
@@ -1343,10 +1405,10 @@ function parseEnums(content) {
|
|
|
1343
1405
|
}
|
|
1344
1406
|
return nodes;
|
|
1345
1407
|
}
|
|
1346
|
-
function
|
|
1408
|
+
function detect2(rootDir) {
|
|
1347
1409
|
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
|
|
1348
1410
|
}
|
|
1349
|
-
function
|
|
1411
|
+
function generate2(rootDir) {
|
|
1350
1412
|
const schemaPath = (0, import_node_path5.join)(rootDir, "prisma", "schema.prisma");
|
|
1351
1413
|
const content = (0, import_node_fs5.readFileSync)(schemaPath, "utf-8");
|
|
1352
1414
|
const { nodes: modelNodes, relations } = parseModels(content);
|
|
@@ -1400,6 +1462,424 @@ function generate3(rootDir) {
|
|
|
1400
1462
|
var prismaSchemaParser = {
|
|
1401
1463
|
id: "prisma-schema",
|
|
1402
1464
|
layer: "db",
|
|
1465
|
+
detect: detect2,
|
|
1466
|
+
generate: generate2
|
|
1467
|
+
};
|
|
1468
|
+
|
|
1469
|
+
// src/server/graph/parsers/db/sql-migrations.ts
|
|
1470
|
+
var import_node_fs6 = require("node:fs");
|
|
1471
|
+
var import_node_path6 = require("node:path");
|
|
1472
|
+
var PG_TO_PRISMA = {
|
|
1473
|
+
"TEXT": "String",
|
|
1474
|
+
"VARCHAR": "String",
|
|
1475
|
+
"CHAR": "String",
|
|
1476
|
+
"INTEGER": "Int",
|
|
1477
|
+
"INT": "Int",
|
|
1478
|
+
"SMALLINT": "Int",
|
|
1479
|
+
"BIGINT": "BigInt",
|
|
1480
|
+
"SERIAL": "Int",
|
|
1481
|
+
"BOOLEAN": "Boolean",
|
|
1482
|
+
"BOOL": "Boolean",
|
|
1483
|
+
"TIMESTAMP(3)": "DateTime",
|
|
1484
|
+
"TIMESTAMP": "DateTime",
|
|
1485
|
+
"TIMESTAMPTZ": "DateTime",
|
|
1486
|
+
"DATE": "DateTime",
|
|
1487
|
+
"DOUBLE PRECISION": "Float",
|
|
1488
|
+
"FLOAT": "Float",
|
|
1489
|
+
"REAL": "Float",
|
|
1490
|
+
"DECIMAL": "Decimal",
|
|
1491
|
+
"NUMERIC": "Decimal",
|
|
1492
|
+
"JSONB": "Json",
|
|
1493
|
+
"JSON": "Json",
|
|
1494
|
+
"BYTEA": "Bytes",
|
|
1495
|
+
"UUID": "String",
|
|
1496
|
+
"TEXT[]": "String[]"
|
|
1497
|
+
};
|
|
1498
|
+
function pgTypeToPrisma(pgType) {
|
|
1499
|
+
const upper = pgType.toUpperCase().trim();
|
|
1500
|
+
return PG_TO_PRISMA[upper] ?? upper;
|
|
1501
|
+
}
|
|
1502
|
+
function parseCreateTable(sql, state) {
|
|
1503
|
+
const re = /CREATE\s+TABLE\s+"(\w+)"\s*\(([\s\S]*?)\);/gi;
|
|
1504
|
+
let m;
|
|
1505
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1506
|
+
const tableName = m[1];
|
|
1507
|
+
const body = m[2];
|
|
1508
|
+
const columns = /* @__PURE__ */ new Map();
|
|
1509
|
+
let primaryCol = null;
|
|
1510
|
+
for (const line of body.split("\n")) {
|
|
1511
|
+
const trimmed = line.trim().replace(/,\s*$/, "");
|
|
1512
|
+
if (!trimmed || trimmed.startsWith("--")) continue;
|
|
1513
|
+
const pkMatch = trimmed.match(/CONSTRAINT\s+"[^"]+"\s+PRIMARY\s+KEY\s*\("(\w+)"\)/i);
|
|
1514
|
+
if (pkMatch) {
|
|
1515
|
+
primaryCol = pkMatch[1];
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
if (/^\s*CONSTRAINT\s/i.test(trimmed)) continue;
|
|
1519
|
+
const colMatch = trimmed.match(/^"(\w+)"\s+(.+)/);
|
|
1520
|
+
if (!colMatch) continue;
|
|
1521
|
+
const colName = colMatch[1];
|
|
1522
|
+
let rest = colMatch[2];
|
|
1523
|
+
const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
|
|
1524
|
+
const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)(?:\s*,?\s*$)/i);
|
|
1525
|
+
const defaultVal = defaultMatch ? defaultMatch[1].trim() : null;
|
|
1526
|
+
let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim().replace(/,\s*$/, "").trim();
|
|
1527
|
+
columns.set(colName, {
|
|
1528
|
+
name: colName,
|
|
1529
|
+
type: colType,
|
|
1530
|
+
nullable: !isNotNull,
|
|
1531
|
+
primary: false,
|
|
1532
|
+
unique: false,
|
|
1533
|
+
default: defaultVal
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
if (primaryCol && columns.has(primaryCol)) {
|
|
1537
|
+
columns.get(primaryCol).primary = true;
|
|
1538
|
+
}
|
|
1539
|
+
state.tables.set(tableName, { name: tableName, columns });
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
function parseCreateEnum(sql, state) {
|
|
1543
|
+
const re = /CREATE\s+TYPE\s+"(\w+)"\s+AS\s+ENUM\s*\(([^)]+)\)/gi;
|
|
1544
|
+
let m;
|
|
1545
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1546
|
+
const enumName = m[1];
|
|
1547
|
+
const valuesStr = m[2];
|
|
1548
|
+
const values = new Set(
|
|
1549
|
+
valuesStr.split(",").map((v) => v.trim().replace(/^'(.*)'$/, "$1")).filter(Boolean)
|
|
1550
|
+
);
|
|
1551
|
+
state.enums.set(enumName, { name: enumName, values });
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
function parseAlterTable(sql, state) {
|
|
1555
|
+
const addColRe = /ALTER\s+TABLE\s+"(\w+)"\s+ADD\s+COLUMN\s+"(\w+)"\s+(.+?);/gi;
|
|
1556
|
+
let m;
|
|
1557
|
+
while ((m = addColRe.exec(sql)) !== null) {
|
|
1558
|
+
const tableName = m[1];
|
|
1559
|
+
const colName = m[2];
|
|
1560
|
+
let rest = m[3];
|
|
1561
|
+
const table = state.tables.get(tableName);
|
|
1562
|
+
if (!table) continue;
|
|
1563
|
+
const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
|
|
1564
|
+
const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)$/i);
|
|
1565
|
+
const defaultVal = defaultMatch ? defaultMatch[1].trim() : null;
|
|
1566
|
+
let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim();
|
|
1567
|
+
table.columns.set(colName, {
|
|
1568
|
+
name: colName,
|
|
1569
|
+
type: colType,
|
|
1570
|
+
nullable: !isNotNull,
|
|
1571
|
+
primary: false,
|
|
1572
|
+
unique: false,
|
|
1573
|
+
default: defaultVal
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
const dropColRe = /ALTER\s+TABLE\s+"(\w+)"\s+DROP\s+COLUMN\s+"(\w+)"/gi;
|
|
1577
|
+
while ((m = dropColRe.exec(sql)) !== null) {
|
|
1578
|
+
const table = state.tables.get(m[1]);
|
|
1579
|
+
if (table) table.columns.delete(m[2]);
|
|
1580
|
+
}
|
|
1581
|
+
const fkRe = /ALTER\s+TABLE\s+"(\w+)"\s+ADD\s+CONSTRAINT\s+"([^"]+)"\s+FOREIGN\s+KEY\s*\("(\w+)"\)\s+REFERENCES\s+"(\w+)"\("(\w+)"\)(?:\s+ON\s+DELETE\s+(\w+(?:\s+\w+)?))?/gi;
|
|
1582
|
+
while ((m = fkRe.exec(sql)) !== null) {
|
|
1583
|
+
state.fks.push({
|
|
1584
|
+
constraintName: m[2],
|
|
1585
|
+
sourceTable: m[1],
|
|
1586
|
+
sourceColumn: m[3],
|
|
1587
|
+
targetTable: m[4],
|
|
1588
|
+
targetColumn: m[5],
|
|
1589
|
+
onDelete: m[6] ?? null
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
function parseAlterEnum(sql, state) {
|
|
1594
|
+
const re = /ALTER\s+TYPE\s+"(\w+)"\s+ADD\s+VALUE\s+'([^']+)'/gi;
|
|
1595
|
+
let m;
|
|
1596
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1597
|
+
const en = state.enums.get(m[1]);
|
|
1598
|
+
if (en) en.values.add(m[2]);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
function parseDropTable(sql, state) {
|
|
1602
|
+
const re = /DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?"(\w+)"/gi;
|
|
1603
|
+
let m;
|
|
1604
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1605
|
+
state.tables.delete(m[1]);
|
|
1606
|
+
state.fks = state.fks.filter((fk) => fk.sourceTable !== m[1] && fk.targetTable !== m[1]);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
function parseUniqueIndex(sql, state) {
|
|
1610
|
+
const re = /CREATE\s+UNIQUE\s+INDEX\s+"[^"]+"\s+ON\s+"(\w+)"\("(\w+)"\)/gi;
|
|
1611
|
+
let m;
|
|
1612
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1613
|
+
const table = state.tables.get(m[1]);
|
|
1614
|
+
const col = table?.columns.get(m[2]);
|
|
1615
|
+
if (col) col.unique = true;
|
|
1616
|
+
if (!state.uniqueIndexes.has(m[1])) state.uniqueIndexes.set(m[1], /* @__PURE__ */ new Set());
|
|
1617
|
+
state.uniqueIndexes.get(m[1]).add(m[2]);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
function parseMigrations(rootDir) {
|
|
1621
|
+
const migrationsDir = (0, import_node_path6.join)(rootDir, "prisma", "migrations");
|
|
1622
|
+
const state = {
|
|
1623
|
+
tables: /* @__PURE__ */ new Map(),
|
|
1624
|
+
enums: /* @__PURE__ */ new Map(),
|
|
1625
|
+
fks: [],
|
|
1626
|
+
uniqueIndexes: /* @__PURE__ */ new Map()
|
|
1627
|
+
};
|
|
1628
|
+
if (!(0, import_node_fs6.existsSync)(migrationsDir)) return state;
|
|
1629
|
+
const dirs = (0, import_node_fs6.readdirSync)(migrationsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
1630
|
+
for (const dir of dirs) {
|
|
1631
|
+
const sqlPath = (0, import_node_path6.join)(migrationsDir, dir, "migration.sql");
|
|
1632
|
+
if (!(0, import_node_fs6.existsSync)(sqlPath)) continue;
|
|
1633
|
+
const sql = (0, import_node_fs6.readFileSync)(sqlPath, "utf-8");
|
|
1634
|
+
parseCreateEnum(sql, state);
|
|
1635
|
+
parseCreateTable(sql, state);
|
|
1636
|
+
parseAlterTable(sql, state);
|
|
1637
|
+
parseAlterEnum(sql, state);
|
|
1638
|
+
parseDropTable(sql, state);
|
|
1639
|
+
parseUniqueIndex(sql, state);
|
|
1640
|
+
}
|
|
1641
|
+
return state;
|
|
1642
|
+
}
|
|
1643
|
+
function loadPrismaState(rootDir) {
|
|
1644
|
+
const schemaPath = (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
|
|
1645
|
+
if (!(0, import_node_fs6.existsSync)(schemaPath)) return null;
|
|
1646
|
+
const content = (0, import_node_fs6.readFileSync)(schemaPath, "utf-8");
|
|
1647
|
+
const tables = /* @__PURE__ */ new Map();
|
|
1648
|
+
const enums = /* @__PURE__ */ new Map();
|
|
1649
|
+
const relations = [];
|
|
1650
|
+
const modelRe = /model\s+(\w+)\s*\{([^}]+)\}/g;
|
|
1651
|
+
let m;
|
|
1652
|
+
while ((m = modelRe.exec(content)) !== null) {
|
|
1653
|
+
const modelName = m[1];
|
|
1654
|
+
const body = m[2];
|
|
1655
|
+
const cols = [];
|
|
1656
|
+
for (const line of body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("@@"))) {
|
|
1657
|
+
const fm = line.match(/^(\w+)\s+(\S+)(.*)/);
|
|
1658
|
+
if (!fm) continue;
|
|
1659
|
+
const fieldName = fm[1];
|
|
1660
|
+
const fieldType = fm[2];
|
|
1661
|
+
const rest = fm[3] ?? "";
|
|
1662
|
+
const baseType = fieldType.replace(/[?\[\]]/g, "");
|
|
1663
|
+
const isRelation = rest.includes("@relation");
|
|
1664
|
+
if (isRelation) {
|
|
1665
|
+
const relArgs = rest.match(/@relation\(([^)]*)\)/)?.[1] ?? "";
|
|
1666
|
+
const fieldsMatch = relArgs.match(/fields:\s*\[([^\]]+)\]/);
|
|
1667
|
+
if (fieldsMatch) {
|
|
1668
|
+
relations.push({ source: modelName, target: baseType, fk: fieldsMatch[1].trim() });
|
|
1669
|
+
}
|
|
1670
|
+
continue;
|
|
1671
|
+
}
|
|
1672
|
+
if (fieldType.endsWith("[]") || fieldType.endsWith("?") && content.includes(`model ${baseType}`)) {
|
|
1673
|
+
if (new RegExp(`model\\s+${baseType}\\s*\\{`).test(content)) continue;
|
|
1674
|
+
}
|
|
1675
|
+
cols.push({
|
|
1676
|
+
name: fieldName,
|
|
1677
|
+
type: fieldType.replace("?", ""),
|
|
1678
|
+
nullable: fieldType.endsWith("?") || fieldType.includes("?"),
|
|
1679
|
+
primary: rest.includes("@id"),
|
|
1680
|
+
unique: rest.includes("@unique")
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
tables.set(modelName, { columns: cols });
|
|
1684
|
+
}
|
|
1685
|
+
const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
|
|
1686
|
+
while ((m = enumRe.exec(content)) !== null) {
|
|
1687
|
+
const values = m[2].split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
|
|
1688
|
+
enums.set(m[1], values);
|
|
1689
|
+
}
|
|
1690
|
+
return { tables, enums, relations };
|
|
1691
|
+
}
|
|
1692
|
+
function verify(sqlState, prisma) {
|
|
1693
|
+
const contradictions = [];
|
|
1694
|
+
const flaggedEdges = [];
|
|
1695
|
+
for (const [name] of sqlState.tables) {
|
|
1696
|
+
if (!prisma.tables.has(name)) {
|
|
1697
|
+
contradictions.push({
|
|
1698
|
+
entity: name,
|
|
1699
|
+
source_a: "sql-migrations",
|
|
1700
|
+
source_b: "prisma-schema",
|
|
1701
|
+
detail: `Table "${name}" exists in migrations but not in schema.prisma`
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
for (const [name] of prisma.tables) {
|
|
1706
|
+
if (!sqlState.tables.has(name)) {
|
|
1707
|
+
contradictions.push({
|
|
1708
|
+
entity: name,
|
|
1709
|
+
source_a: "prisma-schema",
|
|
1710
|
+
source_b: "sql-migrations",
|
|
1711
|
+
detail: `Table "${name}" in schema.prisma has no CREATE TABLE in migrations`
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
for (const [tableName, prismaTable] of prisma.tables) {
|
|
1716
|
+
const sqlTable = sqlState.tables.get(tableName);
|
|
1717
|
+
if (!sqlTable) continue;
|
|
1718
|
+
for (const prismaCol of prismaTable.columns) {
|
|
1719
|
+
const sqlCol = sqlTable.columns.get(prismaCol.name);
|
|
1720
|
+
if (!sqlCol) {
|
|
1721
|
+
contradictions.push({
|
|
1722
|
+
entity: `${tableName}.${prismaCol.name}`,
|
|
1723
|
+
source_a: "prisma-schema",
|
|
1724
|
+
source_b: "sql-migrations",
|
|
1725
|
+
detail: `Column "${tableName}.${prismaCol.name}" in schema.prisma but not in migrations`
|
|
1726
|
+
});
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
const mappedSqlType = pgTypeToPrisma(sqlCol.type);
|
|
1730
|
+
const prismaBaseType = prismaCol.type.replace(/[?\[\]]/g, "");
|
|
1731
|
+
if (mappedSqlType !== prismaBaseType && mappedSqlType !== prismaCol.type) {
|
|
1732
|
+
if (!sqlState.enums.has(prismaBaseType)) {
|
|
1733
|
+
contradictions.push({
|
|
1734
|
+
entity: `${tableName}.${prismaCol.name}`,
|
|
1735
|
+
source_a: "sql-migrations",
|
|
1736
|
+
source_b: "prisma-schema",
|
|
1737
|
+
detail: `Column "${tableName}.${prismaCol.name}": SQL type "${sqlCol.type}" (\u2192${mappedSqlType}) vs Prisma type "${prismaCol.type}"`
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
if (sqlCol.nullable !== prismaCol.nullable) {
|
|
1742
|
+
contradictions.push({
|
|
1743
|
+
entity: `${tableName}.${prismaCol.name}`,
|
|
1744
|
+
source_a: "sql-migrations",
|
|
1745
|
+
source_b: "prisma-schema",
|
|
1746
|
+
detail: `Column "${tableName}.${prismaCol.name}": ${sqlCol.nullable ? "nullable" : "NOT NULL"} in SQL vs ${prismaCol.nullable ? "optional" : "required"} in Prisma`
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
for (const [colName] of sqlTable.columns) {
|
|
1751
|
+
const inPrisma = prismaTable.columns.some((c) => c.name === colName);
|
|
1752
|
+
if (!inPrisma) {
|
|
1753
|
+
contradictions.push({
|
|
1754
|
+
entity: `${tableName}.${colName}`,
|
|
1755
|
+
source_a: "sql-migrations",
|
|
1756
|
+
source_b: "prisma-schema",
|
|
1757
|
+
detail: `Column "${tableName}.${colName}" in migrations but not in schema.prisma`
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
for (const [name, sqlEnum] of sqlState.enums) {
|
|
1763
|
+
const prismaValues = prisma.enums.get(name);
|
|
1764
|
+
if (!prismaValues) {
|
|
1765
|
+
contradictions.push({
|
|
1766
|
+
entity: name,
|
|
1767
|
+
source_a: "sql-migrations",
|
|
1768
|
+
source_b: "prisma-schema",
|
|
1769
|
+
detail: `Enum "${name}" exists in migrations but not in schema.prisma`
|
|
1770
|
+
});
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
const prismaSet = new Set(prismaValues);
|
|
1774
|
+
for (const val of sqlEnum.values) {
|
|
1775
|
+
if (!prismaSet.has(val)) {
|
|
1776
|
+
contradictions.push({
|
|
1777
|
+
entity: `${name}.${val}`,
|
|
1778
|
+
source_a: "sql-migrations",
|
|
1779
|
+
source_b: "prisma-schema",
|
|
1780
|
+
detail: `Enum "${name}": value "${val}" in migrations but not in schema.prisma`
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
for (const val of prismaValues) {
|
|
1785
|
+
if (!sqlEnum.values.has(val)) {
|
|
1786
|
+
contradictions.push({
|
|
1787
|
+
entity: `${name}.${val}`,
|
|
1788
|
+
source_a: "prisma-schema",
|
|
1789
|
+
source_b: "sql-migrations",
|
|
1790
|
+
detail: `Enum "${name}": value "${val}" in schema.prisma but not in migrations`
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
const prismaFkSet = new Set(prisma.relations.map((r) => `${r.source}|${r.target}|${r.fk}`));
|
|
1796
|
+
for (const fk of sqlState.fks) {
|
|
1797
|
+
const key = `${fk.sourceTable}|${fk.targetTable}|${fk.sourceColumn}`;
|
|
1798
|
+
if (!prismaFkSet.has(key)) {
|
|
1799
|
+
flaggedEdges.push({
|
|
1800
|
+
source: fk.sourceTable,
|
|
1801
|
+
target: fk.targetTable,
|
|
1802
|
+
type: "belongs_to",
|
|
1803
|
+
label: `FK "${fk.constraintName}" (${fk.sourceColumn}\u2192${fk.targetTable}) in migrations but no @relation in schema.prisma`,
|
|
1804
|
+
confidence: "high"
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
return { contradictions, flaggedEdges };
|
|
1809
|
+
}
|
|
1810
|
+
function detect3(rootDir) {
|
|
1811
|
+
const migrationsDir = (0, import_node_path6.join)(rootDir, "prisma", "migrations");
|
|
1812
|
+
if (!(0, import_node_fs6.existsSync)(migrationsDir)) return false;
|
|
1813
|
+
return (0, import_node_fs6.readdirSync)(migrationsDir, { withFileTypes: true }).some((d) => d.isDirectory() && (0, import_node_fs6.existsSync)((0, import_node_path6.join)(migrationsDir, d.name, "migration.sql")));
|
|
1814
|
+
}
|
|
1815
|
+
function generate3(rootDir) {
|
|
1816
|
+
const sqlState = parseMigrations(rootDir);
|
|
1817
|
+
const prisma = loadPrismaState(rootDir);
|
|
1818
|
+
const prismaTableIds = prisma ? new Set(prisma.tables.keys()) : /* @__PURE__ */ new Set();
|
|
1819
|
+
const prismaEnumIds = prisma ? new Set(prisma.enums.keys()) : /* @__PURE__ */ new Set();
|
|
1820
|
+
const nodes = [];
|
|
1821
|
+
for (const [name, table] of sqlState.tables) {
|
|
1822
|
+
if (prismaTableIds.has(name)) continue;
|
|
1823
|
+
nodes.push({
|
|
1824
|
+
id: name,
|
|
1825
|
+
type: "table",
|
|
1826
|
+
name,
|
|
1827
|
+
source: "sql",
|
|
1828
|
+
columns: [...table.columns.values()].map((c) => ({
|
|
1829
|
+
name: c.name,
|
|
1830
|
+
type: c.type,
|
|
1831
|
+
primary: c.primary,
|
|
1832
|
+
unique: c.unique,
|
|
1833
|
+
nullable: c.nullable,
|
|
1834
|
+
default: c.default,
|
|
1835
|
+
isRelation: false,
|
|
1836
|
+
comment: null
|
|
1837
|
+
}))
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
for (const [name, sqlEnum] of sqlState.enums) {
|
|
1841
|
+
if (prismaEnumIds.has(name)) continue;
|
|
1842
|
+
nodes.push({
|
|
1843
|
+
id: name,
|
|
1844
|
+
type: "enum",
|
|
1845
|
+
name,
|
|
1846
|
+
source: "sql",
|
|
1847
|
+
values: [...sqlEnum.values]
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
const sqlOnlyTables = new Set(nodes.filter((n) => n.type === "table").map((n) => n.id));
|
|
1851
|
+
const edges = sqlState.fks.filter((fk) => sqlOnlyTables.has(fk.sourceTable)).map((fk) => ({
|
|
1852
|
+
source: fk.sourceTable,
|
|
1853
|
+
target: fk.targetTable,
|
|
1854
|
+
type: "belongs_to",
|
|
1855
|
+
fk: fk.sourceColumn,
|
|
1856
|
+
onDelete: fk.onDelete
|
|
1857
|
+
}));
|
|
1858
|
+
const { contradictions, flaggedEdges } = prisma ? verify(sqlState, prisma) : { contradictions: [], flaggedEdges: [] };
|
|
1859
|
+
return {
|
|
1860
|
+
metadata: {
|
|
1861
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1862
|
+
scope: "sql-migrations",
|
|
1863
|
+
source: "prisma/migrations/",
|
|
1864
|
+
layer: "db",
|
|
1865
|
+
sql_tables: sqlState.tables.size,
|
|
1866
|
+
sql_enums: sqlState.enums.size,
|
|
1867
|
+
sql_fks: sqlState.fks.length,
|
|
1868
|
+
additive_nodes: nodes.length,
|
|
1869
|
+
contradictions_found: contradictions.length,
|
|
1870
|
+
flagged_edges_found: flaggedEdges.length
|
|
1871
|
+
},
|
|
1872
|
+
nodes,
|
|
1873
|
+
edges,
|
|
1874
|
+
cross_refs: [],
|
|
1875
|
+
contradictions,
|
|
1876
|
+
warnings: [],
|
|
1877
|
+
flagged_edges: flaggedEdges
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
var sqlMigrationsParser = {
|
|
1881
|
+
id: "sql-migrations",
|
|
1882
|
+
layer: "db",
|
|
1403
1883
|
detect: detect3,
|
|
1404
1884
|
generate: generate3
|
|
1405
1885
|
};
|
|
@@ -1527,6 +2007,7 @@ function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
|
|
|
1527
2007
|
var fetchResolverParser = {
|
|
1528
2008
|
id: "fetch-resolver",
|
|
1529
2009
|
layer: "crosslayer",
|
|
2010
|
+
concern: "api-binding",
|
|
1530
2011
|
detect(_rootDir) {
|
|
1531
2012
|
return true;
|
|
1532
2013
|
},
|
|
@@ -1617,31 +2098,32 @@ var fetchResolverParser = {
|
|
|
1617
2098
|
};
|
|
1618
2099
|
|
|
1619
2100
|
// src/server/graph/parsers/crosslayer/api-annotations.ts
|
|
1620
|
-
var
|
|
1621
|
-
var
|
|
2101
|
+
var import_node_fs7 = require("node:fs");
|
|
2102
|
+
var import_node_path7 = require("node:path");
|
|
1622
2103
|
var API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
|
|
1623
|
-
function
|
|
1624
|
-
if (!(0,
|
|
2104
|
+
function walk2(dir, exts) {
|
|
2105
|
+
if (!(0, import_node_fs7.existsSync)(dir)) return [];
|
|
1625
2106
|
const results = [];
|
|
1626
|
-
for (const entry of (0,
|
|
2107
|
+
for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
|
|
1627
2108
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1628
|
-
const full = (0,
|
|
2109
|
+
const full = (0, import_node_path7.join)(dir, entry.name);
|
|
1629
2110
|
if (entry.isDirectory()) {
|
|
1630
|
-
results.push(...
|
|
1631
|
-
} else if (exts.includes((0,
|
|
2111
|
+
results.push(...walk2(full, exts));
|
|
2112
|
+
} else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
|
|
1632
2113
|
results.push(full);
|
|
1633
2114
|
}
|
|
1634
2115
|
}
|
|
1635
2116
|
return results;
|
|
1636
2117
|
}
|
|
1637
2118
|
function toNodeId2(srcDir, absPath) {
|
|
1638
|
-
return (0,
|
|
2119
|
+
return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
1639
2120
|
}
|
|
1640
2121
|
var apiAnnotationsParser = {
|
|
1641
2122
|
id: "api-annotations",
|
|
1642
2123
|
layer: "crosslayer",
|
|
2124
|
+
concern: "api-binding",
|
|
1643
2125
|
detect(rootDir) {
|
|
1644
|
-
return (0,
|
|
2126
|
+
return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
|
|
1645
2127
|
},
|
|
1646
2128
|
generate(rootDir, layerOutputs) {
|
|
1647
2129
|
const apiOutput = layerOutputs.get("api");
|
|
@@ -1652,13 +2134,13 @@ var apiAnnotationsParser = {
|
|
|
1652
2134
|
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
1653
2135
|
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1654
2136
|
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1655
|
-
const srcDir = (0,
|
|
1656
|
-
const files =
|
|
2137
|
+
const srcDir = (0, import_node_path7.join)(rootDir, "src");
|
|
2138
|
+
const files = walk2(srcDir, [".ts", ".tsx"]);
|
|
1657
2139
|
const crossRefs = [];
|
|
1658
2140
|
const flaggedEdges = [];
|
|
1659
2141
|
const seen = /* @__PURE__ */ new Set();
|
|
1660
2142
|
for (const absPath of files) {
|
|
1661
|
-
const content = (0,
|
|
2143
|
+
const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
|
|
1662
2144
|
const sourceId = toNodeId2(srcDir, absPath);
|
|
1663
2145
|
if (!uiNodeIds.has(sourceId)) continue;
|
|
1664
2146
|
let match;
|
|
@@ -1701,69 +2183,725 @@ var apiAnnotationsParser = {
|
|
|
1701
2183
|
}
|
|
1702
2184
|
};
|
|
1703
2185
|
|
|
1704
|
-
// src/server/graph/parsers/crosslayer/url-literal-scanner.ts
|
|
1705
|
-
var
|
|
1706
|
-
var
|
|
1707
|
-
|
|
2186
|
+
// src/server/graph/parsers/crosslayer/url-literal-scanner.ts
|
|
2187
|
+
var import_node_fs8 = require("node:fs");
|
|
2188
|
+
var import_node_path8 = require("node:path");
|
|
2189
|
+
init_config();
|
|
2190
|
+
var URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
|
|
2191
|
+
function walk3(dir, exts) {
|
|
2192
|
+
if (!(0, import_node_fs8.existsSync)(dir)) return [];
|
|
2193
|
+
const results = [];
|
|
2194
|
+
for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
|
|
2195
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2196
|
+
const full = (0, import_node_path8.join)(dir, entry.name);
|
|
2197
|
+
if (entry.isDirectory()) {
|
|
2198
|
+
results.push(...walk3(full, exts));
|
|
2199
|
+
} else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
|
|
2200
|
+
results.push(full);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
return results;
|
|
2204
|
+
}
|
|
2205
|
+
function toNodeId3(srcDir, absPath) {
|
|
2206
|
+
return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
2207
|
+
}
|
|
2208
|
+
var urlLiteralScannerParser = {
|
|
2209
|
+
id: "url-literal-scanner",
|
|
2210
|
+
layer: "crosslayer",
|
|
2211
|
+
concern: "api-binding",
|
|
2212
|
+
detect(rootDir) {
|
|
2213
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2214
|
+
return paths !== null;
|
|
2215
|
+
},
|
|
2216
|
+
generate(rootDir, layerOutputs) {
|
|
2217
|
+
const apiOutput = layerOutputs.get("api");
|
|
2218
|
+
if (!apiOutput) {
|
|
2219
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2220
|
+
}
|
|
2221
|
+
const uiOutput = layerOutputs.get("ui");
|
|
2222
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
2223
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
2224
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
2225
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2226
|
+
const srcDir = paths.srcDir;
|
|
2227
|
+
const clientDir = (0, import_node_path8.join)(srcDir, "client");
|
|
2228
|
+
const files = [
|
|
2229
|
+
...walk3(clientDir, [".ts", ".tsx"]),
|
|
2230
|
+
...walk3(paths.appDir, [".ts", ".tsx"])
|
|
2231
|
+
];
|
|
2232
|
+
const crossRefs = [];
|
|
2233
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2234
|
+
for (const absPath of files) {
|
|
2235
|
+
const sourceId = toNodeId3(srcDir, absPath);
|
|
2236
|
+
if (!uiNodeIds.has(sourceId)) continue;
|
|
2237
|
+
const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
|
|
2238
|
+
let match;
|
|
2239
|
+
URL_LITERAL_RE.lastIndex = 0;
|
|
2240
|
+
while ((match = URL_LITERAL_RE.exec(content)) !== null) {
|
|
2241
|
+
const urlPath = match[1];
|
|
2242
|
+
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
2243
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
2244
|
+
const key = `${sourceId}|${result.nodeId}|references_api`;
|
|
2245
|
+
if (seen.has(key)) continue;
|
|
2246
|
+
seen.add(key);
|
|
2247
|
+
crossRefs.push({
|
|
2248
|
+
source: sourceId,
|
|
2249
|
+
target: result.nodeId,
|
|
2250
|
+
type: "references_api",
|
|
2251
|
+
layer: "api"
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
return {
|
|
2257
|
+
cross_refs: crossRefs,
|
|
2258
|
+
flagged_edges: [],
|
|
2259
|
+
warnings: [],
|
|
2260
|
+
patterns: {
|
|
2261
|
+
url_literals_resolved: crossRefs.length
|
|
2262
|
+
}
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
};
|
|
2266
|
+
|
|
2267
|
+
// src/server/graph/parsers/static/static-values.ts
|
|
2268
|
+
var import_node_fs9 = require("node:fs");
|
|
2269
|
+
var import_node_path9 = require("node:path");
|
|
2270
|
+
var parseCode = null;
|
|
2271
|
+
function tryLoadTreeSitter() {
|
|
2272
|
+
if (parseCode) return true;
|
|
2273
|
+
try {
|
|
2274
|
+
const extractor = (init_ts_extractor(), __toCommonJS(ts_extractor_exports));
|
|
2275
|
+
if (typeof extractor.parseCodeTS === "function") {
|
|
2276
|
+
parseCode = extractor.parseCodeTS;
|
|
2277
|
+
return true;
|
|
2278
|
+
}
|
|
2279
|
+
} catch {
|
|
2280
|
+
}
|
|
2281
|
+
return false;
|
|
2282
|
+
}
|
|
2283
|
+
var SHARED_MODELS = /* @__PURE__ */ new Set(["permission", "role", "tag"]);
|
|
2284
|
+
var DB_MODELS = /* @__PURE__ */ new Set(["subscriptionPlan", "providerDefinition", "pipelineMasterTemplate"]);
|
|
2285
|
+
function classifyScope(source, model) {
|
|
2286
|
+
if (source.includes("prisma/schema.prisma")) return "shared";
|
|
2287
|
+
if (source.includes("prisma/seed") && model) {
|
|
2288
|
+
if (SHARED_MODELS.has(model)) return "shared";
|
|
2289
|
+
if (DB_MODELS.has(model)) return "db";
|
|
2290
|
+
return "shared";
|
|
2291
|
+
}
|
|
2292
|
+
if (source.startsWith("src/client/") || source.startsWith("src/app/")) return "fe";
|
|
2293
|
+
if (source.startsWith("src/server/")) return "be";
|
|
2294
|
+
if (source.startsWith("src/config/")) return "be";
|
|
2295
|
+
if (source.startsWith("src/lib/")) return "shared";
|
|
2296
|
+
return "shared";
|
|
2297
|
+
}
|
|
2298
|
+
function extractEnumValues(rootDir) {
|
|
2299
|
+
const nodes = [];
|
|
2300
|
+
const edges = [];
|
|
2301
|
+
const schemaPaths = [
|
|
2302
|
+
(0, import_node_path9.join)(rootDir, "prisma", "schema.prisma"),
|
|
2303
|
+
(0, import_node_path9.join)(rootDir, "prisma", "schema")
|
|
2304
|
+
];
|
|
2305
|
+
let content = "";
|
|
2306
|
+
for (const p of schemaPaths) {
|
|
2307
|
+
if ((0, import_node_fs9.existsSync)(p)) {
|
|
2308
|
+
try {
|
|
2309
|
+
const stat = (0, import_node_fs9.statSync)(p);
|
|
2310
|
+
if (stat.isFile()) {
|
|
2311
|
+
content = (0, import_node_fs9.readFileSync)(p, "utf-8");
|
|
2312
|
+
} else if (stat.isDirectory()) {
|
|
2313
|
+
const files = (0, import_node_fs9.readdirSync)(p).filter((f) => f.endsWith(".prisma"));
|
|
2314
|
+
content = files.map((f) => (0, import_node_fs9.readFileSync)((0, import_node_path9.join)(p, f), "utf-8")).join("\n");
|
|
2315
|
+
}
|
|
2316
|
+
} catch {
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
break;
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
if (!content) return { nodes, edges };
|
|
2323
|
+
const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
|
|
2324
|
+
let m;
|
|
2325
|
+
while ((m = enumRe.exec(content)) !== null) {
|
|
2326
|
+
const enumName = m[1];
|
|
2327
|
+
const body = m[2];
|
|
2328
|
+
const values = body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
|
|
2329
|
+
const scope = classifyScope("prisma/schema.prisma");
|
|
2330
|
+
for (const val of values) {
|
|
2331
|
+
const nodeId = `${enumName}.${val}`;
|
|
2332
|
+
nodes.push({
|
|
2333
|
+
id: nodeId,
|
|
2334
|
+
type: "enum_value",
|
|
2335
|
+
name: val,
|
|
2336
|
+
parentEnum: enumName,
|
|
2337
|
+
value: val,
|
|
2338
|
+
scope,
|
|
2339
|
+
source: "prisma/schema.prisma"
|
|
2340
|
+
});
|
|
2341
|
+
edges.push({
|
|
2342
|
+
source: nodeId,
|
|
2343
|
+
target: `enum:${enumName}`,
|
|
2344
|
+
type: "member_of"
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
nodes.push({
|
|
2348
|
+
id: `enum:${enumName}`,
|
|
2349
|
+
type: "enum_group",
|
|
2350
|
+
name: enumName,
|
|
2351
|
+
valueCount: values.length,
|
|
2352
|
+
scope,
|
|
2353
|
+
source: "prisma/schema.prisma"
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
return { nodes, edges };
|
|
2357
|
+
}
|
|
2358
|
+
function extractPropsFromObjectNode(node) {
|
|
2359
|
+
const props = {};
|
|
2360
|
+
for (const child of node.namedChildren) {
|
|
2361
|
+
if (child.type !== "pair") continue;
|
|
2362
|
+
const keyNode = child.childForFieldName("key");
|
|
2363
|
+
const valNode = child.childForFieldName("value");
|
|
2364
|
+
if (!keyNode || !valNode) continue;
|
|
2365
|
+
const key = keyNode.type === "property_identifier" ? keyNode.text : keyNode.text.replace(/['"]/g, "");
|
|
2366
|
+
if (valNode.type === "string" || valNode.type === "template_string") {
|
|
2367
|
+
const fragment = valNode.namedChildren.find((c) => c.type === "string_fragment" || c.type === "template_substitution");
|
|
2368
|
+
props[key] = fragment ? fragment.text : valNode.text.replace(/^['"`]|['"`]$/g, "");
|
|
2369
|
+
} else if (valNode.type === "number") {
|
|
2370
|
+
props[key] = valNode.text;
|
|
2371
|
+
} else if (valNode.type === "true" || valNode.type === "false") {
|
|
2372
|
+
props[key] = valNode.text;
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
return props;
|
|
2376
|
+
}
|
|
2377
|
+
function extractStringArrayFromNode(node) {
|
|
2378
|
+
const values = [];
|
|
2379
|
+
for (const child of node.namedChildren) {
|
|
2380
|
+
if (child.type === "string") {
|
|
2381
|
+
const frag = child.namedChildren.find((c) => c.type === "string_fragment");
|
|
2382
|
+
if (frag) values.push(frag.text);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
return values;
|
|
2386
|
+
}
|
|
2387
|
+
function findArrayDecl(root, varName) {
|
|
2388
|
+
function walk5(node) {
|
|
2389
|
+
if (node.type === "variable_declarator") {
|
|
2390
|
+
const nameNode = node.childForFieldName("name");
|
|
2391
|
+
const valueNode = node.childForFieldName("value");
|
|
2392
|
+
if (nameNode?.text === varName && valueNode?.type === "array") {
|
|
2393
|
+
return valueNode;
|
|
2394
|
+
}
|
|
2395
|
+
if (nameNode?.text === varName && valueNode?.type === "as_expression") {
|
|
2396
|
+
const inner = valueNode.namedChildren.find((c) => c.type === "array");
|
|
2397
|
+
if (inner) return inner;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
for (const child of node.namedChildren) {
|
|
2401
|
+
const found = walk5(child);
|
|
2402
|
+
if (found) return found;
|
|
2403
|
+
}
|
|
2404
|
+
return null;
|
|
2405
|
+
}
|
|
2406
|
+
return walk5(root);
|
|
2407
|
+
}
|
|
2408
|
+
function extractObjectPropsRegex(objStr) {
|
|
2409
|
+
const props = {};
|
|
2410
|
+
const propRe = /(\w+):\s*['"]([^'"]*)['"]/g;
|
|
2411
|
+
let m;
|
|
2412
|
+
while ((m = propRe.exec(objStr)) !== null) props[m[1]] = m[2];
|
|
2413
|
+
const numRe = /(\w+):\s*(-?\d+(?:\.\d+)?)\s*[,\n}]/g;
|
|
2414
|
+
while ((m = numRe.exec(objStr)) !== null) if (!props[m[1]]) props[m[1]] = m[2];
|
|
2415
|
+
return props;
|
|
2416
|
+
}
|
|
2417
|
+
function extractStringArrayRegex(arrStr) {
|
|
2418
|
+
return (arrStr.match(/'([^']+)'/g) ?? []).map((s) => s.replace(/'/g, ""));
|
|
2419
|
+
}
|
|
2420
|
+
function splitArrayObjectsRegex(arrayBody) {
|
|
2421
|
+
const objects = [];
|
|
2422
|
+
let depth = 0;
|
|
2423
|
+
let start = -1;
|
|
2424
|
+
for (let i = 0; i < arrayBody.length; i++) {
|
|
2425
|
+
if (arrayBody[i] === "{") {
|
|
2426
|
+
if (depth === 0) start = i;
|
|
2427
|
+
depth++;
|
|
2428
|
+
} else if (arrayBody[i] === "}") {
|
|
2429
|
+
depth--;
|
|
2430
|
+
if (depth === 0 && start >= 0) {
|
|
2431
|
+
objects.push(arrayBody.slice(start, i + 1));
|
|
2432
|
+
start = -1;
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
return objects;
|
|
2437
|
+
}
|
|
2438
|
+
function detectSeededArrays(content, sourceFile) {
|
|
2439
|
+
const results = [];
|
|
2440
|
+
const forOfRe = /for\s*\(\s*const\s+\w+\s+of\s+(\w+)\s*\)\s*\{/g;
|
|
2441
|
+
let fm;
|
|
2442
|
+
while ((fm = forOfRe.exec(content)) !== null) {
|
|
2443
|
+
const arrayName = fm[1];
|
|
2444
|
+
const lookahead = content.slice(fm.index + fm[0].length, fm.index + fm[0].length + 500);
|
|
2445
|
+
const prismaMatch = lookahead.match(/prisma\.(\w+)\.(create|upsert|update|createMany|findFirst)/);
|
|
2446
|
+
if (!prismaMatch) continue;
|
|
2447
|
+
results.push({ arrayName, prismaModel: prismaMatch[1], sourceFile });
|
|
2448
|
+
}
|
|
2449
|
+
return results;
|
|
2450
|
+
}
|
|
2451
|
+
function pickIdField(props) {
|
|
2452
|
+
for (const key of ["key", "slug", "id", "name", "templateId"]) {
|
|
2453
|
+
if (props[key]) return props[key];
|
|
2454
|
+
}
|
|
2455
|
+
return null;
|
|
2456
|
+
}
|
|
2457
|
+
function pickNameField(props) {
|
|
2458
|
+
for (const key of ["name", "slug", "key", "id"]) {
|
|
2459
|
+
if (props[key]) return props[key];
|
|
2460
|
+
}
|
|
2461
|
+
return null;
|
|
2462
|
+
}
|
|
2463
|
+
function modelToNodeType(model) {
|
|
2464
|
+
return `seed_${model.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "")}`;
|
|
2465
|
+
}
|
|
2466
|
+
function extractSeedData(rootDir) {
|
|
2467
|
+
const nodes = [];
|
|
2468
|
+
const edges = [];
|
|
2469
|
+
const seedFiles = [
|
|
2470
|
+
(0, import_node_path9.join)(rootDir, "prisma", "seed.ts"),
|
|
2471
|
+
(0, import_node_path9.join)(rootDir, "prisma", "seed.js"),
|
|
2472
|
+
(0, import_node_path9.join)(rootDir, "src", "server", "lib", "system-tags.ts")
|
|
2473
|
+
].filter(import_node_fs9.existsSync);
|
|
2474
|
+
const useTreeSitter = tryLoadTreeSitter();
|
|
2475
|
+
for (const filePath of seedFiles) {
|
|
2476
|
+
const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
|
|
2477
|
+
const relPath = (0, import_node_path9.relative)(rootDir, filePath);
|
|
2478
|
+
const seeded = detectSeededArrays(content, relPath);
|
|
2479
|
+
let astRoot = null;
|
|
2480
|
+
if (useTreeSitter && parseCode) {
|
|
2481
|
+
try {
|
|
2482
|
+
const tree = parseCode(content);
|
|
2483
|
+
astRoot = tree.rootNode;
|
|
2484
|
+
} catch {
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
for (const { arrayName, prismaModel, sourceFile } of seeded) {
|
|
2488
|
+
const nodeType = modelToNodeType(prismaModel);
|
|
2489
|
+
const scope = classifyScope(sourceFile, prismaModel);
|
|
2490
|
+
if (astRoot) {
|
|
2491
|
+
const arrayNode = findArrayDecl(astRoot, arrayName);
|
|
2492
|
+
if (!arrayNode) continue;
|
|
2493
|
+
for (const child of arrayNode.namedChildren) {
|
|
2494
|
+
if (child.type !== "object") continue;
|
|
2495
|
+
const props = extractPropsFromObjectNode(child);
|
|
2496
|
+
const idVal = pickIdField(props);
|
|
2497
|
+
if (!idVal) continue;
|
|
2498
|
+
const nameVal = pickNameField(props) ?? idVal;
|
|
2499
|
+
const nodeId = `seed:${prismaModel}:${idVal}`;
|
|
2500
|
+
const { scope: _seedScope, ...safeProps } = props;
|
|
2501
|
+
nodes.push({
|
|
2502
|
+
id: nodeId,
|
|
2503
|
+
type: nodeType,
|
|
2504
|
+
name: nameVal,
|
|
2505
|
+
value: idVal,
|
|
2506
|
+
model: prismaModel,
|
|
2507
|
+
...safeProps,
|
|
2508
|
+
seedScope: _seedScope ?? void 0,
|
|
2509
|
+
// preserve as seedScope
|
|
2510
|
+
scope,
|
|
2511
|
+
source: sourceFile
|
|
2512
|
+
});
|
|
2513
|
+
const permsArrayNode = child.namedChildren.filter((c) => c.type === "pair").find((c) => c.childForFieldName("key")?.text === "permissions");
|
|
2514
|
+
if (permsArrayNode) {
|
|
2515
|
+
const valNode = permsArrayNode.childForFieldName("value");
|
|
2516
|
+
if (valNode?.type === "array") {
|
|
2517
|
+
const permKeys = extractStringArrayFromNode(valNode);
|
|
2518
|
+
for (const pk of permKeys) {
|
|
2519
|
+
if (pk === "*") continue;
|
|
2520
|
+
edges.push({ source: nodeId, target: `seed:permission:${pk}`, type: "grants" });
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
} else {
|
|
2526
|
+
const constRe = new RegExp(`const\\s+${arrayName}\\s*(?::[^=]+)?=\\s*\\[`, "g");
|
|
2527
|
+
const cm = constRe.exec(content);
|
|
2528
|
+
if (!cm) continue;
|
|
2529
|
+
const bracketStart = cm.index + cm[0].length - 1;
|
|
2530
|
+
let depth = 1, i = bracketStart + 1;
|
|
2531
|
+
while (i < content.length && depth > 0) {
|
|
2532
|
+
if (content[i] === "[") depth++;
|
|
2533
|
+
else if (content[i] === "]") depth--;
|
|
2534
|
+
i++;
|
|
2535
|
+
}
|
|
2536
|
+
if (depth !== 0) continue;
|
|
2537
|
+
const body = content.slice(bracketStart + 1, i - 1);
|
|
2538
|
+
const objects = splitArrayObjectsRegex(body);
|
|
2539
|
+
for (const objStr of objects) {
|
|
2540
|
+
const props = extractObjectPropsRegex(objStr);
|
|
2541
|
+
const idVal = pickIdField(props);
|
|
2542
|
+
if (!idVal) continue;
|
|
2543
|
+
const nameVal = pickNameField(props) ?? idVal;
|
|
2544
|
+
const nodeId = `seed:${prismaModel}:${idVal}`;
|
|
2545
|
+
const { scope: _rScope, ...rSafeProps } = props;
|
|
2546
|
+
nodes.push({
|
|
2547
|
+
id: nodeId,
|
|
2548
|
+
type: nodeType,
|
|
2549
|
+
name: nameVal,
|
|
2550
|
+
value: idVal,
|
|
2551
|
+
model: prismaModel,
|
|
2552
|
+
...rSafeProps,
|
|
2553
|
+
seedScope: _rScope ?? void 0,
|
|
2554
|
+
scope,
|
|
2555
|
+
source: sourceFile
|
|
2556
|
+
});
|
|
2557
|
+
const permArrayMatch = objStr.match(/permissions:\s*\[([^\]]*)\]/);
|
|
2558
|
+
if (permArrayMatch) {
|
|
2559
|
+
for (const pk of extractStringArrayRegex(permArrayMatch[1])) {
|
|
2560
|
+
if (pk === "*") continue;
|
|
2561
|
+
edges.push({ source: nodeId, target: `seed:permission:${pk}`, type: "grants" });
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
return { nodes, edges };
|
|
2569
|
+
}
|
|
2570
|
+
function walkDir(dir, exts) {
|
|
2571
|
+
if (!(0, import_node_fs9.existsSync)(dir)) return [];
|
|
2572
|
+
const results = [];
|
|
2573
|
+
for (const entry of (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true })) {
|
|
2574
|
+
if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
|
|
2575
|
+
const full = (0, import_node_path9.join)(dir, entry.name);
|
|
2576
|
+
if (entry.isDirectory()) results.push(...walkDir(full, exts));
|
|
2577
|
+
else if (exts.some((ext) => entry.name.endsWith(ext))) results.push(full);
|
|
2578
|
+
}
|
|
2579
|
+
return results;
|
|
2580
|
+
}
|
|
2581
|
+
function extractConstants(rootDir) {
|
|
2582
|
+
const nodes = [];
|
|
2583
|
+
const srcDir = (0, import_node_path9.join)(rootDir, "src");
|
|
2584
|
+
if (!(0, import_node_fs9.existsSync)(srcDir)) return { nodes };
|
|
2585
|
+
for (const filePath of walkDir(srcDir, [".ts", ".tsx"])) {
|
|
2586
|
+
const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
|
|
2587
|
+
const relPath = (0, import_node_path9.relative)(rootDir, filePath);
|
|
2588
|
+
const constArrayRe = /export\s+const\s+([A-Z][A-Z_0-9]+)\s*(?::[^=]+)?\s*=\s*\[/g;
|
|
2589
|
+
let cm;
|
|
2590
|
+
while ((cm = constArrayRe.exec(content)) !== null) {
|
|
2591
|
+
const constName = cm[1];
|
|
2592
|
+
const bracketStart = cm.index + cm[0].length - 1;
|
|
2593
|
+
let depth = 1, i = bracketStart + 1;
|
|
2594
|
+
while (i < content.length && depth > 0) {
|
|
2595
|
+
if (content[i] === "[") depth++;
|
|
2596
|
+
else if (content[i] === "]") depth--;
|
|
2597
|
+
i++;
|
|
2598
|
+
}
|
|
2599
|
+
if (depth !== 0) continue;
|
|
2600
|
+
const body = content.slice(bracketStart + 1, i - 1);
|
|
2601
|
+
const stringValues = extractStringArrayRegex(body);
|
|
2602
|
+
const objectCount = splitArrayObjectsRegex(body).length;
|
|
2603
|
+
const valueCount = Math.max(stringValues.length, objectCount);
|
|
2604
|
+
if (valueCount < 2) continue;
|
|
2605
|
+
const scope = classifyScope(relPath);
|
|
2606
|
+
nodes.push({
|
|
2607
|
+
id: `const:${constName}`,
|
|
2608
|
+
type: "constant",
|
|
2609
|
+
name: constName,
|
|
2610
|
+
valueCount,
|
|
2611
|
+
values: stringValues.length > 0 && stringValues.length <= 30 ? stringValues : void 0,
|
|
2612
|
+
scope,
|
|
2613
|
+
source: relPath
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
return { nodes };
|
|
2618
|
+
}
|
|
2619
|
+
function detect4(rootDir) {
|
|
2620
|
+
return (0, import_node_fs9.existsSync)((0, import_node_path9.join)(rootDir, "prisma", "schema.prisma")) || (0, import_node_fs9.existsSync)((0, import_node_path9.join)(rootDir, "prisma", "seed.ts"));
|
|
2621
|
+
}
|
|
2622
|
+
function generate4(rootDir) {
|
|
2623
|
+
const enumResult = extractEnumValues(rootDir);
|
|
2624
|
+
const seedResult = extractSeedData(rootDir);
|
|
2625
|
+
const constResult = extractConstants(rootDir);
|
|
2626
|
+
const allNodes = [...enumResult.nodes, ...seedResult.nodes, ...constResult.nodes];
|
|
2627
|
+
const allEdges = [...enumResult.edges, ...seedResult.edges];
|
|
2628
|
+
const typeOrder = {
|
|
2629
|
+
enum_group: 0,
|
|
2630
|
+
enum_value: 1,
|
|
2631
|
+
seed_permission: 2,
|
|
2632
|
+
seed_role: 3,
|
|
2633
|
+
seed_tag: 4,
|
|
2634
|
+
seed_plan: 5,
|
|
2635
|
+
seed_provider: 6,
|
|
2636
|
+
constant: 7
|
|
2637
|
+
};
|
|
2638
|
+
allNodes.sort((a, b) => {
|
|
2639
|
+
const ta = typeOrder[a.type] ?? 8;
|
|
2640
|
+
const tb = typeOrder[b.type] ?? 8;
|
|
2641
|
+
if (ta !== tb) return ta - tb;
|
|
2642
|
+
return a.name.localeCompare(b.name);
|
|
2643
|
+
});
|
|
2644
|
+
const enumGroups = allNodes.filter((n) => n.type === "enum_group").length;
|
|
2645
|
+
const enumValues = allNodes.filter((n) => n.type === "enum_value").length;
|
|
2646
|
+
const seedNodes = allNodes.filter((n) => n.type.startsWith("seed_")).length;
|
|
2647
|
+
const constNodes = allNodes.filter((n) => n.type === "constant").length;
|
|
2648
|
+
const byScope = {};
|
|
2649
|
+
for (const n of allNodes) {
|
|
2650
|
+
const s = n.scope ?? "unknown";
|
|
2651
|
+
byScope[s] = (byScope[s] ?? 0) + 1;
|
|
2652
|
+
}
|
|
2653
|
+
return {
|
|
2654
|
+
metadata: {
|
|
2655
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
2656
|
+
scope: "static-values",
|
|
2657
|
+
layer: "static",
|
|
2658
|
+
enum_groups: enumGroups,
|
|
2659
|
+
enum_values: enumValues,
|
|
2660
|
+
seed_records: seedNodes,
|
|
2661
|
+
constants: constNodes,
|
|
2662
|
+
total_nodes: allNodes.length,
|
|
2663
|
+
total_edges: allEdges.length,
|
|
2664
|
+
parser: tryLoadTreeSitter() ? "tree-sitter" : "regex-fallback"
|
|
2665
|
+
},
|
|
2666
|
+
nodes: allNodes,
|
|
2667
|
+
edges: allEdges,
|
|
2668
|
+
cross_refs: [],
|
|
2669
|
+
contradictions: [],
|
|
2670
|
+
warnings: [],
|
|
2671
|
+
flagged_edges: [],
|
|
2672
|
+
patterns: {
|
|
2673
|
+
by_type: {
|
|
2674
|
+
enum_group: enumGroups,
|
|
2675
|
+
enum_value: enumValues,
|
|
2676
|
+
seed_permission: allNodes.filter((n) => n.type === "seed_permission").length,
|
|
2677
|
+
seed_role: allNodes.filter((n) => n.type === "seed_role").length,
|
|
2678
|
+
seed_tag: allNodes.filter((n) => n.type === "seed_tag").length,
|
|
2679
|
+
seed_plan: allNodes.filter((n) => n.type === "seed_plan").length,
|
|
2680
|
+
seed_provider: allNodes.filter((n) => n.type === "seed_provider").length,
|
|
2681
|
+
constant: constNodes
|
|
2682
|
+
},
|
|
2683
|
+
by_scope: byScope
|
|
2684
|
+
}
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
var staticValuesParser = {
|
|
2688
|
+
id: "static-values",
|
|
2689
|
+
layer: "static",
|
|
2690
|
+
detect: detect4,
|
|
2691
|
+
generate: generate4
|
|
2692
|
+
};
|
|
2693
|
+
|
|
2694
|
+
// src/server/graph/parsers/crosslayer/static-ref-scanner.ts
|
|
2695
|
+
var import_node_fs10 = require("node:fs");
|
|
2696
|
+
var import_node_path10 = require("node:path");
|
|
2697
|
+
init_config();
|
|
1708
2698
|
function walk4(dir, exts) {
|
|
1709
|
-
if (!(0,
|
|
2699
|
+
if (!(0, import_node_fs10.existsSync)(dir)) return [];
|
|
1710
2700
|
const results = [];
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
2701
|
+
function recurse(d) {
|
|
2702
|
+
for (const entry of (0, import_node_fs10.readdirSync)(d, { withFileTypes: true })) {
|
|
2703
|
+
const full = (0, import_node_path10.join)(d, entry.name);
|
|
2704
|
+
if (entry.isDirectory()) {
|
|
2705
|
+
if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
|
|
2706
|
+
recurse(full);
|
|
2707
|
+
} else if (exts.some((ext) => entry.name.endsWith(ext))) {
|
|
2708
|
+
results.push(full);
|
|
2709
|
+
}
|
|
1718
2710
|
}
|
|
1719
2711
|
}
|
|
2712
|
+
recurse(dir);
|
|
1720
2713
|
return results;
|
|
1721
2714
|
}
|
|
1722
|
-
|
|
1723
|
-
|
|
2715
|
+
var MIN_VALUE_LENGTH = 4;
|
|
2716
|
+
var SKIP_VALUES = /* @__PURE__ */ new Set([
|
|
2717
|
+
"true",
|
|
2718
|
+
"false",
|
|
2719
|
+
"null",
|
|
2720
|
+
"undefined",
|
|
2721
|
+
"none",
|
|
2722
|
+
"default",
|
|
2723
|
+
"name",
|
|
2724
|
+
"type",
|
|
2725
|
+
"data",
|
|
2726
|
+
"text",
|
|
2727
|
+
"info",
|
|
2728
|
+
"error",
|
|
2729
|
+
"open",
|
|
2730
|
+
"read",
|
|
2731
|
+
"user",
|
|
2732
|
+
"test",
|
|
2733
|
+
"json",
|
|
2734
|
+
"form"
|
|
2735
|
+
]);
|
|
2736
|
+
function isInCommentOrType(node) {
|
|
2737
|
+
let current = node.parent;
|
|
2738
|
+
while (current) {
|
|
2739
|
+
if (current.type === "comment" || current.type === "type_annotation" || current.type === "type_alias_declaration" || current.type === "interface_declaration" || current.type === "jsdoc") {
|
|
2740
|
+
return true;
|
|
2741
|
+
}
|
|
2742
|
+
current = current.parent;
|
|
2743
|
+
}
|
|
2744
|
+
return false;
|
|
1724
2745
|
}
|
|
1725
|
-
|
|
1726
|
-
|
|
2746
|
+
function collectStaticRefs(root, valueLookup) {
|
|
2747
|
+
const refs = [];
|
|
2748
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2749
|
+
function visit(node) {
|
|
2750
|
+
if (node.type === "string_fragment") {
|
|
2751
|
+
const val = node.text;
|
|
2752
|
+
if (val.length >= MIN_VALUE_LENGTH && !SKIP_VALUES.has(val.toLowerCase())) {
|
|
2753
|
+
const targets = valueLookup.get(val);
|
|
2754
|
+
if (targets && !isInCommentOrType(node)) {
|
|
2755
|
+
for (const t of targets) {
|
|
2756
|
+
const key = t;
|
|
2757
|
+
if (!seen.has(key)) {
|
|
2758
|
+
seen.add(key);
|
|
2759
|
+
refs.push({ value: val, targetIds: [t] });
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
if (node.type === "member_expression") {
|
|
2766
|
+
const objNode = node.namedChildren.find((c) => c.type === "identifier");
|
|
2767
|
+
const propNode = node.namedChildren.find((c) => c.type === "property_identifier");
|
|
2768
|
+
if (objNode && propNode) {
|
|
2769
|
+
const combined = `${objNode.text}.${propNode.text}`;
|
|
2770
|
+
const targets = valueLookup.get(propNode.text);
|
|
2771
|
+
const directTarget = valueLookup.get(combined);
|
|
2772
|
+
if (directTarget && !isInCommentOrType(node)) {
|
|
2773
|
+
for (const t of directTarget) {
|
|
2774
|
+
if (!seen.has(t)) {
|
|
2775
|
+
seen.add(t);
|
|
2776
|
+
refs.push({ value: combined, targetIds: [t] });
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
} else if (targets && !isInCommentOrType(node)) {
|
|
2780
|
+
for (const t of targets) {
|
|
2781
|
+
if (!seen.has(t)) {
|
|
2782
|
+
seen.add(t);
|
|
2783
|
+
refs.push({ value: propNode.text, targetIds: [t] });
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
for (const child of node.namedChildren) {
|
|
2790
|
+
visit(child);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
visit(root);
|
|
2794
|
+
return refs;
|
|
2795
|
+
}
|
|
2796
|
+
function collectStaticRefsRegex(content, valueLookup, allValues) {
|
|
2797
|
+
const refs = [];
|
|
2798
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2799
|
+
const escaped = allValues.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
2800
|
+
const pattern = new RegExp(
|
|
2801
|
+
`(?:['"\`])(${escaped.join("|")})(?:['"\`])|\\b(\\w+)\\.(${escaped.join("|")})\\b`,
|
|
2802
|
+
"g"
|
|
2803
|
+
);
|
|
2804
|
+
let match;
|
|
2805
|
+
pattern.lastIndex = 0;
|
|
2806
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
2807
|
+
const val = match[1] ?? match[3];
|
|
2808
|
+
if (!val) continue;
|
|
2809
|
+
const targets = valueLookup.get(val);
|
|
2810
|
+
if (!targets) continue;
|
|
2811
|
+
for (const t of targets) {
|
|
2812
|
+
if (!seen.has(t)) {
|
|
2813
|
+
seen.add(t);
|
|
2814
|
+
refs.push({ value: val, targetIds: [t] });
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
return refs;
|
|
2819
|
+
}
|
|
2820
|
+
var staticRefScannerParser = {
|
|
2821
|
+
id: "static-ref-scanner",
|
|
1727
2822
|
layer: "crosslayer",
|
|
2823
|
+
concern: "static-ref",
|
|
1728
2824
|
detect(rootDir) {
|
|
1729
|
-
|
|
2825
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2826
|
+
return paths !== null;
|
|
1730
2827
|
},
|
|
1731
2828
|
generate(rootDir, layerOutputs) {
|
|
1732
|
-
const
|
|
1733
|
-
if (!
|
|
2829
|
+
const staticOutput = layerOutputs.get("static");
|
|
2830
|
+
if (!staticOutput || staticOutput.nodes.length === 0) {
|
|
1734
2831
|
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1735
2832
|
}
|
|
1736
|
-
const
|
|
1737
|
-
const
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
2833
|
+
const valueLookup = /* @__PURE__ */ new Map();
|
|
2834
|
+
for (const node of staticOutput.nodes) {
|
|
2835
|
+
const type = node.type;
|
|
2836
|
+
let valueStr = null;
|
|
2837
|
+
if (type === "enum_value") {
|
|
2838
|
+
valueStr = node.value;
|
|
2839
|
+
const fullId = node.id;
|
|
2840
|
+
if (!valueLookup.has(fullId)) valueLookup.set(fullId, []);
|
|
2841
|
+
valueLookup.get(fullId).push(node.id);
|
|
2842
|
+
} else if (type.startsWith("seed_")) {
|
|
2843
|
+
valueStr = node.value;
|
|
2844
|
+
}
|
|
2845
|
+
if (!valueStr || valueStr.length < MIN_VALUE_LENGTH || SKIP_VALUES.has(valueStr.toLowerCase())) continue;
|
|
2846
|
+
if (!valueLookup.has(valueStr)) valueLookup.set(valueStr, []);
|
|
2847
|
+
valueLookup.get(valueStr).push(node.id);
|
|
2848
|
+
}
|
|
2849
|
+
const allValues = [...valueLookup.keys()].sort((a, b) => b.length - a.length);
|
|
2850
|
+
if (allValues.length === 0) {
|
|
2851
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2852
|
+
}
|
|
2853
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2854
|
+
if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2855
|
+
const srcDir = paths.srcDir;
|
|
1743
2856
|
const files = [
|
|
1744
|
-
...walk4(
|
|
1745
|
-
...walk4(appDir, [".ts", ".tsx"])
|
|
2857
|
+
...walk4((0, import_node_path10.join)(srcDir, "client"), [".ts", ".tsx"]),
|
|
2858
|
+
...walk4(paths.appDir, [".ts", ".tsx"]),
|
|
2859
|
+
...walk4((0, import_node_path10.join)(srcDir, "server"), [".ts", ".tsx"]),
|
|
2860
|
+
...walk4((0, import_node_path10.join)(srcDir, "lib"), [".ts", ".tsx"]),
|
|
2861
|
+
...walk4((0, import_node_path10.join)(srcDir, "config"), [".ts", ".tsx"])
|
|
1746
2862
|
];
|
|
2863
|
+
const uiOutput = layerOutputs.get("ui");
|
|
2864
|
+
const apiOutput = layerOutputs.get("api");
|
|
2865
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
2866
|
+
const apiNodeIds = new Set(apiOutput?.nodes.map((n) => n.id) ?? []);
|
|
2867
|
+
let parseCode2 = null;
|
|
2868
|
+
try {
|
|
2869
|
+
const extractor = (init_ts_extractor(), __toCommonJS(ts_extractor_exports));
|
|
2870
|
+
if (typeof extractor.parseCodeTS === "function") {
|
|
2871
|
+
parseCode2 = extractor.parseCodeTS;
|
|
2872
|
+
}
|
|
2873
|
+
} catch {
|
|
2874
|
+
}
|
|
1747
2875
|
const crossRefs = [];
|
|
1748
2876
|
const seen = /* @__PURE__ */ new Set();
|
|
2877
|
+
let filesScanned = 0;
|
|
1749
2878
|
for (const absPath of files) {
|
|
1750
|
-
const sourceId =
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
2879
|
+
const sourceId = (0, import_node_path10.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
2880
|
+
const sourceLayer = uiNodeIds.has(sourceId) ? "ui" : apiNodeIds.has(sourceId) ? "api" : null;
|
|
2881
|
+
if (!sourceLayer) continue;
|
|
2882
|
+
const content = (0, import_node_fs10.readFileSync)(absPath, "utf-8");
|
|
2883
|
+
filesScanned++;
|
|
2884
|
+
let fileRefs;
|
|
2885
|
+
if (parseCode2) {
|
|
2886
|
+
try {
|
|
2887
|
+
const tree = parseCode2(content);
|
|
2888
|
+
fileRefs = collectStaticRefs(tree.rootNode, valueLookup);
|
|
2889
|
+
} catch {
|
|
2890
|
+
fileRefs = collectStaticRefsRegex(content, valueLookup, allValues);
|
|
2891
|
+
}
|
|
2892
|
+
} else {
|
|
2893
|
+
fileRefs = collectStaticRefsRegex(content, valueLookup, allValues);
|
|
2894
|
+
}
|
|
2895
|
+
for (const ref of fileRefs) {
|
|
2896
|
+
for (const targetId of ref.targetIds) {
|
|
2897
|
+
const key = `${sourceId}|${targetId}`;
|
|
1760
2898
|
if (seen.has(key)) continue;
|
|
1761
2899
|
seen.add(key);
|
|
1762
2900
|
crossRefs.push({
|
|
1763
2901
|
source: sourceId,
|
|
1764
|
-
target:
|
|
1765
|
-
type: "
|
|
1766
|
-
layer: "
|
|
2902
|
+
target: targetId,
|
|
2903
|
+
type: "references_static",
|
|
2904
|
+
layer: "static"
|
|
1767
2905
|
});
|
|
1768
2906
|
}
|
|
1769
2907
|
}
|
|
@@ -1773,16 +2911,23 @@ var urlLiteralScannerParser = {
|
|
|
1773
2911
|
flagged_edges: [],
|
|
1774
2912
|
warnings: [],
|
|
1775
2913
|
patterns: {
|
|
1776
|
-
|
|
2914
|
+
files_scanned: filesScanned,
|
|
2915
|
+
static_values_tracked: valueLookup.size,
|
|
2916
|
+
references_found: crossRefs.length,
|
|
2917
|
+
parser: parseCode2 ? "tree-sitter" : "regex-fallback"
|
|
1777
2918
|
}
|
|
1778
2919
|
};
|
|
1779
2920
|
}
|
|
1780
2921
|
};
|
|
1781
2922
|
|
|
1782
2923
|
// src/server/graph/core/parser-registry.ts
|
|
2924
|
+
function isMultiLayerParser(p) {
|
|
2925
|
+
return "layers" in p && Array.isArray(p.layers);
|
|
2926
|
+
}
|
|
1783
2927
|
var ParserRegistry = class {
|
|
1784
2928
|
constructor() {
|
|
1785
|
-
this.
|
|
2929
|
+
this.singleLayerParsers = /* @__PURE__ */ new Map();
|
|
2930
|
+
this.multiLayerParsers = [];
|
|
1786
2931
|
this.ids = /* @__PURE__ */ new Set();
|
|
1787
2932
|
}
|
|
1788
2933
|
register(parser) {
|
|
@@ -1790,30 +2935,57 @@ var ParserRegistry = class {
|
|
|
1790
2935
|
throw new Error(`Duplicate parser id: ${parser.id}`);
|
|
1791
2936
|
}
|
|
1792
2937
|
this.ids.add(parser.id);
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
2938
|
+
if (isMultiLayerParser(parser)) {
|
|
2939
|
+
this.multiLayerParsers.push(parser);
|
|
2940
|
+
} else {
|
|
2941
|
+
const list = this.singleLayerParsers.get(parser.layer) ?? [];
|
|
2942
|
+
list.push(parser);
|
|
2943
|
+
this.singleLayerParsers.set(parser.layer, list);
|
|
2944
|
+
}
|
|
1796
2945
|
}
|
|
2946
|
+
/** Get single-layer parsers for a specific layer. */
|
|
1797
2947
|
getParsers(layer) {
|
|
1798
|
-
return this.
|
|
2948
|
+
return this.singleLayerParsers.get(layer) ?? [];
|
|
2949
|
+
}
|
|
2950
|
+
/** Get multi-layer parsers that can produce output for the given layer. */
|
|
2951
|
+
getMultiLayerParsersFor(layer) {
|
|
2952
|
+
return this.multiLayerParsers.filter((p) => p.layers.includes(layer));
|
|
2953
|
+
}
|
|
2954
|
+
/** Get all multi-layer parsers. */
|
|
2955
|
+
getMultiLayerParsers() {
|
|
2956
|
+
return [...this.multiLayerParsers];
|
|
1799
2957
|
}
|
|
1800
2958
|
getCrossLayerParsers() {
|
|
1801
|
-
return this.
|
|
2959
|
+
return this.singleLayerParsers.get("crosslayer") ?? [];
|
|
2960
|
+
}
|
|
2961
|
+
/** All layers that registered parsers can produce (single + multi). */
|
|
2962
|
+
getAvailableLayers() {
|
|
2963
|
+
const layers = /* @__PURE__ */ new Set();
|
|
2964
|
+
for (const key of this.singleLayerParsers.keys()) {
|
|
2965
|
+
if (key !== "crosslayer") layers.add(key);
|
|
2966
|
+
}
|
|
2967
|
+
for (const mp of this.multiLayerParsers) {
|
|
2968
|
+
for (const l of mp.layers) layers.add(l);
|
|
2969
|
+
}
|
|
2970
|
+
return [...layers];
|
|
1802
2971
|
}
|
|
1803
2972
|
getAll() {
|
|
1804
2973
|
const all = [];
|
|
1805
|
-
for (const list of this.
|
|
2974
|
+
for (const list of this.singleLayerParsers.values()) all.push(...list);
|
|
2975
|
+
all.push(...this.multiLayerParsers);
|
|
1806
2976
|
return all;
|
|
1807
2977
|
}
|
|
1808
2978
|
};
|
|
1809
2979
|
function registerBuiltins(registry, disabled) {
|
|
1810
2980
|
const builtins = [
|
|
1811
|
-
|
|
1812
|
-
nextjsRoutesParser,
|
|
2981
|
+
typescriptProjectParser,
|
|
1813
2982
|
prismaSchemaParser,
|
|
2983
|
+
sqlMigrationsParser,
|
|
2984
|
+
staticValuesParser,
|
|
1814
2985
|
fetchResolverParser,
|
|
1815
2986
|
apiAnnotationsParser,
|
|
1816
|
-
urlLiteralScannerParser
|
|
2987
|
+
urlLiteralScannerParser,
|
|
2988
|
+
staticRefScannerParser
|
|
1817
2989
|
];
|
|
1818
2990
|
for (const parser of builtins) {
|
|
1819
2991
|
if (disabled.has(parser.id)) continue;
|
|
@@ -1823,16 +2995,19 @@ function registerBuiltins(registry, disabled) {
|
|
|
1823
2995
|
function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
1824
2996
|
for (const entry of config.parsers?.custom ?? []) {
|
|
1825
2997
|
try {
|
|
1826
|
-
const absPath = (0,
|
|
2998
|
+
const absPath = (0, import_node_path11.resolve)(rootDir, entry.path);
|
|
1827
2999
|
const mod = require(absPath);
|
|
1828
3000
|
const parser = "default" in mod ? mod.default : mod;
|
|
1829
3001
|
if (disabled.has(parser.id)) continue;
|
|
1830
|
-
if (parser.layer !== entry.layer) {
|
|
3002
|
+
if (!isMultiLayerParser(parser) && parser.layer !== entry.layer) {
|
|
1831
3003
|
process.stderr.write(
|
|
1832
3004
|
`[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
|
|
1833
3005
|
`
|
|
1834
3006
|
);
|
|
1835
3007
|
}
|
|
3008
|
+
if (parser.layer === "crosslayer" && entry.concern && !("concern" in parser && parser.concern)) {
|
|
3009
|
+
parser.concern = entry.concern;
|
|
3010
|
+
}
|
|
1836
3011
|
registry.register(parser);
|
|
1837
3012
|
} catch (err) {
|
|
1838
3013
|
process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err}
|
|
@@ -1930,53 +3105,30 @@ function dedupCrossRefs(refs) {
|
|
|
1930
3105
|
}
|
|
1931
3106
|
return result;
|
|
1932
3107
|
}
|
|
1933
|
-
function applyCrossLayerResults(uiOutput, results
|
|
1934
|
-
const allCrossRefs = [...uiOutput.cross_refs];
|
|
1935
|
-
const allFlagged = [...uiOutput.flagged_edges];
|
|
1936
|
-
const allWarnings = [...uiOutput.warnings];
|
|
1937
|
-
const primaryResult = results.find((r) => r.parserId === primaryId);
|
|
1938
|
-
const secondaryResults = results.filter((r) => r.parserId !== primaryId);
|
|
1939
|
-
if (primaryResult) {
|
|
1940
|
-
allCrossRefs.push(...primaryResult.output.cross_refs);
|
|
1941
|
-
allFlagged.push(...primaryResult.output.flagged_edges);
|
|
1942
|
-
allWarnings.push(...primaryResult.output.warnings);
|
|
1943
|
-
}
|
|
1944
|
-
const primarySet = new Set(
|
|
1945
|
-
(primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
|
|
1946
|
-
);
|
|
1947
|
-
for (const sec of secondaryResults) {
|
|
1948
|
-
for (const ref of sec.output.cross_refs) {
|
|
1949
|
-
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
1950
|
-
if (primarySet.has(key)) {
|
|
1951
|
-
allCrossRefs.push(ref);
|
|
1952
|
-
} else {
|
|
1953
|
-
allFlagged.push({
|
|
1954
|
-
source: ref.source,
|
|
1955
|
-
target: ref.target,
|
|
1956
|
-
type: "out_of_pattern",
|
|
1957
|
-
label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
|
|
1958
|
-
confidence: "medium"
|
|
1959
|
-
});
|
|
1960
|
-
allCrossRefs.push(ref);
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
allFlagged.push(...sec.output.flagged_edges);
|
|
1964
|
-
allWarnings.push(...sec.output.warnings);
|
|
1965
|
-
}
|
|
3108
|
+
function applyCrossLayerResults(uiOutput, results) {
|
|
1966
3109
|
return {
|
|
1967
3110
|
...uiOutput,
|
|
1968
|
-
cross_refs: dedupCrossRefs(
|
|
1969
|
-
|
|
1970
|
-
|
|
3111
|
+
cross_refs: dedupCrossRefs([
|
|
3112
|
+
...uiOutput.cross_refs,
|
|
3113
|
+
...results.flatMap((r) => r.output.cross_refs)
|
|
3114
|
+
]),
|
|
3115
|
+
flagged_edges: [
|
|
3116
|
+
...uiOutput.flagged_edges,
|
|
3117
|
+
...results.flatMap((r) => r.output.flagged_edges)
|
|
3118
|
+
],
|
|
3119
|
+
warnings: [
|
|
3120
|
+
...uiOutput.warnings,
|
|
3121
|
+
...results.flatMap((r) => r.output.warnings)
|
|
3122
|
+
]
|
|
1971
3123
|
};
|
|
1972
3124
|
}
|
|
1973
3125
|
|
|
1974
3126
|
// src/server/graph/core/graph-builder.ts
|
|
1975
3127
|
function readGraphFromDisk(rootDir, layer) {
|
|
1976
|
-
const filePath = (0,
|
|
1977
|
-
if (!(0,
|
|
3128
|
+
const filePath = (0, import_node_path12.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
3129
|
+
if (!(0, import_node_fs11.existsSync)(filePath)) return null;
|
|
1978
3130
|
try {
|
|
1979
|
-
return JSON.parse((0,
|
|
3131
|
+
return JSON.parse((0, import_node_fs11.readFileSync)(filePath, "utf-8"));
|
|
1980
3132
|
} catch {
|
|
1981
3133
|
return null;
|
|
1982
3134
|
}
|
|
@@ -1984,26 +3136,31 @@ function readGraphFromDisk(rootDir, layer) {
|
|
|
1984
3136
|
function generateLayer(rootDir, layer) {
|
|
1985
3137
|
const config = loadConfig(rootDir);
|
|
1986
3138
|
const registry = createRegistry(config, rootDir);
|
|
1987
|
-
const parsers = registry.getParsers(layer);
|
|
1988
3139
|
const outputs = [];
|
|
1989
|
-
for (const parser of
|
|
3140
|
+
for (const parser of registry.getParsers(layer)) {
|
|
1990
3141
|
if (!parser.detect(rootDir)) continue;
|
|
1991
3142
|
outputs.push(parser.generate(rootDir));
|
|
1992
3143
|
}
|
|
3144
|
+
for (const mp of registry.getMultiLayerParsersFor(layer)) {
|
|
3145
|
+
if (!mp.detect(rootDir)) continue;
|
|
3146
|
+
const multiOutput = mp.generate(rootDir);
|
|
3147
|
+
const layerOutput = multiOutput.get(layer);
|
|
3148
|
+
if (layerOutput) outputs.push(layerOutput);
|
|
3149
|
+
}
|
|
1993
3150
|
if (outputs.length === 0) return null;
|
|
1994
3151
|
let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
1995
3152
|
if (layer === "ui") {
|
|
1996
3153
|
const layerOutputs = /* @__PURE__ */ new Map();
|
|
1997
3154
|
layerOutputs.set("ui", merged);
|
|
1998
|
-
for (const otherLayer of
|
|
3155
|
+
for (const otherLayer of registry.getAvailableLayers()) {
|
|
3156
|
+
if (otherLayer === "ui") continue;
|
|
1999
3157
|
const existing = readGraphFromDisk(rootDir, otherLayer);
|
|
2000
3158
|
if (existing) layerOutputs.set(otherLayer, existing);
|
|
2001
3159
|
}
|
|
2002
3160
|
const crossParsers = registry.getCrossLayerParsers();
|
|
2003
|
-
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
2004
3161
|
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
2005
3162
|
if (crossResults.length > 0) {
|
|
2006
|
-
merged = applyCrossLayerResults(merged, crossResults
|
|
3163
|
+
merged = applyCrossLayerResults(merged, crossResults);
|
|
2007
3164
|
}
|
|
2008
3165
|
}
|
|
2009
3166
|
return {
|
|
@@ -2016,16 +3173,29 @@ function generateLayer(rootDir, layer) {
|
|
|
2016
3173
|
function generateAll(rootDir) {
|
|
2017
3174
|
const config = loadConfig(rootDir);
|
|
2018
3175
|
const registry = createRegistry(config, rootDir);
|
|
2019
|
-
const
|
|
3176
|
+
const allLayers = registry.getAvailableLayers();
|
|
3177
|
+
const layerOrder = [
|
|
3178
|
+
...allLayers.filter((l) => l !== "ui"),
|
|
3179
|
+
...allLayers.filter((l) => l === "ui")
|
|
3180
|
+
];
|
|
2020
3181
|
const layerOutputs = /* @__PURE__ */ new Map();
|
|
2021
3182
|
const results = [];
|
|
3183
|
+
const multiLayerResults = /* @__PURE__ */ new Map();
|
|
2022
3184
|
for (const layer of layerOrder) {
|
|
2023
|
-
const parsers = registry.getParsers(layer);
|
|
2024
3185
|
const outputs = [];
|
|
2025
|
-
for (const parser of
|
|
3186
|
+
for (const parser of registry.getParsers(layer)) {
|
|
2026
3187
|
if (!parser.detect(rootDir)) continue;
|
|
2027
3188
|
outputs.push(parser.generate(rootDir));
|
|
2028
3189
|
}
|
|
3190
|
+
for (const mp of registry.getMultiLayerParsersFor(layer)) {
|
|
3191
|
+
if (!mp.detect(rootDir)) continue;
|
|
3192
|
+
if (!multiLayerResults.has(mp.id)) {
|
|
3193
|
+
multiLayerResults.set(mp.id, mp.generate(rootDir));
|
|
3194
|
+
}
|
|
3195
|
+
const cached = multiLayerResults.get(mp.id);
|
|
3196
|
+
const layerOutput = cached.get(layer);
|
|
3197
|
+
if (layerOutput) outputs.push(layerOutput);
|
|
3198
|
+
}
|
|
2029
3199
|
if (outputs.length === 0) continue;
|
|
2030
3200
|
const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
2031
3201
|
layerOutputs.set(layer, merged);
|
|
@@ -2037,11 +3207,10 @@ function generateAll(rootDir) {
|
|
|
2037
3207
|
});
|
|
2038
3208
|
}
|
|
2039
3209
|
const crossParsers = registry.getCrossLayerParsers();
|
|
2040
|
-
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
2041
3210
|
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
2042
3211
|
if (crossResults.length > 0 && layerOutputs.has("ui")) {
|
|
2043
3212
|
const uiOutput = layerOutputs.get("ui");
|
|
2044
|
-
const merged = applyCrossLayerResults(uiOutput, crossResults
|
|
3213
|
+
const merged = applyCrossLayerResults(uiOutput, crossResults);
|
|
2045
3214
|
layerOutputs.set("ui", merged);
|
|
2046
3215
|
const uiResult = results.find((r) => r.layer === "ui");
|
|
2047
3216
|
if (uiResult) {
|
|
@@ -2051,18 +3220,20 @@ function generateAll(rootDir) {
|
|
|
2051
3220
|
}
|
|
2052
3221
|
}
|
|
2053
3222
|
const byLayer = new Map(results.map((r) => [r.layer, r]));
|
|
2054
|
-
|
|
3223
|
+
const wellKnownOrder = ["ui", "api", "db"];
|
|
3224
|
+
const extras = [...byLayer.keys()].filter((l) => !wellKnownOrder.includes(l)).sort();
|
|
3225
|
+
return [...wellKnownOrder, ...extras].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
2055
3226
|
}
|
|
2056
3227
|
|
|
2057
3228
|
// src/server/graph/index.ts
|
|
2058
3229
|
init_config();
|
|
2059
3230
|
|
|
2060
3231
|
// src/server/graph/core/tagger-registry.ts
|
|
2061
|
-
var
|
|
3232
|
+
var import_node_path14 = require("node:path");
|
|
2062
3233
|
|
|
2063
3234
|
// src/server/graph/taggers/module-tagger.ts
|
|
2064
|
-
var
|
|
2065
|
-
var
|
|
3235
|
+
var import_node_fs12 = require("node:fs");
|
|
3236
|
+
var import_node_path13 = require("node:path");
|
|
2066
3237
|
function matchGlob(pattern, id) {
|
|
2067
3238
|
const patParts = pattern.split("/");
|
|
2068
3239
|
const idParts = id.split("/");
|
|
@@ -2095,18 +3266,18 @@ function detectConventionDirs(rootDir, extraConventionDirs = []) {
|
|
|
2095
3266
|
const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
|
|
2096
3267
|
const searchDirs = [
|
|
2097
3268
|
rootDir,
|
|
2098
|
-
(0,
|
|
2099
|
-
(0,
|
|
2100
|
-
(0,
|
|
3269
|
+
(0, import_node_path13.join)(rootDir, "src"),
|
|
3270
|
+
(0, import_node_path13.join)(rootDir, "app"),
|
|
3271
|
+
(0, import_node_path13.join)(rootDir, "lib")
|
|
2101
3272
|
];
|
|
2102
3273
|
for (const base of searchDirs) {
|
|
2103
3274
|
for (const convention of conventionDirs) {
|
|
2104
|
-
const dir = (0,
|
|
2105
|
-
if (!(0,
|
|
3275
|
+
const dir = (0, import_node_path13.join)(base, convention);
|
|
3276
|
+
if (!(0, import_node_fs12.existsSync)(dir)) continue;
|
|
2106
3277
|
try {
|
|
2107
|
-
const stat = (0,
|
|
3278
|
+
const stat = (0, import_node_fs12.statSync)(dir);
|
|
2108
3279
|
if (!stat.isDirectory()) continue;
|
|
2109
|
-
const entries = (0,
|
|
3280
|
+
const entries = (0, import_node_fs12.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
2110
3281
|
if (entries.length > 0) {
|
|
2111
3282
|
const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
|
|
2112
3283
|
result.set(relPath, entries);
|
|
@@ -2400,7 +3571,7 @@ function loadCustomTaggers(registry, config, rootDir, disabled) {
|
|
|
2400
3571
|
for (const entry of config.taggers?.custom ?? []) {
|
|
2401
3572
|
if (disabled.has(entry.id)) continue;
|
|
2402
3573
|
try {
|
|
2403
|
-
const absPath = (0,
|
|
3574
|
+
const absPath = (0, import_node_path14.resolve)(rootDir, entry.path);
|
|
2404
3575
|
const mod = require(absPath);
|
|
2405
3576
|
const tagger = "default" in mod ? mod.default : mod;
|
|
2406
3577
|
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
@@ -2423,24 +3594,24 @@ function createTaggerRegistry(config, rootDir) {
|
|
|
2423
3594
|
}
|
|
2424
3595
|
|
|
2425
3596
|
// src/server/graph/core/tag-store.ts
|
|
2426
|
-
var
|
|
2427
|
-
var
|
|
3597
|
+
var import_node_fs13 = require("node:fs");
|
|
3598
|
+
var import_node_path15 = require("node:path");
|
|
2428
3599
|
var TAGS_FILENAME = "tags.json";
|
|
2429
3600
|
var GRAPHS_DIR = ".launchsecure/graphs";
|
|
2430
3601
|
var tagCache = /* @__PURE__ */ new Map();
|
|
2431
3602
|
function tagsFilePath(rootDir) {
|
|
2432
|
-
return (0,
|
|
3603
|
+
return (0, import_node_path15.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
|
|
2433
3604
|
}
|
|
2434
3605
|
function readTagStore(rootDir) {
|
|
2435
3606
|
const filePath = tagsFilePath(rootDir);
|
|
2436
|
-
if (!(0,
|
|
2437
|
-
const stat = (0,
|
|
3607
|
+
if (!(0, import_node_fs13.existsSync)(filePath)) return {};
|
|
3608
|
+
const stat = (0, import_node_fs13.statSync)(filePath);
|
|
2438
3609
|
const cached = tagCache.get(filePath);
|
|
2439
3610
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
2440
3611
|
return cached.store;
|
|
2441
3612
|
}
|
|
2442
3613
|
try {
|
|
2443
|
-
const content = (0,
|
|
3614
|
+
const content = (0, import_node_fs13.readFileSync)(filePath, "utf-8");
|
|
2444
3615
|
const store = JSON.parse(content);
|
|
2445
3616
|
tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
|
|
2446
3617
|
return store;
|
|
@@ -2450,15 +3621,15 @@ function readTagStore(rootDir) {
|
|
|
2450
3621
|
}
|
|
2451
3622
|
function writeTagStore(rootDir, store) {
|
|
2452
3623
|
const filePath = tagsFilePath(rootDir);
|
|
2453
|
-
const dir = (0,
|
|
2454
|
-
(0,
|
|
3624
|
+
const dir = (0, import_node_path15.dirname)(filePath);
|
|
3625
|
+
(0, import_node_fs13.mkdirSync)(dir, { recursive: true });
|
|
2455
3626
|
const cleaned = {};
|
|
2456
3627
|
for (const [nodeId, tags] of Object.entries(store)) {
|
|
2457
3628
|
if (Object.keys(tags).length > 0) {
|
|
2458
3629
|
cleaned[nodeId] = tags;
|
|
2459
3630
|
}
|
|
2460
3631
|
}
|
|
2461
|
-
(0,
|
|
3632
|
+
(0, import_node_fs13.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
|
|
2462
3633
|
tagCache.delete(filePath);
|
|
2463
3634
|
}
|
|
2464
3635
|
function setTag(rootDir, nodeId, key, value) {
|
|
@@ -2478,22 +3649,27 @@ function removeTag(rootDir, nodeId, key) {
|
|
|
2478
3649
|
}
|
|
2479
3650
|
|
|
2480
3651
|
// src/server/graph/index.ts
|
|
3652
|
+
init_ts_extractor();
|
|
2481
3653
|
var GRAPHS_DIR2 = ".launchsecure/graphs";
|
|
2482
|
-
|
|
3654
|
+
function getAvailableLayers(rootDir) {
|
|
3655
|
+
const dir = (0, import_node_path16.join)(rootDir, GRAPHS_DIR2);
|
|
3656
|
+
if (!(0, import_node_fs14.existsSync)(dir)) return [];
|
|
3657
|
+
return (0, import_node_fs14.readdirSync)(dir).filter((f) => f.endsWith(".json") && f !== "tags.json").map((f) => f.replace(".json", ""));
|
|
3658
|
+
}
|
|
2483
3659
|
var graphCache = /* @__PURE__ */ new Map();
|
|
2484
3660
|
var taggedCache = /* @__PURE__ */ new Map();
|
|
2485
3661
|
function graphsDir(rootDir) {
|
|
2486
|
-
return (0,
|
|
3662
|
+
return (0, import_node_path16.join)(rootDir, GRAPHS_DIR2);
|
|
2487
3663
|
}
|
|
2488
3664
|
function graphFilePath(rootDir, layer) {
|
|
2489
|
-
return (0,
|
|
3665
|
+
return (0, import_node_path16.join)(graphsDir(rootDir), `${layer}.json`);
|
|
2490
3666
|
}
|
|
2491
3667
|
function tagsFilePath2(rootDir) {
|
|
2492
|
-
return (0,
|
|
3668
|
+
return (0, import_node_path16.join)(graphsDir(rootDir), "tags.json");
|
|
2493
3669
|
}
|
|
2494
3670
|
function getMtimeMs(filePath) {
|
|
2495
|
-
if (!(0,
|
|
2496
|
-
return (0,
|
|
3671
|
+
if (!(0, import_node_fs14.existsSync)(filePath)) return 0;
|
|
3672
|
+
return (0, import_node_fs14.statSync)(filePath).mtimeMs;
|
|
2497
3673
|
}
|
|
2498
3674
|
function invalidateCache(filePath) {
|
|
2499
3675
|
graphCache.delete(filePath);
|
|
@@ -2532,20 +3708,20 @@ function applyTags(graph, layer, rootDir) {
|
|
|
2532
3708
|
}
|
|
2533
3709
|
function readGraphRaw(rootDir, layer) {
|
|
2534
3710
|
const filePath = graphFilePath(rootDir, layer);
|
|
2535
|
-
if (!(0,
|
|
2536
|
-
const stat = (0,
|
|
3711
|
+
if (!(0, import_node_fs14.existsSync)(filePath)) return null;
|
|
3712
|
+
const stat = (0, import_node_fs14.statSync)(filePath);
|
|
2537
3713
|
const cached = graphCache.get(filePath);
|
|
2538
3714
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
2539
3715
|
return cached.graph;
|
|
2540
3716
|
}
|
|
2541
|
-
const content = (0,
|
|
3717
|
+
const content = (0, import_node_fs14.readFileSync)(filePath, "utf-8");
|
|
2542
3718
|
const graph = JSON.parse(content);
|
|
2543
3719
|
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
2544
3720
|
return graph;
|
|
2545
3721
|
}
|
|
2546
3722
|
function readGraph(rootDir, layer) {
|
|
2547
3723
|
const rawFilePath = graphFilePath(rootDir, layer);
|
|
2548
|
-
if (!(0,
|
|
3724
|
+
if (!(0, import_node_fs14.existsSync)(rawFilePath)) return null;
|
|
2549
3725
|
const rawMtime = getMtimeMs(rawFilePath);
|
|
2550
3726
|
const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
|
|
2551
3727
|
const cacheKey = `${rootDir}:${layer}`;
|
|
@@ -2561,7 +3737,7 @@ function readGraph(rootDir, layer) {
|
|
|
2561
3737
|
}
|
|
2562
3738
|
function readAllGraphs(rootDir) {
|
|
2563
3739
|
const result = {};
|
|
2564
|
-
for (const layer of
|
|
3740
|
+
for (const layer of getAvailableLayers(rootDir)) {
|
|
2565
3741
|
const graph = readGraph(rootDir, layer);
|
|
2566
3742
|
if (graph) result[layer] = graph;
|
|
2567
3743
|
}
|
|
@@ -2575,11 +3751,11 @@ async function generateGraph(rootDir, layer) {
|
|
|
2575
3751
|
mutationMethods: config.parsers?.patterns?.mutationMethods
|
|
2576
3752
|
});
|
|
2577
3753
|
const dir = graphsDir(rootDir);
|
|
2578
|
-
(0,
|
|
3754
|
+
(0, import_node_fs14.mkdirSync)(dir, { recursive: true });
|
|
2579
3755
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
2580
3756
|
for (const result of results) {
|
|
2581
3757
|
const filePath = graphFilePath(rootDir, result.layer);
|
|
2582
|
-
(0,
|
|
3758
|
+
(0, import_node_fs14.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
2583
3759
|
invalidateCache(filePath);
|
|
2584
3760
|
invalidateTaggedCache(rootDir, result.layer);
|
|
2585
3761
|
}
|
|
@@ -2588,28 +3764,28 @@ async function generateGraph(rootDir, layer) {
|
|
|
2588
3764
|
|
|
2589
3765
|
// src/server/lockfile.ts
|
|
2590
3766
|
var import_node_child_process = require("node:child_process");
|
|
2591
|
-
var
|
|
3767
|
+
var import_node_fs15 = require("node:fs");
|
|
2592
3768
|
var import_node_os = require("node:os");
|
|
2593
|
-
var
|
|
3769
|
+
var import_node_path17 = require("node:path");
|
|
2594
3770
|
function lockDir(projectRoot) {
|
|
2595
3771
|
if (projectRoot) {
|
|
2596
|
-
return (0,
|
|
3772
|
+
return (0, import_node_path17.join)(projectRoot, ".launchsecure");
|
|
2597
3773
|
}
|
|
2598
|
-
return (0,
|
|
3774
|
+
return (0, import_node_path17.join)((0, import_node_os.homedir)(), ".launchsecure");
|
|
2599
3775
|
}
|
|
2600
3776
|
function lockPath(projectRoot) {
|
|
2601
|
-
return (0,
|
|
3777
|
+
return (0, import_node_path17.join)(lockDir(projectRoot), "launch-chart.lock");
|
|
2602
3778
|
}
|
|
2603
3779
|
var _activeProjectRoot;
|
|
2604
3780
|
function readLock(projectRoot) {
|
|
2605
3781
|
const root = projectRoot ?? _activeProjectRoot;
|
|
2606
3782
|
const p = lockPath(root);
|
|
2607
|
-
if (!(0,
|
|
3783
|
+
if (!(0, import_node_fs15.existsSync)(p)) {
|
|
2608
3784
|
if (root) {
|
|
2609
3785
|
const globalP = lockPath();
|
|
2610
|
-
if ((0,
|
|
3786
|
+
if ((0, import_node_fs15.existsSync)(globalP)) {
|
|
2611
3787
|
try {
|
|
2612
|
-
const data = JSON.parse((0,
|
|
3788
|
+
const data = JSON.parse((0, import_node_fs15.readFileSync)(globalP, "utf-8"));
|
|
2613
3789
|
if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
|
|
2614
3790
|
return data;
|
|
2615
3791
|
}
|
|
@@ -2620,7 +3796,7 @@ function readLock(projectRoot) {
|
|
|
2620
3796
|
return null;
|
|
2621
3797
|
}
|
|
2622
3798
|
try {
|
|
2623
|
-
const data = JSON.parse((0,
|
|
3799
|
+
const data = JSON.parse((0, import_node_fs15.readFileSync)(p, "utf-8"));
|
|
2624
3800
|
if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
|
|
2625
3801
|
return data;
|
|
2626
3802
|
} catch {
|
|
@@ -2657,7 +3833,7 @@ function getLiveLock(projectRoot) {
|
|
|
2657
3833
|
const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
|
|
2658
3834
|
if (!live) {
|
|
2659
3835
|
try {
|
|
2660
|
-
(0,
|
|
3836
|
+
(0, import_node_fs15.unlinkSync)(lockPath(root));
|
|
2661
3837
|
} catch {
|
|
2662
3838
|
}
|
|
2663
3839
|
return null;
|
|
@@ -2666,20 +3842,299 @@ function getLiveLock(projectRoot) {
|
|
|
2666
3842
|
}
|
|
2667
3843
|
function writeLock(data, projectRoot) {
|
|
2668
3844
|
const root = projectRoot ?? _activeProjectRoot;
|
|
2669
|
-
(0,
|
|
2670
|
-
(0,
|
|
3845
|
+
(0, import_node_fs15.mkdirSync)(lockDir(root), { recursive: true });
|
|
3846
|
+
(0, import_node_fs15.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
2671
3847
|
if (root) _activeProjectRoot = root;
|
|
2672
3848
|
}
|
|
2673
3849
|
function clearLock(projectRoot) {
|
|
2674
3850
|
const root = projectRoot ?? _activeProjectRoot;
|
|
2675
3851
|
try {
|
|
2676
|
-
(0,
|
|
3852
|
+
(0, import_node_fs15.unlinkSync)(lockPath(root));
|
|
2677
3853
|
} catch {
|
|
2678
3854
|
}
|
|
2679
3855
|
}
|
|
2680
3856
|
|
|
2681
3857
|
// src/server/chart-serve.ts
|
|
2682
3858
|
init_config();
|
|
3859
|
+
|
|
3860
|
+
// src/server/graph/core/audit-core.ts
|
|
3861
|
+
var import_node_fs16 = require("node:fs");
|
|
3862
|
+
var import_node_path18 = require("node:path");
|
|
3863
|
+
function readGraphFile(rootDir, layer) {
|
|
3864
|
+
const filePath = (0, import_node_path18.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
3865
|
+
if (!(0, import_node_fs16.existsSync)(filePath)) return null;
|
|
3866
|
+
try {
|
|
3867
|
+
return JSON.parse((0, import_node_fs16.readFileSync)(filePath, "utf-8"));
|
|
3868
|
+
} catch {
|
|
3869
|
+
return null;
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
function checkSchemaDrift(rootDir) {
|
|
3873
|
+
const findings = [];
|
|
3874
|
+
const db = readGraphFile(rootDir, "db");
|
|
3875
|
+
if (!db) {
|
|
3876
|
+
findings.push({ id: "no-db-graph", severity: "info", category: "schema_drift", title: "No DB graph", detail: "Run generate_graph first to populate the DB layer." });
|
|
3877
|
+
return buildReport("db", "schema_drift", findings);
|
|
3878
|
+
}
|
|
3879
|
+
for (const c of db.contradictions ?? []) {
|
|
3880
|
+
const isTableLevel = c.detail.includes("Table ") && (c.detail.includes("has no CREATE TABLE") || c.detail.includes("not in schema.prisma"));
|
|
3881
|
+
findings.push({
|
|
3882
|
+
id: `drift:${c.entity}`,
|
|
3883
|
+
severity: isTableLevel ? "error" : "warning",
|
|
3884
|
+
category: "schema_drift",
|
|
3885
|
+
title: c.entity,
|
|
3886
|
+
detail: c.detail
|
|
3887
|
+
});
|
|
3888
|
+
}
|
|
3889
|
+
return buildReport("db", "schema_drift", findings);
|
|
3890
|
+
}
|
|
3891
|
+
function checkOrphanFks(rootDir) {
|
|
3892
|
+
const findings = [];
|
|
3893
|
+
const db = readGraphFile(rootDir, "db");
|
|
3894
|
+
if (!db) return buildReport("db", "orphan_fks", findings);
|
|
3895
|
+
for (const f of db.flagged_edges ?? []) {
|
|
3896
|
+
findings.push({
|
|
3897
|
+
id: `fk:${f.source}->${f.target}`,
|
|
3898
|
+
severity: "warning",
|
|
3899
|
+
category: "orphan_fks",
|
|
3900
|
+
title: `${f.source} \u2192 ${f.target}`,
|
|
3901
|
+
detail: f.label
|
|
3902
|
+
});
|
|
3903
|
+
}
|
|
3904
|
+
return buildReport("db", "orphan_fks", findings);
|
|
3905
|
+
}
|
|
3906
|
+
function checkUnprotectedRoutes(rootDir) {
|
|
3907
|
+
const findings = [];
|
|
3908
|
+
const api = readGraphFile(rootDir, "api");
|
|
3909
|
+
const staticGraph = readGraphFile(rootDir, "static");
|
|
3910
|
+
if (!api) return buildReport("api", "unprotected_routes", findings);
|
|
3911
|
+
const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
|
|
3912
|
+
let routePermsContent = "";
|
|
3913
|
+
if ((0, import_node_fs16.existsSync)(routePermsPath)) {
|
|
3914
|
+
routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
|
|
3915
|
+
}
|
|
3916
|
+
const registeredRoutes = /* @__PURE__ */ new Set();
|
|
3917
|
+
const routeEntryRe = /path:\s*'([^']+)'/g;
|
|
3918
|
+
let rm;
|
|
3919
|
+
while ((rm = routeEntryRe.exec(routePermsContent)) !== null) {
|
|
3920
|
+
registeredRoutes.add(rm[1].replace(/:(\w+)/g, "[$1]"));
|
|
3921
|
+
}
|
|
3922
|
+
for (const node of api.nodes) {
|
|
3923
|
+
if (node.type !== "endpoint") continue;
|
|
3924
|
+
const route = node.route ?? "";
|
|
3925
|
+
if (!route) continue;
|
|
3926
|
+
const normalized = "/api" + (route.startsWith("/") ? route : "/" + route);
|
|
3927
|
+
const isRegistered = registeredRoutes.has(normalized) || [...registeredRoutes].some((r) => routeMatchesPattern(normalized, r));
|
|
3928
|
+
if (!isRegistered) {
|
|
3929
|
+
const methods = node.methods ?? [];
|
|
3930
|
+
findings.push({
|
|
3931
|
+
id: `unprotected:${node.id}`,
|
|
3932
|
+
severity: "warning",
|
|
3933
|
+
category: "unprotected_routes",
|
|
3934
|
+
title: `${methods.join(",")} ${route}`,
|
|
3935
|
+
detail: `API endpoint has no entry in ROUTE_PERMISSIONS. Methods: ${methods.join(", ")}`,
|
|
3936
|
+
file: node.id
|
|
3937
|
+
});
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
return buildReport("api", "unprotected_routes", findings);
|
|
3941
|
+
}
|
|
3942
|
+
function routeMatchesPattern(route, pattern) {
|
|
3943
|
+
const routeParts = route.split("/");
|
|
3944
|
+
const patternParts = pattern.split("/");
|
|
3945
|
+
if (routeParts.length !== patternParts.length) return false;
|
|
3946
|
+
for (let i = 0; i < routeParts.length; i++) {
|
|
3947
|
+
const rp = routeParts[i];
|
|
3948
|
+
const pp = patternParts[i];
|
|
3949
|
+
if (rp === pp) continue;
|
|
3950
|
+
if (pp.startsWith("[") || pp.startsWith(":")) continue;
|
|
3951
|
+
if (rp.startsWith("[") || rp.startsWith(":")) continue;
|
|
3952
|
+
return false;
|
|
3953
|
+
}
|
|
3954
|
+
return true;
|
|
3955
|
+
}
|
|
3956
|
+
function checkDeadScreens(rootDir) {
|
|
3957
|
+
const findings = [];
|
|
3958
|
+
const ui = readGraphFile(rootDir, "ui");
|
|
3959
|
+
if (!ui) return buildReport("ui", "dead_screens", findings);
|
|
3960
|
+
const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
|
|
3961
|
+
const navTargets = /* @__PURE__ */ new Set();
|
|
3962
|
+
for (const e of ui.edges) {
|
|
3963
|
+
if (e.type === "navigates" || e.type === "renders" || e.type === "imports") {
|
|
3964
|
+
navTargets.add(e.target);
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
for (const cr of ui.cross_refs ?? []) {
|
|
3968
|
+
navTargets.add(cr.target);
|
|
3969
|
+
}
|
|
3970
|
+
for (const page of pages) {
|
|
3971
|
+
if (page.id.endsWith("layout.tsx") && page.id.split("/").length <= 2) continue;
|
|
3972
|
+
if (["error.tsx", "loading.tsx", "not-found.tsx", "template.tsx"].some((s) => page.id.endsWith(s))) continue;
|
|
3973
|
+
if (!navTargets.has(page.id)) {
|
|
3974
|
+
findings.push({
|
|
3975
|
+
id: `dead:${page.id}`,
|
|
3976
|
+
severity: "info",
|
|
3977
|
+
category: "dead_screens",
|
|
3978
|
+
title: page.name,
|
|
3979
|
+
detail: `Page "${page.id}" has no incoming navigation, render, or import edges.`,
|
|
3980
|
+
file: page.id
|
|
3981
|
+
});
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
return buildReport("ui", "dead_screens", findings);
|
|
3985
|
+
}
|
|
3986
|
+
function checkUnenforcedPermissions(rootDir) {
|
|
3987
|
+
const findings = [];
|
|
3988
|
+
const staticGraph = readGraphFile(rootDir, "static");
|
|
3989
|
+
if (!staticGraph) return buildReport("static", "unenforced_permissions", findings);
|
|
3990
|
+
const permissions = staticGraph.nodes.filter((n) => n.type === "seed_permission").map((n) => ({ id: n.id, key: n.value, name: n.name }));
|
|
3991
|
+
const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
|
|
3992
|
+
let routePermsContent = "";
|
|
3993
|
+
if ((0, import_node_fs16.existsSync)(routePermsPath)) {
|
|
3994
|
+
routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
|
|
3995
|
+
}
|
|
3996
|
+
for (const perm of permissions) {
|
|
3997
|
+
const regex = new RegExp(`permission:\\s*['"]${perm.key}['"]`);
|
|
3998
|
+
if (!regex.test(routePermsContent)) {
|
|
3999
|
+
findings.push({
|
|
4000
|
+
id: `unenforced:${perm.key}`,
|
|
4001
|
+
severity: "warning",
|
|
4002
|
+
category: "unenforced_permissions",
|
|
4003
|
+
title: `${perm.name} (${perm.key})`,
|
|
4004
|
+
detail: `Permission "${perm.key}" exists in seed data but has no entry in ROUTE_PERMISSIONS \u2014 no API route requires it.`
|
|
4005
|
+
});
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
return buildReport("static", "unenforced_permissions", findings);
|
|
4009
|
+
}
|
|
4010
|
+
function checkHardcodedValues(rootDir) {
|
|
4011
|
+
const findings = [];
|
|
4012
|
+
const staticGraph = readGraphFile(rootDir, "static");
|
|
4013
|
+
if (!staticGraph) return buildReport("static", "hardcoded_values", findings);
|
|
4014
|
+
const knownValues = /* @__PURE__ */ new Set();
|
|
4015
|
+
for (const n of staticGraph.nodes) {
|
|
4016
|
+
if (n.type === "enum_value") knownValues.add(n.value);
|
|
4017
|
+
}
|
|
4018
|
+
const api = readGraphFile(rootDir, "api");
|
|
4019
|
+
if (!api) return buildReport("static", "hardcoded_values", findings);
|
|
4020
|
+
const allCapsRe = /['"]([A-Z][A-Z_]{2,})['"]/g;
|
|
4021
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4022
|
+
for (const node of api.nodes) {
|
|
4023
|
+
if (node.type !== "endpoint") continue;
|
|
4024
|
+
const filePath = (0, import_node_path18.join)(rootDir, "src", node.id);
|
|
4025
|
+
if (!(0, import_node_fs16.existsSync)(filePath)) continue;
|
|
4026
|
+
const content = (0, import_node_fs16.readFileSync)(filePath, "utf-8");
|
|
4027
|
+
let m;
|
|
4028
|
+
allCapsRe.lastIndex = 0;
|
|
4029
|
+
while ((m = allCapsRe.exec(content)) !== null) {
|
|
4030
|
+
const val = m[1];
|
|
4031
|
+
if (knownValues.has(val)) continue;
|
|
4032
|
+
if (["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "UTF", "NULL", "TRUE", "FALSE", "JSON", "HTML", "CSS", "ENV"].includes(val)) continue;
|
|
4033
|
+
const key = `${node.id}:${val}`;
|
|
4034
|
+
if (seen.has(key)) continue;
|
|
4035
|
+
seen.add(key);
|
|
4036
|
+
findings.push({
|
|
4037
|
+
id: `hardcoded:${key}`,
|
|
4038
|
+
severity: "info",
|
|
4039
|
+
category: "hardcoded_values",
|
|
4040
|
+
title: `"${val}" in ${node.id}`,
|
|
4041
|
+
detail: `ALL_CAPS string literal "${val}" appears in API code but is not in the enum/seed inventory. May be an unregistered constant.`,
|
|
4042
|
+
file: node.id
|
|
4043
|
+
});
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
return buildReport("static", "hardcoded_values", findings);
|
|
4047
|
+
}
|
|
4048
|
+
function buildReport(layer, check, findings) {
|
|
4049
|
+
return {
|
|
4050
|
+
layer,
|
|
4051
|
+
check,
|
|
4052
|
+
findings,
|
|
4053
|
+
summary: {
|
|
4054
|
+
errors: findings.filter((f) => f.severity === "error").length,
|
|
4055
|
+
warnings: findings.filter((f) => f.severity === "warning").length,
|
|
4056
|
+
info: findings.filter((f) => f.severity === "info").length
|
|
4057
|
+
},
|
|
4058
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
4061
|
+
var CHECKS = {
|
|
4062
|
+
db: {
|
|
4063
|
+
schema_drift: checkSchemaDrift,
|
|
4064
|
+
orphan_fks: checkOrphanFks
|
|
4065
|
+
},
|
|
4066
|
+
api: {
|
|
4067
|
+
unprotected_routes: checkUnprotectedRoutes
|
|
4068
|
+
},
|
|
4069
|
+
ui: {
|
|
4070
|
+
dead_screens: checkDeadScreens
|
|
4071
|
+
},
|
|
4072
|
+
static: {
|
|
4073
|
+
unenforced_permissions: checkUnenforcedPermissions,
|
|
4074
|
+
hardcoded_values: checkHardcodedValues
|
|
4075
|
+
}
|
|
4076
|
+
};
|
|
4077
|
+
function getAvailableChecks() {
|
|
4078
|
+
const result = {};
|
|
4079
|
+
for (const [layer, checks] of Object.entries(CHECKS)) {
|
|
4080
|
+
result[layer] = Object.keys(checks);
|
|
4081
|
+
}
|
|
4082
|
+
return result;
|
|
4083
|
+
}
|
|
4084
|
+
function runAudit(rootDir, layer, check) {
|
|
4085
|
+
if (layer === "all") {
|
|
4086
|
+
const reports = [];
|
|
4087
|
+
for (const [l, checks] of Object.entries(CHECKS)) {
|
|
4088
|
+
for (const fn of Object.values(checks)) {
|
|
4089
|
+
reports.push(fn(rootDir));
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
return reports;
|
|
4093
|
+
}
|
|
4094
|
+
const layerChecks = CHECKS[layer];
|
|
4095
|
+
if (!layerChecks) {
|
|
4096
|
+
return [{
|
|
4097
|
+
layer,
|
|
4098
|
+
check: "unknown",
|
|
4099
|
+
findings: [{ id: "invalid-layer", severity: "error", category: "system", title: "Invalid layer", detail: `Layer "${layer}" has no audit checks. Available: ${Object.keys(CHECKS).join(", ")}` }],
|
|
4100
|
+
summary: { errors: 1, warnings: 0, info: 0 },
|
|
4101
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4102
|
+
}];
|
|
4103
|
+
}
|
|
4104
|
+
if (check) {
|
|
4105
|
+
const fn = layerChecks[check];
|
|
4106
|
+
if (!fn) {
|
|
4107
|
+
return [{
|
|
4108
|
+
layer,
|
|
4109
|
+
check,
|
|
4110
|
+
findings: [{ id: "invalid-check", severity: "error", category: "system", title: "Invalid check", detail: `Check "${check}" not found for layer "${layer}". Available: ${Object.keys(layerChecks).join(", ")}` }],
|
|
4111
|
+
summary: { errors: 1, warnings: 0, info: 0 },
|
|
4112
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4113
|
+
}];
|
|
4114
|
+
}
|
|
4115
|
+
return [fn(rootDir)];
|
|
4116
|
+
}
|
|
4117
|
+
return Object.values(layerChecks).map((fn) => fn(rootDir));
|
|
4118
|
+
}
|
|
4119
|
+
function formatAsPrompt(reports) {
|
|
4120
|
+
const lines = [];
|
|
4121
|
+
for (const report of reports) {
|
|
4122
|
+
if (report.findings.length === 0) continue;
|
|
4123
|
+
lines.push(`## ${report.layer.toUpperCase()} \u2014 ${report.check} (${report.findings.length} findings)`);
|
|
4124
|
+
lines.push("");
|
|
4125
|
+
for (const f of report.findings) {
|
|
4126
|
+
const tag = f.severity === "error" ? "ERROR" : f.severity === "warning" ? "WARNING" : "INFO";
|
|
4127
|
+
const filePart = f.file ? ` (${f.file}${f.line ? `:${f.line}` : ""})` : "";
|
|
4128
|
+
lines.push(`- [${tag}] ${f.title}${filePart}`);
|
|
4129
|
+
lines.push(` ${f.detail}`);
|
|
4130
|
+
}
|
|
4131
|
+
lines.push("");
|
|
4132
|
+
}
|
|
4133
|
+
if (lines.length === 0) return "No audit findings.";
|
|
4134
|
+
return lines.join("\n");
|
|
4135
|
+
}
|
|
4136
|
+
|
|
4137
|
+
// src/server/chart-serve.ts
|
|
2683
4138
|
var MAX_PORT_SCAN = 3;
|
|
2684
4139
|
function randomPort() {
|
|
2685
4140
|
return 49152 + Math.floor(Math.random() * (65535 - 49152));
|
|
@@ -2698,31 +4153,39 @@ var MIME_TYPES = {
|
|
|
2698
4153
|
function findProjectRoot(startDir) {
|
|
2699
4154
|
let dir = startDir;
|
|
2700
4155
|
for (let i = 0; i < 8; i++) {
|
|
2701
|
-
const graphsDir2 =
|
|
2702
|
-
if (
|
|
2703
|
-
const parent =
|
|
4156
|
+
const graphsDir2 = import_node_path19.default.join(dir, ".launchsecure", "graphs");
|
|
4157
|
+
if (import_node_fs17.default.existsSync(import_node_path19.default.join(graphsDir2, "ui.json")) || import_node_fs17.default.existsSync(import_node_path19.default.join(graphsDir2, "api.json")) || import_node_fs17.default.existsSync(import_node_path19.default.join(graphsDir2, "db.json"))) return dir;
|
|
4158
|
+
const parent = import_node_path19.default.dirname(dir);
|
|
2704
4159
|
if (parent === dir) break;
|
|
2705
4160
|
dir = parent;
|
|
2706
4161
|
}
|
|
2707
4162
|
dir = startDir;
|
|
2708
4163
|
for (let i = 0; i < 8; i++) {
|
|
2709
|
-
if (
|
|
2710
|
-
const parent =
|
|
4164
|
+
if (import_node_fs17.default.existsSync(import_node_path19.default.join(dir, ".git"))) return dir;
|
|
4165
|
+
const parent = import_node_path19.default.dirname(dir);
|
|
2711
4166
|
if (parent === dir) break;
|
|
2712
4167
|
dir = parent;
|
|
2713
4168
|
}
|
|
2714
4169
|
return startDir;
|
|
2715
4170
|
}
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
if (!
|
|
2719
|
-
|
|
2720
|
-
|
|
4171
|
+
function resolveRequestRoot(url, monorepoRoot, projects) {
|
|
4172
|
+
const projectParam = url.searchParams.get("project");
|
|
4173
|
+
if (!projectParam || projects.length === 0) return monorepoRoot;
|
|
4174
|
+
const resolved = import_node_path19.default.resolve(monorepoRoot, projectParam);
|
|
4175
|
+
if (!resolved.startsWith(monorepoRoot)) {
|
|
4176
|
+
throw new Error("Project path outside monorepo root");
|
|
4177
|
+
}
|
|
4178
|
+
return resolved;
|
|
4179
|
+
}
|
|
4180
|
+
async function buildMergedGraph(root) {
|
|
4181
|
+
let graphs = readAllGraphs(root);
|
|
4182
|
+
if (Object.keys(graphs).length === 0) {
|
|
4183
|
+
await generateGraph(root);
|
|
4184
|
+
graphs = readAllGraphs(root);
|
|
2721
4185
|
}
|
|
2722
4186
|
const nodes = [];
|
|
2723
4187
|
const rawLinks = [];
|
|
2724
|
-
const
|
|
2725
|
-
for (const layer of LAYERS2) {
|
|
4188
|
+
for (const layer of Object.keys(graphs)) {
|
|
2726
4189
|
const g = graphs[layer];
|
|
2727
4190
|
if (!g) continue;
|
|
2728
4191
|
for (const n of g.nodes) {
|
|
@@ -2759,16 +4222,16 @@ async function buildMergedGraph(projectRoot) {
|
|
|
2759
4222
|
};
|
|
2760
4223
|
}
|
|
2761
4224
|
function serveStatic(res, filePath) {
|
|
2762
|
-
if (!
|
|
2763
|
-
const ext =
|
|
4225
|
+
if (!import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) return false;
|
|
4226
|
+
const ext = import_node_path19.default.extname(filePath).toLowerCase();
|
|
2764
4227
|
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
2765
4228
|
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
2766
|
-
|
|
4229
|
+
import_node_fs17.default.createReadStream(filePath).pipe(res);
|
|
2767
4230
|
return true;
|
|
2768
4231
|
}
|
|
2769
4232
|
function serveIndex(res, clientDir) {
|
|
2770
|
-
const indexPath =
|
|
2771
|
-
if (!
|
|
4233
|
+
const indexPath = import_node_path19.default.join(clientDir, "index.html");
|
|
4234
|
+
if (!import_node_fs17.default.existsSync(indexPath)) {
|
|
2772
4235
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
2773
4236
|
res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
|
|
2774
4237
|
return;
|
|
@@ -2820,19 +4283,40 @@ async function startChartServer(opts = {}) {
|
|
|
2820
4283
|
}
|
|
2821
4284
|
return { port: existing.port, url: existing.url };
|
|
2822
4285
|
}
|
|
2823
|
-
const clientDir = opts.clientDir ??
|
|
4286
|
+
const clientDir = opts.clientDir ?? import_node_path19.default.join(__dirname, "..", "chart-client");
|
|
4287
|
+
const rootConfig = loadConfig(projectRoot);
|
|
4288
|
+
const projects = rootConfig.projects ?? [];
|
|
2824
4289
|
const server = import_node_http.default.createServer((req, res) => {
|
|
2825
4290
|
try {
|
|
2826
4291
|
const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
4292
|
+
let reqRoot;
|
|
4293
|
+
try {
|
|
4294
|
+
reqRoot = resolveRequestRoot(url2, projectRoot, projects);
|
|
4295
|
+
} catch (err) {
|
|
4296
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4297
|
+
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
4298
|
+
return;
|
|
4299
|
+
}
|
|
4300
|
+
if (req.method === "GET" && url2.pathname === "/api/projects") {
|
|
4301
|
+
const projectList = projects.length > 0 ? projects.map((p) => {
|
|
4302
|
+
const absRoot = import_node_path19.default.resolve(projectRoot, p.root);
|
|
4303
|
+
const hasGraphs = import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, ".launchsecure", "graphs"));
|
|
4304
|
+
const hasNextConfig2 = import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, "next.config.ts")) || import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, "next.config.js")) || import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, "next.config.mjs"));
|
|
4305
|
+
return { name: p.name, root: p.root, hasGraphs, hasNextConfig: hasNextConfig2 };
|
|
4306
|
+
}) : [{ name: import_node_path19.default.basename(projectRoot), root: ".", hasGraphs: import_node_fs17.default.existsSync(import_node_path19.default.join(projectRoot, ".launchsecure", "graphs")), hasNextConfig: true }];
|
|
4307
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4308
|
+
res.end(JSON.stringify({ projects: projectList, monorepoRoot: projectRoot }));
|
|
4309
|
+
return;
|
|
4310
|
+
}
|
|
2827
4311
|
if (req.method === "GET" && url2.pathname === "/api/project-graph") {
|
|
2828
4312
|
const regenerate = url2.searchParams.get("regenerate") === "1";
|
|
2829
4313
|
(async () => {
|
|
2830
|
-
if (regenerate) await generateGraph(
|
|
2831
|
-
const merged = await buildMergedGraph(
|
|
4314
|
+
if (regenerate) await generateGraph(reqRoot);
|
|
4315
|
+
const merged = await buildMergedGraph(reqRoot);
|
|
2832
4316
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2833
4317
|
res.end(JSON.stringify({
|
|
2834
4318
|
...merged,
|
|
2835
|
-
debug: { cwd, projectRoot }
|
|
4319
|
+
debug: { cwd, projectRoot: reqRoot }
|
|
2836
4320
|
}));
|
|
2837
4321
|
})().catch((e) => {
|
|
2838
4322
|
res.writeHead(500);
|
|
@@ -2841,15 +4325,15 @@ async function startChartServer(opts = {}) {
|
|
|
2841
4325
|
return;
|
|
2842
4326
|
}
|
|
2843
4327
|
if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
|
|
2844
|
-
const graphs = readAllGraphs(
|
|
4328
|
+
const graphs = readAllGraphs(reqRoot);
|
|
2845
4329
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2846
4330
|
res.end(JSON.stringify({ ui: graphs.ui ?? null, api: graphs.api ?? null, db: graphs.db ?? null }));
|
|
2847
4331
|
return;
|
|
2848
4332
|
}
|
|
2849
4333
|
if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
|
|
2850
4334
|
(async () => {
|
|
2851
|
-
await generateGraph(
|
|
2852
|
-
const graphs = readAllGraphs(
|
|
4335
|
+
await generateGraph(reqRoot);
|
|
4336
|
+
const graphs = readAllGraphs(reqRoot);
|
|
2853
4337
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2854
4338
|
res.end(JSON.stringify({
|
|
2855
4339
|
ok: true,
|
|
@@ -2865,41 +4349,49 @@ async function startChartServer(opts = {}) {
|
|
|
2865
4349
|
}
|
|
2866
4350
|
if (req.method === "GET" && url2.pathname === "/api/file-content") {
|
|
2867
4351
|
const relPath = url2.searchParams.get("path");
|
|
2868
|
-
if (!relPath || relPath.includes("..") ||
|
|
4352
|
+
if (!relPath || relPath.includes("..") || import_node_path19.default.isAbsolute(relPath)) {
|
|
2869
4353
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2870
4354
|
res.end(JSON.stringify({ error: "Invalid path" }));
|
|
2871
4355
|
return;
|
|
2872
4356
|
}
|
|
2873
|
-
const filePath =
|
|
2874
|
-
if (!filePath.startsWith(
|
|
4357
|
+
const filePath = import_node_path19.default.join(reqRoot, relPath);
|
|
4358
|
+
if (!filePath.startsWith(reqRoot) || !import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) {
|
|
2875
4359
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2876
4360
|
res.end(JSON.stringify({ error: "File not found" }));
|
|
2877
4361
|
return;
|
|
2878
4362
|
}
|
|
2879
|
-
const ext =
|
|
4363
|
+
const ext = import_node_path19.default.extname(filePath).toLowerCase();
|
|
2880
4364
|
const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
|
|
2881
|
-
const content =
|
|
4365
|
+
const content = import_node_fs17.default.readFileSync(filePath, "utf-8");
|
|
2882
4366
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2883
4367
|
res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
|
|
2884
4368
|
return;
|
|
2885
4369
|
}
|
|
2886
4370
|
if (req.method === "GET" && url2.pathname === "/api/health") {
|
|
2887
4371
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2888
|
-
res.end(JSON.stringify({ ok: true, projectRoot }));
|
|
4372
|
+
res.end(JSON.stringify({ ok: true, projectRoot: reqRoot }));
|
|
2889
4373
|
return;
|
|
2890
4374
|
}
|
|
2891
4375
|
if (req.method === "GET" && url2.pathname === "/api/parser-config") {
|
|
2892
|
-
const config2 = loadConfig(
|
|
2893
|
-
const
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
4376
|
+
const config2 = loadConfig(reqRoot);
|
|
4377
|
+
const registry = createRegistry(config2, reqRoot);
|
|
4378
|
+
const toLabel = (id) => id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
4379
|
+
const detection = [];
|
|
4380
|
+
for (const parser of registry.getAll()) {
|
|
4381
|
+
if ("layers" in parser && Array.isArray(parser.layers)) {
|
|
4382
|
+
const mp = parser;
|
|
4383
|
+
detection.push({ id: mp.id, layers: mp.layers, label: toLabel(mp.id), detected: mp.detect(reqRoot) });
|
|
4384
|
+
} else if ("layer" in parser && parser.layer !== "crosslayer") {
|
|
4385
|
+
const sp = parser;
|
|
4386
|
+
detection.push({ id: sp.id, layers: [sp.layer], label: toLabel(sp.id), detected: sp.detect(reqRoot) });
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
const crosslayerParsers = {};
|
|
4390
|
+
for (const p of registry.getCrossLayerParsers()) {
|
|
4391
|
+
const concern = p.concern ?? "api-binding";
|
|
4392
|
+
if (!crosslayerParsers[concern]) crosslayerParsers[concern] = [];
|
|
4393
|
+
crosslayerParsers[concern].push({ id: p.id, label: toLabel(p.id) });
|
|
4394
|
+
}
|
|
2903
4395
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2904
4396
|
res.end(JSON.stringify({ config: config2, detection, crosslayerParsers }));
|
|
2905
4397
|
return;
|
|
@@ -2912,8 +4404,8 @@ async function startChartServer(opts = {}) {
|
|
|
2912
4404
|
req.on("end", () => {
|
|
2913
4405
|
try {
|
|
2914
4406
|
const newConfig = JSON.parse(body);
|
|
2915
|
-
const configPath =
|
|
2916
|
-
|
|
4407
|
+
const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
|
|
4408
|
+
import_node_fs17.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
|
|
2917
4409
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2918
4410
|
res.end(JSON.stringify({ ok: true }));
|
|
2919
4411
|
} catch (err) {
|
|
@@ -2924,7 +4416,7 @@ async function startChartServer(opts = {}) {
|
|
|
2924
4416
|
return;
|
|
2925
4417
|
}
|
|
2926
4418
|
if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
|
|
2927
|
-
const config2 = loadConfig(
|
|
4419
|
+
const config2 = loadConfig(reqRoot);
|
|
2928
4420
|
const builtinTaggers = [
|
|
2929
4421
|
{ id: "module", tagKey: "module", trackUntagged: config2.taggers?.trackUntagged?.module ?? true },
|
|
2930
4422
|
{ id: "screen", tagKey: "screen", trackUntagged: config2.taggers?.trackUntagged?.screen ?? true }
|
|
@@ -2944,10 +4436,10 @@ async function startChartServer(opts = {}) {
|
|
|
2944
4436
|
req.on("end", () => {
|
|
2945
4437
|
try {
|
|
2946
4438
|
const taggerConfig = JSON.parse(body);
|
|
2947
|
-
const config2 = loadConfig(
|
|
4439
|
+
const config2 = loadConfig(reqRoot);
|
|
2948
4440
|
config2.taggers = taggerConfig;
|
|
2949
|
-
const configPath =
|
|
2950
|
-
|
|
4441
|
+
const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
|
|
4442
|
+
import_node_fs17.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
2951
4443
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2952
4444
|
res.end(JSON.stringify({ ok: true }));
|
|
2953
4445
|
} catch (err) {
|
|
@@ -2958,7 +4450,7 @@ async function startChartServer(opts = {}) {
|
|
|
2958
4450
|
return;
|
|
2959
4451
|
}
|
|
2960
4452
|
if (req.method === "GET" && url2.pathname === "/api/tags") {
|
|
2961
|
-
const store = readTagStore(
|
|
4453
|
+
const store = readTagStore(reqRoot);
|
|
2962
4454
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2963
4455
|
res.end(JSON.stringify(store));
|
|
2964
4456
|
return;
|
|
@@ -2976,7 +4468,7 @@ async function startChartServer(opts = {}) {
|
|
|
2976
4468
|
res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
|
|
2977
4469
|
return;
|
|
2978
4470
|
}
|
|
2979
|
-
setTag(
|
|
4471
|
+
setTag(reqRoot, nodeId, key, value);
|
|
2980
4472
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2981
4473
|
res.end(JSON.stringify({ ok: true }));
|
|
2982
4474
|
} catch (err) {
|
|
@@ -2999,7 +4491,81 @@ async function startChartServer(opts = {}) {
|
|
|
2999
4491
|
res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
|
|
3000
4492
|
return;
|
|
3001
4493
|
}
|
|
3002
|
-
removeTag(
|
|
4494
|
+
removeTag(reqRoot, nodeId, key);
|
|
4495
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4496
|
+
res.end(JSON.stringify({ ok: true }));
|
|
4497
|
+
} catch (err) {
|
|
4498
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4499
|
+
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
4500
|
+
}
|
|
4501
|
+
});
|
|
4502
|
+
return;
|
|
4503
|
+
}
|
|
4504
|
+
if (req.method === "GET" && url2.pathname === "/api/detected-paths") {
|
|
4505
|
+
const config2 = loadConfig(reqRoot);
|
|
4506
|
+
const paths = resolveProjectPaths(reqRoot, config2);
|
|
4507
|
+
const overrides = {
|
|
4508
|
+
appDir: !!config2.paths?.appDir,
|
|
4509
|
+
dbDir: !!config2.paths?.dbDir
|
|
4510
|
+
};
|
|
4511
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4512
|
+
res.end(JSON.stringify({
|
|
4513
|
+
projectRoot: reqRoot,
|
|
4514
|
+
detected: paths ? {
|
|
4515
|
+
srcDir: import_node_path19.default.relative(reqRoot, paths.srcDir) || ".",
|
|
4516
|
+
appDir: import_node_path19.default.relative(reqRoot, paths.appDir),
|
|
4517
|
+
apiDir: import_node_path19.default.relative(reqRoot, paths.apiDir),
|
|
4518
|
+
dbDir: paths.dbDir ? import_node_path19.default.relative(reqRoot, paths.dbDir) : null
|
|
4519
|
+
} : null,
|
|
4520
|
+
overrides,
|
|
4521
|
+
isOverride: overrides.appDir
|
|
4522
|
+
}));
|
|
4523
|
+
return;
|
|
4524
|
+
}
|
|
4525
|
+
if (req.method === "GET" && url2.pathname === "/api/browse-dir") {
|
|
4526
|
+
const browsePath = url2.searchParams.get("path") || projectRoot;
|
|
4527
|
+
const abs = import_node_path19.default.resolve(browsePath);
|
|
4528
|
+
const twoUp = import_node_path19.default.resolve(projectRoot, "..", "..");
|
|
4529
|
+
if (!abs.startsWith(twoUp)) {
|
|
4530
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
4531
|
+
res.end(JSON.stringify({ ok: false, error: "Path outside allowed range" }));
|
|
4532
|
+
return;
|
|
4533
|
+
}
|
|
4534
|
+
try {
|
|
4535
|
+
const entries = import_node_fs17.default.readdirSync(abs, { withFileTypes: true });
|
|
4536
|
+
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "dist" && e.name !== ".next").map((e) => e.name).sort();
|
|
4537
|
+
const parent = abs !== twoUp ? import_node_path19.default.dirname(abs) : null;
|
|
4538
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4539
|
+
res.end(JSON.stringify({ current: abs, parent, dirs, relative: import_node_path19.default.relative(projectRoot, abs) || "." }));
|
|
4540
|
+
} catch (err) {
|
|
4541
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4542
|
+
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
4543
|
+
}
|
|
4544
|
+
return;
|
|
4545
|
+
}
|
|
4546
|
+
if (req.method === "GET" && url2.pathname === "/api/audit") {
|
|
4547
|
+
const layer = url2.searchParams.get("layer") ?? "all";
|
|
4548
|
+
const check = url2.searchParams.get("check") ?? void 0;
|
|
4549
|
+
const reports = runAudit(reqRoot, layer, check);
|
|
4550
|
+
const prompt = formatAsPrompt(reports);
|
|
4551
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4552
|
+
res.end(JSON.stringify({ reports, prompt, checks: getAvailableChecks() }));
|
|
4553
|
+
return;
|
|
4554
|
+
}
|
|
4555
|
+
if (req.method === "POST" && url2.pathname === "/api/projects") {
|
|
4556
|
+
let body = "";
|
|
4557
|
+
req.on("data", (chunk) => {
|
|
4558
|
+
body += chunk.toString();
|
|
4559
|
+
});
|
|
4560
|
+
req.on("end", () => {
|
|
4561
|
+
try {
|
|
4562
|
+
const { projects: newProjects } = JSON.parse(body);
|
|
4563
|
+
const config2 = loadConfig(projectRoot);
|
|
4564
|
+
config2.projects = newProjects.length > 0 ? newProjects : void 0;
|
|
4565
|
+
const configPath = import_node_path19.default.join(projectRoot, ".launchchart.json");
|
|
4566
|
+
import_node_fs17.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
4567
|
+
projects.length = 0;
|
|
4568
|
+
if (config2.projects) projects.push(...config2.projects);
|
|
3003
4569
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3004
4570
|
res.end(JSON.stringify({ ok: true }));
|
|
3005
4571
|
} catch (err) {
|
|
@@ -3010,7 +4576,7 @@ async function startChartServer(opts = {}) {
|
|
|
3010
4576
|
return;
|
|
3011
4577
|
}
|
|
3012
4578
|
if (url2.pathname !== "/") {
|
|
3013
|
-
const staticPath =
|
|
4579
|
+
const staticPath = import_node_path19.default.join(clientDir, url2.pathname);
|
|
3014
4580
|
if (serveStatic(res, staticPath)) return;
|
|
3015
4581
|
}
|
|
3016
4582
|
serveIndex(res, clientDir);
|
|
@@ -3048,6 +4614,10 @@ async function startChartServer(opts = {}) {
|
|
|
3048
4614
|
`);
|
|
3049
4615
|
process.stderr.write(`[launch-chart] project root: ${projectRoot}
|
|
3050
4616
|
`);
|
|
4617
|
+
if (projects.length > 0) {
|
|
4618
|
+
process.stderr.write(`[launch-chart] multi-project mode: ${projects.length} projects
|
|
4619
|
+
`);
|
|
4620
|
+
}
|
|
3051
4621
|
}
|
|
3052
4622
|
return { port, url };
|
|
3053
4623
|
}
|