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