@launchsecure/launch-kit 0.0.24 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/dist/beacon/beacon.mjs +1016 -0
- package/dist/beacon/beacon.mjs.map +1 -0
- package/dist/beacon/beacon.umd.js +87 -0
- package/dist/beacon/beacon.umd.js.map +1 -0
- package/dist/beacon/index-DAIDnjfR.mjs +513 -0
- package/dist/beacon/index-DAIDnjfR.mjs.map +1 -0
- package/dist/beacon/types/capture/element.d.ts +3 -0
- package/dist/beacon/types/capture/element.d.ts.map +1 -0
- package/dist/beacon/types/capture/framework.d.ts +3 -0
- package/dist/beacon/types/capture/framework.d.ts.map +1 -0
- package/dist/beacon/types/capture/metadata.d.ts +3 -0
- package/dist/beacon/types/capture/metadata.d.ts.map +1 -0
- package/dist/beacon/types/capture/overlay.d.ts +7 -0
- package/dist/beacon/types/capture/overlay.d.ts.map +1 -0
- package/dist/beacon/types/capture/picker.d.ts +12 -0
- package/dist/beacon/types/capture/picker.d.ts.map +1 -0
- package/dist/beacon/types/capture/screenshot.d.ts +7 -0
- package/dist/beacon/types/capture/screenshot.d.ts.map +1 -0
- package/dist/beacon/types/capture/selector.d.ts +2 -0
- package/dist/beacon/types/capture/selector.d.ts.map +1 -0
- package/dist/beacon/types/element.d.ts +50 -0
- package/dist/beacon/types/element.d.ts.map +1 -0
- package/dist/beacon/types/index.d.ts +4 -0
- package/dist/beacon/types/index.d.ts.map +1 -0
- package/dist/beacon/types/transport/submit.d.ts +3 -0
- package/dist/beacon/types/transport/submit.d.ts.map +1 -0
- package/dist/beacon/types/types.d.ts +88 -0
- package/dist/beacon/types/types.d.ts.map +1 -0
- package/dist/beacon/types/ui/button.d.ts +2 -0
- package/dist/beacon/types/ui/button.d.ts.map +1 -0
- package/dist/beacon/types/ui/drawer.d.ts +31 -0
- package/dist/beacon/types/ui/drawer.d.ts.map +1 -0
- package/dist/beacon/types/ui/icons.d.ts +9 -0
- package/dist/beacon/types/ui/icons.d.ts.map +1 -0
- package/dist/beacon/types/ui/pick-mode-overlay.d.ts +25 -0
- package/dist/beacon/types/ui/pick-mode-overlay.d.ts.map +1 -0
- package/dist/beacon/types/ui/pin-popover.d.ts +14 -0
- package/dist/beacon/types/ui/pin-popover.d.ts.map +1 -0
- package/dist/chart-client/assets/{index-C8ANseEa.js → index-Bk1hawjD.js} +63 -58
- package/dist/chart-client/assets/index-DpaGa3bY.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-Bfel4OQ5.css +32 -0
- package/dist/client/assets/{index-Ds9UP_cj.js → index-eC-WuUWB.js} +58 -58
- package/dist/client/index.html +2 -2
- package/dist/council-client/assets/{index-Dc41S-R2.js → index-Cs_MVXHf.js} +14 -14
- package/dist/council-client/assets/index-P5kMsT5a.css +1 -0
- package/dist/council-client/index.html +2 -2
- package/dist/deck-client/assets/{_baseUniq-2gclQXo7.js → _baseUniq-C2xT_eYu.js} +1 -1
- package/dist/deck-client/assets/{arc-DcMY5Wm0.js → arc-CmVL9pGd.js} +1 -1
- package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-B8iirmmJ.js → architectureDiagram-Q4EWVU46-BSFgdjve.js} +1 -1
- package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-B4JBLjmJ.js → blockDiagram-DXYQGD6D-DuLzscvP.js} +1 -1
- package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-CojrJAk8.js → c4Diagram-AHTNJAMY-CfCJB8eY.js} +1 -1
- package/dist/deck-client/assets/channel-B4aNO8ZB.js +1 -0
- package/dist/deck-client/assets/{chunk-4BX2VUAB-Bmb_BMDo.js → chunk-4BX2VUAB-DxmLYTWZ.js} +1 -1
- package/dist/deck-client/assets/{chunk-4TB4RGXK-CumBy8qe.js → chunk-4TB4RGXK-CCnf7GFE.js} +1 -1
- package/dist/deck-client/assets/{chunk-55IACEB6-Ka8Hb1wD.js → chunk-55IACEB6-Db9DApcj.js} +1 -1
- package/dist/deck-client/assets/{chunk-EDXVE4YY-B3sIPiQo.js → chunk-EDXVE4YY-DmYDq8ZI.js} +1 -1
- package/dist/deck-client/assets/{chunk-FMBD7UC4-C1tYkaqu.js → chunk-FMBD7UC4-BGhUlF20.js} +1 -1
- package/dist/deck-client/assets/{chunk-OYMX7WX6-D7Wacbky.js → chunk-OYMX7WX6-CpEnicQZ.js} +1 -1
- package/dist/deck-client/assets/{chunk-QZHKN3VN-ChXI0vO3.js → chunk-QZHKN3VN-Doa7LKwf.js} +1 -1
- package/dist/deck-client/assets/{chunk-YZCP3GAM-BXhiqf8u.js → chunk-YZCP3GAM-CpkIlH6V.js} +1 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-BHTI0yWz.js +1 -0
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-BHTI0yWz.js +1 -0
- package/dist/deck-client/assets/clone-HduFm7qU.js +1 -0
- package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-Bqp3p68D.js → cose-bilkent-S5V4N54A-Bkh8Bfcb.js} +1 -1
- package/dist/deck-client/assets/{dagre-KV5264BT-BS-rtyhZ.js → dagre-KV5264BT-Bp0XpTgH.js} +1 -1
- package/dist/deck-client/assets/{diagram-5BDNPKRD-BIrj9YGI.js → diagram-5BDNPKRD-ZHiyGYPQ.js} +1 -1
- package/dist/deck-client/assets/{diagram-G4DWMVQ6-noHWPIg4.js → diagram-G4DWMVQ6-BW-Q8_H5.js} +1 -1
- package/dist/deck-client/assets/{diagram-MMDJMWI5-C2qHxvqV.js → diagram-MMDJMWI5-6I3LTafu.js} +1 -1
- package/dist/deck-client/assets/{diagram-TYMM5635-BytnGQr-.js → diagram-TYMM5635-CyM5YK28.js} +1 -1
- package/dist/deck-client/assets/{erDiagram-SMLLAGMA-BfK5m2YQ.js → erDiagram-SMLLAGMA-CjNxVJHk.js} +1 -1
- package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-Cq925G1Z.js → flowDiagram-DWJPFMVM-BDQHuAJR.js} +1 -1
- package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-DhhHPAmj.js → ganttDiagram-T4ZO3ILL-B7MnkpbP.js} +1 -1
- package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-B3Lc0h9q.js → gitGraphDiagram-UUTBAWPF-C9dZAcYD.js} +1 -1
- package/dist/deck-client/assets/{graph-RTawgVWm.js → graph-CjdBnzUy.js} +1 -1
- package/dist/deck-client/assets/{index-BfIfJXmS.js → index-DeIVPW63.js} +68 -68
- package/dist/deck-client/assets/index-LKZDAS9S.css +1 -0
- package/dist/deck-client/assets/{infoDiagram-42DDH7IO-BlR584kX.js → infoDiagram-42DDH7IO-C7d3iRC3.js} +1 -1
- package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DygKoNGY.js → ishikawaDiagram-UXIWVN3A-BcYGKj09.js} +1 -1
- package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-BnaiYp9N.js → journeyDiagram-VCZTEJTY-DqFlRrOL.js} +1 -1
- package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-BQBUBzJC.js → kanban-definition-6JOO6SKY-BJhPp1NR.js} +1 -1
- package/dist/deck-client/assets/{layout-DeZ8HI1T.js → layout-DIeS6GvK.js} +1 -1
- package/dist/deck-client/assets/{linear-C6roLi_9.js → linear-He_yJy5H.js} +1 -1
- package/dist/deck-client/assets/{min-CbUksbuI.js → min-DQ6Kx06t.js} +1 -1
- package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-iNxV62yN.js → mindmap-definition-QFDTVHPH-sQ62L8T2.js} +1 -1
- package/dist/deck-client/assets/{pieDiagram-DEJITSTG-DHVA0jaG.js → pieDiagram-DEJITSTG-BqCWmU2K.js} +1 -1
- package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-DBeKKLUQ.js → quadrantDiagram-34T5L4WZ-rQ1TJOoe.js} +1 -1
- package/dist/deck-client/assets/{requirementDiagram-MS252O5E-CBwITx7p.js → requirementDiagram-MS252O5E-BO2MPBOM.js} +1 -1
- package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-BtE-1YTU.js → sankeyDiagram-XADWPNL6-BgsHEVex.js} +1 -1
- package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-DN96yPP2.js → sequenceDiagram-FGHM5R23-B3j1yMLU.js} +1 -1
- package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-VUkKC2uJ.js → stateDiagram-FHFEXIEX-C8jFlZou.js} +1 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-BoqepHW0.js +1 -0
- package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-oUeZhRns.js → timeline-definition-GMOUNBTQ-tM-qo4Zk.js} +1 -1
- package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-D87fK90n.js → vennDiagram-DHZGUBPP-B0-6kOEu.js} +1 -1
- package/dist/deck-client/assets/wardley-RL74JXVD-HpBk07P-.js +162 -0
- package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-Ca_i0QRA.js → wardleyDiagram-NUSXRM2D-BkA1NLDE.js} +1 -1
- package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-CUOJVIvq.js → xychartDiagram-5P7HB3ND-CEKGSuI-.js} +1 -1
- package/dist/deck-client/index.html +2 -2
- package/dist/server/chart-serve.js +1336 -141
- package/dist/server/cli.js +28423 -6671
- package/dist/server/council-entry.js +0 -0
- package/dist/server/deck-mcp-entry.js +332 -3
- package/dist/server/deck-serve.js +288 -0
- package/dist/server/fb-wizard.js +0 -0
- package/dist/server/graph/queries/classify.scm +8 -0
- package/dist/server/graph/queries/exports.scm +7 -0
- package/dist/server/graph-mcp-entry.js +1987 -224
- package/dist/server/recall-entry.js +1112 -0
- package/package.json +47 -21
- package/dist/chart-client/assets/index--120d9P9.css +0 -1
- package/dist/client/assets/index-Bf8zdL3x.css +0 -32
- package/dist/council-client/assets/index-CofZh7pS.css +0 -1
- package/dist/deck-client/assets/channel-ERh5jKXV.js +0 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-CMi1Gaev.js +0 -1
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-CMi1Gaev.js +0 -1
- package/dist/deck-client/assets/clone-DfWhlD4X.js +0 -1
- package/dist/deck-client/assets/index-765AIQ9z.css +0 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CA0IjulK.js +0 -1
- package/dist/deck-client/assets/wardley-RL74JXVD-DYbYcpDp.js +0 -162
- package/dist/server/deck-server/deck-mcp-entry.js +0 -1789
- package/dist/server/deck-server/deck-serve.js +0 -1275
- package/dist/server/server/chart-serve.js +0 -4643
- package/dist/server/server/cli.js +0 -13360
- package/dist/server/server/fb-wizard.js +0 -136
- package/dist/server/server/graph-mcp-entry.js +0 -6776
|
@@ -1,4643 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __esm = (fn, res) => function __init() {
|
|
9
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
-
};
|
|
11
|
-
var __export = (target, all) => {
|
|
12
|
-
for (var name in all)
|
|
13
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
-
};
|
|
15
|
-
var __copyProps = (to, from, except, desc) => {
|
|
16
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
-
for (let key of __getOwnPropNames(from))
|
|
18
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
-
}
|
|
21
|
-
return to;
|
|
22
|
-
};
|
|
23
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
-
mod
|
|
30
|
-
));
|
|
31
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
-
|
|
33
|
-
// src/server/graph/core/config.ts
|
|
34
|
-
var config_exports = {};
|
|
35
|
-
__export(config_exports, {
|
|
36
|
-
loadConfig: () => loadConfig
|
|
37
|
-
});
|
|
38
|
-
function loadConfig(rootDir) {
|
|
39
|
-
const configPath = (0, import_node_path.join)(rootDir, CONFIG_FILENAME);
|
|
40
|
-
if (!(0, import_node_fs.existsSync)(configPath)) return {};
|
|
41
|
-
try {
|
|
42
|
-
return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
|
|
43
|
-
} catch {
|
|
44
|
-
return {};
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
var import_node_fs, import_node_path, CONFIG_FILENAME;
|
|
48
|
-
var init_config = __esm({
|
|
49
|
-
"src/server/graph/core/config.ts"() {
|
|
50
|
-
"use strict";
|
|
51
|
-
import_node_fs = require("node:fs");
|
|
52
|
-
import_node_path = require("node:path");
|
|
53
|
-
CONFIG_FILENAME = ".launchchart.json";
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// src/server/graph/core/ts-extractor.ts
|
|
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
|
-
});
|
|
70
|
-
async function initTreeSitter() {
|
|
71
|
-
if (initialized) return;
|
|
72
|
-
if (initPromise) return initPromise;
|
|
73
|
-
initPromise = (async () => {
|
|
74
|
-
const TreeSitter = require("web-tree-sitter");
|
|
75
|
-
await TreeSitter.init();
|
|
76
|
-
const wasmPath = require.resolve("tree-sitter-typescript/tree-sitter-tsx.wasm");
|
|
77
|
-
tsxLanguage = await TreeSitter.Language.load(wasmPath);
|
|
78
|
-
parserInstance = new TreeSitter();
|
|
79
|
-
parserInstance.setLanguage(tsxLanguage);
|
|
80
|
-
initialized = true;
|
|
81
|
-
})();
|
|
82
|
-
return initPromise;
|
|
83
|
-
}
|
|
84
|
-
function ensureInit() {
|
|
85
|
-
if (!initialized || !tsxLanguage || !parserInstance) {
|
|
86
|
-
throw new Error("Tree-sitter not initialized. Call initTreeSitter() first.");
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
function getQuery(name) {
|
|
90
|
-
ensureInit();
|
|
91
|
-
const cached = queryCache.get(name);
|
|
92
|
-
if (cached) return cached;
|
|
93
|
-
const scmPath = (0, import_node_path3.join)(queriesDir, `${name}.scm`);
|
|
94
|
-
const scm = (0, import_node_fs3.readFileSync)(scmPath, "utf-8");
|
|
95
|
-
const query = tsxLanguage.query(scm);
|
|
96
|
-
queryCache.set(name, query);
|
|
97
|
-
return query;
|
|
98
|
-
}
|
|
99
|
-
function parseSource(absPath) {
|
|
100
|
-
ensureInit();
|
|
101
|
-
const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
|
|
102
|
-
return parserInstance.parse(content);
|
|
103
|
-
}
|
|
104
|
-
function parseCodeTS(code) {
|
|
105
|
-
ensureInit();
|
|
106
|
-
return parserInstance.parse(code);
|
|
107
|
-
}
|
|
108
|
-
function createQuery(pattern) {
|
|
109
|
-
ensureInit();
|
|
110
|
-
return tsxLanguage.query(pattern);
|
|
111
|
-
}
|
|
112
|
-
function setExtractorConfig(config) {
|
|
113
|
-
extraDbIdentifiers = config.dbIdentifiers ?? [];
|
|
114
|
-
extraMutationMethods = config.mutationMethods ?? [];
|
|
115
|
-
}
|
|
116
|
-
function getMutationMethods() {
|
|
117
|
-
return /* @__PURE__ */ new Set([...PRISMA_MUTATION_METHODS_BUILTIN, ...extraMutationMethods]);
|
|
118
|
-
}
|
|
119
|
-
function getFallbackDbIdentifiers() {
|
|
120
|
-
return /* @__PURE__ */ new Set([...DB_IDENTIFIERS_FALLBACK, ...extraDbIdentifiers]);
|
|
121
|
-
}
|
|
122
|
-
function looksLikeUrl(s) {
|
|
123
|
-
return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
|
|
124
|
-
}
|
|
125
|
-
function templateStartsWithSlash(text) {
|
|
126
|
-
const content = text.slice(1);
|
|
127
|
-
return content.startsWith("/");
|
|
128
|
-
}
|
|
129
|
-
function captureMap(match) {
|
|
130
|
-
const map = {};
|
|
131
|
-
for (const c of match.captures) {
|
|
132
|
-
map[c.name] = c.node.text;
|
|
133
|
-
}
|
|
134
|
-
return map;
|
|
135
|
-
}
|
|
136
|
-
function childrenOfType(node, type) {
|
|
137
|
-
return node.children.filter((n) => n.type === type);
|
|
138
|
-
}
|
|
139
|
-
function childOfType(node, type) {
|
|
140
|
-
return node.children.find((n) => n.type === type);
|
|
141
|
-
}
|
|
142
|
-
function parseFileTS(absPath) {
|
|
143
|
-
const tree = parseSource(absPath);
|
|
144
|
-
const root = tree.rootNode;
|
|
145
|
-
const imports = [];
|
|
146
|
-
const importStatements = childrenOfType(root, "import_statement");
|
|
147
|
-
for (const stmt of importStatements) {
|
|
148
|
-
const sourceNode = childOfType(stmt, "string");
|
|
149
|
-
const frag = sourceNode ? childOfType(sourceNode, "string_fragment") : void 0;
|
|
150
|
-
if (!frag) continue;
|
|
151
|
-
const specifier = frag.text;
|
|
152
|
-
const hasTypeKeyword = stmt.children.some(
|
|
153
|
-
(n) => n.type === "type" && n.text === "type"
|
|
154
|
-
);
|
|
155
|
-
const clause = childOfType(stmt, "import_clause");
|
|
156
|
-
if (!clause) {
|
|
157
|
-
imports.push({ names: [], specifier, isTypeOnly: false, typeNames: /* @__PURE__ */ new Set() });
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
const names = [];
|
|
161
|
-
const typeNames = /* @__PURE__ */ new Set();
|
|
162
|
-
const defaultId = childOfType(clause, "identifier");
|
|
163
|
-
if (defaultId) names.push(defaultId.text);
|
|
164
|
-
const nsImport = childOfType(clause, "namespace_import");
|
|
165
|
-
if (nsImport) {
|
|
166
|
-
const nsId = childOfType(nsImport, "identifier");
|
|
167
|
-
if (nsId) names.push(nsId.text);
|
|
168
|
-
}
|
|
169
|
-
const namedImports = childOfType(clause, "named_imports");
|
|
170
|
-
if (namedImports) {
|
|
171
|
-
for (const spec of childrenOfType(namedImports, "import_specifier")) {
|
|
172
|
-
const identifiers = childrenOfType(spec, "identifier");
|
|
173
|
-
const importedName = identifiers.length > 1 ? identifiers[identifiers.length - 1].text : identifiers[0]?.text;
|
|
174
|
-
if (importedName) {
|
|
175
|
-
names.push(importedName);
|
|
176
|
-
const specIsType = spec.children.some(
|
|
177
|
-
(n) => n.type === "type" && n.text === "type"
|
|
178
|
-
);
|
|
179
|
-
if (specIsType) typeNames.add(importedName);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
if (names.length > 0 || hasTypeKeyword) {
|
|
184
|
-
imports.push({ names, specifier, isTypeOnly: hasTypeKeyword, typeNames });
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
const importQuery = getQuery("imports");
|
|
188
|
-
const importMatches = importQuery.matches(root);
|
|
189
|
-
for (const m of importMatches) {
|
|
190
|
-
const caps = captureMap(m);
|
|
191
|
-
if (caps["import.dynamic"]) {
|
|
192
|
-
imports.push({
|
|
193
|
-
names: [],
|
|
194
|
-
specifier: caps["import.dynamic"],
|
|
195
|
-
isTypeOnly: false,
|
|
196
|
-
typeNames: /* @__PURE__ */ new Set()
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
const exportsSet = /* @__PURE__ */ new Set();
|
|
201
|
-
const exportsOrdered = [];
|
|
202
|
-
let defaultName = null;
|
|
203
|
-
let firstValueExport = null;
|
|
204
|
-
let firstTypeExport = null;
|
|
205
|
-
function addExport(name2, kind) {
|
|
206
|
-
if (!exportsSet.has(name2)) {
|
|
207
|
-
exportsSet.add(name2);
|
|
208
|
-
exportsOrdered.push(name2);
|
|
209
|
-
}
|
|
210
|
-
if (kind === "default") defaultName = name2;
|
|
211
|
-
else if (kind === "value" && !firstValueExport) firstValueExport = name2;
|
|
212
|
-
else if (kind === "type" && !firstTypeExport) firstTypeExport = name2;
|
|
213
|
-
}
|
|
214
|
-
const exportQuery = getQuery("exports");
|
|
215
|
-
const exportMatches = exportQuery.matches(root);
|
|
216
|
-
for (const m of exportMatches) {
|
|
217
|
-
const caps = captureMap(m);
|
|
218
|
-
if (caps["export.default.func"]) addExport(caps["export.default.func"], "default");
|
|
219
|
-
else if (caps["export.default.class"]) addExport(caps["export.default.class"], "default");
|
|
220
|
-
else if (caps["export.default.value"]) addExport(caps["export.default.value"], "default");
|
|
221
|
-
else if (caps["export.named.func"]) addExport(caps["export.named.func"], "value");
|
|
222
|
-
else if (caps["export.named.class"]) addExport(caps["export.named.class"], "value");
|
|
223
|
-
else if (caps["export.named.const"]) addExport(caps["export.named.const"], "value");
|
|
224
|
-
else if (caps["export.named.enum"]) addExport(caps["export.named.enum"], "value");
|
|
225
|
-
else if (caps["export.named.type"]) addExport(caps["export.named.type"], "type");
|
|
226
|
-
else if (caps["export.named.interface"]) addExport(caps["export.named.interface"], "type");
|
|
227
|
-
else if (caps["export.bare.name"]) addExport(caps["export.bare.name"], "value");
|
|
228
|
-
if (caps["reexport.name"]) addExport(caps["reexport.name"], "value");
|
|
229
|
-
}
|
|
230
|
-
for (const stmt of childrenOfType(root, "export_statement")) {
|
|
231
|
-
const hasDefault = stmt.children.some((n) => n.text === "default");
|
|
232
|
-
if (hasDefault && !defaultName) {
|
|
233
|
-
defaultName = "default";
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
const reExports = [];
|
|
237
|
-
for (const m of exportMatches) {
|
|
238
|
-
const caps = captureMap(m);
|
|
239
|
-
if (caps["reexport.name"] && caps["reexport.source"]) {
|
|
240
|
-
reExports.push({ name: caps["reexport.name"], from: caps["reexport.source"] });
|
|
241
|
-
}
|
|
242
|
-
if (caps["reexport.wildcard.source"]) {
|
|
243
|
-
reExports.push({ name: "*", from: caps["reexport.wildcard.source"], isWildcard: true });
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
const jsxElements = /* @__PURE__ */ new Set();
|
|
247
|
-
const jsxQuery = getQuery("jsx-elements");
|
|
248
|
-
const jsxCaptures = jsxQuery.captures(root);
|
|
249
|
-
for (const c of jsxCaptures) {
|
|
250
|
-
if (c.name === "jsx.tag") {
|
|
251
|
-
if (/^[A-Z]/.test(c.node.text)) {
|
|
252
|
-
jsxElements.add(c.node.text);
|
|
253
|
-
}
|
|
254
|
-
} else if (c.name === "jsx.member_tag") {
|
|
255
|
-
const rootName = c.node.text.split(".")[0];
|
|
256
|
-
if (rootName && /^[A-Z]/.test(rootName)) {
|
|
257
|
-
jsxElements.add(rootName);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
const navigations = [];
|
|
262
|
-
const navQuery = getQuery("navigations");
|
|
263
|
-
const navMatches = navQuery.matches(root);
|
|
264
|
-
for (const m of navMatches) {
|
|
265
|
-
const caps = captureMap(m);
|
|
266
|
-
if (caps["nav.target.literal"] && caps["nav.method"]) {
|
|
267
|
-
navigations.push({
|
|
268
|
-
kind: caps["nav.method"] === "push" ? "router-push" : "router-replace",
|
|
269
|
-
target: caps["nav.target.literal"],
|
|
270
|
-
isTemplate: false
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
if (caps["nav.target.template"] && caps["nav.method.template"]) {
|
|
274
|
-
navigations.push({
|
|
275
|
-
kind: caps["nav.method.template"] === "push" ? "router-push" : "router-replace",
|
|
276
|
-
target: caps["nav.target.template"],
|
|
277
|
-
isTemplate: true
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
const linkLiteral = caps["nav.link.literal"] || caps["nav.link.literal.self"];
|
|
281
|
-
if (linkLiteral) {
|
|
282
|
-
navigations.push({ kind: "link-href", target: linkLiteral, isTemplate: false });
|
|
283
|
-
}
|
|
284
|
-
const linkTemplate = caps["nav.link.template"] || caps["nav.link.template.self"];
|
|
285
|
-
if (linkTemplate) {
|
|
286
|
-
navigations.push({ kind: "link-href", target: linkTemplate, isTemplate: true });
|
|
287
|
-
}
|
|
288
|
-
if (caps["nav.window.literal"]) {
|
|
289
|
-
navigations.push({ kind: "window-location", target: caps["nav.window.literal"], isTemplate: false });
|
|
290
|
-
}
|
|
291
|
-
if (caps["nav.window.assign.target"]) {
|
|
292
|
-
navigations.push({ kind: "window-location", target: caps["nav.window.assign.target"], isTemplate: false });
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
const fetchCalls = [];
|
|
296
|
-
const fetchQuery = getQuery("fetch-calls");
|
|
297
|
-
const fetchMatches = fetchQuery.matches(root);
|
|
298
|
-
for (const m of fetchMatches) {
|
|
299
|
-
const caps = captureMap(m);
|
|
300
|
-
if (caps["fetch.url.literal"] && looksLikeUrl(caps["fetch.url.literal"])) {
|
|
301
|
-
fetchCalls.push({ url: caps["fetch.url.literal"], isTemplate: false, kind: "fetch" });
|
|
302
|
-
}
|
|
303
|
-
if (caps["fetch.url.template"] && templateStartsWithSlash(caps["fetch.url.template"])) {
|
|
304
|
-
fetchCalls.push({ url: caps["fetch.url.template"], isTemplate: true, kind: "fetch" });
|
|
305
|
-
}
|
|
306
|
-
const clientUrl = caps["fetch.client.url.literal"] || caps["fetch.await.url.literal"];
|
|
307
|
-
const clientMethod = caps["fetch.method"] || caps["fetch.await.method"];
|
|
308
|
-
if (clientUrl && clientMethod && looksLikeUrl(clientUrl)) {
|
|
309
|
-
fetchCalls.push({
|
|
310
|
-
method: clientMethod.toUpperCase(),
|
|
311
|
-
url: clientUrl,
|
|
312
|
-
isTemplate: false,
|
|
313
|
-
kind: "client-method"
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
const clientUrlTpl = caps["fetch.client.url.template"] || caps["fetch.await.url.template"];
|
|
317
|
-
const clientMethodTpl = caps["fetch.method.template"] || caps["fetch.await.method.template"];
|
|
318
|
-
if (clientUrlTpl && clientMethodTpl && templateStartsWithSlash(clientUrlTpl)) {
|
|
319
|
-
fetchCalls.push({
|
|
320
|
-
method: clientMethodTpl.toUpperCase(),
|
|
321
|
-
url: clientUrlTpl,
|
|
322
|
-
isTemplate: true,
|
|
323
|
-
kind: "client-method"
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
const name = defaultName ?? firstValueExport ?? firstTypeExport ?? "";
|
|
328
|
-
return { name, exports: exportsOrdered, imports, reExports, jsxElements, navigations, fetchCalls };
|
|
329
|
-
}
|
|
330
|
-
function extractDbCallsTS(absPath) {
|
|
331
|
-
const tree = parseSource(absPath);
|
|
332
|
-
const root = tree.rootNode;
|
|
333
|
-
const dbQuery = getQuery("db-calls");
|
|
334
|
-
const matches = dbQuery.matches(root);
|
|
335
|
-
const dbIdentifiers = /* @__PURE__ */ new Set();
|
|
336
|
-
for (const stmt of childrenOfType(root, "import_statement")) {
|
|
337
|
-
const sourceNode = childOfType(stmt, "string");
|
|
338
|
-
const frag = sourceNode ? childOfType(sourceNode, "string_fragment") : void 0;
|
|
339
|
-
if (!frag) continue;
|
|
340
|
-
const spec = frag.text;
|
|
341
|
-
if (spec.includes("prisma") || spec.includes("/db") || spec === "@prisma/client") {
|
|
342
|
-
const clause = childOfType(stmt, "import_clause");
|
|
343
|
-
if (clause) {
|
|
344
|
-
const defaultId = childOfType(clause, "identifier");
|
|
345
|
-
if (defaultId) dbIdentifiers.add(defaultId.text);
|
|
346
|
-
const named = childOfType(clause, "named_imports");
|
|
347
|
-
if (named) {
|
|
348
|
-
for (const specNode of childrenOfType(named, "import_specifier")) {
|
|
349
|
-
const ids = childrenOfType(specNode, "identifier");
|
|
350
|
-
const importedName = ids[ids.length - 1];
|
|
351
|
-
if (importedName) dbIdentifiers.add(importedName.text);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
if (dbIdentifiers.size === 0) {
|
|
358
|
-
for (const id of getFallbackDbIdentifiers()) dbIdentifiers.add(id);
|
|
359
|
-
} else {
|
|
360
|
-
for (const id of extraDbIdentifiers) dbIdentifiers.add(id);
|
|
361
|
-
}
|
|
362
|
-
const calls = [];
|
|
363
|
-
const seen = /* @__PURE__ */ new Set();
|
|
364
|
-
for (const m of matches) {
|
|
365
|
-
const caps = captureMap(m);
|
|
366
|
-
const identifier = caps["db.identifier"];
|
|
367
|
-
const model = caps["db.model"];
|
|
368
|
-
const method = caps["db.method"];
|
|
369
|
-
if (!identifier || !model || !method) continue;
|
|
370
|
-
if (!dbIdentifiers.has(identifier)) continue;
|
|
371
|
-
const key = `${model}.${method}`;
|
|
372
|
-
if (seen.has(key)) continue;
|
|
373
|
-
seen.add(key);
|
|
374
|
-
calls.push({ model, method, isMutation: getMutationMethods().has(method) });
|
|
375
|
-
}
|
|
376
|
-
return calls;
|
|
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
|
-
}
|
|
396
|
-
function extractAuthWrappersTS(absPath) {
|
|
397
|
-
const tree = parseSource(absPath);
|
|
398
|
-
const root = tree.rootNode;
|
|
399
|
-
const wrapperQuery = getQuery("wrappers");
|
|
400
|
-
const matches = wrapperQuery.matches(root);
|
|
401
|
-
const wrappers = /* @__PURE__ */ new Set();
|
|
402
|
-
for (const m of matches) {
|
|
403
|
-
const caps = captureMap(m);
|
|
404
|
-
if (caps["wrapper.fn_name"]) {
|
|
405
|
-
wrappers.add(caps["wrapper.fn_name"]);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
return wrappers;
|
|
409
|
-
}
|
|
410
|
-
function trunc(s, max = 120) {
|
|
411
|
-
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
412
|
-
}
|
|
413
|
-
function extractDeep(absPath) {
|
|
414
|
-
const tree = parseSource(absPath);
|
|
415
|
-
const root = tree.rootNode;
|
|
416
|
-
const elements = [];
|
|
417
|
-
const elQuery = getQuery("deep/jsx-semantic");
|
|
418
|
-
const elMatches = elQuery.matches(root);
|
|
419
|
-
const elementMap = /* @__PURE__ */ new Map();
|
|
420
|
-
for (const m of elMatches) {
|
|
421
|
-
const tag = m.captures.find((c) => c.name === "el.tag")?.node;
|
|
422
|
-
if (!tag || !/^[A-Z]/.test(tag.text)) continue;
|
|
423
|
-
const elNode = m.captures.find((c) => c.name === "el.self" || c.name === "el.open")?.node;
|
|
424
|
-
const key = elNode ? `${elNode.startPosition.row}:${elNode.startPosition.column}` : `${tag.startPosition.row}:${tag.startPosition.column}`;
|
|
425
|
-
if (!elementMap.has(key)) {
|
|
426
|
-
elementMap.set(key, { tag: tag.text, props: {}, nodeKey: key });
|
|
427
|
-
}
|
|
428
|
-
const entry = elementMap.get(key);
|
|
429
|
-
const propName = m.captures.find((c) => c.name === "el.prop.name")?.node.text;
|
|
430
|
-
const propVal = m.captures.find((c) => c.name === "el.prop.value.str")?.node.text;
|
|
431
|
-
const propExpr = m.captures.find((c) => c.name === "el.prop.value.expr")?.node.text;
|
|
432
|
-
if (propName) {
|
|
433
|
-
entry.props[propName] = propVal ?? (propExpr ? trunc(propExpr, 60) : "true");
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
for (const m of elMatches) {
|
|
437
|
-
const textTag = m.captures.find((c) => c.name === "el.text.tag")?.node;
|
|
438
|
-
const textContent = m.captures.find((c) => c.name === "el.text.content")?.node;
|
|
439
|
-
if (textTag && textContent && /^[A-Z]/.test(textTag.text)) {
|
|
440
|
-
const key = `${textTag.startPosition.row}:${textTag.startPosition.column}`;
|
|
441
|
-
if (!elementMap.has(key)) {
|
|
442
|
-
elementMap.set(key, { tag: textTag.text, props: {}, nodeKey: key });
|
|
443
|
-
}
|
|
444
|
-
const entry = elementMap.get(key);
|
|
445
|
-
const text = textContent.text.trim();
|
|
446
|
-
if (text) entry.props["_text"] = text;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
for (const entry of elementMap.values()) {
|
|
450
|
-
const el = { tag: entry.tag, props: entry.props };
|
|
451
|
-
const hasExpr = Object.values(entry.props).some((v) => v.includes("{") || v.includes("("));
|
|
452
|
-
if (hasExpr) el.dynamic = true;
|
|
453
|
-
if (entry.props["_text"]) {
|
|
454
|
-
el.text = entry.props["_text"];
|
|
455
|
-
delete el.props["_text"];
|
|
456
|
-
}
|
|
457
|
-
elements.push(el);
|
|
458
|
-
}
|
|
459
|
-
const stateVars = [];
|
|
460
|
-
const hookQuery = getQuery("deep/state-hooks");
|
|
461
|
-
const hookMatches = hookQuery.matches(root);
|
|
462
|
-
for (const m of hookMatches) {
|
|
463
|
-
const caps = captureMap(m);
|
|
464
|
-
if (caps["hook.var"] && caps["hook.setter"] && caps["hook.fn"]) {
|
|
465
|
-
stateVars.push({
|
|
466
|
-
name: caps["hook.var"],
|
|
467
|
-
setter: caps["hook.setter"],
|
|
468
|
-
hook: caps["hook.fn"],
|
|
469
|
-
init: trunc(caps["hook.init"] ?? "", 80)
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
if (caps["reducer.var"] && caps["reducer.dispatch"]) {
|
|
473
|
-
stateVars.push({
|
|
474
|
-
name: caps["reducer.var"],
|
|
475
|
-
setter: caps["reducer.dispatch"],
|
|
476
|
-
hook: "useReducer",
|
|
477
|
-
init: trunc(caps["reducer.init"] ?? "", 80)
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
const conditions = [];
|
|
482
|
-
const condQuery = getQuery("deep/conditions");
|
|
483
|
-
const condMatches = condQuery.matches(root);
|
|
484
|
-
for (const m of condMatches) {
|
|
485
|
-
const caps = captureMap(m);
|
|
486
|
-
if (caps["cond.test"]) {
|
|
487
|
-
conditions.push({
|
|
488
|
-
kind: "if",
|
|
489
|
-
test: trunc(caps["cond.test"], 100),
|
|
490
|
-
consequence: caps["cond.consequence"] ? trunc(caps["cond.consequence"], 80) : void 0
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
if (caps["ternary.test"]) {
|
|
494
|
-
conditions.push({
|
|
495
|
-
kind: "ternary",
|
|
496
|
-
test: trunc(caps["ternary.test"], 100)
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
const variables = [];
|
|
501
|
-
const varQuery = getQuery("deep/variables");
|
|
502
|
-
const varMatches = varQuery.matches(root);
|
|
503
|
-
for (const m of varMatches) {
|
|
504
|
-
const caps = captureMap(m);
|
|
505
|
-
const declNode = m.captures.find((c) => c.name === "var.decl" || c.name === "var.destructured" || c.name === "var.array")?.node;
|
|
506
|
-
const kind = declNode?.children.find((n) => n.type === "const" || n.type === "let" || n.type === "var")?.type ?? "const";
|
|
507
|
-
if (caps["var.name"] && caps["var.init"]) {
|
|
508
|
-
variables.push({
|
|
509
|
-
name: caps["var.name"],
|
|
510
|
-
kind,
|
|
511
|
-
init: trunc(caps["var.init"], 100)
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
if (caps["var.destructured.obj"]) {
|
|
515
|
-
variables.push({
|
|
516
|
-
name: trunc(caps["var.destructured.obj"], 60),
|
|
517
|
-
kind,
|
|
518
|
-
init: trunc(caps["var.destructured.init"], 100)
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
if (caps["var.array.pattern"]) {
|
|
522
|
-
variables.push({
|
|
523
|
-
name: trunc(caps["var.array.pattern"], 60),
|
|
524
|
-
kind,
|
|
525
|
-
init: trunc(caps["var.array.init"], 100)
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
const responses = [];
|
|
530
|
-
const respQuery = getQuery("deep/responses");
|
|
531
|
-
const respMatches = respQuery.matches(root);
|
|
532
|
-
const explicitBodies = /* @__PURE__ */ new Set();
|
|
533
|
-
for (const m of respMatches) {
|
|
534
|
-
const caps = captureMap(m);
|
|
535
|
-
if (caps["resp.status"] && caps["resp.body"]) {
|
|
536
|
-
explicitBodies.add(trunc(caps["resp.body"], 80));
|
|
537
|
-
responses.push({
|
|
538
|
-
status: caps["resp.status"],
|
|
539
|
-
body: trunc(caps["resp.body"], 80)
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
for (const m of respMatches) {
|
|
544
|
-
const caps = captureMap(m);
|
|
545
|
-
if (caps["resp.body.default"] && !caps["resp.status"]) {
|
|
546
|
-
const bodyText = trunc(caps["resp.body.default"], 80);
|
|
547
|
-
if (!explicitBodies.has(bodyText)) {
|
|
548
|
-
responses.push({ status: "200", body: bodyText });
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
const params = [];
|
|
553
|
-
const paramQuery = getQuery("deep/request-params");
|
|
554
|
-
const paramMatches = paramQuery.matches(root);
|
|
555
|
-
for (const m of paramMatches) {
|
|
556
|
-
const caps = captureMap(m);
|
|
557
|
-
if (caps["param.name"]) {
|
|
558
|
-
params.push({ name: caps["param.name"], source: "body-field" });
|
|
559
|
-
}
|
|
560
|
-
if (caps["param.body"]) {
|
|
561
|
-
params.push({ name: caps["param.body"], source: "body" });
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
return { elements, stateVars, conditions, variables, responses, params };
|
|
565
|
-
}
|
|
566
|
-
var import_node_fs3, import_node_path3, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods;
|
|
567
|
-
var init_ts_extractor = __esm({
|
|
568
|
-
"src/server/graph/core/ts-extractor.ts"() {
|
|
569
|
-
"use strict";
|
|
570
|
-
import_node_fs3 = require("node:fs");
|
|
571
|
-
import_node_path3 = require("node:path");
|
|
572
|
-
initialized = false;
|
|
573
|
-
queriesDir = (() => {
|
|
574
|
-
const srcPath = (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "..", "queries");
|
|
575
|
-
if (require("fs").existsSync(srcPath)) return srcPath;
|
|
576
|
-
return (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "graph", "queries");
|
|
577
|
-
})();
|
|
578
|
-
queryCache = /* @__PURE__ */ new Map();
|
|
579
|
-
PRISMA_MUTATION_METHODS_BUILTIN = [
|
|
580
|
-
"create",
|
|
581
|
-
"createMany",
|
|
582
|
-
"createManyAndReturn",
|
|
583
|
-
"update",
|
|
584
|
-
"updateMany",
|
|
585
|
-
"updateManyAndReturn",
|
|
586
|
-
"upsert",
|
|
587
|
-
"delete",
|
|
588
|
-
"deleteMany"
|
|
589
|
-
];
|
|
590
|
-
DB_IDENTIFIERS_FALLBACK = ["db", "prisma"];
|
|
591
|
-
extraDbIdentifiers = [];
|
|
592
|
-
extraMutationMethods = [];
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
// src/server/chart-serve.ts
|
|
597
|
-
var chart_serve_exports = {};
|
|
598
|
-
__export(chart_serve_exports, {
|
|
599
|
-
runServeCli: () => runServeCli,
|
|
600
|
-
startChartServer: () => startChartServer
|
|
601
|
-
});
|
|
602
|
-
module.exports = __toCommonJS(chart_serve_exports);
|
|
603
|
-
var import_node_http = __toESM(require("node:http"));
|
|
604
|
-
var import_node_fs17 = __toESM(require("node:fs"));
|
|
605
|
-
var import_node_path19 = __toESM(require("node:path"));
|
|
606
|
-
|
|
607
|
-
// src/server/graph/index.ts
|
|
608
|
-
var import_node_fs14 = require("node:fs");
|
|
609
|
-
var import_node_path16 = require("node:path");
|
|
610
|
-
|
|
611
|
-
// src/server/graph/core/graph-builder.ts
|
|
612
|
-
var import_node_fs11 = require("node:fs");
|
|
613
|
-
var import_node_path12 = require("node:path");
|
|
614
|
-
init_config();
|
|
615
|
-
|
|
616
|
-
// src/server/graph/core/parser-registry.ts
|
|
617
|
-
var import_node_path11 = require("node:path");
|
|
618
|
-
|
|
619
|
-
// src/server/graph/parsers/ts/typescript-project.ts
|
|
620
|
-
var import_node_fs4 = require("node:fs");
|
|
621
|
-
var import_node_path4 = require("node:path");
|
|
622
|
-
init_config();
|
|
623
|
-
|
|
624
|
-
// src/server/graph/core/resolve-paths.ts
|
|
625
|
-
var import_node_fs2 = require("node:fs");
|
|
626
|
-
var import_node_path2 = require("node:path");
|
|
627
|
-
function detectDbDir(rootDir, config) {
|
|
628
|
-
if (config.paths?.dbDir) return (0, import_node_path2.join)(rootDir, config.paths.dbDir);
|
|
629
|
-
const prismaDir = (0, import_node_path2.join)(rootDir, "prisma");
|
|
630
|
-
if ((0, import_node_fs2.existsSync)(prismaDir)) return prismaDir;
|
|
631
|
-
return null;
|
|
632
|
-
}
|
|
633
|
-
function resolveProjectPaths(rootDir, config) {
|
|
634
|
-
const dbDir = detectDbDir(rootDir, config);
|
|
635
|
-
if (config.paths?.appDir) {
|
|
636
|
-
const appDir = (0, import_node_path2.join)(rootDir, config.paths.appDir);
|
|
637
|
-
const srcDir = config.paths.srcDir ? (0, import_node_path2.join)(rootDir, config.paths.srcDir) : (0, import_node_path2.dirname)(appDir);
|
|
638
|
-
return { srcDir, appDir, apiDir: (0, import_node_path2.join)(appDir, "api"), dbDir };
|
|
639
|
-
}
|
|
640
|
-
const srcApp = (0, import_node_path2.join)(rootDir, "src", "app");
|
|
641
|
-
if ((0, import_node_fs2.existsSync)(srcApp)) {
|
|
642
|
-
return { srcDir: (0, import_node_path2.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path2.join)(srcApp, "api"), dbDir };
|
|
643
|
-
}
|
|
644
|
-
const rootApp = (0, import_node_path2.join)(rootDir, "app");
|
|
645
|
-
if ((0, import_node_fs2.existsSync)(rootApp)) {
|
|
646
|
-
return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path2.join)(rootApp, "api"), dbDir };
|
|
647
|
-
}
|
|
648
|
-
return null;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// src/server/graph/parsers/ts/typescript-project.ts
|
|
652
|
-
init_ts_extractor();
|
|
653
|
-
var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
654
|
-
var CLASSIFICATION_TO_LAYER = {
|
|
655
|
-
endpoint: "api",
|
|
656
|
-
page: "ui",
|
|
657
|
-
layout: "ui",
|
|
658
|
-
component: "ui",
|
|
659
|
-
ui: "ui",
|
|
660
|
-
hook: "ui",
|
|
661
|
-
context: "ui",
|
|
662
|
-
config: "ui",
|
|
663
|
-
lib: "ui",
|
|
664
|
-
"mcp-tool": "ui",
|
|
665
|
-
external: "ui"
|
|
666
|
-
};
|
|
667
|
-
function walk(dir, exts) {
|
|
668
|
-
const results = [];
|
|
669
|
-
if (!(0, import_node_fs4.existsSync)(dir)) return results;
|
|
670
|
-
for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
671
|
-
const full = (0, import_node_path4.join)(dir, entry.name);
|
|
672
|
-
if (entry.isDirectory()) {
|
|
673
|
-
results.push(...walk(full, exts));
|
|
674
|
-
} else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
|
|
675
|
-
results.push(full);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
return results;
|
|
679
|
-
}
|
|
680
|
-
function walkWithIgnore(dir, exts, ignoreDirs) {
|
|
681
|
-
const results = [];
|
|
682
|
-
if (!(0, import_node_fs4.existsSync)(dir)) return results;
|
|
683
|
-
for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
684
|
-
if (entry.isDirectory()) {
|
|
685
|
-
if (ignoreDirs.has(entry.name)) continue;
|
|
686
|
-
results.push(...walkWithIgnore((0, import_node_path4.join)(dir, entry.name), exts, ignoreDirs));
|
|
687
|
-
} else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
|
|
688
|
-
results.push((0, import_node_path4.join)(dir, entry.name));
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
return results;
|
|
692
|
-
}
|
|
693
|
-
function toNodeId(srcDir, absPath) {
|
|
694
|
-
return (0, import_node_path4.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
695
|
-
}
|
|
696
|
-
function resolveImport(srcDir, specifier) {
|
|
697
|
-
if (!specifier.startsWith("@/")) return null;
|
|
698
|
-
const rel = specifier.slice(2);
|
|
699
|
-
const base = (0, import_node_path4.join)(srcDir, rel);
|
|
700
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
|
|
701
|
-
if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
|
|
702
|
-
}
|
|
703
|
-
return null;
|
|
704
|
-
}
|
|
705
|
-
function resolveRelativeImport(fromFile, specifier) {
|
|
706
|
-
const base = (0, import_node_path4.join)((0, import_node_path4.dirname)(fromFile), specifier);
|
|
707
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
|
|
708
|
-
if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
|
|
709
|
-
}
|
|
710
|
-
return null;
|
|
711
|
-
}
|
|
712
|
-
function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
|
|
713
|
-
const cached = memo.get(barrelAbsPath);
|
|
714
|
-
if (cached) return cached;
|
|
715
|
-
if (visiting.has(barrelAbsPath)) return /* @__PURE__ */ new Map();
|
|
716
|
-
visiting.add(barrelAbsPath);
|
|
717
|
-
const parsed = parsedByPath.get(barrelAbsPath);
|
|
718
|
-
const map = /* @__PURE__ */ new Map();
|
|
719
|
-
if (!parsed) {
|
|
720
|
-
visiting.delete(barrelAbsPath);
|
|
721
|
-
memo.set(barrelAbsPath, map);
|
|
722
|
-
return map;
|
|
723
|
-
}
|
|
724
|
-
for (const re of parsed.reExports) {
|
|
725
|
-
if (!re.from.startsWith(".")) continue;
|
|
726
|
-
const resolved = resolveRelativeImport(barrelAbsPath, re.from);
|
|
727
|
-
if (!resolved) continue;
|
|
728
|
-
if (re.isWildcard) {
|
|
729
|
-
const targetBn = (0, import_node_path4.basename)(resolved);
|
|
730
|
-
const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
|
|
731
|
-
if (targetIsBarrel) {
|
|
732
|
-
const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
|
|
733
|
-
for (const [name, target] of nested) {
|
|
734
|
-
if (!map.has(name)) map.set(name, target);
|
|
735
|
-
}
|
|
736
|
-
} else {
|
|
737
|
-
const targetParsed = parsedByPath.get(resolved);
|
|
738
|
-
if (targetParsed) {
|
|
739
|
-
for (const exp of targetParsed.exports) {
|
|
740
|
-
if (!map.has(exp)) map.set(exp, resolved);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
} else {
|
|
745
|
-
if (!map.has(re.name)) map.set(re.name, resolved);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
visiting.delete(barrelAbsPath);
|
|
749
|
-
memo.set(barrelAbsPath, map);
|
|
750
|
-
return map;
|
|
751
|
-
}
|
|
752
|
-
function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
753
|
-
const barrels = /* @__PURE__ */ new Map();
|
|
754
|
-
const memo = /* @__PURE__ */ new Map();
|
|
755
|
-
for (const [absPath, parsed] of parsedByPath) {
|
|
756
|
-
const bn = (0, import_node_path4.basename)(absPath);
|
|
757
|
-
if (bn !== "index.ts" && bn !== "index.tsx") continue;
|
|
758
|
-
if (parsed.reExports.length === 0) continue;
|
|
759
|
-
const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
|
|
760
|
-
if (map.size > 0) {
|
|
761
|
-
const barrelId = (0, import_node_path4.relative)(srcDir, (0, import_node_path4.dirname)(absPath)).replace(/\\/g, "/");
|
|
762
|
-
barrels.set(barrelId, map);
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
return barrels;
|
|
766
|
-
}
|
|
767
|
-
function classifyType(absPath, id) {
|
|
768
|
-
const contentType = classifyFile(absPath);
|
|
769
|
-
if (contentType === "lib" && id.startsWith("server/mcp/")) return "mcp-tool";
|
|
770
|
-
return contentType;
|
|
771
|
-
}
|
|
772
|
-
function extractRoute(id) {
|
|
773
|
-
if (!id.endsWith("/page.tsx")) return null;
|
|
774
|
-
let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
|
|
775
|
-
route = route.replace(/\/\([^)]+\)/g, "");
|
|
776
|
-
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
777
|
-
route = route.replace(/\/+/g, "/");
|
|
778
|
-
if (!route.startsWith("/")) route = "/" + route;
|
|
779
|
-
return route || "/";
|
|
780
|
-
}
|
|
781
|
-
function nameFromFilename(absPath) {
|
|
782
|
-
return (0, import_node_path4.basename)(absPath, (0, import_node_path4.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
|
|
783
|
-
}
|
|
784
|
-
function filePathToApiRoute(apiDir, absPath) {
|
|
785
|
-
let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
786
|
-
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
787
|
-
route = route.replace(/\/+/g, "/");
|
|
788
|
-
if (route === "/") return "/api";
|
|
789
|
-
return "/api" + route;
|
|
790
|
-
}
|
|
791
|
-
function camelToPascal(s) {
|
|
792
|
-
if (!s) return s;
|
|
793
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
794
|
-
}
|
|
795
|
-
function resolveTemplateLiteralRoute(template, routeToNodeId) {
|
|
796
|
-
const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
797
|
-
const cleaned = expr.trim();
|
|
798
|
-
if (cleaned.includes(".")) {
|
|
799
|
-
const parts = cleaned.split(".");
|
|
800
|
-
const last = parts[parts.length - 1];
|
|
801
|
-
const secondLast = parts.length > 1 ? parts[parts.length - 2] : "";
|
|
802
|
-
if (last === "slug" && secondLast === "project") return ":projectSlug";
|
|
803
|
-
if (last === "slug") return ":projectSlug";
|
|
804
|
-
if (last === "id" && /cred/i.test(secondLast)) return ":credentialId";
|
|
805
|
-
if (last === "id" && /run/i.test(secondLast)) return ":runId";
|
|
806
|
-
if (last === "sha") return ":commitSha";
|
|
807
|
-
if (last === "id") return ":id";
|
|
808
|
-
return `:${last}`;
|
|
809
|
-
}
|
|
810
|
-
if (/orgSlug/i.test(cleaned)) return ":orgSlug";
|
|
811
|
-
if (/projectSlug/i.test(cleaned)) return ":projectSlug";
|
|
812
|
-
if (/runId/i.test(cleaned)) return ":runId";
|
|
813
|
-
if (/credentialId/i.test(cleaned)) return ":credentialId";
|
|
814
|
-
if (/commitSha/i.test(cleaned)) return ":commitSha";
|
|
815
|
-
if (/token/i.test(cleaned)) return ":token";
|
|
816
|
-
return `:${cleaned}`;
|
|
817
|
-
});
|
|
818
|
-
const normalized = parameterized.replace(/\/+/g, "/").replace(/\/$/, "") || "/";
|
|
819
|
-
if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
|
|
820
|
-
let bestScore = -1;
|
|
821
|
-
let bestId = null;
|
|
822
|
-
for (const [route, nodeId] of routeToNodeId) {
|
|
823
|
-
const score = routeMatchScore(normalized, route);
|
|
824
|
-
if (score > bestScore) {
|
|
825
|
-
bestScore = score;
|
|
826
|
-
bestId = nodeId;
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
return bestScore > 0 ? bestId : null;
|
|
830
|
-
}
|
|
831
|
-
function routeMatchScore(candidate, known) {
|
|
832
|
-
const segsA = candidate.split("/");
|
|
833
|
-
const segsB = known.split("/");
|
|
834
|
-
if (segsA.length !== segsB.length) return -1;
|
|
835
|
-
let score = 0;
|
|
836
|
-
for (let i = 0; i < segsA.length; i++) {
|
|
837
|
-
const a = segsA[i], b = segsB[i];
|
|
838
|
-
if (a === b) {
|
|
839
|
-
score += 3;
|
|
840
|
-
continue;
|
|
841
|
-
}
|
|
842
|
-
if (a.startsWith(":") && b.startsWith(":")) {
|
|
843
|
-
score += 2;
|
|
844
|
-
continue;
|
|
845
|
-
}
|
|
846
|
-
if (a.startsWith(":") || b.startsWith(":")) {
|
|
847
|
-
score += 0;
|
|
848
|
-
continue;
|
|
849
|
-
}
|
|
850
|
-
return -1;
|
|
851
|
-
}
|
|
852
|
-
return score;
|
|
853
|
-
}
|
|
854
|
-
function templateToRoute(template) {
|
|
855
|
-
return template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
856
|
-
const cleaned = expr.trim();
|
|
857
|
-
if (cleaned.includes(".")) {
|
|
858
|
-
const parts = cleaned.split(".");
|
|
859
|
-
const last = parts[parts.length - 1];
|
|
860
|
-
const secondLast = parts.length > 1 ? parts[parts.length - 2] : "";
|
|
861
|
-
if (last === "slug" && /project/i.test(secondLast)) return ":projectSlug";
|
|
862
|
-
if (last === "slug") return ":slug";
|
|
863
|
-
if (last === "sha") return ":commitSha";
|
|
864
|
-
return `:${last}`;
|
|
865
|
-
}
|
|
866
|
-
return `:${cleaned}`;
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
function matchRouteToPage(route, routeToNodeId) {
|
|
870
|
-
const normalized = route.replace(/\/$/, "") || "/";
|
|
871
|
-
if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
|
|
872
|
-
return null;
|
|
873
|
-
}
|
|
874
|
-
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps, routeToNodeId) {
|
|
875
|
-
const edges = [];
|
|
876
|
-
const flagged = [];
|
|
877
|
-
const seen = /* @__PURE__ */ new Set();
|
|
878
|
-
function addEdge(target, type, label) {
|
|
879
|
-
const key = `${sourceId}\u2192${target}\u2192${type}`;
|
|
880
|
-
if (seen.has(key)) return;
|
|
881
|
-
seen.add(key);
|
|
882
|
-
const edge = { source: sourceId, target, type };
|
|
883
|
-
if (label) edge.label = label;
|
|
884
|
-
edges.push(edge);
|
|
885
|
-
}
|
|
886
|
-
function edgeTypeFor(isTypeOnlyImport, importedNames) {
|
|
887
|
-
if (isTypeOnlyImport) return "imports";
|
|
888
|
-
const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
|
|
889
|
-
if (anyRendered) return "renders";
|
|
890
|
-
return "imports";
|
|
891
|
-
}
|
|
892
|
-
for (const imp of parsed.imports) {
|
|
893
|
-
const { names, specifier, isTypeOnly, typeNames } = imp;
|
|
894
|
-
if (specifier.startsWith("@/")) {
|
|
895
|
-
const relToSrc = specifier.slice(2);
|
|
896
|
-
const barrelMap = barrelMaps.get(relToSrc);
|
|
897
|
-
if (barrelMap && names.length > 0) {
|
|
898
|
-
const byTarget = /* @__PURE__ */ new Map();
|
|
899
|
-
for (const name of names) {
|
|
900
|
-
const targetAbs = barrelMap.get(name);
|
|
901
|
-
if (targetAbs) {
|
|
902
|
-
const targetId = toNodeId(srcDir, targetAbs);
|
|
903
|
-
if (nodeIdSet.has(targetId)) {
|
|
904
|
-
if (!byTarget.has(targetId)) byTarget.set(targetId, []);
|
|
905
|
-
byTarget.get(targetId).push(name);
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
for (const [targetId, targetNames] of byTarget) {
|
|
910
|
-
const allType = isTypeOnly || targetNames.every((n) => typeNames.has(n));
|
|
911
|
-
addEdge(targetId, edgeTypeFor(allType, targetNames));
|
|
912
|
-
}
|
|
913
|
-
} else {
|
|
914
|
-
const resolved = resolveImport(srcDir, specifier);
|
|
915
|
-
if (resolved) {
|
|
916
|
-
const targetId = toNodeId(srcDir, resolved);
|
|
917
|
-
if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
|
|
918
|
-
addEdge(targetId, edgeTypeFor(isTypeOnly, names));
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
} else if (specifier.startsWith(".")) {
|
|
923
|
-
const resolved = resolveRelativeImport(absPath, specifier);
|
|
924
|
-
if (resolved) {
|
|
925
|
-
const targetId = toNodeId(srcDir, resolved);
|
|
926
|
-
if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
|
|
927
|
-
addEdge(targetId, edgeTypeFor(isTypeOnly, names));
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
for (const nav of parsed.navigations) {
|
|
933
|
-
if (nav.kind === "window-location") {
|
|
934
|
-
flagged.push({
|
|
935
|
-
source: sourceId,
|
|
936
|
-
target: "EXTERNAL",
|
|
937
|
-
type: "navigates",
|
|
938
|
-
label: `window.location to ${nav.target}`,
|
|
939
|
-
confidence: "high"
|
|
940
|
-
});
|
|
941
|
-
continue;
|
|
942
|
-
}
|
|
943
|
-
if (!nav.isTemplate) {
|
|
944
|
-
const targetId = matchRouteToPage(nav.target, routeToNodeId);
|
|
945
|
-
if (targetId && targetId !== sourceId) {
|
|
946
|
-
const label = nav.kind === "link-href" ? `Link to ${nav.target}` : `router.${nav.kind === "router-push" ? "push" : "replace"}('${nav.target}')`;
|
|
947
|
-
addEdge(targetId, "navigates", label);
|
|
948
|
-
}
|
|
949
|
-
} else {
|
|
950
|
-
const template = nav.target.replace(/^`|`$/g, "");
|
|
951
|
-
if (!template.includes("${")) continue;
|
|
952
|
-
const targetId = resolveTemplateLiteralRoute(template, routeToNodeId);
|
|
953
|
-
if (targetId && targetId !== sourceId) {
|
|
954
|
-
const label = nav.kind === "link-href" ? `Link to ${templateToRoute(template)}` : `router.${nav.kind === "router-push" ? "push" : "replace"}('${templateToRoute(template)}')`;
|
|
955
|
-
addEdge(targetId, "navigates", label);
|
|
956
|
-
} else {
|
|
957
|
-
flagged.push({
|
|
958
|
-
source: sourceId,
|
|
959
|
-
target: "DYNAMIC",
|
|
960
|
-
type: "navigates",
|
|
961
|
-
label: nav.kind === "link-href" ? `Link with template: \`${template}\`` : `router.${nav.kind === "router-push" ? "push" : "replace"} with template: \`${template}\``,
|
|
962
|
-
confidence: "medium"
|
|
963
|
-
});
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
return { edges, flagged };
|
|
968
|
-
}
|
|
969
|
-
function hasNextConfig(rootDir) {
|
|
970
|
-
return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.ts")) || (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.js")) || (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.mjs"));
|
|
971
|
-
}
|
|
972
|
-
function detect(rootDir) {
|
|
973
|
-
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
974
|
-
return paths !== null && hasNextConfig(rootDir);
|
|
975
|
-
}
|
|
976
|
-
function generate(rootDir) {
|
|
977
|
-
const config = loadConfig(rootDir);
|
|
978
|
-
const paths = resolveProjectPaths(rootDir, config);
|
|
979
|
-
const srcDir = paths.srcDir;
|
|
980
|
-
const apiDir = paths.apiDir;
|
|
981
|
-
const appFiles = walk(paths.appDir, [".tsx", ".ts"]);
|
|
982
|
-
const clientFiles = walk((0, import_node_path4.join)(srcDir, "client"), [".tsx", ".ts"]);
|
|
983
|
-
const serverFiles = walk((0, import_node_path4.join)(srcDir, "server"), [".ts", ".tsx"]);
|
|
984
|
-
const libFiles = walk((0, import_node_path4.join)(srcDir, "lib"), [".ts", ".tsx"]);
|
|
985
|
-
const configFiles = walk((0, import_node_path4.join)(srcDir, "config"), [".ts", ".tsx"]);
|
|
986
|
-
const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
|
|
987
|
-
const parsedByPath = /* @__PURE__ */ new Map();
|
|
988
|
-
for (const absPath of allDiscovered) {
|
|
989
|
-
parsedByPath.set(absPath, parseFileTS(absPath));
|
|
990
|
-
}
|
|
991
|
-
const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
|
|
992
|
-
const uiNodes = [];
|
|
993
|
-
const apiNodes = [];
|
|
994
|
-
const nodeIdSet = /* @__PURE__ */ new Set();
|
|
995
|
-
const routeToNodeId = /* @__PURE__ */ new Map();
|
|
996
|
-
const fileSet = allDiscovered.filter((f) => !(0, import_node_path4.basename)(f).startsWith("index."));
|
|
997
|
-
for (const absPath of fileSet) {
|
|
998
|
-
const id = toNodeId(srcDir, absPath);
|
|
999
|
-
const type = classifyType(absPath, id);
|
|
1000
|
-
if (type === "test" || type === "story") continue;
|
|
1001
|
-
const parsed = parsedByPath.get(absPath);
|
|
1002
|
-
const name = parsed.name || nameFromFilename(absPath);
|
|
1003
|
-
const layer = CLASSIFICATION_TO_LAYER[type] ?? "ui";
|
|
1004
|
-
nodeIdSet.add(id);
|
|
1005
|
-
if (layer === "api") {
|
|
1006
|
-
const methods = [];
|
|
1007
|
-
for (const exp of parsed.exports) {
|
|
1008
|
-
if (HTTP_METHODS.has(exp)) methods.push(exp);
|
|
1009
|
-
}
|
|
1010
|
-
const dbCalls = extractDbCallsTS(absPath);
|
|
1011
|
-
const authWrappers = extractAuthWrappersTS(absPath);
|
|
1012
|
-
const deep = extractDeep(absPath);
|
|
1013
|
-
const routePath = (0, import_node_fs4.existsSync)(apiDir) ? filePathToApiRoute(apiDir, absPath) : `/api/${id.replace(/\/route\.tsx?$/, "")}`;
|
|
1014
|
-
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
1015
|
-
const mutates = mutations.length > 0;
|
|
1016
|
-
const authStrategy = [...authWrappers];
|
|
1017
|
-
apiNodes.push({
|
|
1018
|
-
id,
|
|
1019
|
-
type: "endpoint",
|
|
1020
|
-
name: routePath,
|
|
1021
|
-
layer: "api",
|
|
1022
|
-
path: routePath,
|
|
1023
|
-
methods,
|
|
1024
|
-
handler: id,
|
|
1025
|
-
mutates,
|
|
1026
|
-
auth: authStrategy.length > 0 ? authStrategy : ["public"],
|
|
1027
|
-
db_models: [...new Set(dbCalls.map((c) => c.model))],
|
|
1028
|
-
db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
|
|
1029
|
-
conditions: deep.conditions,
|
|
1030
|
-
variables: deep.variables,
|
|
1031
|
-
responses: deep.responses,
|
|
1032
|
-
params: deep.params,
|
|
1033
|
-
_dbCalls: dbCalls
|
|
1034
|
-
// temp: used for cross-ref building below
|
|
1035
|
-
});
|
|
1036
|
-
} else {
|
|
1037
|
-
const route = extractRoute(id);
|
|
1038
|
-
const deep = extractDeep(absPath);
|
|
1039
|
-
uiNodes.push({
|
|
1040
|
-
id,
|
|
1041
|
-
type,
|
|
1042
|
-
name,
|
|
1043
|
-
layer: "ui",
|
|
1044
|
-
route,
|
|
1045
|
-
exports: parsed.exports,
|
|
1046
|
-
elements: deep.elements,
|
|
1047
|
-
stateVars: deep.stateVars,
|
|
1048
|
-
conditions: deep.conditions,
|
|
1049
|
-
variables: deep.variables
|
|
1050
|
-
});
|
|
1051
|
-
if (route) routeToNodeId.set(route, id);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
const uiEdges = [];
|
|
1055
|
-
const uiFlagged = [];
|
|
1056
|
-
for (const absPath of fileSet) {
|
|
1057
|
-
const id = toNodeId(srcDir, absPath);
|
|
1058
|
-
if (!nodeIdSet.has(id)) continue;
|
|
1059
|
-
const parsed = parsedByPath.get(absPath);
|
|
1060
|
-
const { edges, flagged } = extractEdges(
|
|
1061
|
-
srcDir,
|
|
1062
|
-
absPath,
|
|
1063
|
-
id,
|
|
1064
|
-
parsed,
|
|
1065
|
-
nodeIdSet,
|
|
1066
|
-
barrelMaps,
|
|
1067
|
-
routeToNodeId
|
|
1068
|
-
);
|
|
1069
|
-
uiEdges.push(...edges);
|
|
1070
|
-
uiFlagged.push(...flagged);
|
|
1071
|
-
}
|
|
1072
|
-
const fetchCallEntries = [];
|
|
1073
|
-
for (const absPath of fileSet) {
|
|
1074
|
-
const sourceId = toNodeId(srcDir, absPath);
|
|
1075
|
-
if (!nodeIdSet.has(sourceId)) continue;
|
|
1076
|
-
const parsed = parsedByPath.get(absPath);
|
|
1077
|
-
if (parsed.fetchCalls.length === 0) continue;
|
|
1078
|
-
fetchCallEntries.push({
|
|
1079
|
-
nodeId: sourceId,
|
|
1080
|
-
calls: parsed.fetchCalls.map((c) => ({
|
|
1081
|
-
url: c.url,
|
|
1082
|
-
method: c.method,
|
|
1083
|
-
isTemplate: c.isTemplate,
|
|
1084
|
-
isConcat: c.isConcat,
|
|
1085
|
-
kind: c.kind
|
|
1086
|
-
}))
|
|
1087
|
-
});
|
|
1088
|
-
}
|
|
1089
|
-
const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
|
|
1090
|
-
const IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
1091
|
-
"node_modules",
|
|
1092
|
-
".next",
|
|
1093
|
-
"dist",
|
|
1094
|
-
".launchsecure",
|
|
1095
|
-
".git",
|
|
1096
|
-
"src",
|
|
1097
|
-
"coverage",
|
|
1098
|
-
".turbo",
|
|
1099
|
-
"build",
|
|
1100
|
-
"out",
|
|
1101
|
-
".vercel"
|
|
1102
|
-
]);
|
|
1103
|
-
const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS);
|
|
1104
|
-
for (const absPath of externalCandidates) {
|
|
1105
|
-
const normalized = absPath.replace(/\\/g, "/");
|
|
1106
|
-
if (externalScanned.has(normalized)) continue;
|
|
1107
|
-
let parsed;
|
|
1108
|
-
try {
|
|
1109
|
-
parsed = parseFileTS(absPath);
|
|
1110
|
-
} catch {
|
|
1111
|
-
continue;
|
|
1112
|
-
}
|
|
1113
|
-
const externalId = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1114
|
-
const edgesFromThis = [];
|
|
1115
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1116
|
-
for (const imp of parsed.imports) {
|
|
1117
|
-
const { specifier, names } = imp;
|
|
1118
|
-
let resolved = null;
|
|
1119
|
-
if (specifier.startsWith("@/")) {
|
|
1120
|
-
const relToSrc = specifier.slice(2);
|
|
1121
|
-
const barrelMap = barrelMaps.get(relToSrc);
|
|
1122
|
-
if (barrelMap && names.length > 0) {
|
|
1123
|
-
for (const name of names) {
|
|
1124
|
-
const targetAbs = barrelMap.get(name);
|
|
1125
|
-
if (!targetAbs) continue;
|
|
1126
|
-
const targetId2 = toNodeId(srcDir, targetAbs);
|
|
1127
|
-
if (!nodeIdSet.has(targetId2)) continue;
|
|
1128
|
-
const key2 = `${externalId}\u2192${targetId2}`;
|
|
1129
|
-
if (seen.has(key2)) continue;
|
|
1130
|
-
seen.add(key2);
|
|
1131
|
-
edgesFromThis.push({ source: externalId, target: targetId2, type: "imports" });
|
|
1132
|
-
}
|
|
1133
|
-
continue;
|
|
1134
|
-
}
|
|
1135
|
-
resolved = resolveImport(srcDir, specifier);
|
|
1136
|
-
} else if (specifier.startsWith(".")) {
|
|
1137
|
-
resolved = resolveRelativeImport(absPath, specifier);
|
|
1138
|
-
}
|
|
1139
|
-
if (!resolved) continue;
|
|
1140
|
-
const targetId = toNodeId(srcDir, resolved);
|
|
1141
|
-
if (!nodeIdSet.has(targetId)) continue;
|
|
1142
|
-
if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
|
|
1143
|
-
const key = `${externalId}\u2192${targetId}`;
|
|
1144
|
-
if (seen.has(key)) continue;
|
|
1145
|
-
seen.add(key);
|
|
1146
|
-
edgesFromThis.push({ source: externalId, target: targetId, type: "imports" });
|
|
1147
|
-
}
|
|
1148
|
-
if (edgesFromThis.length === 0) continue;
|
|
1149
|
-
uiNodes.push({
|
|
1150
|
-
id: externalId,
|
|
1151
|
-
type: "external",
|
|
1152
|
-
name: parsed.name || nameFromFilename(absPath),
|
|
1153
|
-
layer: "ui",
|
|
1154
|
-
route: null,
|
|
1155
|
-
exports: parsed.exports
|
|
1156
|
-
});
|
|
1157
|
-
nodeIdSet.add(externalId);
|
|
1158
|
-
uiEdges.push(...edgesFromThis);
|
|
1159
|
-
}
|
|
1160
|
-
const apiCrossRefs = [];
|
|
1161
|
-
for (const node of apiNodes) {
|
|
1162
|
-
const dbCalls = node._dbCalls;
|
|
1163
|
-
if (!dbCalls) continue;
|
|
1164
|
-
const seenModels = /* @__PURE__ */ new Set();
|
|
1165
|
-
for (const call of dbCalls) {
|
|
1166
|
-
if (seenModels.has(call.model)) continue;
|
|
1167
|
-
seenModels.add(call.model);
|
|
1168
|
-
apiCrossRefs.push({
|
|
1169
|
-
source: node.id,
|
|
1170
|
-
target: camelToPascal(call.model),
|
|
1171
|
-
type: call.isMutation ? "mutates" : "reads",
|
|
1172
|
-
layer: "db"
|
|
1173
|
-
});
|
|
1174
|
-
}
|
|
1175
|
-
delete node._dbCalls;
|
|
1176
|
-
}
|
|
1177
|
-
const apiNodeIds = new Set(apiNodes.map((n) => n.id));
|
|
1178
|
-
const apiEdges = [];
|
|
1179
|
-
const uiOnlyEdges = [];
|
|
1180
|
-
for (const edge of uiEdges) {
|
|
1181
|
-
if (apiNodeIds.has(edge.source)) {
|
|
1182
|
-
apiEdges.push(edge);
|
|
1183
|
-
} else {
|
|
1184
|
-
uiOnlyEdges.push(edge);
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
const flaggedSet = /* @__PURE__ */ new Set();
|
|
1188
|
-
const dedupedFlagged = uiFlagged.filter((f) => {
|
|
1189
|
-
const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
|
|
1190
|
-
if (flaggedSet.has(key)) return false;
|
|
1191
|
-
flaggedSet.add(key);
|
|
1192
|
-
return true;
|
|
1193
|
-
});
|
|
1194
|
-
const typePriority = {
|
|
1195
|
-
layout: 0,
|
|
1196
|
-
page: 1,
|
|
1197
|
-
component: 2,
|
|
1198
|
-
ui: 3,
|
|
1199
|
-
context: 4,
|
|
1200
|
-
config: 5,
|
|
1201
|
-
util: 6,
|
|
1202
|
-
hook: 7,
|
|
1203
|
-
lib: 8
|
|
1204
|
-
};
|
|
1205
|
-
uiNodes.sort((a, b) => (typePriority[a.type] ?? 99) - (typePriority[b.type] ?? 99) || a.id.localeCompare(b.id));
|
|
1206
|
-
uiOnlyEdges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1207
|
-
apiNodes.sort((a, b) => (a.path ?? "").localeCompare(b.path ?? ""));
|
|
1208
|
-
apiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1209
|
-
const byType = (t) => uiNodes.filter((n) => n.type === t).length;
|
|
1210
|
-
const uiStats = {
|
|
1211
|
-
total_pages: byType("page"),
|
|
1212
|
-
total_layouts: byType("layout"),
|
|
1213
|
-
total_components: byType("component"),
|
|
1214
|
-
total_ui: byType("ui"),
|
|
1215
|
-
total_hooks: byType("hook"),
|
|
1216
|
-
total_contexts: byType("context"),
|
|
1217
|
-
total_configs: byType("config"),
|
|
1218
|
-
total_utils: byType("util"),
|
|
1219
|
-
total_libs: byType("lib"),
|
|
1220
|
-
total_external: byType("external"),
|
|
1221
|
-
total_edges: uiOnlyEdges.length,
|
|
1222
|
-
total_flagged: dedupedFlagged.length
|
|
1223
|
-
};
|
|
1224
|
-
const stripLayer = (nodes) => nodes.map(({ layer: _, ...rest }) => rest);
|
|
1225
|
-
const result = /* @__PURE__ */ new Map();
|
|
1226
|
-
result.set("ui", {
|
|
1227
|
-
metadata: {
|
|
1228
|
-
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1229
|
-
scope: "main-app-only",
|
|
1230
|
-
app_root: "src/",
|
|
1231
|
-
layer: "ui",
|
|
1232
|
-
parser: "typescript-project",
|
|
1233
|
-
...uiStats,
|
|
1234
|
-
notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
|
|
1235
|
-
},
|
|
1236
|
-
nodes: stripLayer(uiNodes),
|
|
1237
|
-
edges: uiOnlyEdges,
|
|
1238
|
-
cross_refs: [],
|
|
1239
|
-
contradictions: [],
|
|
1240
|
-
warnings: [],
|
|
1241
|
-
flagged_edges: dedupedFlagged,
|
|
1242
|
-
patterns: {
|
|
1243
|
-
total_nodes: uiNodes.length,
|
|
1244
|
-
by_type: uiStats,
|
|
1245
|
-
by_edge_type: {
|
|
1246
|
-
renders: uiOnlyEdges.filter((e) => e.type === "renders").length,
|
|
1247
|
-
imports: uiOnlyEdges.filter((e) => e.type === "imports").length,
|
|
1248
|
-
navigates: uiOnlyEdges.filter((e) => e.type === "navigates").length
|
|
1249
|
-
},
|
|
1250
|
-
fetch_calls: fetchCallEntries
|
|
1251
|
-
}
|
|
1252
|
-
});
|
|
1253
|
-
if (apiNodes.length > 0) {
|
|
1254
|
-
const mutatorNodes = apiNodes.filter((n) => n.mutates).length;
|
|
1255
|
-
const readOnlyNodes = apiNodes.filter((n) => !n.mutates).length;
|
|
1256
|
-
const authUsage = {};
|
|
1257
|
-
let endpointsWithAuth = 0;
|
|
1258
|
-
for (const n of apiNodes) {
|
|
1259
|
-
const auth = n.auth;
|
|
1260
|
-
if (auth.length > 0 && auth[0] !== "public") {
|
|
1261
|
-
endpointsWithAuth++;
|
|
1262
|
-
for (const w of auth) {
|
|
1263
|
-
authUsage[w] = (authUsage[w] ?? 0) + 1;
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
const mutatorCount = {};
|
|
1268
|
-
for (const n of apiNodes) {
|
|
1269
|
-
const ops = n.db_operations;
|
|
1270
|
-
for (const op of ops) {
|
|
1271
|
-
const method = op.split(".")[1];
|
|
1272
|
-
if (method) mutatorCount[method] = (mutatorCount[method] ?? 0) + 1;
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
result.set("api", {
|
|
1276
|
-
metadata: {
|
|
1277
|
-
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1278
|
-
scope: "main-app-only",
|
|
1279
|
-
stack: "nextjs-app-router",
|
|
1280
|
-
layer: "api",
|
|
1281
|
-
parser: "typescript-project",
|
|
1282
|
-
total_endpoints: apiNodes.length,
|
|
1283
|
-
total_methods: apiNodes.reduce((sum, n) => sum + n.methods.length, 0),
|
|
1284
|
-
endpoints_with_auth: endpointsWithAuth,
|
|
1285
|
-
endpoints_with_db_access: apiNodes.filter((n) => n.db_models.length > 0).length,
|
|
1286
|
-
mutator_endpoints: mutatorNodes,
|
|
1287
|
-
read_only_endpoints: readOnlyNodes
|
|
1288
|
-
},
|
|
1289
|
-
nodes: stripLayer(apiNodes),
|
|
1290
|
-
edges: apiEdges,
|
|
1291
|
-
cross_refs: apiCrossRefs,
|
|
1292
|
-
contradictions: [],
|
|
1293
|
-
warnings: [],
|
|
1294
|
-
flagged_edges: [],
|
|
1295
|
-
patterns: {
|
|
1296
|
-
total_endpoints: apiNodes.length,
|
|
1297
|
-
methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
|
|
1298
|
-
acc[m] = apiNodes.filter((n) => n.methods.includes(m)).length;
|
|
1299
|
-
return acc;
|
|
1300
|
-
}, {}),
|
|
1301
|
-
auth_strategies: authUsage,
|
|
1302
|
-
mutation_operations: mutatorCount,
|
|
1303
|
-
mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
|
|
1304
|
-
}
|
|
1305
|
-
});
|
|
1306
|
-
}
|
|
1307
|
-
return result;
|
|
1308
|
-
}
|
|
1309
|
-
var typescriptProjectParser = {
|
|
1310
|
-
id: "typescript-project",
|
|
1311
|
-
layers: ["ui", "api"],
|
|
1312
|
-
detect,
|
|
1313
|
-
generate
|
|
1314
|
-
};
|
|
1315
|
-
|
|
1316
|
-
// src/server/graph/parsers/db/prisma-schema.ts
|
|
1317
|
-
var import_node_fs5 = require("node:fs");
|
|
1318
|
-
var import_node_path5 = require("node:path");
|
|
1319
|
-
function parseModels(content) {
|
|
1320
|
-
const nodes = [];
|
|
1321
|
-
const relations = [];
|
|
1322
|
-
const modelRe = /model\s+(\w+)\s*\{([^}]+)\}/g;
|
|
1323
|
-
let m;
|
|
1324
|
-
while ((m = modelRe.exec(content)) !== null) {
|
|
1325
|
-
const modelName = m[1];
|
|
1326
|
-
const body = m[2];
|
|
1327
|
-
const columns = [];
|
|
1328
|
-
const lines = body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("@@"));
|
|
1329
|
-
for (const line of lines) {
|
|
1330
|
-
const fieldMatch = line.match(/^(\w+)\s+(\S+)(.*)/);
|
|
1331
|
-
if (!fieldMatch) continue;
|
|
1332
|
-
const fieldName = fieldMatch[1];
|
|
1333
|
-
const fieldType = fieldMatch[2];
|
|
1334
|
-
const rest = fieldMatch[3] ?? "";
|
|
1335
|
-
const commentMatch = rest.match(/\/\/\s*(.+)$/);
|
|
1336
|
-
const comment = commentMatch ? commentMatch[1].trim() : null;
|
|
1337
|
-
const restNoComment = commentMatch ? rest.slice(0, commentMatch.index).trim() : rest;
|
|
1338
|
-
const isPrimary = restNoComment.includes("@id");
|
|
1339
|
-
const isUnique = restNoComment.includes("@unique");
|
|
1340
|
-
const isNullable = fieldType.endsWith("?");
|
|
1341
|
-
const baseType = fieldType.replace(/[?\[\]]/g, "");
|
|
1342
|
-
const defaultMatch = restNoComment.match(/@default\(([^)]+)\)/);
|
|
1343
|
-
const defaultVal = defaultMatch ? defaultMatch[1] : null;
|
|
1344
|
-
const relationMatch = restNoComment.match(/@relation\(([^)]*)\)/);
|
|
1345
|
-
const isRelationField = !!relationMatch;
|
|
1346
|
-
columns.push({
|
|
1347
|
-
name: fieldName,
|
|
1348
|
-
type: fieldType,
|
|
1349
|
-
primary: isPrimary,
|
|
1350
|
-
unique: isUnique,
|
|
1351
|
-
nullable: isNullable,
|
|
1352
|
-
default: defaultVal,
|
|
1353
|
-
isRelation: isRelationField,
|
|
1354
|
-
comment
|
|
1355
|
-
});
|
|
1356
|
-
if (relationMatch) {
|
|
1357
|
-
const relArgs = relationMatch[1];
|
|
1358
|
-
const fieldsMatch = relArgs.match(/fields:\s*\[([^\]]+)\]/);
|
|
1359
|
-
const refsMatch = relArgs.match(/references:\s*\[([^\]]+)\]/);
|
|
1360
|
-
const onDeleteMatch = relArgs.match(/onDelete:\s*(\w+)/);
|
|
1361
|
-
if (fieldsMatch && refsMatch) {
|
|
1362
|
-
const fk = fieldsMatch[1].trim();
|
|
1363
|
-
relations.push({
|
|
1364
|
-
source: modelName,
|
|
1365
|
-
target: baseType,
|
|
1366
|
-
type: "belongs_to",
|
|
1367
|
-
fk,
|
|
1368
|
-
onDelete: onDeleteMatch ? onDeleteMatch[1] : null
|
|
1369
|
-
});
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
if (fieldType.endsWith("[]") && !relationMatch) {
|
|
1373
|
-
relations.push({
|
|
1374
|
-
source: modelName,
|
|
1375
|
-
target: baseType,
|
|
1376
|
-
type: "has_many",
|
|
1377
|
-
fk: null,
|
|
1378
|
-
onDelete: null
|
|
1379
|
-
});
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
nodes.push({
|
|
1383
|
-
id: modelName,
|
|
1384
|
-
type: "table",
|
|
1385
|
-
name: modelName,
|
|
1386
|
-
columns: columns.filter((c) => !c.isRelation || c.primary)
|
|
1387
|
-
});
|
|
1388
|
-
}
|
|
1389
|
-
return { nodes, relations };
|
|
1390
|
-
}
|
|
1391
|
-
function parseEnums(content) {
|
|
1392
|
-
const nodes = [];
|
|
1393
|
-
const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
|
|
1394
|
-
let m;
|
|
1395
|
-
while ((m = enumRe.exec(content)) !== null) {
|
|
1396
|
-
const enumName = m[1];
|
|
1397
|
-
const body = m[2];
|
|
1398
|
-
const values = body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
|
|
1399
|
-
nodes.push({
|
|
1400
|
-
id: enumName,
|
|
1401
|
-
type: "enum",
|
|
1402
|
-
name: enumName,
|
|
1403
|
-
values
|
|
1404
|
-
});
|
|
1405
|
-
}
|
|
1406
|
-
return nodes;
|
|
1407
|
-
}
|
|
1408
|
-
function detect2(rootDir) {
|
|
1409
|
-
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
|
|
1410
|
-
}
|
|
1411
|
-
function generate2(rootDir) {
|
|
1412
|
-
const schemaPath = (0, import_node_path5.join)(rootDir, "prisma", "schema.prisma");
|
|
1413
|
-
const content = (0, import_node_fs5.readFileSync)(schemaPath, "utf-8");
|
|
1414
|
-
const { nodes: modelNodes, relations } = parseModels(content);
|
|
1415
|
-
const enumNodes = parseEnums(content);
|
|
1416
|
-
const allNodes = [...modelNodes, ...enumNodes];
|
|
1417
|
-
const edges = relations.map((r) => ({
|
|
1418
|
-
source: r.source,
|
|
1419
|
-
target: r.target,
|
|
1420
|
-
type: r.type,
|
|
1421
|
-
fk: r.fk,
|
|
1422
|
-
onDelete: r.onDelete
|
|
1423
|
-
}));
|
|
1424
|
-
allNodes.sort((a, b) => {
|
|
1425
|
-
if (a.type !== b.type) return a.type === "table" ? -1 : 1;
|
|
1426
|
-
return a.name.localeCompare(b.name);
|
|
1427
|
-
});
|
|
1428
|
-
edges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1429
|
-
return {
|
|
1430
|
-
metadata: {
|
|
1431
|
-
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1432
|
-
scope: "prisma-schema",
|
|
1433
|
-
source: "prisma/schema.prisma",
|
|
1434
|
-
provider: "postgresql",
|
|
1435
|
-
layer: "db",
|
|
1436
|
-
total_models: modelNodes.length,
|
|
1437
|
-
total_enums: enumNodes.length,
|
|
1438
|
-
total_relations: edges.length
|
|
1439
|
-
},
|
|
1440
|
-
nodes: allNodes,
|
|
1441
|
-
edges,
|
|
1442
|
-
cross_refs: [],
|
|
1443
|
-
contradictions: [],
|
|
1444
|
-
warnings: [
|
|
1445
|
-
{
|
|
1446
|
-
type: "schema_file_only",
|
|
1447
|
-
detail: "Live DB introspection not yet implemented. Graph derived from prisma/schema.prisma."
|
|
1448
|
-
}
|
|
1449
|
-
],
|
|
1450
|
-
flagged_edges: [],
|
|
1451
|
-
patterns: {
|
|
1452
|
-
total_tables: modelNodes.length,
|
|
1453
|
-
total_enums: enumNodes.length,
|
|
1454
|
-
total_relations: edges.length,
|
|
1455
|
-
relation_types: {
|
|
1456
|
-
belongs_to: edges.filter((e) => e.type === "belongs_to").length,
|
|
1457
|
-
has_many: edges.filter((e) => e.type === "has_many").length
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
};
|
|
1461
|
-
}
|
|
1462
|
-
var prismaSchemaParser = {
|
|
1463
|
-
id: "prisma-schema",
|
|
1464
|
-
layer: "db",
|
|
1465
|
-
detect: detect2,
|
|
1466
|
-
generate: generate2
|
|
1467
|
-
};
|
|
1468
|
-
|
|
1469
|
-
// src/server/graph/parsers/db/sql-migrations.ts
|
|
1470
|
-
var import_node_fs6 = require("node:fs");
|
|
1471
|
-
var import_node_path6 = require("node:path");
|
|
1472
|
-
var PG_TO_PRISMA = {
|
|
1473
|
-
"TEXT": "String",
|
|
1474
|
-
"VARCHAR": "String",
|
|
1475
|
-
"CHAR": "String",
|
|
1476
|
-
"INTEGER": "Int",
|
|
1477
|
-
"INT": "Int",
|
|
1478
|
-
"SMALLINT": "Int",
|
|
1479
|
-
"BIGINT": "BigInt",
|
|
1480
|
-
"SERIAL": "Int",
|
|
1481
|
-
"BOOLEAN": "Boolean",
|
|
1482
|
-
"BOOL": "Boolean",
|
|
1483
|
-
"TIMESTAMP(3)": "DateTime",
|
|
1484
|
-
"TIMESTAMP": "DateTime",
|
|
1485
|
-
"TIMESTAMPTZ": "DateTime",
|
|
1486
|
-
"DATE": "DateTime",
|
|
1487
|
-
"DOUBLE PRECISION": "Float",
|
|
1488
|
-
"FLOAT": "Float",
|
|
1489
|
-
"REAL": "Float",
|
|
1490
|
-
"DECIMAL": "Decimal",
|
|
1491
|
-
"NUMERIC": "Decimal",
|
|
1492
|
-
"JSONB": "Json",
|
|
1493
|
-
"JSON": "Json",
|
|
1494
|
-
"BYTEA": "Bytes",
|
|
1495
|
-
"UUID": "String",
|
|
1496
|
-
"TEXT[]": "String[]"
|
|
1497
|
-
};
|
|
1498
|
-
function pgTypeToPrisma(pgType) {
|
|
1499
|
-
const upper = pgType.toUpperCase().trim();
|
|
1500
|
-
return PG_TO_PRISMA[upper] ?? upper;
|
|
1501
|
-
}
|
|
1502
|
-
function parseCreateTable(sql, state) {
|
|
1503
|
-
const re = /CREATE\s+TABLE\s+"(\w+)"\s*\(([\s\S]*?)\);/gi;
|
|
1504
|
-
let m;
|
|
1505
|
-
while ((m = re.exec(sql)) !== null) {
|
|
1506
|
-
const tableName = m[1];
|
|
1507
|
-
const body = m[2];
|
|
1508
|
-
const columns = /* @__PURE__ */ new Map();
|
|
1509
|
-
let primaryCol = null;
|
|
1510
|
-
for (const line of body.split("\n")) {
|
|
1511
|
-
const trimmed = line.trim().replace(/,\s*$/, "");
|
|
1512
|
-
if (!trimmed || trimmed.startsWith("--")) continue;
|
|
1513
|
-
const pkMatch = trimmed.match(/CONSTRAINT\s+"[^"]+"\s+PRIMARY\s+KEY\s*\("(\w+)"\)/i);
|
|
1514
|
-
if (pkMatch) {
|
|
1515
|
-
primaryCol = pkMatch[1];
|
|
1516
|
-
continue;
|
|
1517
|
-
}
|
|
1518
|
-
if (/^\s*CONSTRAINT\s/i.test(trimmed)) continue;
|
|
1519
|
-
const colMatch = trimmed.match(/^"(\w+)"\s+(.+)/);
|
|
1520
|
-
if (!colMatch) continue;
|
|
1521
|
-
const colName = colMatch[1];
|
|
1522
|
-
let rest = colMatch[2];
|
|
1523
|
-
const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
|
|
1524
|
-
const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)(?:\s*,?\s*$)/i);
|
|
1525
|
-
const defaultVal = defaultMatch ? defaultMatch[1].trim() : null;
|
|
1526
|
-
let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim().replace(/,\s*$/, "").trim();
|
|
1527
|
-
columns.set(colName, {
|
|
1528
|
-
name: colName,
|
|
1529
|
-
type: colType,
|
|
1530
|
-
nullable: !isNotNull,
|
|
1531
|
-
primary: false,
|
|
1532
|
-
unique: false,
|
|
1533
|
-
default: defaultVal
|
|
1534
|
-
});
|
|
1535
|
-
}
|
|
1536
|
-
if (primaryCol && columns.has(primaryCol)) {
|
|
1537
|
-
columns.get(primaryCol).primary = true;
|
|
1538
|
-
}
|
|
1539
|
-
state.tables.set(tableName, { name: tableName, columns });
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
function parseCreateEnum(sql, state) {
|
|
1543
|
-
const re = /CREATE\s+TYPE\s+"(\w+)"\s+AS\s+ENUM\s*\(([^)]+)\)/gi;
|
|
1544
|
-
let m;
|
|
1545
|
-
while ((m = re.exec(sql)) !== null) {
|
|
1546
|
-
const enumName = m[1];
|
|
1547
|
-
const valuesStr = m[2];
|
|
1548
|
-
const values = new Set(
|
|
1549
|
-
valuesStr.split(",").map((v) => v.trim().replace(/^'(.*)'$/, "$1")).filter(Boolean)
|
|
1550
|
-
);
|
|
1551
|
-
state.enums.set(enumName, { name: enumName, values });
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
function parseAlterTable(sql, state) {
|
|
1555
|
-
const addColRe = /ALTER\s+TABLE\s+"(\w+)"\s+ADD\s+COLUMN\s+"(\w+)"\s+(.+?);/gi;
|
|
1556
|
-
let m;
|
|
1557
|
-
while ((m = addColRe.exec(sql)) !== null) {
|
|
1558
|
-
const tableName = m[1];
|
|
1559
|
-
const colName = m[2];
|
|
1560
|
-
let rest = m[3];
|
|
1561
|
-
const table = state.tables.get(tableName);
|
|
1562
|
-
if (!table) continue;
|
|
1563
|
-
const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
|
|
1564
|
-
const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)$/i);
|
|
1565
|
-
const defaultVal = defaultMatch ? defaultMatch[1].trim() : null;
|
|
1566
|
-
let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim();
|
|
1567
|
-
table.columns.set(colName, {
|
|
1568
|
-
name: colName,
|
|
1569
|
-
type: colType,
|
|
1570
|
-
nullable: !isNotNull,
|
|
1571
|
-
primary: false,
|
|
1572
|
-
unique: false,
|
|
1573
|
-
default: defaultVal
|
|
1574
|
-
});
|
|
1575
|
-
}
|
|
1576
|
-
const dropColRe = /ALTER\s+TABLE\s+"(\w+)"\s+DROP\s+COLUMN\s+"(\w+)"/gi;
|
|
1577
|
-
while ((m = dropColRe.exec(sql)) !== null) {
|
|
1578
|
-
const table = state.tables.get(m[1]);
|
|
1579
|
-
if (table) table.columns.delete(m[2]);
|
|
1580
|
-
}
|
|
1581
|
-
const fkRe = /ALTER\s+TABLE\s+"(\w+)"\s+ADD\s+CONSTRAINT\s+"([^"]+)"\s+FOREIGN\s+KEY\s*\("(\w+)"\)\s+REFERENCES\s+"(\w+)"\("(\w+)"\)(?:\s+ON\s+DELETE\s+(\w+(?:\s+\w+)?))?/gi;
|
|
1582
|
-
while ((m = fkRe.exec(sql)) !== null) {
|
|
1583
|
-
state.fks.push({
|
|
1584
|
-
constraintName: m[2],
|
|
1585
|
-
sourceTable: m[1],
|
|
1586
|
-
sourceColumn: m[3],
|
|
1587
|
-
targetTable: m[4],
|
|
1588
|
-
targetColumn: m[5],
|
|
1589
|
-
onDelete: m[6] ?? null
|
|
1590
|
-
});
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
function parseAlterEnum(sql, state) {
|
|
1594
|
-
const re = /ALTER\s+TYPE\s+"(\w+)"\s+ADD\s+VALUE\s+'([^']+)'/gi;
|
|
1595
|
-
let m;
|
|
1596
|
-
while ((m = re.exec(sql)) !== null) {
|
|
1597
|
-
const en = state.enums.get(m[1]);
|
|
1598
|
-
if (en) en.values.add(m[2]);
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
function parseDropTable(sql, state) {
|
|
1602
|
-
const re = /DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?"(\w+)"/gi;
|
|
1603
|
-
let m;
|
|
1604
|
-
while ((m = re.exec(sql)) !== null) {
|
|
1605
|
-
state.tables.delete(m[1]);
|
|
1606
|
-
state.fks = state.fks.filter((fk) => fk.sourceTable !== m[1] && fk.targetTable !== m[1]);
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
function parseUniqueIndex(sql, state) {
|
|
1610
|
-
const re = /CREATE\s+UNIQUE\s+INDEX\s+"[^"]+"\s+ON\s+"(\w+)"\("(\w+)"\)/gi;
|
|
1611
|
-
let m;
|
|
1612
|
-
while ((m = re.exec(sql)) !== null) {
|
|
1613
|
-
const table = state.tables.get(m[1]);
|
|
1614
|
-
const col = table?.columns.get(m[2]);
|
|
1615
|
-
if (col) col.unique = true;
|
|
1616
|
-
if (!state.uniqueIndexes.has(m[1])) state.uniqueIndexes.set(m[1], /* @__PURE__ */ new Set());
|
|
1617
|
-
state.uniqueIndexes.get(m[1]).add(m[2]);
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
function parseMigrations(rootDir) {
|
|
1621
|
-
const migrationsDir = (0, import_node_path6.join)(rootDir, "prisma", "migrations");
|
|
1622
|
-
const state = {
|
|
1623
|
-
tables: /* @__PURE__ */ new Map(),
|
|
1624
|
-
enums: /* @__PURE__ */ new Map(),
|
|
1625
|
-
fks: [],
|
|
1626
|
-
uniqueIndexes: /* @__PURE__ */ new Map()
|
|
1627
|
-
};
|
|
1628
|
-
if (!(0, import_node_fs6.existsSync)(migrationsDir)) return state;
|
|
1629
|
-
const dirs = (0, import_node_fs6.readdirSync)(migrationsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
1630
|
-
for (const dir of dirs) {
|
|
1631
|
-
const sqlPath = (0, import_node_path6.join)(migrationsDir, dir, "migration.sql");
|
|
1632
|
-
if (!(0, import_node_fs6.existsSync)(sqlPath)) continue;
|
|
1633
|
-
const sql = (0, import_node_fs6.readFileSync)(sqlPath, "utf-8");
|
|
1634
|
-
parseCreateEnum(sql, state);
|
|
1635
|
-
parseCreateTable(sql, state);
|
|
1636
|
-
parseAlterTable(sql, state);
|
|
1637
|
-
parseAlterEnum(sql, state);
|
|
1638
|
-
parseDropTable(sql, state);
|
|
1639
|
-
parseUniqueIndex(sql, state);
|
|
1640
|
-
}
|
|
1641
|
-
return state;
|
|
1642
|
-
}
|
|
1643
|
-
function loadPrismaState(rootDir) {
|
|
1644
|
-
const schemaPath = (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
|
|
1645
|
-
if (!(0, import_node_fs6.existsSync)(schemaPath)) return null;
|
|
1646
|
-
const content = (0, import_node_fs6.readFileSync)(schemaPath, "utf-8");
|
|
1647
|
-
const tables = /* @__PURE__ */ new Map();
|
|
1648
|
-
const enums = /* @__PURE__ */ new Map();
|
|
1649
|
-
const relations = [];
|
|
1650
|
-
const modelRe = /model\s+(\w+)\s*\{([^}]+)\}/g;
|
|
1651
|
-
let m;
|
|
1652
|
-
while ((m = modelRe.exec(content)) !== null) {
|
|
1653
|
-
const modelName = m[1];
|
|
1654
|
-
const body = m[2];
|
|
1655
|
-
const cols = [];
|
|
1656
|
-
for (const line of body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("@@"))) {
|
|
1657
|
-
const fm = line.match(/^(\w+)\s+(\S+)(.*)/);
|
|
1658
|
-
if (!fm) continue;
|
|
1659
|
-
const fieldName = fm[1];
|
|
1660
|
-
const fieldType = fm[2];
|
|
1661
|
-
const rest = fm[3] ?? "";
|
|
1662
|
-
const baseType = fieldType.replace(/[?\[\]]/g, "");
|
|
1663
|
-
const isRelation = rest.includes("@relation");
|
|
1664
|
-
if (isRelation) {
|
|
1665
|
-
const relArgs = rest.match(/@relation\(([^)]*)\)/)?.[1] ?? "";
|
|
1666
|
-
const fieldsMatch = relArgs.match(/fields:\s*\[([^\]]+)\]/);
|
|
1667
|
-
if (fieldsMatch) {
|
|
1668
|
-
relations.push({ source: modelName, target: baseType, fk: fieldsMatch[1].trim() });
|
|
1669
|
-
}
|
|
1670
|
-
continue;
|
|
1671
|
-
}
|
|
1672
|
-
if (fieldType.endsWith("[]") || fieldType.endsWith("?") && content.includes(`model ${baseType}`)) {
|
|
1673
|
-
if (new RegExp(`model\\s+${baseType}\\s*\\{`).test(content)) continue;
|
|
1674
|
-
}
|
|
1675
|
-
cols.push({
|
|
1676
|
-
name: fieldName,
|
|
1677
|
-
type: fieldType.replace("?", ""),
|
|
1678
|
-
nullable: fieldType.endsWith("?") || fieldType.includes("?"),
|
|
1679
|
-
primary: rest.includes("@id"),
|
|
1680
|
-
unique: rest.includes("@unique")
|
|
1681
|
-
});
|
|
1682
|
-
}
|
|
1683
|
-
tables.set(modelName, { columns: cols });
|
|
1684
|
-
}
|
|
1685
|
-
const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
|
|
1686
|
-
while ((m = enumRe.exec(content)) !== null) {
|
|
1687
|
-
const values = m[2].split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
|
|
1688
|
-
enums.set(m[1], values);
|
|
1689
|
-
}
|
|
1690
|
-
return { tables, enums, relations };
|
|
1691
|
-
}
|
|
1692
|
-
function verify(sqlState, prisma) {
|
|
1693
|
-
const contradictions = [];
|
|
1694
|
-
const flaggedEdges = [];
|
|
1695
|
-
for (const [name] of sqlState.tables) {
|
|
1696
|
-
if (!prisma.tables.has(name)) {
|
|
1697
|
-
contradictions.push({
|
|
1698
|
-
entity: name,
|
|
1699
|
-
source_a: "sql-migrations",
|
|
1700
|
-
source_b: "prisma-schema",
|
|
1701
|
-
detail: `Table "${name}" exists in migrations but not in schema.prisma`
|
|
1702
|
-
});
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
for (const [name] of prisma.tables) {
|
|
1706
|
-
if (!sqlState.tables.has(name)) {
|
|
1707
|
-
contradictions.push({
|
|
1708
|
-
entity: name,
|
|
1709
|
-
source_a: "prisma-schema",
|
|
1710
|
-
source_b: "sql-migrations",
|
|
1711
|
-
detail: `Table "${name}" in schema.prisma has no CREATE TABLE in migrations`
|
|
1712
|
-
});
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
for (const [tableName, prismaTable] of prisma.tables) {
|
|
1716
|
-
const sqlTable = sqlState.tables.get(tableName);
|
|
1717
|
-
if (!sqlTable) continue;
|
|
1718
|
-
for (const prismaCol of prismaTable.columns) {
|
|
1719
|
-
const sqlCol = sqlTable.columns.get(prismaCol.name);
|
|
1720
|
-
if (!sqlCol) {
|
|
1721
|
-
contradictions.push({
|
|
1722
|
-
entity: `${tableName}.${prismaCol.name}`,
|
|
1723
|
-
source_a: "prisma-schema",
|
|
1724
|
-
source_b: "sql-migrations",
|
|
1725
|
-
detail: `Column "${tableName}.${prismaCol.name}" in schema.prisma but not in migrations`
|
|
1726
|
-
});
|
|
1727
|
-
continue;
|
|
1728
|
-
}
|
|
1729
|
-
const mappedSqlType = pgTypeToPrisma(sqlCol.type);
|
|
1730
|
-
const prismaBaseType = prismaCol.type.replace(/[?\[\]]/g, "");
|
|
1731
|
-
if (mappedSqlType !== prismaBaseType && mappedSqlType !== prismaCol.type) {
|
|
1732
|
-
if (!sqlState.enums.has(prismaBaseType)) {
|
|
1733
|
-
contradictions.push({
|
|
1734
|
-
entity: `${tableName}.${prismaCol.name}`,
|
|
1735
|
-
source_a: "sql-migrations",
|
|
1736
|
-
source_b: "prisma-schema",
|
|
1737
|
-
detail: `Column "${tableName}.${prismaCol.name}": SQL type "${sqlCol.type}" (\u2192${mappedSqlType}) vs Prisma type "${prismaCol.type}"`
|
|
1738
|
-
});
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
if (sqlCol.nullable !== prismaCol.nullable) {
|
|
1742
|
-
contradictions.push({
|
|
1743
|
-
entity: `${tableName}.${prismaCol.name}`,
|
|
1744
|
-
source_a: "sql-migrations",
|
|
1745
|
-
source_b: "prisma-schema",
|
|
1746
|
-
detail: `Column "${tableName}.${prismaCol.name}": ${sqlCol.nullable ? "nullable" : "NOT NULL"} in SQL vs ${prismaCol.nullable ? "optional" : "required"} in Prisma`
|
|
1747
|
-
});
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
for (const [colName] of sqlTable.columns) {
|
|
1751
|
-
const inPrisma = prismaTable.columns.some((c) => c.name === colName);
|
|
1752
|
-
if (!inPrisma) {
|
|
1753
|
-
contradictions.push({
|
|
1754
|
-
entity: `${tableName}.${colName}`,
|
|
1755
|
-
source_a: "sql-migrations",
|
|
1756
|
-
source_b: "prisma-schema",
|
|
1757
|
-
detail: `Column "${tableName}.${colName}" in migrations but not in schema.prisma`
|
|
1758
|
-
});
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
for (const [name, sqlEnum] of sqlState.enums) {
|
|
1763
|
-
const prismaValues = prisma.enums.get(name);
|
|
1764
|
-
if (!prismaValues) {
|
|
1765
|
-
contradictions.push({
|
|
1766
|
-
entity: name,
|
|
1767
|
-
source_a: "sql-migrations",
|
|
1768
|
-
source_b: "prisma-schema",
|
|
1769
|
-
detail: `Enum "${name}" exists in migrations but not in schema.prisma`
|
|
1770
|
-
});
|
|
1771
|
-
continue;
|
|
1772
|
-
}
|
|
1773
|
-
const prismaSet = new Set(prismaValues);
|
|
1774
|
-
for (const val of sqlEnum.values) {
|
|
1775
|
-
if (!prismaSet.has(val)) {
|
|
1776
|
-
contradictions.push({
|
|
1777
|
-
entity: `${name}.${val}`,
|
|
1778
|
-
source_a: "sql-migrations",
|
|
1779
|
-
source_b: "prisma-schema",
|
|
1780
|
-
detail: `Enum "${name}": value "${val}" in migrations but not in schema.prisma`
|
|
1781
|
-
});
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
for (const val of prismaValues) {
|
|
1785
|
-
if (!sqlEnum.values.has(val)) {
|
|
1786
|
-
contradictions.push({
|
|
1787
|
-
entity: `${name}.${val}`,
|
|
1788
|
-
source_a: "prisma-schema",
|
|
1789
|
-
source_b: "sql-migrations",
|
|
1790
|
-
detail: `Enum "${name}": value "${val}" in schema.prisma but not in migrations`
|
|
1791
|
-
});
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
const prismaFkSet = new Set(prisma.relations.map((r) => `${r.source}|${r.target}|${r.fk}`));
|
|
1796
|
-
for (const fk of sqlState.fks) {
|
|
1797
|
-
const key = `${fk.sourceTable}|${fk.targetTable}|${fk.sourceColumn}`;
|
|
1798
|
-
if (!prismaFkSet.has(key)) {
|
|
1799
|
-
flaggedEdges.push({
|
|
1800
|
-
source: fk.sourceTable,
|
|
1801
|
-
target: fk.targetTable,
|
|
1802
|
-
type: "belongs_to",
|
|
1803
|
-
label: `FK "${fk.constraintName}" (${fk.sourceColumn}\u2192${fk.targetTable}) in migrations but no @relation in schema.prisma`,
|
|
1804
|
-
confidence: "high"
|
|
1805
|
-
});
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
return { contradictions, flaggedEdges };
|
|
1809
|
-
}
|
|
1810
|
-
function detect3(rootDir) {
|
|
1811
|
-
const migrationsDir = (0, import_node_path6.join)(rootDir, "prisma", "migrations");
|
|
1812
|
-
if (!(0, import_node_fs6.existsSync)(migrationsDir)) return false;
|
|
1813
|
-
return (0, import_node_fs6.readdirSync)(migrationsDir, { withFileTypes: true }).some((d) => d.isDirectory() && (0, import_node_fs6.existsSync)((0, import_node_path6.join)(migrationsDir, d.name, "migration.sql")));
|
|
1814
|
-
}
|
|
1815
|
-
function generate3(rootDir) {
|
|
1816
|
-
const sqlState = parseMigrations(rootDir);
|
|
1817
|
-
const prisma = loadPrismaState(rootDir);
|
|
1818
|
-
const prismaTableIds = prisma ? new Set(prisma.tables.keys()) : /* @__PURE__ */ new Set();
|
|
1819
|
-
const prismaEnumIds = prisma ? new Set(prisma.enums.keys()) : /* @__PURE__ */ new Set();
|
|
1820
|
-
const nodes = [];
|
|
1821
|
-
for (const [name, table] of sqlState.tables) {
|
|
1822
|
-
if (prismaTableIds.has(name)) continue;
|
|
1823
|
-
nodes.push({
|
|
1824
|
-
id: name,
|
|
1825
|
-
type: "table",
|
|
1826
|
-
name,
|
|
1827
|
-
source: "sql",
|
|
1828
|
-
columns: [...table.columns.values()].map((c) => ({
|
|
1829
|
-
name: c.name,
|
|
1830
|
-
type: c.type,
|
|
1831
|
-
primary: c.primary,
|
|
1832
|
-
unique: c.unique,
|
|
1833
|
-
nullable: c.nullable,
|
|
1834
|
-
default: c.default,
|
|
1835
|
-
isRelation: false,
|
|
1836
|
-
comment: null
|
|
1837
|
-
}))
|
|
1838
|
-
});
|
|
1839
|
-
}
|
|
1840
|
-
for (const [name, sqlEnum] of sqlState.enums) {
|
|
1841
|
-
if (prismaEnumIds.has(name)) continue;
|
|
1842
|
-
nodes.push({
|
|
1843
|
-
id: name,
|
|
1844
|
-
type: "enum",
|
|
1845
|
-
name,
|
|
1846
|
-
source: "sql",
|
|
1847
|
-
values: [...sqlEnum.values]
|
|
1848
|
-
});
|
|
1849
|
-
}
|
|
1850
|
-
const sqlOnlyTables = new Set(nodes.filter((n) => n.type === "table").map((n) => n.id));
|
|
1851
|
-
const edges = sqlState.fks.filter((fk) => sqlOnlyTables.has(fk.sourceTable)).map((fk) => ({
|
|
1852
|
-
source: fk.sourceTable,
|
|
1853
|
-
target: fk.targetTable,
|
|
1854
|
-
type: "belongs_to",
|
|
1855
|
-
fk: fk.sourceColumn,
|
|
1856
|
-
onDelete: fk.onDelete
|
|
1857
|
-
}));
|
|
1858
|
-
const { contradictions, flaggedEdges } = prisma ? verify(sqlState, prisma) : { contradictions: [], flaggedEdges: [] };
|
|
1859
|
-
return {
|
|
1860
|
-
metadata: {
|
|
1861
|
-
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1862
|
-
scope: "sql-migrations",
|
|
1863
|
-
source: "prisma/migrations/",
|
|
1864
|
-
layer: "db",
|
|
1865
|
-
sql_tables: sqlState.tables.size,
|
|
1866
|
-
sql_enums: sqlState.enums.size,
|
|
1867
|
-
sql_fks: sqlState.fks.length,
|
|
1868
|
-
additive_nodes: nodes.length,
|
|
1869
|
-
contradictions_found: contradictions.length,
|
|
1870
|
-
flagged_edges_found: flaggedEdges.length
|
|
1871
|
-
},
|
|
1872
|
-
nodes,
|
|
1873
|
-
edges,
|
|
1874
|
-
cross_refs: [],
|
|
1875
|
-
contradictions,
|
|
1876
|
-
warnings: [],
|
|
1877
|
-
flagged_edges: flaggedEdges
|
|
1878
|
-
};
|
|
1879
|
-
}
|
|
1880
|
-
var sqlMigrationsParser = {
|
|
1881
|
-
id: "sql-migrations",
|
|
1882
|
-
layer: "db",
|
|
1883
|
-
detect: detect3,
|
|
1884
|
-
generate: generate3
|
|
1885
|
-
};
|
|
1886
|
-
|
|
1887
|
-
// src/server/graph/core/api-route-matching.ts
|
|
1888
|
-
function loadApiRoutesFromOutput(apiOutput) {
|
|
1889
|
-
const routes = [];
|
|
1890
|
-
for (const n of apiOutput.nodes) {
|
|
1891
|
-
const path2 = n.path;
|
|
1892
|
-
if (!path2 || typeof path2 !== "string") continue;
|
|
1893
|
-
routes.push({
|
|
1894
|
-
path: path2,
|
|
1895
|
-
nodeId: n.id,
|
|
1896
|
-
segments: path2.split("/").filter(Boolean)
|
|
1897
|
-
});
|
|
1898
|
-
}
|
|
1899
|
-
return routes;
|
|
1900
|
-
}
|
|
1901
|
-
function buildApiPathMap(routes) {
|
|
1902
|
-
const map = /* @__PURE__ */ new Map();
|
|
1903
|
-
for (const r of routes) {
|
|
1904
|
-
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
1905
|
-
}
|
|
1906
|
-
return map;
|
|
1907
|
-
}
|
|
1908
|
-
function normalizeFetchUrl(raw) {
|
|
1909
|
-
let s = raw.replace(/^`|`$/g, "");
|
|
1910
|
-
const qIdx = s.indexOf("?");
|
|
1911
|
-
if (qIdx >= 0) s = s.slice(0, qIdx);
|
|
1912
|
-
const hIdx = s.indexOf("#");
|
|
1913
|
-
if (hIdx >= 0) s = s.slice(0, hIdx);
|
|
1914
|
-
let hadInterpolation = false;
|
|
1915
|
-
s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
1916
|
-
hadInterpolation = true;
|
|
1917
|
-
const cleaned = expr.trim();
|
|
1918
|
-
const last = cleaned.split(".").pop() ?? cleaned;
|
|
1919
|
-
const name = last.replace(/[^\w]/g, "") || "param";
|
|
1920
|
-
return ":" + name;
|
|
1921
|
-
});
|
|
1922
|
-
s = s.replace(/\/+/g, "/");
|
|
1923
|
-
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
1924
|
-
return { path: s || "/", hadInterpolation };
|
|
1925
|
-
}
|
|
1926
|
-
function scoreApiRouteMatch(candidate, known) {
|
|
1927
|
-
if (candidate.length !== known.length) return -1;
|
|
1928
|
-
let score = 0;
|
|
1929
|
-
for (let i = 0; i < candidate.length; i++) {
|
|
1930
|
-
const a = candidate[i];
|
|
1931
|
-
const b = known[i];
|
|
1932
|
-
if (a === b) {
|
|
1933
|
-
score += 3;
|
|
1934
|
-
continue;
|
|
1935
|
-
}
|
|
1936
|
-
if (a.startsWith(":") && b.startsWith(":")) {
|
|
1937
|
-
score += 2;
|
|
1938
|
-
continue;
|
|
1939
|
-
}
|
|
1940
|
-
if (a.startsWith(":") || b.startsWith(":")) {
|
|
1941
|
-
score += 1;
|
|
1942
|
-
continue;
|
|
1943
|
-
}
|
|
1944
|
-
return -1;
|
|
1945
|
-
}
|
|
1946
|
-
return score;
|
|
1947
|
-
}
|
|
1948
|
-
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
1949
|
-
const raw = call.url;
|
|
1950
|
-
if (/^(https?:)?\/\//i.test(raw)) {
|
|
1951
|
-
return { kind: "external", normalizedUrl: raw };
|
|
1952
|
-
}
|
|
1953
|
-
if (call.isConcat) {
|
|
1954
|
-
return { kind: "dynamic", normalizedUrl: raw };
|
|
1955
|
-
}
|
|
1956
|
-
const { path: path2, hadInterpolation } = normalizeFetchUrl(raw);
|
|
1957
|
-
if (!path2.startsWith("/")) {
|
|
1958
|
-
return { kind: "unresolved", normalizedUrl: path2 };
|
|
1959
|
-
}
|
|
1960
|
-
const segs = path2.split("/").filter(Boolean);
|
|
1961
|
-
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
1962
|
-
return { kind: "dynamic", normalizedUrl: path2 };
|
|
1963
|
-
}
|
|
1964
|
-
const exact = apiPathMap.get(path2);
|
|
1965
|
-
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path2 };
|
|
1966
|
-
let bestScore = -1;
|
|
1967
|
-
let bestId = null;
|
|
1968
|
-
for (const r of apiRoutes) {
|
|
1969
|
-
const score = scoreApiRouteMatch(segs, r.segments);
|
|
1970
|
-
if (score > bestScore) {
|
|
1971
|
-
bestScore = score;
|
|
1972
|
-
bestId = r.nodeId;
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
if (bestId && bestScore > 0) {
|
|
1976
|
-
return { kind: "resolved", nodeId: bestId, normalizedUrl: path2 };
|
|
1977
|
-
}
|
|
1978
|
-
return { kind: "unresolved", normalizedUrl: path2 };
|
|
1979
|
-
}
|
|
1980
|
-
function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
|
|
1981
|
-
const { path: path2, hadInterpolation } = normalizeFetchUrl(urlPath);
|
|
1982
|
-
if (!path2.startsWith("/")) {
|
|
1983
|
-
return { kind: "unresolved", normalizedUrl: path2 };
|
|
1984
|
-
}
|
|
1985
|
-
const segs = path2.split("/").filter(Boolean);
|
|
1986
|
-
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
1987
|
-
return { kind: "dynamic", normalizedUrl: path2 };
|
|
1988
|
-
}
|
|
1989
|
-
const exact = apiPathMap.get(path2);
|
|
1990
|
-
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path2 };
|
|
1991
|
-
let bestScore = -1;
|
|
1992
|
-
let bestId = null;
|
|
1993
|
-
for (const r of apiRoutes) {
|
|
1994
|
-
const score = scoreApiRouteMatch(segs, r.segments);
|
|
1995
|
-
if (score > bestScore) {
|
|
1996
|
-
bestScore = score;
|
|
1997
|
-
bestId = r.nodeId;
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
2000
|
-
if (bestId && bestScore > 0) {
|
|
2001
|
-
return { kind: "resolved", nodeId: bestId, normalizedUrl: path2 };
|
|
2002
|
-
}
|
|
2003
|
-
return { kind: "unresolved", normalizedUrl: path2 };
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
// src/server/graph/parsers/crosslayer/fetch-resolver.ts
|
|
2007
|
-
var fetchResolverParser = {
|
|
2008
|
-
id: "fetch-resolver",
|
|
2009
|
-
layer: "crosslayer",
|
|
2010
|
-
concern: "api-binding",
|
|
2011
|
-
detect(_rootDir) {
|
|
2012
|
-
return true;
|
|
2013
|
-
},
|
|
2014
|
-
generate(_rootDir, layerOutputs) {
|
|
2015
|
-
const uiOutput = layerOutputs.get("ui");
|
|
2016
|
-
const apiOutput = layerOutputs.get("api");
|
|
2017
|
-
if (!uiOutput || !apiOutput) {
|
|
2018
|
-
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2019
|
-
}
|
|
2020
|
-
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
2021
|
-
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
2022
|
-
const fetchCallEntries = uiOutput.patterns?.fetch_calls ?? [];
|
|
2023
|
-
if (fetchCallEntries.length === 0) {
|
|
2024
|
-
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2025
|
-
}
|
|
2026
|
-
const includeExternal = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
|
|
2027
|
-
const crossRefs = [];
|
|
2028
|
-
const flaggedEdges = [];
|
|
2029
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2030
|
-
let resolvedCount = 0;
|
|
2031
|
-
let dynamicCount = 0;
|
|
2032
|
-
let unresolvedCount = 0;
|
|
2033
|
-
let externalCount = 0;
|
|
2034
|
-
for (const entry of fetchCallEntries) {
|
|
2035
|
-
for (const call of entry.calls) {
|
|
2036
|
-
const result = resolveFetchCall(call, apiPathMap, apiRoutes);
|
|
2037
|
-
const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
|
|
2038
|
-
if (result.kind === "resolved" && result.nodeId) {
|
|
2039
|
-
const key = `${entry.nodeId}\u2192${result.nodeId}\u2192calls_api`;
|
|
2040
|
-
if (seen.has(key)) continue;
|
|
2041
|
-
seen.add(key);
|
|
2042
|
-
crossRefs.push({
|
|
2043
|
-
source: entry.nodeId,
|
|
2044
|
-
target: result.nodeId,
|
|
2045
|
-
type: "calls_api",
|
|
2046
|
-
layer: "api"
|
|
2047
|
-
});
|
|
2048
|
-
resolvedCount++;
|
|
2049
|
-
continue;
|
|
2050
|
-
}
|
|
2051
|
-
if (result.kind === "dynamic") {
|
|
2052
|
-
dynamicCount++;
|
|
2053
|
-
flaggedEdges.push({
|
|
2054
|
-
source: entry.nodeId,
|
|
2055
|
-
target: "DYNAMIC",
|
|
2056
|
-
type: "calls_api",
|
|
2057
|
-
label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
|
|
2058
|
-
confidence: call.isConcat ? "low" : "medium"
|
|
2059
|
-
});
|
|
2060
|
-
continue;
|
|
2061
|
-
}
|
|
2062
|
-
if (result.kind === "external") {
|
|
2063
|
-
externalCount++;
|
|
2064
|
-
if (!includeExternal) continue;
|
|
2065
|
-
flaggedEdges.push({
|
|
2066
|
-
source: entry.nodeId,
|
|
2067
|
-
target: "EXTERNAL",
|
|
2068
|
-
type: "calls_external",
|
|
2069
|
-
label: `${methodTag} external fetch: ${call.url}`,
|
|
2070
|
-
confidence: "high"
|
|
2071
|
-
});
|
|
2072
|
-
continue;
|
|
2073
|
-
}
|
|
2074
|
-
unresolvedCount++;
|
|
2075
|
-
flaggedEdges.push({
|
|
2076
|
-
source: entry.nodeId,
|
|
2077
|
-
target: "UNRESOLVED",
|
|
2078
|
-
type: "calls_api",
|
|
2079
|
-
label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
|
|
2080
|
-
confidence: "medium"
|
|
2081
|
-
});
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
return {
|
|
2085
|
-
cross_refs: crossRefs,
|
|
2086
|
-
flagged_edges: flaggedEdges,
|
|
2087
|
-
warnings: [],
|
|
2088
|
-
patterns: {
|
|
2089
|
-
api_call_detection: {
|
|
2090
|
-
resolved: resolvedCount,
|
|
2091
|
-
dynamic: dynamicCount,
|
|
2092
|
-
unresolved: unresolvedCount,
|
|
2093
|
-
external: externalCount
|
|
2094
|
-
}
|
|
2095
|
-
}
|
|
2096
|
-
};
|
|
2097
|
-
}
|
|
2098
|
-
};
|
|
2099
|
-
|
|
2100
|
-
// src/server/graph/parsers/crosslayer/api-annotations.ts
|
|
2101
|
-
var import_node_fs7 = require("node:fs");
|
|
2102
|
-
var import_node_path7 = require("node:path");
|
|
2103
|
-
var API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
|
|
2104
|
-
function walk2(dir, exts) {
|
|
2105
|
-
if (!(0, import_node_fs7.existsSync)(dir)) return [];
|
|
2106
|
-
const results = [];
|
|
2107
|
-
for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
|
|
2108
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2109
|
-
const full = (0, import_node_path7.join)(dir, entry.name);
|
|
2110
|
-
if (entry.isDirectory()) {
|
|
2111
|
-
results.push(...walk2(full, exts));
|
|
2112
|
-
} else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
|
|
2113
|
-
results.push(full);
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
return results;
|
|
2117
|
-
}
|
|
2118
|
-
function toNodeId2(srcDir, absPath) {
|
|
2119
|
-
return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
2120
|
-
}
|
|
2121
|
-
var apiAnnotationsParser = {
|
|
2122
|
-
id: "api-annotations",
|
|
2123
|
-
layer: "crosslayer",
|
|
2124
|
-
concern: "api-binding",
|
|
2125
|
-
detect(rootDir) {
|
|
2126
|
-
return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
|
|
2127
|
-
},
|
|
2128
|
-
generate(rootDir, layerOutputs) {
|
|
2129
|
-
const apiOutput = layerOutputs.get("api");
|
|
2130
|
-
if (!apiOutput) {
|
|
2131
|
-
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2132
|
-
}
|
|
2133
|
-
const uiOutput = layerOutputs.get("ui");
|
|
2134
|
-
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
2135
|
-
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
2136
|
-
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
2137
|
-
const srcDir = (0, import_node_path7.join)(rootDir, "src");
|
|
2138
|
-
const files = walk2(srcDir, [".ts", ".tsx"]);
|
|
2139
|
-
const crossRefs = [];
|
|
2140
|
-
const flaggedEdges = [];
|
|
2141
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2142
|
-
for (const absPath of files) {
|
|
2143
|
-
const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
|
|
2144
|
-
const sourceId = toNodeId2(srcDir, absPath);
|
|
2145
|
-
if (!uiNodeIds.has(sourceId)) continue;
|
|
2146
|
-
let match;
|
|
2147
|
-
API_ANNOTATION_RE.lastIndex = 0;
|
|
2148
|
-
while ((match = API_ANNOTATION_RE.exec(content)) !== null) {
|
|
2149
|
-
const method = match[1];
|
|
2150
|
-
const urlPath = match[2];
|
|
2151
|
-
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
2152
|
-
if (result.kind === "resolved" && result.nodeId) {
|
|
2153
|
-
const key = `${sourceId}|${result.nodeId}|calls_api`;
|
|
2154
|
-
if (seen.has(key)) continue;
|
|
2155
|
-
seen.add(key);
|
|
2156
|
-
crossRefs.push({
|
|
2157
|
-
source: sourceId,
|
|
2158
|
-
target: result.nodeId,
|
|
2159
|
-
type: "calls_api",
|
|
2160
|
-
layer: "api"
|
|
2161
|
-
});
|
|
2162
|
-
} else {
|
|
2163
|
-
flaggedEdges.push({
|
|
2164
|
-
source: sourceId,
|
|
2165
|
-
target: "UNRESOLVED",
|
|
2166
|
-
type: "annotation_unresolved",
|
|
2167
|
-
label: `@api ${method} ${urlPath} \u2014 no matching API route found`,
|
|
2168
|
-
confidence: "high"
|
|
2169
|
-
});
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
|
-
}
|
|
2173
|
-
return {
|
|
2174
|
-
cross_refs: crossRefs,
|
|
2175
|
-
flagged_edges: flaggedEdges,
|
|
2176
|
-
warnings: [],
|
|
2177
|
-
patterns: {
|
|
2178
|
-
annotations_found: crossRefs.length + flaggedEdges.length,
|
|
2179
|
-
annotations_resolved: crossRefs.length,
|
|
2180
|
-
annotations_unresolved: flaggedEdges.length
|
|
2181
|
-
}
|
|
2182
|
-
};
|
|
2183
|
-
}
|
|
2184
|
-
};
|
|
2185
|
-
|
|
2186
|
-
// src/server/graph/parsers/crosslayer/url-literal-scanner.ts
|
|
2187
|
-
var import_node_fs8 = require("node:fs");
|
|
2188
|
-
var import_node_path8 = require("node:path");
|
|
2189
|
-
init_config();
|
|
2190
|
-
var URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
|
|
2191
|
-
function walk3(dir, exts) {
|
|
2192
|
-
if (!(0, import_node_fs8.existsSync)(dir)) return [];
|
|
2193
|
-
const results = [];
|
|
2194
|
-
for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
|
|
2195
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2196
|
-
const full = (0, import_node_path8.join)(dir, entry.name);
|
|
2197
|
-
if (entry.isDirectory()) {
|
|
2198
|
-
results.push(...walk3(full, exts));
|
|
2199
|
-
} else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
|
|
2200
|
-
results.push(full);
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
return results;
|
|
2204
|
-
}
|
|
2205
|
-
function toNodeId3(srcDir, absPath) {
|
|
2206
|
-
return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
2207
|
-
}
|
|
2208
|
-
var urlLiteralScannerParser = {
|
|
2209
|
-
id: "url-literal-scanner",
|
|
2210
|
-
layer: "crosslayer",
|
|
2211
|
-
concern: "api-binding",
|
|
2212
|
-
detect(rootDir) {
|
|
2213
|
-
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2214
|
-
return paths !== null;
|
|
2215
|
-
},
|
|
2216
|
-
generate(rootDir, layerOutputs) {
|
|
2217
|
-
const apiOutput = layerOutputs.get("api");
|
|
2218
|
-
if (!apiOutput) {
|
|
2219
|
-
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2220
|
-
}
|
|
2221
|
-
const uiOutput = layerOutputs.get("ui");
|
|
2222
|
-
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
2223
|
-
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
2224
|
-
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
2225
|
-
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2226
|
-
const srcDir = paths.srcDir;
|
|
2227
|
-
const clientDir = (0, import_node_path8.join)(srcDir, "client");
|
|
2228
|
-
const files = [
|
|
2229
|
-
...walk3(clientDir, [".ts", ".tsx"]),
|
|
2230
|
-
...walk3(paths.appDir, [".ts", ".tsx"])
|
|
2231
|
-
];
|
|
2232
|
-
const crossRefs = [];
|
|
2233
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2234
|
-
for (const absPath of files) {
|
|
2235
|
-
const sourceId = toNodeId3(srcDir, absPath);
|
|
2236
|
-
if (!uiNodeIds.has(sourceId)) continue;
|
|
2237
|
-
const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
|
|
2238
|
-
let match;
|
|
2239
|
-
URL_LITERAL_RE.lastIndex = 0;
|
|
2240
|
-
while ((match = URL_LITERAL_RE.exec(content)) !== null) {
|
|
2241
|
-
const urlPath = match[1];
|
|
2242
|
-
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
2243
|
-
if (result.kind === "resolved" && result.nodeId) {
|
|
2244
|
-
const key = `${sourceId}|${result.nodeId}|references_api`;
|
|
2245
|
-
if (seen.has(key)) continue;
|
|
2246
|
-
seen.add(key);
|
|
2247
|
-
crossRefs.push({
|
|
2248
|
-
source: sourceId,
|
|
2249
|
-
target: result.nodeId,
|
|
2250
|
-
type: "references_api",
|
|
2251
|
-
layer: "api"
|
|
2252
|
-
});
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
}
|
|
2256
|
-
return {
|
|
2257
|
-
cross_refs: crossRefs,
|
|
2258
|
-
flagged_edges: [],
|
|
2259
|
-
warnings: [],
|
|
2260
|
-
patterns: {
|
|
2261
|
-
url_literals_resolved: crossRefs.length
|
|
2262
|
-
}
|
|
2263
|
-
};
|
|
2264
|
-
}
|
|
2265
|
-
};
|
|
2266
|
-
|
|
2267
|
-
// src/server/graph/parsers/static/static-values.ts
|
|
2268
|
-
var import_node_fs9 = require("node:fs");
|
|
2269
|
-
var import_node_path9 = require("node:path");
|
|
2270
|
-
var parseCode = null;
|
|
2271
|
-
function tryLoadTreeSitter() {
|
|
2272
|
-
if (parseCode) return true;
|
|
2273
|
-
try {
|
|
2274
|
-
const extractor = (init_ts_extractor(), __toCommonJS(ts_extractor_exports));
|
|
2275
|
-
if (typeof extractor.parseCodeTS === "function") {
|
|
2276
|
-
parseCode = extractor.parseCodeTS;
|
|
2277
|
-
return true;
|
|
2278
|
-
}
|
|
2279
|
-
} catch {
|
|
2280
|
-
}
|
|
2281
|
-
return false;
|
|
2282
|
-
}
|
|
2283
|
-
var SHARED_MODELS = /* @__PURE__ */ new Set(["permission", "role", "tag"]);
|
|
2284
|
-
var DB_MODELS = /* @__PURE__ */ new Set(["subscriptionPlan", "providerDefinition", "pipelineMasterTemplate"]);
|
|
2285
|
-
function classifyScope(source, model) {
|
|
2286
|
-
if (source.includes("prisma/schema.prisma")) return "shared";
|
|
2287
|
-
if (source.includes("prisma/seed") && model) {
|
|
2288
|
-
if (SHARED_MODELS.has(model)) return "shared";
|
|
2289
|
-
if (DB_MODELS.has(model)) return "db";
|
|
2290
|
-
return "shared";
|
|
2291
|
-
}
|
|
2292
|
-
if (source.startsWith("src/client/") || source.startsWith("src/app/")) return "fe";
|
|
2293
|
-
if (source.startsWith("src/server/")) return "be";
|
|
2294
|
-
if (source.startsWith("src/config/")) return "be";
|
|
2295
|
-
if (source.startsWith("src/lib/")) return "shared";
|
|
2296
|
-
return "shared";
|
|
2297
|
-
}
|
|
2298
|
-
function extractEnumValues(rootDir) {
|
|
2299
|
-
const nodes = [];
|
|
2300
|
-
const edges = [];
|
|
2301
|
-
const schemaPaths = [
|
|
2302
|
-
(0, import_node_path9.join)(rootDir, "prisma", "schema.prisma"),
|
|
2303
|
-
(0, import_node_path9.join)(rootDir, "prisma", "schema")
|
|
2304
|
-
];
|
|
2305
|
-
let content = "";
|
|
2306
|
-
for (const p of schemaPaths) {
|
|
2307
|
-
if ((0, import_node_fs9.existsSync)(p)) {
|
|
2308
|
-
try {
|
|
2309
|
-
const stat = (0, import_node_fs9.statSync)(p);
|
|
2310
|
-
if (stat.isFile()) {
|
|
2311
|
-
content = (0, import_node_fs9.readFileSync)(p, "utf-8");
|
|
2312
|
-
} else if (stat.isDirectory()) {
|
|
2313
|
-
const files = (0, import_node_fs9.readdirSync)(p).filter((f) => f.endsWith(".prisma"));
|
|
2314
|
-
content = files.map((f) => (0, import_node_fs9.readFileSync)((0, import_node_path9.join)(p, f), "utf-8")).join("\n");
|
|
2315
|
-
}
|
|
2316
|
-
} catch {
|
|
2317
|
-
continue;
|
|
2318
|
-
}
|
|
2319
|
-
break;
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
if (!content) return { nodes, edges };
|
|
2323
|
-
const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
|
|
2324
|
-
let m;
|
|
2325
|
-
while ((m = enumRe.exec(content)) !== null) {
|
|
2326
|
-
const enumName = m[1];
|
|
2327
|
-
const body = m[2];
|
|
2328
|
-
const values = body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
|
|
2329
|
-
const scope = classifyScope("prisma/schema.prisma");
|
|
2330
|
-
for (const val of values) {
|
|
2331
|
-
const nodeId = `${enumName}.${val}`;
|
|
2332
|
-
nodes.push({
|
|
2333
|
-
id: nodeId,
|
|
2334
|
-
type: "enum_value",
|
|
2335
|
-
name: val,
|
|
2336
|
-
parentEnum: enumName,
|
|
2337
|
-
value: val,
|
|
2338
|
-
scope,
|
|
2339
|
-
source: "prisma/schema.prisma"
|
|
2340
|
-
});
|
|
2341
|
-
edges.push({
|
|
2342
|
-
source: nodeId,
|
|
2343
|
-
target: `enum:${enumName}`,
|
|
2344
|
-
type: "member_of"
|
|
2345
|
-
});
|
|
2346
|
-
}
|
|
2347
|
-
nodes.push({
|
|
2348
|
-
id: `enum:${enumName}`,
|
|
2349
|
-
type: "enum_group",
|
|
2350
|
-
name: enumName,
|
|
2351
|
-
valueCount: values.length,
|
|
2352
|
-
scope,
|
|
2353
|
-
source: "prisma/schema.prisma"
|
|
2354
|
-
});
|
|
2355
|
-
}
|
|
2356
|
-
return { nodes, edges };
|
|
2357
|
-
}
|
|
2358
|
-
function extractPropsFromObjectNode(node) {
|
|
2359
|
-
const props = {};
|
|
2360
|
-
for (const child of node.namedChildren) {
|
|
2361
|
-
if (child.type !== "pair") continue;
|
|
2362
|
-
const keyNode = child.childForFieldName("key");
|
|
2363
|
-
const valNode = child.childForFieldName("value");
|
|
2364
|
-
if (!keyNode || !valNode) continue;
|
|
2365
|
-
const key = keyNode.type === "property_identifier" ? keyNode.text : keyNode.text.replace(/['"]/g, "");
|
|
2366
|
-
if (valNode.type === "string" || valNode.type === "template_string") {
|
|
2367
|
-
const fragment = valNode.namedChildren.find((c) => c.type === "string_fragment" || c.type === "template_substitution");
|
|
2368
|
-
props[key] = fragment ? fragment.text : valNode.text.replace(/^['"`]|['"`]$/g, "");
|
|
2369
|
-
} else if (valNode.type === "number") {
|
|
2370
|
-
props[key] = valNode.text;
|
|
2371
|
-
} else if (valNode.type === "true" || valNode.type === "false") {
|
|
2372
|
-
props[key] = valNode.text;
|
|
2373
|
-
}
|
|
2374
|
-
}
|
|
2375
|
-
return props;
|
|
2376
|
-
}
|
|
2377
|
-
function extractStringArrayFromNode(node) {
|
|
2378
|
-
const values = [];
|
|
2379
|
-
for (const child of node.namedChildren) {
|
|
2380
|
-
if (child.type === "string") {
|
|
2381
|
-
const frag = child.namedChildren.find((c) => c.type === "string_fragment");
|
|
2382
|
-
if (frag) values.push(frag.text);
|
|
2383
|
-
}
|
|
2384
|
-
}
|
|
2385
|
-
return values;
|
|
2386
|
-
}
|
|
2387
|
-
function findArrayDecl(root, varName) {
|
|
2388
|
-
function walk5(node) {
|
|
2389
|
-
if (node.type === "variable_declarator") {
|
|
2390
|
-
const nameNode = node.childForFieldName("name");
|
|
2391
|
-
const valueNode = node.childForFieldName("value");
|
|
2392
|
-
if (nameNode?.text === varName && valueNode?.type === "array") {
|
|
2393
|
-
return valueNode;
|
|
2394
|
-
}
|
|
2395
|
-
if (nameNode?.text === varName && valueNode?.type === "as_expression") {
|
|
2396
|
-
const inner = valueNode.namedChildren.find((c) => c.type === "array");
|
|
2397
|
-
if (inner) return inner;
|
|
2398
|
-
}
|
|
2399
|
-
}
|
|
2400
|
-
for (const child of node.namedChildren) {
|
|
2401
|
-
const found = walk5(child);
|
|
2402
|
-
if (found) return found;
|
|
2403
|
-
}
|
|
2404
|
-
return null;
|
|
2405
|
-
}
|
|
2406
|
-
return walk5(root);
|
|
2407
|
-
}
|
|
2408
|
-
function extractObjectPropsRegex(objStr) {
|
|
2409
|
-
const props = {};
|
|
2410
|
-
const propRe = /(\w+):\s*['"]([^'"]*)['"]/g;
|
|
2411
|
-
let m;
|
|
2412
|
-
while ((m = propRe.exec(objStr)) !== null) props[m[1]] = m[2];
|
|
2413
|
-
const numRe = /(\w+):\s*(-?\d+(?:\.\d+)?)\s*[,\n}]/g;
|
|
2414
|
-
while ((m = numRe.exec(objStr)) !== null) if (!props[m[1]]) props[m[1]] = m[2];
|
|
2415
|
-
return props;
|
|
2416
|
-
}
|
|
2417
|
-
function extractStringArrayRegex(arrStr) {
|
|
2418
|
-
return (arrStr.match(/'([^']+)'/g) ?? []).map((s) => s.replace(/'/g, ""));
|
|
2419
|
-
}
|
|
2420
|
-
function splitArrayObjectsRegex(arrayBody) {
|
|
2421
|
-
const objects = [];
|
|
2422
|
-
let depth = 0;
|
|
2423
|
-
let start = -1;
|
|
2424
|
-
for (let i = 0; i < arrayBody.length; i++) {
|
|
2425
|
-
if (arrayBody[i] === "{") {
|
|
2426
|
-
if (depth === 0) start = i;
|
|
2427
|
-
depth++;
|
|
2428
|
-
} else if (arrayBody[i] === "}") {
|
|
2429
|
-
depth--;
|
|
2430
|
-
if (depth === 0 && start >= 0) {
|
|
2431
|
-
objects.push(arrayBody.slice(start, i + 1));
|
|
2432
|
-
start = -1;
|
|
2433
|
-
}
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
return objects;
|
|
2437
|
-
}
|
|
2438
|
-
function detectSeededArrays(content, sourceFile) {
|
|
2439
|
-
const results = [];
|
|
2440
|
-
const forOfRe = /for\s*\(\s*const\s+\w+\s+of\s+(\w+)\s*\)\s*\{/g;
|
|
2441
|
-
let fm;
|
|
2442
|
-
while ((fm = forOfRe.exec(content)) !== null) {
|
|
2443
|
-
const arrayName = fm[1];
|
|
2444
|
-
const lookahead = content.slice(fm.index + fm[0].length, fm.index + fm[0].length + 500);
|
|
2445
|
-
const prismaMatch = lookahead.match(/prisma\.(\w+)\.(create|upsert|update|createMany|findFirst)/);
|
|
2446
|
-
if (!prismaMatch) continue;
|
|
2447
|
-
results.push({ arrayName, prismaModel: prismaMatch[1], sourceFile });
|
|
2448
|
-
}
|
|
2449
|
-
return results;
|
|
2450
|
-
}
|
|
2451
|
-
function pickIdField(props) {
|
|
2452
|
-
for (const key of ["key", "slug", "id", "name", "templateId"]) {
|
|
2453
|
-
if (props[key]) return props[key];
|
|
2454
|
-
}
|
|
2455
|
-
return null;
|
|
2456
|
-
}
|
|
2457
|
-
function pickNameField(props) {
|
|
2458
|
-
for (const key of ["name", "slug", "key", "id"]) {
|
|
2459
|
-
if (props[key]) return props[key];
|
|
2460
|
-
}
|
|
2461
|
-
return null;
|
|
2462
|
-
}
|
|
2463
|
-
function modelToNodeType(model) {
|
|
2464
|
-
return `seed_${model.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "")}`;
|
|
2465
|
-
}
|
|
2466
|
-
function extractSeedData(rootDir) {
|
|
2467
|
-
const nodes = [];
|
|
2468
|
-
const edges = [];
|
|
2469
|
-
const seedFiles = [
|
|
2470
|
-
(0, import_node_path9.join)(rootDir, "prisma", "seed.ts"),
|
|
2471
|
-
(0, import_node_path9.join)(rootDir, "prisma", "seed.js"),
|
|
2472
|
-
(0, import_node_path9.join)(rootDir, "src", "server", "lib", "system-tags.ts")
|
|
2473
|
-
].filter(import_node_fs9.existsSync);
|
|
2474
|
-
const useTreeSitter = tryLoadTreeSitter();
|
|
2475
|
-
for (const filePath of seedFiles) {
|
|
2476
|
-
const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
|
|
2477
|
-
const relPath = (0, import_node_path9.relative)(rootDir, filePath);
|
|
2478
|
-
const seeded = detectSeededArrays(content, relPath);
|
|
2479
|
-
let astRoot = null;
|
|
2480
|
-
if (useTreeSitter && parseCode) {
|
|
2481
|
-
try {
|
|
2482
|
-
const tree = parseCode(content);
|
|
2483
|
-
astRoot = tree.rootNode;
|
|
2484
|
-
} catch {
|
|
2485
|
-
}
|
|
2486
|
-
}
|
|
2487
|
-
for (const { arrayName, prismaModel, sourceFile } of seeded) {
|
|
2488
|
-
const nodeType = modelToNodeType(prismaModel);
|
|
2489
|
-
const scope = classifyScope(sourceFile, prismaModel);
|
|
2490
|
-
if (astRoot) {
|
|
2491
|
-
const arrayNode = findArrayDecl(astRoot, arrayName);
|
|
2492
|
-
if (!arrayNode) continue;
|
|
2493
|
-
for (const child of arrayNode.namedChildren) {
|
|
2494
|
-
if (child.type !== "object") continue;
|
|
2495
|
-
const props = extractPropsFromObjectNode(child);
|
|
2496
|
-
const idVal = pickIdField(props);
|
|
2497
|
-
if (!idVal) continue;
|
|
2498
|
-
const nameVal = pickNameField(props) ?? idVal;
|
|
2499
|
-
const nodeId = `seed:${prismaModel}:${idVal}`;
|
|
2500
|
-
const { scope: _seedScope, ...safeProps } = props;
|
|
2501
|
-
nodes.push({
|
|
2502
|
-
id: nodeId,
|
|
2503
|
-
type: nodeType,
|
|
2504
|
-
name: nameVal,
|
|
2505
|
-
value: idVal,
|
|
2506
|
-
model: prismaModel,
|
|
2507
|
-
...safeProps,
|
|
2508
|
-
seedScope: _seedScope ?? void 0,
|
|
2509
|
-
// preserve as seedScope
|
|
2510
|
-
scope,
|
|
2511
|
-
source: sourceFile
|
|
2512
|
-
});
|
|
2513
|
-
const permsArrayNode = child.namedChildren.filter((c) => c.type === "pair").find((c) => c.childForFieldName("key")?.text === "permissions");
|
|
2514
|
-
if (permsArrayNode) {
|
|
2515
|
-
const valNode = permsArrayNode.childForFieldName("value");
|
|
2516
|
-
if (valNode?.type === "array") {
|
|
2517
|
-
const permKeys = extractStringArrayFromNode(valNode);
|
|
2518
|
-
for (const pk of permKeys) {
|
|
2519
|
-
if (pk === "*") continue;
|
|
2520
|
-
edges.push({ source: nodeId, target: `seed:permission:${pk}`, type: "grants" });
|
|
2521
|
-
}
|
|
2522
|
-
}
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
} else {
|
|
2526
|
-
const constRe = new RegExp(`const\\s+${arrayName}\\s*(?::[^=]+)?=\\s*\\[`, "g");
|
|
2527
|
-
const cm = constRe.exec(content);
|
|
2528
|
-
if (!cm) continue;
|
|
2529
|
-
const bracketStart = cm.index + cm[0].length - 1;
|
|
2530
|
-
let depth = 1, i = bracketStart + 1;
|
|
2531
|
-
while (i < content.length && depth > 0) {
|
|
2532
|
-
if (content[i] === "[") depth++;
|
|
2533
|
-
else if (content[i] === "]") depth--;
|
|
2534
|
-
i++;
|
|
2535
|
-
}
|
|
2536
|
-
if (depth !== 0) continue;
|
|
2537
|
-
const body = content.slice(bracketStart + 1, i - 1);
|
|
2538
|
-
const objects = splitArrayObjectsRegex(body);
|
|
2539
|
-
for (const objStr of objects) {
|
|
2540
|
-
const props = extractObjectPropsRegex(objStr);
|
|
2541
|
-
const idVal = pickIdField(props);
|
|
2542
|
-
if (!idVal) continue;
|
|
2543
|
-
const nameVal = pickNameField(props) ?? idVal;
|
|
2544
|
-
const nodeId = `seed:${prismaModel}:${idVal}`;
|
|
2545
|
-
const { scope: _rScope, ...rSafeProps } = props;
|
|
2546
|
-
nodes.push({
|
|
2547
|
-
id: nodeId,
|
|
2548
|
-
type: nodeType,
|
|
2549
|
-
name: nameVal,
|
|
2550
|
-
value: idVal,
|
|
2551
|
-
model: prismaModel,
|
|
2552
|
-
...rSafeProps,
|
|
2553
|
-
seedScope: _rScope ?? void 0,
|
|
2554
|
-
scope,
|
|
2555
|
-
source: sourceFile
|
|
2556
|
-
});
|
|
2557
|
-
const permArrayMatch = objStr.match(/permissions:\s*\[([^\]]*)\]/);
|
|
2558
|
-
if (permArrayMatch) {
|
|
2559
|
-
for (const pk of extractStringArrayRegex(permArrayMatch[1])) {
|
|
2560
|
-
if (pk === "*") continue;
|
|
2561
|
-
edges.push({ source: nodeId, target: `seed:permission:${pk}`, type: "grants" });
|
|
2562
|
-
}
|
|
2563
|
-
}
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
}
|
|
2567
|
-
}
|
|
2568
|
-
return { nodes, edges };
|
|
2569
|
-
}
|
|
2570
|
-
function walkDir(dir, exts) {
|
|
2571
|
-
if (!(0, import_node_fs9.existsSync)(dir)) return [];
|
|
2572
|
-
const results = [];
|
|
2573
|
-
for (const entry of (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true })) {
|
|
2574
|
-
if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
|
|
2575
|
-
const full = (0, import_node_path9.join)(dir, entry.name);
|
|
2576
|
-
if (entry.isDirectory()) results.push(...walkDir(full, exts));
|
|
2577
|
-
else if (exts.some((ext) => entry.name.endsWith(ext))) results.push(full);
|
|
2578
|
-
}
|
|
2579
|
-
return results;
|
|
2580
|
-
}
|
|
2581
|
-
function extractConstants(rootDir) {
|
|
2582
|
-
const nodes = [];
|
|
2583
|
-
const srcDir = (0, import_node_path9.join)(rootDir, "src");
|
|
2584
|
-
if (!(0, import_node_fs9.existsSync)(srcDir)) return { nodes };
|
|
2585
|
-
for (const filePath of walkDir(srcDir, [".ts", ".tsx"])) {
|
|
2586
|
-
const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
|
|
2587
|
-
const relPath = (0, import_node_path9.relative)(rootDir, filePath);
|
|
2588
|
-
const constArrayRe = /export\s+const\s+([A-Z][A-Z_0-9]+)\s*(?::[^=]+)?\s*=\s*\[/g;
|
|
2589
|
-
let cm;
|
|
2590
|
-
while ((cm = constArrayRe.exec(content)) !== null) {
|
|
2591
|
-
const constName = cm[1];
|
|
2592
|
-
const bracketStart = cm.index + cm[0].length - 1;
|
|
2593
|
-
let depth = 1, i = bracketStart + 1;
|
|
2594
|
-
while (i < content.length && depth > 0) {
|
|
2595
|
-
if (content[i] === "[") depth++;
|
|
2596
|
-
else if (content[i] === "]") depth--;
|
|
2597
|
-
i++;
|
|
2598
|
-
}
|
|
2599
|
-
if (depth !== 0) continue;
|
|
2600
|
-
const body = content.slice(bracketStart + 1, i - 1);
|
|
2601
|
-
const stringValues = extractStringArrayRegex(body);
|
|
2602
|
-
const objectCount = splitArrayObjectsRegex(body).length;
|
|
2603
|
-
const valueCount = Math.max(stringValues.length, objectCount);
|
|
2604
|
-
if (valueCount < 2) continue;
|
|
2605
|
-
const scope = classifyScope(relPath);
|
|
2606
|
-
nodes.push({
|
|
2607
|
-
id: `const:${constName}`,
|
|
2608
|
-
type: "constant",
|
|
2609
|
-
name: constName,
|
|
2610
|
-
valueCount,
|
|
2611
|
-
values: stringValues.length > 0 && stringValues.length <= 30 ? stringValues : void 0,
|
|
2612
|
-
scope,
|
|
2613
|
-
source: relPath
|
|
2614
|
-
});
|
|
2615
|
-
}
|
|
2616
|
-
}
|
|
2617
|
-
return { nodes };
|
|
2618
|
-
}
|
|
2619
|
-
function detect4(rootDir) {
|
|
2620
|
-
return (0, import_node_fs9.existsSync)((0, import_node_path9.join)(rootDir, "prisma", "schema.prisma")) || (0, import_node_fs9.existsSync)((0, import_node_path9.join)(rootDir, "prisma", "seed.ts"));
|
|
2621
|
-
}
|
|
2622
|
-
function generate4(rootDir) {
|
|
2623
|
-
const enumResult = extractEnumValues(rootDir);
|
|
2624
|
-
const seedResult = extractSeedData(rootDir);
|
|
2625
|
-
const constResult = extractConstants(rootDir);
|
|
2626
|
-
const allNodes = [...enumResult.nodes, ...seedResult.nodes, ...constResult.nodes];
|
|
2627
|
-
const allEdges = [...enumResult.edges, ...seedResult.edges];
|
|
2628
|
-
const typeOrder = {
|
|
2629
|
-
enum_group: 0,
|
|
2630
|
-
enum_value: 1,
|
|
2631
|
-
seed_permission: 2,
|
|
2632
|
-
seed_role: 3,
|
|
2633
|
-
seed_tag: 4,
|
|
2634
|
-
seed_plan: 5,
|
|
2635
|
-
seed_provider: 6,
|
|
2636
|
-
constant: 7
|
|
2637
|
-
};
|
|
2638
|
-
allNodes.sort((a, b) => {
|
|
2639
|
-
const ta = typeOrder[a.type] ?? 8;
|
|
2640
|
-
const tb = typeOrder[b.type] ?? 8;
|
|
2641
|
-
if (ta !== tb) return ta - tb;
|
|
2642
|
-
return a.name.localeCompare(b.name);
|
|
2643
|
-
});
|
|
2644
|
-
const enumGroups = allNodes.filter((n) => n.type === "enum_group").length;
|
|
2645
|
-
const enumValues = allNodes.filter((n) => n.type === "enum_value").length;
|
|
2646
|
-
const seedNodes = allNodes.filter((n) => n.type.startsWith("seed_")).length;
|
|
2647
|
-
const constNodes = allNodes.filter((n) => n.type === "constant").length;
|
|
2648
|
-
const byScope = {};
|
|
2649
|
-
for (const n of allNodes) {
|
|
2650
|
-
const s = n.scope ?? "unknown";
|
|
2651
|
-
byScope[s] = (byScope[s] ?? 0) + 1;
|
|
2652
|
-
}
|
|
2653
|
-
return {
|
|
2654
|
-
metadata: {
|
|
2655
|
-
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
2656
|
-
scope: "static-values",
|
|
2657
|
-
layer: "static",
|
|
2658
|
-
enum_groups: enumGroups,
|
|
2659
|
-
enum_values: enumValues,
|
|
2660
|
-
seed_records: seedNodes,
|
|
2661
|
-
constants: constNodes,
|
|
2662
|
-
total_nodes: allNodes.length,
|
|
2663
|
-
total_edges: allEdges.length,
|
|
2664
|
-
parser: tryLoadTreeSitter() ? "tree-sitter" : "regex-fallback"
|
|
2665
|
-
},
|
|
2666
|
-
nodes: allNodes,
|
|
2667
|
-
edges: allEdges,
|
|
2668
|
-
cross_refs: [],
|
|
2669
|
-
contradictions: [],
|
|
2670
|
-
warnings: [],
|
|
2671
|
-
flagged_edges: [],
|
|
2672
|
-
patterns: {
|
|
2673
|
-
by_type: {
|
|
2674
|
-
enum_group: enumGroups,
|
|
2675
|
-
enum_value: enumValues,
|
|
2676
|
-
seed_permission: allNodes.filter((n) => n.type === "seed_permission").length,
|
|
2677
|
-
seed_role: allNodes.filter((n) => n.type === "seed_role").length,
|
|
2678
|
-
seed_tag: allNodes.filter((n) => n.type === "seed_tag").length,
|
|
2679
|
-
seed_plan: allNodes.filter((n) => n.type === "seed_plan").length,
|
|
2680
|
-
seed_provider: allNodes.filter((n) => n.type === "seed_provider").length,
|
|
2681
|
-
constant: constNodes
|
|
2682
|
-
},
|
|
2683
|
-
by_scope: byScope
|
|
2684
|
-
}
|
|
2685
|
-
};
|
|
2686
|
-
}
|
|
2687
|
-
var staticValuesParser = {
|
|
2688
|
-
id: "static-values",
|
|
2689
|
-
layer: "static",
|
|
2690
|
-
detect: detect4,
|
|
2691
|
-
generate: generate4
|
|
2692
|
-
};
|
|
2693
|
-
|
|
2694
|
-
// src/server/graph/parsers/crosslayer/static-ref-scanner.ts
|
|
2695
|
-
var import_node_fs10 = require("node:fs");
|
|
2696
|
-
var import_node_path10 = require("node:path");
|
|
2697
|
-
init_config();
|
|
2698
|
-
function walk4(dir, exts) {
|
|
2699
|
-
if (!(0, import_node_fs10.existsSync)(dir)) return [];
|
|
2700
|
-
const results = [];
|
|
2701
|
-
function recurse(d) {
|
|
2702
|
-
for (const entry of (0, import_node_fs10.readdirSync)(d, { withFileTypes: true })) {
|
|
2703
|
-
const full = (0, import_node_path10.join)(d, entry.name);
|
|
2704
|
-
if (entry.isDirectory()) {
|
|
2705
|
-
if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
|
|
2706
|
-
recurse(full);
|
|
2707
|
-
} else if (exts.some((ext) => entry.name.endsWith(ext))) {
|
|
2708
|
-
results.push(full);
|
|
2709
|
-
}
|
|
2710
|
-
}
|
|
2711
|
-
}
|
|
2712
|
-
recurse(dir);
|
|
2713
|
-
return results;
|
|
2714
|
-
}
|
|
2715
|
-
var MIN_VALUE_LENGTH = 4;
|
|
2716
|
-
var SKIP_VALUES = /* @__PURE__ */ new Set([
|
|
2717
|
-
"true",
|
|
2718
|
-
"false",
|
|
2719
|
-
"null",
|
|
2720
|
-
"undefined",
|
|
2721
|
-
"none",
|
|
2722
|
-
"default",
|
|
2723
|
-
"name",
|
|
2724
|
-
"type",
|
|
2725
|
-
"data",
|
|
2726
|
-
"text",
|
|
2727
|
-
"info",
|
|
2728
|
-
"error",
|
|
2729
|
-
"open",
|
|
2730
|
-
"read",
|
|
2731
|
-
"user",
|
|
2732
|
-
"test",
|
|
2733
|
-
"json",
|
|
2734
|
-
"form"
|
|
2735
|
-
]);
|
|
2736
|
-
function isInCommentOrType(node) {
|
|
2737
|
-
let current = node.parent;
|
|
2738
|
-
while (current) {
|
|
2739
|
-
if (current.type === "comment" || current.type === "type_annotation" || current.type === "type_alias_declaration" || current.type === "interface_declaration" || current.type === "jsdoc") {
|
|
2740
|
-
return true;
|
|
2741
|
-
}
|
|
2742
|
-
current = current.parent;
|
|
2743
|
-
}
|
|
2744
|
-
return false;
|
|
2745
|
-
}
|
|
2746
|
-
function collectStaticRefs(root, valueLookup) {
|
|
2747
|
-
const refs = [];
|
|
2748
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2749
|
-
function visit(node) {
|
|
2750
|
-
if (node.type === "string_fragment") {
|
|
2751
|
-
const val = node.text;
|
|
2752
|
-
if (val.length >= MIN_VALUE_LENGTH && !SKIP_VALUES.has(val.toLowerCase())) {
|
|
2753
|
-
const targets = valueLookup.get(val);
|
|
2754
|
-
if (targets && !isInCommentOrType(node)) {
|
|
2755
|
-
for (const t of targets) {
|
|
2756
|
-
const key = t;
|
|
2757
|
-
if (!seen.has(key)) {
|
|
2758
|
-
seen.add(key);
|
|
2759
|
-
refs.push({ value: val, targetIds: [t] });
|
|
2760
|
-
}
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
}
|
|
2764
|
-
}
|
|
2765
|
-
if (node.type === "member_expression") {
|
|
2766
|
-
const objNode = node.namedChildren.find((c) => c.type === "identifier");
|
|
2767
|
-
const propNode = node.namedChildren.find((c) => c.type === "property_identifier");
|
|
2768
|
-
if (objNode && propNode) {
|
|
2769
|
-
const combined = `${objNode.text}.${propNode.text}`;
|
|
2770
|
-
const targets = valueLookup.get(propNode.text);
|
|
2771
|
-
const directTarget = valueLookup.get(combined);
|
|
2772
|
-
if (directTarget && !isInCommentOrType(node)) {
|
|
2773
|
-
for (const t of directTarget) {
|
|
2774
|
-
if (!seen.has(t)) {
|
|
2775
|
-
seen.add(t);
|
|
2776
|
-
refs.push({ value: combined, targetIds: [t] });
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
2779
|
-
} else if (targets && !isInCommentOrType(node)) {
|
|
2780
|
-
for (const t of targets) {
|
|
2781
|
-
if (!seen.has(t)) {
|
|
2782
|
-
seen.add(t);
|
|
2783
|
-
refs.push({ value: propNode.text, targetIds: [t] });
|
|
2784
|
-
}
|
|
2785
|
-
}
|
|
2786
|
-
}
|
|
2787
|
-
}
|
|
2788
|
-
}
|
|
2789
|
-
for (const child of node.namedChildren) {
|
|
2790
|
-
visit(child);
|
|
2791
|
-
}
|
|
2792
|
-
}
|
|
2793
|
-
visit(root);
|
|
2794
|
-
return refs;
|
|
2795
|
-
}
|
|
2796
|
-
function collectStaticRefsRegex(content, valueLookup, allValues) {
|
|
2797
|
-
const refs = [];
|
|
2798
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2799
|
-
const escaped = allValues.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
2800
|
-
const pattern = new RegExp(
|
|
2801
|
-
`(?:['"\`])(${escaped.join("|")})(?:['"\`])|\\b(\\w+)\\.(${escaped.join("|")})\\b`,
|
|
2802
|
-
"g"
|
|
2803
|
-
);
|
|
2804
|
-
let match;
|
|
2805
|
-
pattern.lastIndex = 0;
|
|
2806
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
2807
|
-
const val = match[1] ?? match[3];
|
|
2808
|
-
if (!val) continue;
|
|
2809
|
-
const targets = valueLookup.get(val);
|
|
2810
|
-
if (!targets) continue;
|
|
2811
|
-
for (const t of targets) {
|
|
2812
|
-
if (!seen.has(t)) {
|
|
2813
|
-
seen.add(t);
|
|
2814
|
-
refs.push({ value: val, targetIds: [t] });
|
|
2815
|
-
}
|
|
2816
|
-
}
|
|
2817
|
-
}
|
|
2818
|
-
return refs;
|
|
2819
|
-
}
|
|
2820
|
-
var staticRefScannerParser = {
|
|
2821
|
-
id: "static-ref-scanner",
|
|
2822
|
-
layer: "crosslayer",
|
|
2823
|
-
concern: "static-ref",
|
|
2824
|
-
detect(rootDir) {
|
|
2825
|
-
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2826
|
-
return paths !== null;
|
|
2827
|
-
},
|
|
2828
|
-
generate(rootDir, layerOutputs) {
|
|
2829
|
-
const staticOutput = layerOutputs.get("static");
|
|
2830
|
-
if (!staticOutput || staticOutput.nodes.length === 0) {
|
|
2831
|
-
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2832
|
-
}
|
|
2833
|
-
const valueLookup = /* @__PURE__ */ new Map();
|
|
2834
|
-
for (const node of staticOutput.nodes) {
|
|
2835
|
-
const type = node.type;
|
|
2836
|
-
let valueStr = null;
|
|
2837
|
-
if (type === "enum_value") {
|
|
2838
|
-
valueStr = node.value;
|
|
2839
|
-
const fullId = node.id;
|
|
2840
|
-
if (!valueLookup.has(fullId)) valueLookup.set(fullId, []);
|
|
2841
|
-
valueLookup.get(fullId).push(node.id);
|
|
2842
|
-
} else if (type.startsWith("seed_")) {
|
|
2843
|
-
valueStr = node.value;
|
|
2844
|
-
}
|
|
2845
|
-
if (!valueStr || valueStr.length < MIN_VALUE_LENGTH || SKIP_VALUES.has(valueStr.toLowerCase())) continue;
|
|
2846
|
-
if (!valueLookup.has(valueStr)) valueLookup.set(valueStr, []);
|
|
2847
|
-
valueLookup.get(valueStr).push(node.id);
|
|
2848
|
-
}
|
|
2849
|
-
const allValues = [...valueLookup.keys()].sort((a, b) => b.length - a.length);
|
|
2850
|
-
if (allValues.length === 0) {
|
|
2851
|
-
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2852
|
-
}
|
|
2853
|
-
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2854
|
-
if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2855
|
-
const srcDir = paths.srcDir;
|
|
2856
|
-
const files = [
|
|
2857
|
-
...walk4((0, import_node_path10.join)(srcDir, "client"), [".ts", ".tsx"]),
|
|
2858
|
-
...walk4(paths.appDir, [".ts", ".tsx"]),
|
|
2859
|
-
...walk4((0, import_node_path10.join)(srcDir, "server"), [".ts", ".tsx"]),
|
|
2860
|
-
...walk4((0, import_node_path10.join)(srcDir, "lib"), [".ts", ".tsx"]),
|
|
2861
|
-
...walk4((0, import_node_path10.join)(srcDir, "config"), [".ts", ".tsx"])
|
|
2862
|
-
];
|
|
2863
|
-
const uiOutput = layerOutputs.get("ui");
|
|
2864
|
-
const apiOutput = layerOutputs.get("api");
|
|
2865
|
-
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
2866
|
-
const apiNodeIds = new Set(apiOutput?.nodes.map((n) => n.id) ?? []);
|
|
2867
|
-
let parseCode2 = null;
|
|
2868
|
-
try {
|
|
2869
|
-
const extractor = (init_ts_extractor(), __toCommonJS(ts_extractor_exports));
|
|
2870
|
-
if (typeof extractor.parseCodeTS === "function") {
|
|
2871
|
-
parseCode2 = extractor.parseCodeTS;
|
|
2872
|
-
}
|
|
2873
|
-
} catch {
|
|
2874
|
-
}
|
|
2875
|
-
const crossRefs = [];
|
|
2876
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2877
|
-
let filesScanned = 0;
|
|
2878
|
-
for (const absPath of files) {
|
|
2879
|
-
const sourceId = (0, import_node_path10.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
2880
|
-
const sourceLayer = uiNodeIds.has(sourceId) ? "ui" : apiNodeIds.has(sourceId) ? "api" : null;
|
|
2881
|
-
if (!sourceLayer) continue;
|
|
2882
|
-
const content = (0, import_node_fs10.readFileSync)(absPath, "utf-8");
|
|
2883
|
-
filesScanned++;
|
|
2884
|
-
let fileRefs;
|
|
2885
|
-
if (parseCode2) {
|
|
2886
|
-
try {
|
|
2887
|
-
const tree = parseCode2(content);
|
|
2888
|
-
fileRefs = collectStaticRefs(tree.rootNode, valueLookup);
|
|
2889
|
-
} catch {
|
|
2890
|
-
fileRefs = collectStaticRefsRegex(content, valueLookup, allValues);
|
|
2891
|
-
}
|
|
2892
|
-
} else {
|
|
2893
|
-
fileRefs = collectStaticRefsRegex(content, valueLookup, allValues);
|
|
2894
|
-
}
|
|
2895
|
-
for (const ref of fileRefs) {
|
|
2896
|
-
for (const targetId of ref.targetIds) {
|
|
2897
|
-
const key = `${sourceId}|${targetId}`;
|
|
2898
|
-
if (seen.has(key)) continue;
|
|
2899
|
-
seen.add(key);
|
|
2900
|
-
crossRefs.push({
|
|
2901
|
-
source: sourceId,
|
|
2902
|
-
target: targetId,
|
|
2903
|
-
type: "references_static",
|
|
2904
|
-
layer: "static"
|
|
2905
|
-
});
|
|
2906
|
-
}
|
|
2907
|
-
}
|
|
2908
|
-
}
|
|
2909
|
-
return {
|
|
2910
|
-
cross_refs: crossRefs,
|
|
2911
|
-
flagged_edges: [],
|
|
2912
|
-
warnings: [],
|
|
2913
|
-
patterns: {
|
|
2914
|
-
files_scanned: filesScanned,
|
|
2915
|
-
static_values_tracked: valueLookup.size,
|
|
2916
|
-
references_found: crossRefs.length,
|
|
2917
|
-
parser: parseCode2 ? "tree-sitter" : "regex-fallback"
|
|
2918
|
-
}
|
|
2919
|
-
};
|
|
2920
|
-
}
|
|
2921
|
-
};
|
|
2922
|
-
|
|
2923
|
-
// src/server/graph/core/parser-registry.ts
|
|
2924
|
-
function isMultiLayerParser(p) {
|
|
2925
|
-
return "layers" in p && Array.isArray(p.layers);
|
|
2926
|
-
}
|
|
2927
|
-
var ParserRegistry = class {
|
|
2928
|
-
constructor() {
|
|
2929
|
-
this.singleLayerParsers = /* @__PURE__ */ new Map();
|
|
2930
|
-
this.multiLayerParsers = [];
|
|
2931
|
-
this.ids = /* @__PURE__ */ new Set();
|
|
2932
|
-
}
|
|
2933
|
-
register(parser) {
|
|
2934
|
-
if (this.ids.has(parser.id)) {
|
|
2935
|
-
throw new Error(`Duplicate parser id: ${parser.id}`);
|
|
2936
|
-
}
|
|
2937
|
-
this.ids.add(parser.id);
|
|
2938
|
-
if (isMultiLayerParser(parser)) {
|
|
2939
|
-
this.multiLayerParsers.push(parser);
|
|
2940
|
-
} else {
|
|
2941
|
-
const list = this.singleLayerParsers.get(parser.layer) ?? [];
|
|
2942
|
-
list.push(parser);
|
|
2943
|
-
this.singleLayerParsers.set(parser.layer, list);
|
|
2944
|
-
}
|
|
2945
|
-
}
|
|
2946
|
-
/** Get single-layer parsers for a specific layer. */
|
|
2947
|
-
getParsers(layer) {
|
|
2948
|
-
return this.singleLayerParsers.get(layer) ?? [];
|
|
2949
|
-
}
|
|
2950
|
-
/** Get multi-layer parsers that can produce output for the given layer. */
|
|
2951
|
-
getMultiLayerParsersFor(layer) {
|
|
2952
|
-
return this.multiLayerParsers.filter((p) => p.layers.includes(layer));
|
|
2953
|
-
}
|
|
2954
|
-
/** Get all multi-layer parsers. */
|
|
2955
|
-
getMultiLayerParsers() {
|
|
2956
|
-
return [...this.multiLayerParsers];
|
|
2957
|
-
}
|
|
2958
|
-
getCrossLayerParsers() {
|
|
2959
|
-
return this.singleLayerParsers.get("crosslayer") ?? [];
|
|
2960
|
-
}
|
|
2961
|
-
/** All layers that registered parsers can produce (single + multi). */
|
|
2962
|
-
getAvailableLayers() {
|
|
2963
|
-
const layers = /* @__PURE__ */ new Set();
|
|
2964
|
-
for (const key of this.singleLayerParsers.keys()) {
|
|
2965
|
-
if (key !== "crosslayer") layers.add(key);
|
|
2966
|
-
}
|
|
2967
|
-
for (const mp of this.multiLayerParsers) {
|
|
2968
|
-
for (const l of mp.layers) layers.add(l);
|
|
2969
|
-
}
|
|
2970
|
-
return [...layers];
|
|
2971
|
-
}
|
|
2972
|
-
getAll() {
|
|
2973
|
-
const all = [];
|
|
2974
|
-
for (const list of this.singleLayerParsers.values()) all.push(...list);
|
|
2975
|
-
all.push(...this.multiLayerParsers);
|
|
2976
|
-
return all;
|
|
2977
|
-
}
|
|
2978
|
-
};
|
|
2979
|
-
function registerBuiltins(registry, disabled) {
|
|
2980
|
-
const builtins = [
|
|
2981
|
-
typescriptProjectParser,
|
|
2982
|
-
prismaSchemaParser,
|
|
2983
|
-
sqlMigrationsParser,
|
|
2984
|
-
staticValuesParser,
|
|
2985
|
-
fetchResolverParser,
|
|
2986
|
-
apiAnnotationsParser,
|
|
2987
|
-
urlLiteralScannerParser,
|
|
2988
|
-
staticRefScannerParser
|
|
2989
|
-
];
|
|
2990
|
-
for (const parser of builtins) {
|
|
2991
|
-
if (disabled.has(parser.id)) continue;
|
|
2992
|
-
registry.register(parser);
|
|
2993
|
-
}
|
|
2994
|
-
}
|
|
2995
|
-
function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
2996
|
-
for (const entry of config.parsers?.custom ?? []) {
|
|
2997
|
-
try {
|
|
2998
|
-
const absPath = (0, import_node_path11.resolve)(rootDir, entry.path);
|
|
2999
|
-
const mod = require(absPath);
|
|
3000
|
-
const parser = "default" in mod ? mod.default : mod;
|
|
3001
|
-
if (disabled.has(parser.id)) continue;
|
|
3002
|
-
if (!isMultiLayerParser(parser) && parser.layer !== entry.layer) {
|
|
3003
|
-
process.stderr.write(
|
|
3004
|
-
`[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
|
|
3005
|
-
`
|
|
3006
|
-
);
|
|
3007
|
-
}
|
|
3008
|
-
if (parser.layer === "crosslayer" && entry.concern && !("concern" in parser && parser.concern)) {
|
|
3009
|
-
parser.concern = entry.concern;
|
|
3010
|
-
}
|
|
3011
|
-
registry.register(parser);
|
|
3012
|
-
} catch (err) {
|
|
3013
|
-
process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err}
|
|
3014
|
-
`);
|
|
3015
|
-
}
|
|
3016
|
-
}
|
|
3017
|
-
}
|
|
3018
|
-
function createRegistry(config, rootDir) {
|
|
3019
|
-
const registry = new ParserRegistry();
|
|
3020
|
-
const disabled = new Set(config.parsers?.disabled ?? []);
|
|
3021
|
-
registerBuiltins(registry, disabled);
|
|
3022
|
-
loadCustomParsers(registry, config, rootDir, disabled);
|
|
3023
|
-
return registry;
|
|
3024
|
-
}
|
|
3025
|
-
|
|
3026
|
-
// src/server/graph/core/merge.ts
|
|
3027
|
-
function mergeGraphOutputs(outputs, layer) {
|
|
3028
|
-
if (outputs.length === 0) {
|
|
3029
|
-
return {
|
|
3030
|
-
metadata: { generated: (/* @__PURE__ */ new Date()).toISOString(), scope: "", layer },
|
|
3031
|
-
nodes: [],
|
|
3032
|
-
edges: [],
|
|
3033
|
-
cross_refs: [],
|
|
3034
|
-
contradictions: [],
|
|
3035
|
-
warnings: [],
|
|
3036
|
-
flagged_edges: []
|
|
3037
|
-
};
|
|
3038
|
-
}
|
|
3039
|
-
if (outputs.length === 1) return outputs[0];
|
|
3040
|
-
const seenNodes = /* @__PURE__ */ new Set();
|
|
3041
|
-
const seenEdges = /* @__PURE__ */ new Set();
|
|
3042
|
-
const seenCrossRefs = /* @__PURE__ */ new Set();
|
|
3043
|
-
const mergedNodes = [];
|
|
3044
|
-
const mergedEdges = [];
|
|
3045
|
-
const mergedCrossRefs = [];
|
|
3046
|
-
const mergedContradictions = [];
|
|
3047
|
-
const mergedWarnings = [];
|
|
3048
|
-
const mergedFlagged = [];
|
|
3049
|
-
const parserIds = [];
|
|
3050
|
-
for (const output of outputs) {
|
|
3051
|
-
if (output.metadata.parser) {
|
|
3052
|
-
parserIds.push(String(output.metadata.parser));
|
|
3053
|
-
}
|
|
3054
|
-
for (const node of output.nodes) {
|
|
3055
|
-
if (seenNodes.has(node.id)) {
|
|
3056
|
-
mergedWarnings.push({
|
|
3057
|
-
type: "merge_conflict",
|
|
3058
|
-
detail: `Node "${node.id}" produced by multiple parsers; keeping first`
|
|
3059
|
-
});
|
|
3060
|
-
continue;
|
|
3061
|
-
}
|
|
3062
|
-
seenNodes.add(node.id);
|
|
3063
|
-
mergedNodes.push(node);
|
|
3064
|
-
}
|
|
3065
|
-
for (const edge of output.edges) {
|
|
3066
|
-
const key = `${edge.source}|${edge.target}|${edge.type}`;
|
|
3067
|
-
if (seenEdges.has(key)) continue;
|
|
3068
|
-
seenEdges.add(key);
|
|
3069
|
-
mergedEdges.push(edge);
|
|
3070
|
-
}
|
|
3071
|
-
for (const ref of output.cross_refs) {
|
|
3072
|
-
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
3073
|
-
if (seenCrossRefs.has(key)) continue;
|
|
3074
|
-
seenCrossRefs.add(key);
|
|
3075
|
-
mergedCrossRefs.push(ref);
|
|
3076
|
-
}
|
|
3077
|
-
mergedContradictions.push(...output.contradictions);
|
|
3078
|
-
mergedWarnings.push(...output.warnings);
|
|
3079
|
-
mergedFlagged.push(...output.flagged_edges);
|
|
3080
|
-
}
|
|
3081
|
-
const metadata = {
|
|
3082
|
-
...outputs[0].metadata,
|
|
3083
|
-
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3084
|
-
parsers: parserIds
|
|
3085
|
-
};
|
|
3086
|
-
return {
|
|
3087
|
-
metadata,
|
|
3088
|
-
nodes: mergedNodes,
|
|
3089
|
-
edges: mergedEdges,
|
|
3090
|
-
cross_refs: mergedCrossRefs,
|
|
3091
|
-
contradictions: mergedContradictions,
|
|
3092
|
-
warnings: mergedWarnings,
|
|
3093
|
-
flagged_edges: mergedFlagged,
|
|
3094
|
-
patterns: outputs[0].patterns
|
|
3095
|
-
};
|
|
3096
|
-
}
|
|
3097
|
-
function dedupCrossRefs(refs) {
|
|
3098
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3099
|
-
const result = [];
|
|
3100
|
-
for (const ref of refs) {
|
|
3101
|
-
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
3102
|
-
if (seen.has(key)) continue;
|
|
3103
|
-
seen.add(key);
|
|
3104
|
-
result.push(ref);
|
|
3105
|
-
}
|
|
3106
|
-
return result;
|
|
3107
|
-
}
|
|
3108
|
-
function applyCrossLayerResults(uiOutput, results) {
|
|
3109
|
-
return {
|
|
3110
|
-
...uiOutput,
|
|
3111
|
-
cross_refs: dedupCrossRefs([
|
|
3112
|
-
...uiOutput.cross_refs,
|
|
3113
|
-
...results.flatMap((r) => r.output.cross_refs)
|
|
3114
|
-
]),
|
|
3115
|
-
flagged_edges: [
|
|
3116
|
-
...uiOutput.flagged_edges,
|
|
3117
|
-
...results.flatMap((r) => r.output.flagged_edges)
|
|
3118
|
-
],
|
|
3119
|
-
warnings: [
|
|
3120
|
-
...uiOutput.warnings,
|
|
3121
|
-
...results.flatMap((r) => r.output.warnings)
|
|
3122
|
-
]
|
|
3123
|
-
};
|
|
3124
|
-
}
|
|
3125
|
-
|
|
3126
|
-
// src/server/graph/core/graph-builder.ts
|
|
3127
|
-
function readGraphFromDisk(rootDir, layer) {
|
|
3128
|
-
const filePath = (0, import_node_path12.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
3129
|
-
if (!(0, import_node_fs11.existsSync)(filePath)) return null;
|
|
3130
|
-
try {
|
|
3131
|
-
return JSON.parse((0, import_node_fs11.readFileSync)(filePath, "utf-8"));
|
|
3132
|
-
} catch {
|
|
3133
|
-
return null;
|
|
3134
|
-
}
|
|
3135
|
-
}
|
|
3136
|
-
function generateLayer(rootDir, layer) {
|
|
3137
|
-
const config = loadConfig(rootDir);
|
|
3138
|
-
const registry = createRegistry(config, rootDir);
|
|
3139
|
-
const outputs = [];
|
|
3140
|
-
for (const parser of registry.getParsers(layer)) {
|
|
3141
|
-
if (!parser.detect(rootDir)) continue;
|
|
3142
|
-
outputs.push(parser.generate(rootDir));
|
|
3143
|
-
}
|
|
3144
|
-
for (const mp of registry.getMultiLayerParsersFor(layer)) {
|
|
3145
|
-
if (!mp.detect(rootDir)) continue;
|
|
3146
|
-
const multiOutput = mp.generate(rootDir);
|
|
3147
|
-
const layerOutput = multiOutput.get(layer);
|
|
3148
|
-
if (layerOutput) outputs.push(layerOutput);
|
|
3149
|
-
}
|
|
3150
|
-
if (outputs.length === 0) return null;
|
|
3151
|
-
let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
3152
|
-
if (layer === "ui") {
|
|
3153
|
-
const layerOutputs = /* @__PURE__ */ new Map();
|
|
3154
|
-
layerOutputs.set("ui", merged);
|
|
3155
|
-
for (const otherLayer of registry.getAvailableLayers()) {
|
|
3156
|
-
if (otherLayer === "ui") continue;
|
|
3157
|
-
const existing = readGraphFromDisk(rootDir, otherLayer);
|
|
3158
|
-
if (existing) layerOutputs.set(otherLayer, existing);
|
|
3159
|
-
}
|
|
3160
|
-
const crossParsers = registry.getCrossLayerParsers();
|
|
3161
|
-
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
3162
|
-
if (crossResults.length > 0) {
|
|
3163
|
-
merged = applyCrossLayerResults(merged, crossResults);
|
|
3164
|
-
}
|
|
3165
|
-
}
|
|
3166
|
-
return {
|
|
3167
|
-
layer,
|
|
3168
|
-
output: merged,
|
|
3169
|
-
nodeCount: merged.nodes.length,
|
|
3170
|
-
edgeCount: merged.edges.length
|
|
3171
|
-
};
|
|
3172
|
-
}
|
|
3173
|
-
function generateAll(rootDir) {
|
|
3174
|
-
const config = loadConfig(rootDir);
|
|
3175
|
-
const registry = createRegistry(config, rootDir);
|
|
3176
|
-
const allLayers = registry.getAvailableLayers();
|
|
3177
|
-
const layerOrder = [
|
|
3178
|
-
...allLayers.filter((l) => l !== "ui"),
|
|
3179
|
-
...allLayers.filter((l) => l === "ui")
|
|
3180
|
-
];
|
|
3181
|
-
const layerOutputs = /* @__PURE__ */ new Map();
|
|
3182
|
-
const results = [];
|
|
3183
|
-
const multiLayerResults = /* @__PURE__ */ new Map();
|
|
3184
|
-
for (const layer of layerOrder) {
|
|
3185
|
-
const outputs = [];
|
|
3186
|
-
for (const parser of registry.getParsers(layer)) {
|
|
3187
|
-
if (!parser.detect(rootDir)) continue;
|
|
3188
|
-
outputs.push(parser.generate(rootDir));
|
|
3189
|
-
}
|
|
3190
|
-
for (const mp of registry.getMultiLayerParsersFor(layer)) {
|
|
3191
|
-
if (!mp.detect(rootDir)) continue;
|
|
3192
|
-
if (!multiLayerResults.has(mp.id)) {
|
|
3193
|
-
multiLayerResults.set(mp.id, mp.generate(rootDir));
|
|
3194
|
-
}
|
|
3195
|
-
const cached = multiLayerResults.get(mp.id);
|
|
3196
|
-
const layerOutput = cached.get(layer);
|
|
3197
|
-
if (layerOutput) outputs.push(layerOutput);
|
|
3198
|
-
}
|
|
3199
|
-
if (outputs.length === 0) continue;
|
|
3200
|
-
const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
3201
|
-
layerOutputs.set(layer, merged);
|
|
3202
|
-
results.push({
|
|
3203
|
-
layer,
|
|
3204
|
-
output: merged,
|
|
3205
|
-
nodeCount: merged.nodes.length,
|
|
3206
|
-
edgeCount: merged.edges.length
|
|
3207
|
-
});
|
|
3208
|
-
}
|
|
3209
|
-
const crossParsers = registry.getCrossLayerParsers();
|
|
3210
|
-
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
3211
|
-
if (crossResults.length > 0 && layerOutputs.has("ui")) {
|
|
3212
|
-
const uiOutput = layerOutputs.get("ui");
|
|
3213
|
-
const merged = applyCrossLayerResults(uiOutput, crossResults);
|
|
3214
|
-
layerOutputs.set("ui", merged);
|
|
3215
|
-
const uiResult = results.find((r) => r.layer === "ui");
|
|
3216
|
-
if (uiResult) {
|
|
3217
|
-
uiResult.output = merged;
|
|
3218
|
-
uiResult.nodeCount = merged.nodes.length;
|
|
3219
|
-
uiResult.edgeCount = merged.edges.length;
|
|
3220
|
-
}
|
|
3221
|
-
}
|
|
3222
|
-
const byLayer = new Map(results.map((r) => [r.layer, r]));
|
|
3223
|
-
const wellKnownOrder = ["ui", "api", "db"];
|
|
3224
|
-
const extras = [...byLayer.keys()].filter((l) => !wellKnownOrder.includes(l)).sort();
|
|
3225
|
-
return [...wellKnownOrder, ...extras].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
3226
|
-
}
|
|
3227
|
-
|
|
3228
|
-
// src/server/graph/index.ts
|
|
3229
|
-
init_config();
|
|
3230
|
-
|
|
3231
|
-
// src/server/graph/core/tagger-registry.ts
|
|
3232
|
-
var import_node_path14 = require("node:path");
|
|
3233
|
-
|
|
3234
|
-
// src/server/graph/taggers/module-tagger.ts
|
|
3235
|
-
var import_node_fs12 = require("node:fs");
|
|
3236
|
-
var import_node_path13 = require("node:path");
|
|
3237
|
-
function matchGlob(pattern, id) {
|
|
3238
|
-
const patParts = pattern.split("/");
|
|
3239
|
-
const idParts = id.split("/");
|
|
3240
|
-
return matchParts(patParts, 0, idParts, 0);
|
|
3241
|
-
}
|
|
3242
|
-
function matchParts(pat, pi, id, ii) {
|
|
3243
|
-
while (pi < pat.length && ii < id.length) {
|
|
3244
|
-
const p = pat[pi];
|
|
3245
|
-
if (p === "**") {
|
|
3246
|
-
for (let skip = ii; skip <= id.length; skip++) {
|
|
3247
|
-
if (matchParts(pat, pi + 1, id, skip)) return true;
|
|
3248
|
-
}
|
|
3249
|
-
return false;
|
|
3250
|
-
}
|
|
3251
|
-
if (p === "*") {
|
|
3252
|
-
pi++;
|
|
3253
|
-
ii++;
|
|
3254
|
-
continue;
|
|
3255
|
-
}
|
|
3256
|
-
if (p !== id[ii]) return false;
|
|
3257
|
-
pi++;
|
|
3258
|
-
ii++;
|
|
3259
|
-
}
|
|
3260
|
-
while (pi < pat.length && pat[pi] === "**") pi++;
|
|
3261
|
-
return pi === pat.length && ii === id.length;
|
|
3262
|
-
}
|
|
3263
|
-
var CONVENTION_DIRS_BUILTIN = ["features", "modules", "domains", "areas"];
|
|
3264
|
-
function detectConventionDirs(rootDir, extraConventionDirs = []) {
|
|
3265
|
-
const result = /* @__PURE__ */ new Map();
|
|
3266
|
-
const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
|
|
3267
|
-
const searchDirs = [
|
|
3268
|
-
rootDir,
|
|
3269
|
-
(0, import_node_path13.join)(rootDir, "src"),
|
|
3270
|
-
(0, import_node_path13.join)(rootDir, "app"),
|
|
3271
|
-
(0, import_node_path13.join)(rootDir, "lib")
|
|
3272
|
-
];
|
|
3273
|
-
for (const base of searchDirs) {
|
|
3274
|
-
for (const convention of conventionDirs) {
|
|
3275
|
-
const dir = (0, import_node_path13.join)(base, convention);
|
|
3276
|
-
if (!(0, import_node_fs12.existsSync)(dir)) continue;
|
|
3277
|
-
try {
|
|
3278
|
-
const stat = (0, import_node_fs12.statSync)(dir);
|
|
3279
|
-
if (!stat.isDirectory()) continue;
|
|
3280
|
-
const entries = (0, import_node_fs12.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
3281
|
-
if (entries.length > 0) {
|
|
3282
|
-
const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
|
|
3283
|
-
result.set(relPath, entries);
|
|
3284
|
-
}
|
|
3285
|
-
} catch {
|
|
3286
|
-
}
|
|
3287
|
-
}
|
|
3288
|
-
}
|
|
3289
|
-
return result;
|
|
3290
|
-
}
|
|
3291
|
-
function extractRouteGroups(id) {
|
|
3292
|
-
const groups = [];
|
|
3293
|
-
const re = /\(([^)]+)\)/g;
|
|
3294
|
-
let m;
|
|
3295
|
-
while ((m = re.exec(id)) !== null) {
|
|
3296
|
-
groups.push(m[1]);
|
|
3297
|
-
}
|
|
3298
|
-
return groups;
|
|
3299
|
-
}
|
|
3300
|
-
var GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
|
|
3301
|
-
// JS/TS
|
|
3302
|
-
"components",
|
|
3303
|
-
"hooks",
|
|
3304
|
-
"pages",
|
|
3305
|
-
"views",
|
|
3306
|
-
"screens",
|
|
3307
|
-
"layouts",
|
|
3308
|
-
"utils",
|
|
3309
|
-
"helpers",
|
|
3310
|
-
"lib",
|
|
3311
|
-
"libs",
|
|
3312
|
-
"services",
|
|
3313
|
-
"api",
|
|
3314
|
-
"apis",
|
|
3315
|
-
"stores",
|
|
3316
|
-
"state",
|
|
3317
|
-
"store",
|
|
3318
|
-
"context",
|
|
3319
|
-
"contexts",
|
|
3320
|
-
"providers",
|
|
3321
|
-
"types",
|
|
3322
|
-
"interfaces",
|
|
3323
|
-
"models",
|
|
3324
|
-
"schemas",
|
|
3325
|
-
"constants",
|
|
3326
|
-
"config",
|
|
3327
|
-
"configs",
|
|
3328
|
-
"assets",
|
|
3329
|
-
"styles",
|
|
3330
|
-
"public",
|
|
3331
|
-
"middleware",
|
|
3332
|
-
"middlewares",
|
|
3333
|
-
"routes",
|
|
3334
|
-
"router",
|
|
3335
|
-
"tests",
|
|
3336
|
-
"test",
|
|
3337
|
-
"__tests__",
|
|
3338
|
-
"spec",
|
|
3339
|
-
"specs",
|
|
3340
|
-
// Go
|
|
3341
|
-
"cmd",
|
|
3342
|
-
"pkg",
|
|
3343
|
-
"internal",
|
|
3344
|
-
// Python
|
|
3345
|
-
"management",
|
|
3346
|
-
"migrations",
|
|
3347
|
-
"templatetags",
|
|
3348
|
-
"templates",
|
|
3349
|
-
// Java
|
|
3350
|
-
"controller",
|
|
3351
|
-
"controllers",
|
|
3352
|
-
"service",
|
|
3353
|
-
"repository",
|
|
3354
|
-
"repositories",
|
|
3355
|
-
"entity",
|
|
3356
|
-
"entities",
|
|
3357
|
-
"dto",
|
|
3358
|
-
"dtos",
|
|
3359
|
-
// General
|
|
3360
|
-
"shared",
|
|
3361
|
-
"common",
|
|
3362
|
-
"core",
|
|
3363
|
-
"base",
|
|
3364
|
-
"app",
|
|
3365
|
-
// Next.js specific
|
|
3366
|
-
"client",
|
|
3367
|
-
"server"
|
|
3368
|
-
]);
|
|
3369
|
-
var SKIP_SEGMENTS_BUILTIN = /* @__PURE__ */ new Set([
|
|
3370
|
-
"src",
|
|
3371
|
-
"app",
|
|
3372
|
-
"client",
|
|
3373
|
-
"server",
|
|
3374
|
-
"lib",
|
|
3375
|
-
"config"
|
|
3376
|
-
]);
|
|
3377
|
-
function isRouteGroup(segment) {
|
|
3378
|
-
return segment.startsWith("(") && segment.endsWith(")");
|
|
3379
|
-
}
|
|
3380
|
-
function isDynamicSegment(segment) {
|
|
3381
|
-
return segment.startsWith("[") || segment.startsWith(":");
|
|
3382
|
-
}
|
|
3383
|
-
function isDomainDir(segment) {
|
|
3384
|
-
return segment.includes(".") && !segment.endsWith(".tsx") && !segment.endsWith(".ts") && !segment.endsWith(".js") && !segment.endsWith(".jsx") && !segment.endsWith(".vue");
|
|
3385
|
-
}
|
|
3386
|
-
var TRIVIAL_GROUPS = /* @__PURE__ */ new Set([
|
|
3387
|
-
// Generic app wrappers
|
|
3388
|
-
"app",
|
|
3389
|
-
"all",
|
|
3390
|
-
"ee",
|
|
3391
|
-
"home",
|
|
3392
|
-
"root",
|
|
3393
|
-
"main",
|
|
3394
|
-
"site",
|
|
3395
|
-
// Auth/access boundary wrappers — protect routes, not feature modules
|
|
3396
|
-
"protected",
|
|
3397
|
-
"authenticated",
|
|
3398
|
-
"authed",
|
|
3399
|
-
"private",
|
|
3400
|
-
"public",
|
|
3401
|
-
"logged-in",
|
|
3402
|
-
"logged-out",
|
|
3403
|
-
"unprotected",
|
|
3404
|
-
"unauthenticated",
|
|
3405
|
-
"auth-required",
|
|
3406
|
-
"no-auth",
|
|
3407
|
-
"guest-only"
|
|
3408
|
-
]);
|
|
3409
|
-
function isTrivialGroup(name, extraTrivial) {
|
|
3410
|
-
const lower = name.toLowerCase();
|
|
3411
|
-
if (TRIVIAL_GROUPS.has(lower)) return true;
|
|
3412
|
-
if (extraTrivial && [...extraTrivial].some((t) => t.toLowerCase() === lower)) return true;
|
|
3413
|
-
const wrapperPatterns = [
|
|
3414
|
-
/^.*-?wrapper$/,
|
|
3415
|
-
// "page-wrapper", "use-page-wrapper"
|
|
3416
|
-
/^.*-?layout$/,
|
|
3417
|
-
// "admin-layout", "settings-layout"
|
|
3418
|
-
/^use-/,
|
|
3419
|
-
// "use-page-wrapper"
|
|
3420
|
-
/^default$/
|
|
3421
|
-
];
|
|
3422
|
-
return wrapperPatterns.some((p) => p.test(lower));
|
|
3423
|
-
}
|
|
3424
|
-
function normalizeGroupName(name) {
|
|
3425
|
-
return name.toLowerCase().replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
|
|
3426
|
-
}
|
|
3427
|
-
function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
|
|
3428
|
-
const segments = id.split("/");
|
|
3429
|
-
const routeGroups = extractRouteGroups(id);
|
|
3430
|
-
const skipSegments = new Set(SKIP_SEGMENTS_BUILTIN);
|
|
3431
|
-
if (extraSkipSegments) {
|
|
3432
|
-
for (const s of extraSkipSegments) skipSegments.add(s);
|
|
3433
|
-
}
|
|
3434
|
-
const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g, extraTrivial)).map(normalizeGroupName);
|
|
3435
|
-
if (moduleGroups.length > 0) {
|
|
3436
|
-
return moduleGroups[moduleGroups.length - 1];
|
|
3437
|
-
}
|
|
3438
|
-
const meaningful = [];
|
|
3439
|
-
for (const seg of segments) {
|
|
3440
|
-
if (seg.includes(".")) continue;
|
|
3441
|
-
if (isRouteGroup(seg)) continue;
|
|
3442
|
-
if (isDynamicSegment(seg)) continue;
|
|
3443
|
-
if (isDomainDir(seg)) continue;
|
|
3444
|
-
if (skipSegments.has(seg)) continue;
|
|
3445
|
-
meaningful.push(seg);
|
|
3446
|
-
}
|
|
3447
|
-
if (meaningful.length > 0) {
|
|
3448
|
-
return meaningful[0];
|
|
3449
|
-
}
|
|
3450
|
-
return "root";
|
|
3451
|
-
}
|
|
3452
|
-
var cachedRootDir = null;
|
|
3453
|
-
var cachedConventionDirs = /* @__PURE__ */ new Map();
|
|
3454
|
-
var moduleTagger = {
|
|
3455
|
-
id: "module",
|
|
3456
|
-
tagKey: "module",
|
|
3457
|
-
trackUntagged: true,
|
|
3458
|
-
layers: null,
|
|
3459
|
-
// applies to all layers
|
|
3460
|
-
tag(nodes, layer, rootDir) {
|
|
3461
|
-
let configRules = [];
|
|
3462
|
-
let extraTrivial;
|
|
3463
|
-
let extraSkipSegments;
|
|
3464
|
-
let extraConventionDirs = [];
|
|
3465
|
-
try {
|
|
3466
|
-
const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
|
|
3467
|
-
const config = loadConfig2(rootDir);
|
|
3468
|
-
configRules = config.taggers?.module?.rules ?? [];
|
|
3469
|
-
const trivialFromConfig = config.taggers?.module?.trivialGroups;
|
|
3470
|
-
if (trivialFromConfig?.length) {
|
|
3471
|
-
extraTrivial = new Set(trivialFromConfig);
|
|
3472
|
-
}
|
|
3473
|
-
const skipFromConfig = config.taggers?.module?.skipSegments;
|
|
3474
|
-
if (skipFromConfig?.length) {
|
|
3475
|
-
extraSkipSegments = new Set(skipFromConfig);
|
|
3476
|
-
}
|
|
3477
|
-
extraConventionDirs = config.taggers?.module?.conventionDirs ?? [];
|
|
3478
|
-
const roleNamesFromConfig = config.taggers?.module?.genericRoleNames;
|
|
3479
|
-
if (roleNamesFromConfig?.length) {
|
|
3480
|
-
for (const name of roleNamesFromConfig) GENERIC_ROLE_NAMES_BUILTIN.add(name);
|
|
3481
|
-
}
|
|
3482
|
-
} catch {
|
|
3483
|
-
}
|
|
3484
|
-
if (cachedRootDir !== rootDir) {
|
|
3485
|
-
cachedConventionDirs = detectConventionDirs(rootDir, extraConventionDirs);
|
|
3486
|
-
cachedRootDir = rootDir;
|
|
3487
|
-
}
|
|
3488
|
-
const result = /* @__PURE__ */ new Map();
|
|
3489
|
-
for (const node of nodes) {
|
|
3490
|
-
const id = node.id;
|
|
3491
|
-
let matched = false;
|
|
3492
|
-
for (const rule of configRules) {
|
|
3493
|
-
if (matchGlob(rule.match, id)) {
|
|
3494
|
-
result.set(id, rule.module);
|
|
3495
|
-
matched = true;
|
|
3496
|
-
break;
|
|
3497
|
-
}
|
|
3498
|
-
}
|
|
3499
|
-
if (matched) continue;
|
|
3500
|
-
matched = false;
|
|
3501
|
-
for (const [convDir, moduleNames] of cachedConventionDirs) {
|
|
3502
|
-
if (id.startsWith(convDir + "/")) {
|
|
3503
|
-
const rest = id.slice(convDir.length + 1);
|
|
3504
|
-
const firstSeg = rest.split("/")[0];
|
|
3505
|
-
if (moduleNames.includes(firstSeg)) {
|
|
3506
|
-
result.set(id, firstSeg);
|
|
3507
|
-
matched = true;
|
|
3508
|
-
break;
|
|
3509
|
-
}
|
|
3510
|
-
}
|
|
3511
|
-
}
|
|
3512
|
-
if (matched) continue;
|
|
3513
|
-
const module2 = extractModuleFromPath(id, extraTrivial, extraSkipSegments);
|
|
3514
|
-
result.set(id, module2);
|
|
3515
|
-
}
|
|
3516
|
-
return result;
|
|
3517
|
-
}
|
|
3518
|
-
};
|
|
3519
|
-
|
|
3520
|
-
// src/server/graph/taggers/screen-tagger.ts
|
|
3521
|
-
var SCREEN_TYPES = /* @__PURE__ */ new Set(["page", "layout"]);
|
|
3522
|
-
var screenTagger = {
|
|
3523
|
-
id: "screen",
|
|
3524
|
-
tagKey: "screen",
|
|
3525
|
-
trackUntagged: true,
|
|
3526
|
-
layers: ["ui"],
|
|
3527
|
-
tag(nodes, layer) {
|
|
3528
|
-
if (layer !== "ui") return /* @__PURE__ */ new Map();
|
|
3529
|
-
const result = /* @__PURE__ */ new Map();
|
|
3530
|
-
for (const node of nodes) {
|
|
3531
|
-
if (SCREEN_TYPES.has(node.type)) {
|
|
3532
|
-
result.set(node.id, "true");
|
|
3533
|
-
}
|
|
3534
|
-
}
|
|
3535
|
-
return result;
|
|
3536
|
-
}
|
|
3537
|
-
};
|
|
3538
|
-
|
|
3539
|
-
// src/server/graph/core/tagger-registry.ts
|
|
3540
|
-
var TaggerRegistry = class {
|
|
3541
|
-
constructor() {
|
|
3542
|
-
this.taggers = [];
|
|
3543
|
-
this.ids = /* @__PURE__ */ new Set();
|
|
3544
|
-
}
|
|
3545
|
-
register(tagger) {
|
|
3546
|
-
if (this.ids.has(tagger.id)) {
|
|
3547
|
-
throw new Error(`Duplicate tagger id: ${tagger.id}`);
|
|
3548
|
-
}
|
|
3549
|
-
this.ids.add(tagger.id);
|
|
3550
|
-
this.taggers.push(tagger);
|
|
3551
|
-
}
|
|
3552
|
-
getAll() {
|
|
3553
|
-
return this.taggers;
|
|
3554
|
-
}
|
|
3555
|
-
getForLayer(layer) {
|
|
3556
|
-
return this.taggers.filter((t) => t.layers === null || t.layers.includes(layer));
|
|
3557
|
-
}
|
|
3558
|
-
};
|
|
3559
|
-
var BUILTIN_TAGGERS = [moduleTagger, screenTagger];
|
|
3560
|
-
function registerBuiltins2(registry, disabled, config) {
|
|
3561
|
-
for (const tagger of BUILTIN_TAGGERS) {
|
|
3562
|
-
if (disabled.has(tagger.id)) continue;
|
|
3563
|
-
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
3564
|
-
if (override !== void 0) {
|
|
3565
|
-
tagger.trackUntagged = override;
|
|
3566
|
-
}
|
|
3567
|
-
registry.register(tagger);
|
|
3568
|
-
}
|
|
3569
|
-
}
|
|
3570
|
-
function loadCustomTaggers(registry, config, rootDir, disabled) {
|
|
3571
|
-
for (const entry of config.taggers?.custom ?? []) {
|
|
3572
|
-
if (disabled.has(entry.id)) continue;
|
|
3573
|
-
try {
|
|
3574
|
-
const absPath = (0, import_node_path14.resolve)(rootDir, entry.path);
|
|
3575
|
-
const mod = require(absPath);
|
|
3576
|
-
const tagger = "default" in mod ? mod.default : mod;
|
|
3577
|
-
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
3578
|
-
if (override !== void 0) {
|
|
3579
|
-
tagger.trackUntagged = override;
|
|
3580
|
-
}
|
|
3581
|
-
registry.register(tagger);
|
|
3582
|
-
} catch (err) {
|
|
3583
|
-
process.stderr.write(`[launch-chart] failed to load custom tagger from ${entry.path}: ${err}
|
|
3584
|
-
`);
|
|
3585
|
-
}
|
|
3586
|
-
}
|
|
3587
|
-
}
|
|
3588
|
-
function createTaggerRegistry(config, rootDir) {
|
|
3589
|
-
const registry = new TaggerRegistry();
|
|
3590
|
-
const disabled = new Set(config.taggers?.disabled ?? []);
|
|
3591
|
-
registerBuiltins2(registry, disabled, config);
|
|
3592
|
-
loadCustomTaggers(registry, config, rootDir, disabled);
|
|
3593
|
-
return registry;
|
|
3594
|
-
}
|
|
3595
|
-
|
|
3596
|
-
// src/server/graph/core/tag-store.ts
|
|
3597
|
-
var import_node_fs13 = require("node:fs");
|
|
3598
|
-
var import_node_path15 = require("node:path");
|
|
3599
|
-
var TAGS_FILENAME = "tags.json";
|
|
3600
|
-
var GRAPHS_DIR = ".launchsecure/graphs";
|
|
3601
|
-
var tagCache = /* @__PURE__ */ new Map();
|
|
3602
|
-
function tagsFilePath(rootDir) {
|
|
3603
|
-
return (0, import_node_path15.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
|
|
3604
|
-
}
|
|
3605
|
-
function readTagStore(rootDir) {
|
|
3606
|
-
const filePath = tagsFilePath(rootDir);
|
|
3607
|
-
if (!(0, import_node_fs13.existsSync)(filePath)) return {};
|
|
3608
|
-
const stat = (0, import_node_fs13.statSync)(filePath);
|
|
3609
|
-
const cached = tagCache.get(filePath);
|
|
3610
|
-
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
3611
|
-
return cached.store;
|
|
3612
|
-
}
|
|
3613
|
-
try {
|
|
3614
|
-
const content = (0, import_node_fs13.readFileSync)(filePath, "utf-8");
|
|
3615
|
-
const store = JSON.parse(content);
|
|
3616
|
-
tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
|
|
3617
|
-
return store;
|
|
3618
|
-
} catch {
|
|
3619
|
-
return {};
|
|
3620
|
-
}
|
|
3621
|
-
}
|
|
3622
|
-
function writeTagStore(rootDir, store) {
|
|
3623
|
-
const filePath = tagsFilePath(rootDir);
|
|
3624
|
-
const dir = (0, import_node_path15.dirname)(filePath);
|
|
3625
|
-
(0, import_node_fs13.mkdirSync)(dir, { recursive: true });
|
|
3626
|
-
const cleaned = {};
|
|
3627
|
-
for (const [nodeId, tags] of Object.entries(store)) {
|
|
3628
|
-
if (Object.keys(tags).length > 0) {
|
|
3629
|
-
cleaned[nodeId] = tags;
|
|
3630
|
-
}
|
|
3631
|
-
}
|
|
3632
|
-
(0, import_node_fs13.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
|
|
3633
|
-
tagCache.delete(filePath);
|
|
3634
|
-
}
|
|
3635
|
-
function setTag(rootDir, nodeId, key, value) {
|
|
3636
|
-
const store = readTagStore(rootDir);
|
|
3637
|
-
if (!store[nodeId]) store[nodeId] = {};
|
|
3638
|
-
store[nodeId][key] = value;
|
|
3639
|
-
writeTagStore(rootDir, store);
|
|
3640
|
-
}
|
|
3641
|
-
function removeTag(rootDir, nodeId, key) {
|
|
3642
|
-
const store = readTagStore(rootDir);
|
|
3643
|
-
if (!store[nodeId]) return;
|
|
3644
|
-
delete store[nodeId][key];
|
|
3645
|
-
if (Object.keys(store[nodeId]).length === 0) {
|
|
3646
|
-
delete store[nodeId];
|
|
3647
|
-
}
|
|
3648
|
-
writeTagStore(rootDir, store);
|
|
3649
|
-
}
|
|
3650
|
-
|
|
3651
|
-
// src/server/graph/index.ts
|
|
3652
|
-
init_ts_extractor();
|
|
3653
|
-
var GRAPHS_DIR2 = ".launchsecure/graphs";
|
|
3654
|
-
function getAvailableLayers(rootDir) {
|
|
3655
|
-
const dir = (0, import_node_path16.join)(rootDir, GRAPHS_DIR2);
|
|
3656
|
-
if (!(0, import_node_fs14.existsSync)(dir)) return [];
|
|
3657
|
-
return (0, import_node_fs14.readdirSync)(dir).filter((f) => f.endsWith(".json") && f !== "tags.json").map((f) => f.replace(".json", ""));
|
|
3658
|
-
}
|
|
3659
|
-
var graphCache = /* @__PURE__ */ new Map();
|
|
3660
|
-
var taggedCache = /* @__PURE__ */ new Map();
|
|
3661
|
-
function graphsDir(rootDir) {
|
|
3662
|
-
return (0, import_node_path16.join)(rootDir, GRAPHS_DIR2);
|
|
3663
|
-
}
|
|
3664
|
-
function graphFilePath(rootDir, layer) {
|
|
3665
|
-
return (0, import_node_path16.join)(graphsDir(rootDir), `${layer}.json`);
|
|
3666
|
-
}
|
|
3667
|
-
function tagsFilePath2(rootDir) {
|
|
3668
|
-
return (0, import_node_path16.join)(graphsDir(rootDir), "tags.json");
|
|
3669
|
-
}
|
|
3670
|
-
function getMtimeMs(filePath) {
|
|
3671
|
-
if (!(0, import_node_fs14.existsSync)(filePath)) return 0;
|
|
3672
|
-
return (0, import_node_fs14.statSync)(filePath).mtimeMs;
|
|
3673
|
-
}
|
|
3674
|
-
function invalidateCache(filePath) {
|
|
3675
|
-
graphCache.delete(filePath);
|
|
3676
|
-
}
|
|
3677
|
-
function invalidateTaggedCache(rootDir, layer) {
|
|
3678
|
-
taggedCache.delete(`${rootDir}:${layer}`);
|
|
3679
|
-
}
|
|
3680
|
-
function applyTags(graph, layer, rootDir) {
|
|
3681
|
-
const config = loadConfig(rootDir);
|
|
3682
|
-
const registry = createTaggerRegistry(config, rootDir);
|
|
3683
|
-
const manualTags = readTagStore(rootDir);
|
|
3684
|
-
const taggedNodes = graph.nodes.map((n) => ({ ...n }));
|
|
3685
|
-
const taggers = registry.getForLayer(layer);
|
|
3686
|
-
for (const tagger of taggers) {
|
|
3687
|
-
const assignments = tagger.tag(taggedNodes, layer, rootDir);
|
|
3688
|
-
for (const node of taggedNodes) {
|
|
3689
|
-
if (!node.tags) node.tags = {};
|
|
3690
|
-
const tags = node.tags;
|
|
3691
|
-
const value = assignments.get(node.id);
|
|
3692
|
-
if (value !== void 0) {
|
|
3693
|
-
tags[tagger.tagKey] = value;
|
|
3694
|
-
} else if (tagger.trackUntagged) {
|
|
3695
|
-
tags[tagger.tagKey] = "untagged";
|
|
3696
|
-
}
|
|
3697
|
-
}
|
|
3698
|
-
}
|
|
3699
|
-
for (const node of taggedNodes) {
|
|
3700
|
-
const manual = manualTags[node.id];
|
|
3701
|
-
if (manual) {
|
|
3702
|
-
if (!node.tags) node.tags = {};
|
|
3703
|
-
const tags = node.tags;
|
|
3704
|
-
Object.assign(tags, manual);
|
|
3705
|
-
}
|
|
3706
|
-
}
|
|
3707
|
-
return { ...graph, nodes: taggedNodes };
|
|
3708
|
-
}
|
|
3709
|
-
function readGraphRaw(rootDir, layer) {
|
|
3710
|
-
const filePath = graphFilePath(rootDir, layer);
|
|
3711
|
-
if (!(0, import_node_fs14.existsSync)(filePath)) return null;
|
|
3712
|
-
const stat = (0, import_node_fs14.statSync)(filePath);
|
|
3713
|
-
const cached = graphCache.get(filePath);
|
|
3714
|
-
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
3715
|
-
return cached.graph;
|
|
3716
|
-
}
|
|
3717
|
-
const content = (0, import_node_fs14.readFileSync)(filePath, "utf-8");
|
|
3718
|
-
const graph = JSON.parse(content);
|
|
3719
|
-
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
3720
|
-
return graph;
|
|
3721
|
-
}
|
|
3722
|
-
function readGraph(rootDir, layer) {
|
|
3723
|
-
const rawFilePath = graphFilePath(rootDir, layer);
|
|
3724
|
-
if (!(0, import_node_fs14.existsSync)(rawFilePath)) return null;
|
|
3725
|
-
const rawMtime = getMtimeMs(rawFilePath);
|
|
3726
|
-
const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
|
|
3727
|
-
const cacheKey = `${rootDir}:${layer}`;
|
|
3728
|
-
const cached = taggedCache.get(cacheKey);
|
|
3729
|
-
if (cached && cached.rawMtimeMs === rawMtime && cached.tagsMtimeMs === tagsMtime) {
|
|
3730
|
-
return cached.graph;
|
|
3731
|
-
}
|
|
3732
|
-
const raw = readGraphRaw(rootDir, layer);
|
|
3733
|
-
if (!raw) return null;
|
|
3734
|
-
const tagged = applyTags(raw, layer, rootDir);
|
|
3735
|
-
taggedCache.set(cacheKey, { rawMtimeMs: rawMtime, tagsMtimeMs: tagsMtime, graph: tagged });
|
|
3736
|
-
return tagged;
|
|
3737
|
-
}
|
|
3738
|
-
function readAllGraphs(rootDir) {
|
|
3739
|
-
const result = {};
|
|
3740
|
-
for (const layer of getAvailableLayers(rootDir)) {
|
|
3741
|
-
const graph = readGraph(rootDir, layer);
|
|
3742
|
-
if (graph) result[layer] = graph;
|
|
3743
|
-
}
|
|
3744
|
-
return result;
|
|
3745
|
-
}
|
|
3746
|
-
async function generateGraph(rootDir, layer) {
|
|
3747
|
-
await initTreeSitter();
|
|
3748
|
-
const config = loadConfig(rootDir);
|
|
3749
|
-
setExtractorConfig({
|
|
3750
|
-
dbIdentifiers: config.parsers?.patterns?.dbIdentifiers,
|
|
3751
|
-
mutationMethods: config.parsers?.patterns?.mutationMethods
|
|
3752
|
-
});
|
|
3753
|
-
const dir = graphsDir(rootDir);
|
|
3754
|
-
(0, import_node_fs14.mkdirSync)(dir, { recursive: true });
|
|
3755
|
-
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
3756
|
-
for (const result of results) {
|
|
3757
|
-
const filePath = graphFilePath(rootDir, result.layer);
|
|
3758
|
-
(0, import_node_fs14.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
3759
|
-
invalidateCache(filePath);
|
|
3760
|
-
invalidateTaggedCache(rootDir, result.layer);
|
|
3761
|
-
}
|
|
3762
|
-
return results;
|
|
3763
|
-
}
|
|
3764
|
-
|
|
3765
|
-
// src/server/lockfile.ts
|
|
3766
|
-
var import_node_child_process = require("node:child_process");
|
|
3767
|
-
var import_node_fs15 = require("node:fs");
|
|
3768
|
-
var import_node_os = require("node:os");
|
|
3769
|
-
var import_node_path17 = require("node:path");
|
|
3770
|
-
function lockDir(projectRoot) {
|
|
3771
|
-
if (projectRoot) {
|
|
3772
|
-
return (0, import_node_path17.join)(projectRoot, ".launchsecure");
|
|
3773
|
-
}
|
|
3774
|
-
return (0, import_node_path17.join)((0, import_node_os.homedir)(), ".launchsecure");
|
|
3775
|
-
}
|
|
3776
|
-
function lockPath(projectRoot) {
|
|
3777
|
-
return (0, import_node_path17.join)(lockDir(projectRoot), "launch-chart.lock");
|
|
3778
|
-
}
|
|
3779
|
-
var _activeProjectRoot;
|
|
3780
|
-
function readLock(projectRoot) {
|
|
3781
|
-
const root = projectRoot ?? _activeProjectRoot;
|
|
3782
|
-
const p = lockPath(root);
|
|
3783
|
-
if (!(0, import_node_fs15.existsSync)(p)) {
|
|
3784
|
-
if (root) {
|
|
3785
|
-
const globalP = lockPath();
|
|
3786
|
-
if ((0, import_node_fs15.existsSync)(globalP)) {
|
|
3787
|
-
try {
|
|
3788
|
-
const data = JSON.parse((0, import_node_fs15.readFileSync)(globalP, "utf-8"));
|
|
3789
|
-
if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
|
|
3790
|
-
return data;
|
|
3791
|
-
}
|
|
3792
|
-
} catch {
|
|
3793
|
-
}
|
|
3794
|
-
}
|
|
3795
|
-
}
|
|
3796
|
-
return null;
|
|
3797
|
-
}
|
|
3798
|
-
try {
|
|
3799
|
-
const data = JSON.parse((0, import_node_fs15.readFileSync)(p, "utf-8"));
|
|
3800
|
-
if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
|
|
3801
|
-
return data;
|
|
3802
|
-
} catch {
|
|
3803
|
-
return null;
|
|
3804
|
-
}
|
|
3805
|
-
}
|
|
3806
|
-
function isPidAlive(pid) {
|
|
3807
|
-
try {
|
|
3808
|
-
process.kill(pid, 0);
|
|
3809
|
-
return true;
|
|
3810
|
-
} catch {
|
|
3811
|
-
return false;
|
|
3812
|
-
}
|
|
3813
|
-
}
|
|
3814
|
-
function getListenerPid(port) {
|
|
3815
|
-
try {
|
|
3816
|
-
const out = (0, import_node_child_process.execFileSync)("lsof", ["-nP", "-iTCP:" + port, "-sTCP:LISTEN", "-t"], {
|
|
3817
|
-
encoding: "utf-8",
|
|
3818
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
3819
|
-
timeout: 500
|
|
3820
|
-
}).trim();
|
|
3821
|
-
if (!out) return null;
|
|
3822
|
-
const pid = parseInt(out.split("\n")[0], 10);
|
|
3823
|
-
return Number.isFinite(pid) ? pid : null;
|
|
3824
|
-
} catch {
|
|
3825
|
-
return null;
|
|
3826
|
-
}
|
|
3827
|
-
}
|
|
3828
|
-
function getLiveLock(projectRoot) {
|
|
3829
|
-
const root = projectRoot ?? _activeProjectRoot;
|
|
3830
|
-
const lock = readLock(root);
|
|
3831
|
-
if (!lock) return null;
|
|
3832
|
-
const listenerPid = getListenerPid(lock.port);
|
|
3833
|
-
const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
|
|
3834
|
-
if (!live) {
|
|
3835
|
-
try {
|
|
3836
|
-
(0, import_node_fs15.unlinkSync)(lockPath(root));
|
|
3837
|
-
} catch {
|
|
3838
|
-
}
|
|
3839
|
-
return null;
|
|
3840
|
-
}
|
|
3841
|
-
return lock;
|
|
3842
|
-
}
|
|
3843
|
-
function writeLock(data, projectRoot) {
|
|
3844
|
-
const root = projectRoot ?? _activeProjectRoot;
|
|
3845
|
-
(0, import_node_fs15.mkdirSync)(lockDir(root), { recursive: true });
|
|
3846
|
-
(0, import_node_fs15.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
3847
|
-
if (root) _activeProjectRoot = root;
|
|
3848
|
-
}
|
|
3849
|
-
function clearLock(projectRoot) {
|
|
3850
|
-
const root = projectRoot ?? _activeProjectRoot;
|
|
3851
|
-
try {
|
|
3852
|
-
(0, import_node_fs15.unlinkSync)(lockPath(root));
|
|
3853
|
-
} catch {
|
|
3854
|
-
}
|
|
3855
|
-
}
|
|
3856
|
-
|
|
3857
|
-
// src/server/chart-serve.ts
|
|
3858
|
-
init_config();
|
|
3859
|
-
|
|
3860
|
-
// src/server/graph/core/audit-core.ts
|
|
3861
|
-
var import_node_fs16 = require("node:fs");
|
|
3862
|
-
var import_node_path18 = require("node:path");
|
|
3863
|
-
function readGraphFile(rootDir, layer) {
|
|
3864
|
-
const filePath = (0, import_node_path18.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
3865
|
-
if (!(0, import_node_fs16.existsSync)(filePath)) return null;
|
|
3866
|
-
try {
|
|
3867
|
-
return JSON.parse((0, import_node_fs16.readFileSync)(filePath, "utf-8"));
|
|
3868
|
-
} catch {
|
|
3869
|
-
return null;
|
|
3870
|
-
}
|
|
3871
|
-
}
|
|
3872
|
-
function checkSchemaDrift(rootDir) {
|
|
3873
|
-
const findings = [];
|
|
3874
|
-
const db = readGraphFile(rootDir, "db");
|
|
3875
|
-
if (!db) {
|
|
3876
|
-
findings.push({ id: "no-db-graph", severity: "info", category: "schema_drift", title: "No DB graph", detail: "Run generate_graph first to populate the DB layer." });
|
|
3877
|
-
return buildReport("db", "schema_drift", findings);
|
|
3878
|
-
}
|
|
3879
|
-
for (const c of db.contradictions ?? []) {
|
|
3880
|
-
const isTableLevel = c.detail.includes("Table ") && (c.detail.includes("has no CREATE TABLE") || c.detail.includes("not in schema.prisma"));
|
|
3881
|
-
findings.push({
|
|
3882
|
-
id: `drift:${c.entity}`,
|
|
3883
|
-
severity: isTableLevel ? "error" : "warning",
|
|
3884
|
-
category: "schema_drift",
|
|
3885
|
-
title: c.entity,
|
|
3886
|
-
detail: c.detail
|
|
3887
|
-
});
|
|
3888
|
-
}
|
|
3889
|
-
return buildReport("db", "schema_drift", findings);
|
|
3890
|
-
}
|
|
3891
|
-
function checkOrphanFks(rootDir) {
|
|
3892
|
-
const findings = [];
|
|
3893
|
-
const db = readGraphFile(rootDir, "db");
|
|
3894
|
-
if (!db) return buildReport("db", "orphan_fks", findings);
|
|
3895
|
-
for (const f of db.flagged_edges ?? []) {
|
|
3896
|
-
findings.push({
|
|
3897
|
-
id: `fk:${f.source}->${f.target}`,
|
|
3898
|
-
severity: "warning",
|
|
3899
|
-
category: "orphan_fks",
|
|
3900
|
-
title: `${f.source} \u2192 ${f.target}`,
|
|
3901
|
-
detail: f.label
|
|
3902
|
-
});
|
|
3903
|
-
}
|
|
3904
|
-
return buildReport("db", "orphan_fks", findings);
|
|
3905
|
-
}
|
|
3906
|
-
function checkUnprotectedRoutes(rootDir) {
|
|
3907
|
-
const findings = [];
|
|
3908
|
-
const api = readGraphFile(rootDir, "api");
|
|
3909
|
-
const staticGraph = readGraphFile(rootDir, "static");
|
|
3910
|
-
if (!api) return buildReport("api", "unprotected_routes", findings);
|
|
3911
|
-
const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
|
|
3912
|
-
let routePermsContent = "";
|
|
3913
|
-
if ((0, import_node_fs16.existsSync)(routePermsPath)) {
|
|
3914
|
-
routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
|
|
3915
|
-
}
|
|
3916
|
-
const registeredRoutes = /* @__PURE__ */ new Set();
|
|
3917
|
-
const routeEntryRe = /path:\s*'([^']+)'/g;
|
|
3918
|
-
let rm;
|
|
3919
|
-
while ((rm = routeEntryRe.exec(routePermsContent)) !== null) {
|
|
3920
|
-
registeredRoutes.add(rm[1].replace(/:(\w+)/g, "[$1]"));
|
|
3921
|
-
}
|
|
3922
|
-
for (const node of api.nodes) {
|
|
3923
|
-
if (node.type !== "endpoint") continue;
|
|
3924
|
-
const route = node.route ?? "";
|
|
3925
|
-
if (!route) continue;
|
|
3926
|
-
const normalized = "/api" + (route.startsWith("/") ? route : "/" + route);
|
|
3927
|
-
const isRegistered = registeredRoutes.has(normalized) || [...registeredRoutes].some((r) => routeMatchesPattern(normalized, r));
|
|
3928
|
-
if (!isRegistered) {
|
|
3929
|
-
const methods = node.methods ?? [];
|
|
3930
|
-
findings.push({
|
|
3931
|
-
id: `unprotected:${node.id}`,
|
|
3932
|
-
severity: "warning",
|
|
3933
|
-
category: "unprotected_routes",
|
|
3934
|
-
title: `${methods.join(",")} ${route}`,
|
|
3935
|
-
detail: `API endpoint has no entry in ROUTE_PERMISSIONS. Methods: ${methods.join(", ")}`,
|
|
3936
|
-
file: node.id
|
|
3937
|
-
});
|
|
3938
|
-
}
|
|
3939
|
-
}
|
|
3940
|
-
return buildReport("api", "unprotected_routes", findings);
|
|
3941
|
-
}
|
|
3942
|
-
function routeMatchesPattern(route, pattern) {
|
|
3943
|
-
const routeParts = route.split("/");
|
|
3944
|
-
const patternParts = pattern.split("/");
|
|
3945
|
-
if (routeParts.length !== patternParts.length) return false;
|
|
3946
|
-
for (let i = 0; i < routeParts.length; i++) {
|
|
3947
|
-
const rp = routeParts[i];
|
|
3948
|
-
const pp = patternParts[i];
|
|
3949
|
-
if (rp === pp) continue;
|
|
3950
|
-
if (pp.startsWith("[") || pp.startsWith(":")) continue;
|
|
3951
|
-
if (rp.startsWith("[") || rp.startsWith(":")) continue;
|
|
3952
|
-
return false;
|
|
3953
|
-
}
|
|
3954
|
-
return true;
|
|
3955
|
-
}
|
|
3956
|
-
function checkDeadScreens(rootDir) {
|
|
3957
|
-
const findings = [];
|
|
3958
|
-
const ui = readGraphFile(rootDir, "ui");
|
|
3959
|
-
if (!ui) return buildReport("ui", "dead_screens", findings);
|
|
3960
|
-
const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
|
|
3961
|
-
const navTargets = /* @__PURE__ */ new Set();
|
|
3962
|
-
for (const e of ui.edges) {
|
|
3963
|
-
if (e.type === "navigates" || e.type === "renders" || e.type === "imports") {
|
|
3964
|
-
navTargets.add(e.target);
|
|
3965
|
-
}
|
|
3966
|
-
}
|
|
3967
|
-
for (const cr of ui.cross_refs ?? []) {
|
|
3968
|
-
navTargets.add(cr.target);
|
|
3969
|
-
}
|
|
3970
|
-
for (const page of pages) {
|
|
3971
|
-
if (page.id.endsWith("layout.tsx") && page.id.split("/").length <= 2) continue;
|
|
3972
|
-
if (["error.tsx", "loading.tsx", "not-found.tsx", "template.tsx"].some((s) => page.id.endsWith(s))) continue;
|
|
3973
|
-
if (!navTargets.has(page.id)) {
|
|
3974
|
-
findings.push({
|
|
3975
|
-
id: `dead:${page.id}`,
|
|
3976
|
-
severity: "info",
|
|
3977
|
-
category: "dead_screens",
|
|
3978
|
-
title: page.name,
|
|
3979
|
-
detail: `Page "${page.id}" has no incoming navigation, render, or import edges.`,
|
|
3980
|
-
file: page.id
|
|
3981
|
-
});
|
|
3982
|
-
}
|
|
3983
|
-
}
|
|
3984
|
-
return buildReport("ui", "dead_screens", findings);
|
|
3985
|
-
}
|
|
3986
|
-
function checkUnenforcedPermissions(rootDir) {
|
|
3987
|
-
const findings = [];
|
|
3988
|
-
const staticGraph = readGraphFile(rootDir, "static");
|
|
3989
|
-
if (!staticGraph) return buildReport("static", "unenforced_permissions", findings);
|
|
3990
|
-
const permissions = staticGraph.nodes.filter((n) => n.type === "seed_permission").map((n) => ({ id: n.id, key: n.value, name: n.name }));
|
|
3991
|
-
const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
|
|
3992
|
-
let routePermsContent = "";
|
|
3993
|
-
if ((0, import_node_fs16.existsSync)(routePermsPath)) {
|
|
3994
|
-
routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
|
|
3995
|
-
}
|
|
3996
|
-
for (const perm of permissions) {
|
|
3997
|
-
const regex = new RegExp(`permission:\\s*['"]${perm.key}['"]`);
|
|
3998
|
-
if (!regex.test(routePermsContent)) {
|
|
3999
|
-
findings.push({
|
|
4000
|
-
id: `unenforced:${perm.key}`,
|
|
4001
|
-
severity: "warning",
|
|
4002
|
-
category: "unenforced_permissions",
|
|
4003
|
-
title: `${perm.name} (${perm.key})`,
|
|
4004
|
-
detail: `Permission "${perm.key}" exists in seed data but has no entry in ROUTE_PERMISSIONS \u2014 no API route requires it.`
|
|
4005
|
-
});
|
|
4006
|
-
}
|
|
4007
|
-
}
|
|
4008
|
-
return buildReport("static", "unenforced_permissions", findings);
|
|
4009
|
-
}
|
|
4010
|
-
function checkHardcodedValues(rootDir) {
|
|
4011
|
-
const findings = [];
|
|
4012
|
-
const staticGraph = readGraphFile(rootDir, "static");
|
|
4013
|
-
if (!staticGraph) return buildReport("static", "hardcoded_values", findings);
|
|
4014
|
-
const knownValues = /* @__PURE__ */ new Set();
|
|
4015
|
-
for (const n of staticGraph.nodes) {
|
|
4016
|
-
if (n.type === "enum_value") knownValues.add(n.value);
|
|
4017
|
-
}
|
|
4018
|
-
const api = readGraphFile(rootDir, "api");
|
|
4019
|
-
if (!api) return buildReport("static", "hardcoded_values", findings);
|
|
4020
|
-
const allCapsRe = /['"]([A-Z][A-Z_]{2,})['"]/g;
|
|
4021
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4022
|
-
for (const node of api.nodes) {
|
|
4023
|
-
if (node.type !== "endpoint") continue;
|
|
4024
|
-
const filePath = (0, import_node_path18.join)(rootDir, "src", node.id);
|
|
4025
|
-
if (!(0, import_node_fs16.existsSync)(filePath)) continue;
|
|
4026
|
-
const content = (0, import_node_fs16.readFileSync)(filePath, "utf-8");
|
|
4027
|
-
let m;
|
|
4028
|
-
allCapsRe.lastIndex = 0;
|
|
4029
|
-
while ((m = allCapsRe.exec(content)) !== null) {
|
|
4030
|
-
const val = m[1];
|
|
4031
|
-
if (knownValues.has(val)) continue;
|
|
4032
|
-
if (["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "UTF", "NULL", "TRUE", "FALSE", "JSON", "HTML", "CSS", "ENV"].includes(val)) continue;
|
|
4033
|
-
const key = `${node.id}:${val}`;
|
|
4034
|
-
if (seen.has(key)) continue;
|
|
4035
|
-
seen.add(key);
|
|
4036
|
-
findings.push({
|
|
4037
|
-
id: `hardcoded:${key}`,
|
|
4038
|
-
severity: "info",
|
|
4039
|
-
category: "hardcoded_values",
|
|
4040
|
-
title: `"${val}" in ${node.id}`,
|
|
4041
|
-
detail: `ALL_CAPS string literal "${val}" appears in API code but is not in the enum/seed inventory. May be an unregistered constant.`,
|
|
4042
|
-
file: node.id
|
|
4043
|
-
});
|
|
4044
|
-
}
|
|
4045
|
-
}
|
|
4046
|
-
return buildReport("static", "hardcoded_values", findings);
|
|
4047
|
-
}
|
|
4048
|
-
function buildReport(layer, check, findings) {
|
|
4049
|
-
return {
|
|
4050
|
-
layer,
|
|
4051
|
-
check,
|
|
4052
|
-
findings,
|
|
4053
|
-
summary: {
|
|
4054
|
-
errors: findings.filter((f) => f.severity === "error").length,
|
|
4055
|
-
warnings: findings.filter((f) => f.severity === "warning").length,
|
|
4056
|
-
info: findings.filter((f) => f.severity === "info").length
|
|
4057
|
-
},
|
|
4058
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4059
|
-
};
|
|
4060
|
-
}
|
|
4061
|
-
var CHECKS = {
|
|
4062
|
-
db: {
|
|
4063
|
-
schema_drift: checkSchemaDrift,
|
|
4064
|
-
orphan_fks: checkOrphanFks
|
|
4065
|
-
},
|
|
4066
|
-
api: {
|
|
4067
|
-
unprotected_routes: checkUnprotectedRoutes
|
|
4068
|
-
},
|
|
4069
|
-
ui: {
|
|
4070
|
-
dead_screens: checkDeadScreens
|
|
4071
|
-
},
|
|
4072
|
-
static: {
|
|
4073
|
-
unenforced_permissions: checkUnenforcedPermissions,
|
|
4074
|
-
hardcoded_values: checkHardcodedValues
|
|
4075
|
-
}
|
|
4076
|
-
};
|
|
4077
|
-
function getAvailableChecks() {
|
|
4078
|
-
const result = {};
|
|
4079
|
-
for (const [layer, checks] of Object.entries(CHECKS)) {
|
|
4080
|
-
result[layer] = Object.keys(checks);
|
|
4081
|
-
}
|
|
4082
|
-
return result;
|
|
4083
|
-
}
|
|
4084
|
-
function runAudit(rootDir, layer, check) {
|
|
4085
|
-
if (layer === "all") {
|
|
4086
|
-
const reports = [];
|
|
4087
|
-
for (const [l, checks] of Object.entries(CHECKS)) {
|
|
4088
|
-
for (const fn of Object.values(checks)) {
|
|
4089
|
-
reports.push(fn(rootDir));
|
|
4090
|
-
}
|
|
4091
|
-
}
|
|
4092
|
-
return reports;
|
|
4093
|
-
}
|
|
4094
|
-
const layerChecks = CHECKS[layer];
|
|
4095
|
-
if (!layerChecks) {
|
|
4096
|
-
return [{
|
|
4097
|
-
layer,
|
|
4098
|
-
check: "unknown",
|
|
4099
|
-
findings: [{ id: "invalid-layer", severity: "error", category: "system", title: "Invalid layer", detail: `Layer "${layer}" has no audit checks. Available: ${Object.keys(CHECKS).join(", ")}` }],
|
|
4100
|
-
summary: { errors: 1, warnings: 0, info: 0 },
|
|
4101
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4102
|
-
}];
|
|
4103
|
-
}
|
|
4104
|
-
if (check) {
|
|
4105
|
-
const fn = layerChecks[check];
|
|
4106
|
-
if (!fn) {
|
|
4107
|
-
return [{
|
|
4108
|
-
layer,
|
|
4109
|
-
check,
|
|
4110
|
-
findings: [{ id: "invalid-check", severity: "error", category: "system", title: "Invalid check", detail: `Check "${check}" not found for layer "${layer}". Available: ${Object.keys(layerChecks).join(", ")}` }],
|
|
4111
|
-
summary: { errors: 1, warnings: 0, info: 0 },
|
|
4112
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4113
|
-
}];
|
|
4114
|
-
}
|
|
4115
|
-
return [fn(rootDir)];
|
|
4116
|
-
}
|
|
4117
|
-
return Object.values(layerChecks).map((fn) => fn(rootDir));
|
|
4118
|
-
}
|
|
4119
|
-
function formatAsPrompt(reports) {
|
|
4120
|
-
const lines = [];
|
|
4121
|
-
for (const report of reports) {
|
|
4122
|
-
if (report.findings.length === 0) continue;
|
|
4123
|
-
lines.push(`## ${report.layer.toUpperCase()} \u2014 ${report.check} (${report.findings.length} findings)`);
|
|
4124
|
-
lines.push("");
|
|
4125
|
-
for (const f of report.findings) {
|
|
4126
|
-
const tag = f.severity === "error" ? "ERROR" : f.severity === "warning" ? "WARNING" : "INFO";
|
|
4127
|
-
const filePart = f.file ? ` (${f.file}${f.line ? `:${f.line}` : ""})` : "";
|
|
4128
|
-
lines.push(`- [${tag}] ${f.title}${filePart}`);
|
|
4129
|
-
lines.push(` ${f.detail}`);
|
|
4130
|
-
}
|
|
4131
|
-
lines.push("");
|
|
4132
|
-
}
|
|
4133
|
-
if (lines.length === 0) return "No audit findings.";
|
|
4134
|
-
return lines.join("\n");
|
|
4135
|
-
}
|
|
4136
|
-
|
|
4137
|
-
// src/server/chart-serve.ts
|
|
4138
|
-
var MAX_PORT_SCAN = 3;
|
|
4139
|
-
function randomPort() {
|
|
4140
|
-
return 49152 + Math.floor(Math.random() * (65535 - 49152));
|
|
4141
|
-
}
|
|
4142
|
-
var MIME_TYPES = {
|
|
4143
|
-
".html": "text/html; charset=utf-8",
|
|
4144
|
-
".js": "application/javascript; charset=utf-8",
|
|
4145
|
-
".css": "text/css; charset=utf-8",
|
|
4146
|
-
".json": "application/json; charset=utf-8",
|
|
4147
|
-
".png": "image/png",
|
|
4148
|
-
".svg": "image/svg+xml",
|
|
4149
|
-
".ico": "image/x-icon",
|
|
4150
|
-
".woff": "font/woff",
|
|
4151
|
-
".woff2": "font/woff2"
|
|
4152
|
-
};
|
|
4153
|
-
function findProjectRoot(startDir) {
|
|
4154
|
-
let dir = startDir;
|
|
4155
|
-
for (let i = 0; i < 8; i++) {
|
|
4156
|
-
const graphsDir2 = import_node_path19.default.join(dir, ".launchsecure", "graphs");
|
|
4157
|
-
if (import_node_fs17.default.existsSync(import_node_path19.default.join(graphsDir2, "ui.json")) || import_node_fs17.default.existsSync(import_node_path19.default.join(graphsDir2, "api.json")) || import_node_fs17.default.existsSync(import_node_path19.default.join(graphsDir2, "db.json"))) return dir;
|
|
4158
|
-
const parent = import_node_path19.default.dirname(dir);
|
|
4159
|
-
if (parent === dir) break;
|
|
4160
|
-
dir = parent;
|
|
4161
|
-
}
|
|
4162
|
-
dir = startDir;
|
|
4163
|
-
for (let i = 0; i < 8; i++) {
|
|
4164
|
-
if (import_node_fs17.default.existsSync(import_node_path19.default.join(dir, ".git"))) return dir;
|
|
4165
|
-
const parent = import_node_path19.default.dirname(dir);
|
|
4166
|
-
if (parent === dir) break;
|
|
4167
|
-
dir = parent;
|
|
4168
|
-
}
|
|
4169
|
-
return startDir;
|
|
4170
|
-
}
|
|
4171
|
-
function resolveRequestRoot(url, monorepoRoot, projects) {
|
|
4172
|
-
const projectParam = url.searchParams.get("project");
|
|
4173
|
-
if (!projectParam || projects.length === 0) return monorepoRoot;
|
|
4174
|
-
const resolved = import_node_path19.default.resolve(monorepoRoot, projectParam);
|
|
4175
|
-
if (!resolved.startsWith(monorepoRoot)) {
|
|
4176
|
-
throw new Error("Project path outside monorepo root");
|
|
4177
|
-
}
|
|
4178
|
-
return resolved;
|
|
4179
|
-
}
|
|
4180
|
-
async function buildMergedGraph(root) {
|
|
4181
|
-
let graphs = readAllGraphs(root);
|
|
4182
|
-
if (Object.keys(graphs).length === 0) {
|
|
4183
|
-
await generateGraph(root);
|
|
4184
|
-
graphs = readAllGraphs(root);
|
|
4185
|
-
}
|
|
4186
|
-
const nodes = [];
|
|
4187
|
-
const rawLinks = [];
|
|
4188
|
-
for (const layer of Object.keys(graphs)) {
|
|
4189
|
-
const g = graphs[layer];
|
|
4190
|
-
if (!g) continue;
|
|
4191
|
-
for (const n of g.nodes) {
|
|
4192
|
-
nodes.push({
|
|
4193
|
-
id: `${layer}:${n.id}`,
|
|
4194
|
-
name: n.name,
|
|
4195
|
-
type: n.type,
|
|
4196
|
-
layer,
|
|
4197
|
-
module: n.module ?? null,
|
|
4198
|
-
path: n.path ?? n.id
|
|
4199
|
-
});
|
|
4200
|
-
}
|
|
4201
|
-
for (const e of g.edges) {
|
|
4202
|
-
rawLinks.push({ source: `${layer}:${e.source}`, target: `${layer}:${e.target}`, type: e.type, layer, cross: false });
|
|
4203
|
-
}
|
|
4204
|
-
for (const c of g.cross_refs ?? []) {
|
|
4205
|
-
rawLinks.push({ source: `${layer}:${c.source}`, target: `${c.layer}:${c.target}`, type: c.type, layer, cross: true });
|
|
4206
|
-
}
|
|
4207
|
-
}
|
|
4208
|
-
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
4209
|
-
const links = rawLinks.filter((l) => nodeIds.has(l.source) && nodeIds.has(l.target));
|
|
4210
|
-
return {
|
|
4211
|
-
nodes,
|
|
4212
|
-
links,
|
|
4213
|
-
stats: {
|
|
4214
|
-
nodes: nodes.length,
|
|
4215
|
-
links: links.length,
|
|
4216
|
-
byLayer: {
|
|
4217
|
-
ui: graphs.ui ? graphs.ui.nodes.length : 0,
|
|
4218
|
-
api: graphs.api ? graphs.api.nodes.length : 0,
|
|
4219
|
-
db: graphs.db ? graphs.db.nodes.length : 0
|
|
4220
|
-
}
|
|
4221
|
-
}
|
|
4222
|
-
};
|
|
4223
|
-
}
|
|
4224
|
-
function serveStatic(res, filePath) {
|
|
4225
|
-
if (!import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) return false;
|
|
4226
|
-
const ext = import_node_path19.default.extname(filePath).toLowerCase();
|
|
4227
|
-
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
4228
|
-
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
4229
|
-
import_node_fs17.default.createReadStream(filePath).pipe(res);
|
|
4230
|
-
return true;
|
|
4231
|
-
}
|
|
4232
|
-
function serveIndex(res, clientDir) {
|
|
4233
|
-
const indexPath = import_node_path19.default.join(clientDir, "index.html");
|
|
4234
|
-
if (!import_node_fs17.default.existsSync(indexPath)) {
|
|
4235
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
4236
|
-
res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
|
|
4237
|
-
return;
|
|
4238
|
-
}
|
|
4239
|
-
serveStatic(res, indexPath);
|
|
4240
|
-
}
|
|
4241
|
-
function tryListen(server, port) {
|
|
4242
|
-
return new Promise((resolve3, reject) => {
|
|
4243
|
-
const onError = (err) => {
|
|
4244
|
-
server.off("listening", onListening);
|
|
4245
|
-
reject(err);
|
|
4246
|
-
};
|
|
4247
|
-
const onListening = () => {
|
|
4248
|
-
server.off("error", onError);
|
|
4249
|
-
resolve3(port);
|
|
4250
|
-
};
|
|
4251
|
-
server.once("error", onError);
|
|
4252
|
-
server.once("listening", onListening);
|
|
4253
|
-
server.listen(port, "127.0.0.1");
|
|
4254
|
-
});
|
|
4255
|
-
}
|
|
4256
|
-
async function bindWithFallback(server, startPort) {
|
|
4257
|
-
let lastErr = null;
|
|
4258
|
-
for (let i = 0; i < MAX_PORT_SCAN; i++) {
|
|
4259
|
-
const port = startPort + i;
|
|
4260
|
-
try {
|
|
4261
|
-
return await tryListen(server, port);
|
|
4262
|
-
} catch (err) {
|
|
4263
|
-
const code = err.code;
|
|
4264
|
-
if (code === "EADDRINUSE") {
|
|
4265
|
-
lastErr = err;
|
|
4266
|
-
continue;
|
|
4267
|
-
}
|
|
4268
|
-
throw err;
|
|
4269
|
-
}
|
|
4270
|
-
}
|
|
4271
|
-
throw lastErr ?? new Error("Failed to bind any port");
|
|
4272
|
-
}
|
|
4273
|
-
async function startChartServer(opts = {}) {
|
|
4274
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
4275
|
-
const projectRoot = findProjectRoot(cwd);
|
|
4276
|
-
const existing = getLiveLock(projectRoot);
|
|
4277
|
-
if (existing) {
|
|
4278
|
-
if (!opts.quiet) {
|
|
4279
|
-
process.stderr.write(
|
|
4280
|
-
`[launch-chart] already running (pid ${existing.pid}) at ${existing.url}
|
|
4281
|
-
`
|
|
4282
|
-
);
|
|
4283
|
-
}
|
|
4284
|
-
return { port: existing.port, url: existing.url };
|
|
4285
|
-
}
|
|
4286
|
-
const clientDir = opts.clientDir ?? import_node_path19.default.join(__dirname, "..", "chart-client");
|
|
4287
|
-
const rootConfig = loadConfig(projectRoot);
|
|
4288
|
-
const projects = rootConfig.projects ?? [];
|
|
4289
|
-
const server = import_node_http.default.createServer((req, res) => {
|
|
4290
|
-
try {
|
|
4291
|
-
const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
4292
|
-
let reqRoot;
|
|
4293
|
-
try {
|
|
4294
|
-
reqRoot = resolveRequestRoot(url2, projectRoot, projects);
|
|
4295
|
-
} catch (err) {
|
|
4296
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4297
|
-
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
4298
|
-
return;
|
|
4299
|
-
}
|
|
4300
|
-
if (req.method === "GET" && url2.pathname === "/api/projects") {
|
|
4301
|
-
const projectList = projects.length > 0 ? projects.map((p) => {
|
|
4302
|
-
const absRoot = import_node_path19.default.resolve(projectRoot, p.root);
|
|
4303
|
-
const hasGraphs = import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, ".launchsecure", "graphs"));
|
|
4304
|
-
const hasNextConfig2 = import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, "next.config.ts")) || import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, "next.config.js")) || import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, "next.config.mjs"));
|
|
4305
|
-
return { name: p.name, root: p.root, hasGraphs, hasNextConfig: hasNextConfig2 };
|
|
4306
|
-
}) : [{ name: import_node_path19.default.basename(projectRoot), root: ".", hasGraphs: import_node_fs17.default.existsSync(import_node_path19.default.join(projectRoot, ".launchsecure", "graphs")), hasNextConfig: true }];
|
|
4307
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4308
|
-
res.end(JSON.stringify({ projects: projectList, monorepoRoot: projectRoot }));
|
|
4309
|
-
return;
|
|
4310
|
-
}
|
|
4311
|
-
if (req.method === "GET" && url2.pathname === "/api/project-graph") {
|
|
4312
|
-
const regenerate = url2.searchParams.get("regenerate") === "1";
|
|
4313
|
-
(async () => {
|
|
4314
|
-
if (regenerate) await generateGraph(reqRoot);
|
|
4315
|
-
const merged = await buildMergedGraph(reqRoot);
|
|
4316
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4317
|
-
res.end(JSON.stringify({
|
|
4318
|
-
...merged,
|
|
4319
|
-
debug: { cwd, projectRoot: reqRoot }
|
|
4320
|
-
}));
|
|
4321
|
-
})().catch((e) => {
|
|
4322
|
-
res.writeHead(500);
|
|
4323
|
-
res.end(String(e));
|
|
4324
|
-
});
|
|
4325
|
-
return;
|
|
4326
|
-
}
|
|
4327
|
-
if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
|
|
4328
|
-
const graphs = readAllGraphs(reqRoot);
|
|
4329
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4330
|
-
res.end(JSON.stringify({ ui: graphs.ui ?? null, api: graphs.api ?? null, db: graphs.db ?? null }));
|
|
4331
|
-
return;
|
|
4332
|
-
}
|
|
4333
|
-
if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
|
|
4334
|
-
(async () => {
|
|
4335
|
-
await generateGraph(reqRoot);
|
|
4336
|
-
const graphs = readAllGraphs(reqRoot);
|
|
4337
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4338
|
-
res.end(JSON.stringify({
|
|
4339
|
-
ok: true,
|
|
4340
|
-
ui: graphs.ui ?? null,
|
|
4341
|
-
api: graphs.api ?? null,
|
|
4342
|
-
db: graphs.db ?? null
|
|
4343
|
-
}));
|
|
4344
|
-
})().catch((err) => {
|
|
4345
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
4346
|
-
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
4347
|
-
});
|
|
4348
|
-
return;
|
|
4349
|
-
}
|
|
4350
|
-
if (req.method === "GET" && url2.pathname === "/api/file-content") {
|
|
4351
|
-
const relPath = url2.searchParams.get("path");
|
|
4352
|
-
if (!relPath || relPath.includes("..") || import_node_path19.default.isAbsolute(relPath)) {
|
|
4353
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4354
|
-
res.end(JSON.stringify({ error: "Invalid path" }));
|
|
4355
|
-
return;
|
|
4356
|
-
}
|
|
4357
|
-
const filePath = import_node_path19.default.join(reqRoot, relPath);
|
|
4358
|
-
if (!filePath.startsWith(reqRoot) || !import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) {
|
|
4359
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
4360
|
-
res.end(JSON.stringify({ error: "File not found" }));
|
|
4361
|
-
return;
|
|
4362
|
-
}
|
|
4363
|
-
const ext = import_node_path19.default.extname(filePath).toLowerCase();
|
|
4364
|
-
const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
|
|
4365
|
-
const content = import_node_fs17.default.readFileSync(filePath, "utf-8");
|
|
4366
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4367
|
-
res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
|
|
4368
|
-
return;
|
|
4369
|
-
}
|
|
4370
|
-
if (req.method === "GET" && url2.pathname === "/api/health") {
|
|
4371
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4372
|
-
res.end(JSON.stringify({ ok: true, projectRoot: reqRoot }));
|
|
4373
|
-
return;
|
|
4374
|
-
}
|
|
4375
|
-
if (req.method === "GET" && url2.pathname === "/api/parser-config") {
|
|
4376
|
-
const config2 = loadConfig(reqRoot);
|
|
4377
|
-
const registry = createRegistry(config2, reqRoot);
|
|
4378
|
-
const toLabel = (id) => id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
4379
|
-
const detection = [];
|
|
4380
|
-
for (const parser of registry.getAll()) {
|
|
4381
|
-
if ("layers" in parser && Array.isArray(parser.layers)) {
|
|
4382
|
-
const mp = parser;
|
|
4383
|
-
detection.push({ id: mp.id, layers: mp.layers, label: toLabel(mp.id), detected: mp.detect(reqRoot) });
|
|
4384
|
-
} else if ("layer" in parser && parser.layer !== "crosslayer") {
|
|
4385
|
-
const sp = parser;
|
|
4386
|
-
detection.push({ id: sp.id, layers: [sp.layer], label: toLabel(sp.id), detected: sp.detect(reqRoot) });
|
|
4387
|
-
}
|
|
4388
|
-
}
|
|
4389
|
-
const crosslayerParsers = {};
|
|
4390
|
-
for (const p of registry.getCrossLayerParsers()) {
|
|
4391
|
-
const concern = p.concern ?? "api-binding";
|
|
4392
|
-
if (!crosslayerParsers[concern]) crosslayerParsers[concern] = [];
|
|
4393
|
-
crosslayerParsers[concern].push({ id: p.id, label: toLabel(p.id) });
|
|
4394
|
-
}
|
|
4395
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4396
|
-
res.end(JSON.stringify({ config: config2, detection, crosslayerParsers }));
|
|
4397
|
-
return;
|
|
4398
|
-
}
|
|
4399
|
-
if (req.method === "POST" && url2.pathname === "/api/parser-config") {
|
|
4400
|
-
let body = "";
|
|
4401
|
-
req.on("data", (chunk) => {
|
|
4402
|
-
body += chunk.toString();
|
|
4403
|
-
});
|
|
4404
|
-
req.on("end", () => {
|
|
4405
|
-
try {
|
|
4406
|
-
const newConfig = JSON.parse(body);
|
|
4407
|
-
const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
|
|
4408
|
-
import_node_fs17.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
|
|
4409
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4410
|
-
res.end(JSON.stringify({ ok: true }));
|
|
4411
|
-
} catch (err) {
|
|
4412
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4413
|
-
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
4414
|
-
}
|
|
4415
|
-
});
|
|
4416
|
-
return;
|
|
4417
|
-
}
|
|
4418
|
-
if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
|
|
4419
|
-
const config2 = loadConfig(reqRoot);
|
|
4420
|
-
const builtinTaggers = [
|
|
4421
|
-
{ id: "module", tagKey: "module", trackUntagged: config2.taggers?.trackUntagged?.module ?? true },
|
|
4422
|
-
{ id: "screen", tagKey: "screen", trackUntagged: config2.taggers?.trackUntagged?.screen ?? true }
|
|
4423
|
-
];
|
|
4424
|
-
const disabled = config2.taggers?.disabled ?? [];
|
|
4425
|
-
const customTaggers = config2.taggers?.custom ?? [];
|
|
4426
|
-
const moduleRules = config2.taggers?.module?.rules ?? [];
|
|
4427
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4428
|
-
res.end(JSON.stringify({ builtinTaggers, disabled, customTaggers, moduleRules }));
|
|
4429
|
-
return;
|
|
4430
|
-
}
|
|
4431
|
-
if (req.method === "POST" && url2.pathname === "/api/tagger-config") {
|
|
4432
|
-
let body = "";
|
|
4433
|
-
req.on("data", (chunk) => {
|
|
4434
|
-
body += chunk.toString();
|
|
4435
|
-
});
|
|
4436
|
-
req.on("end", () => {
|
|
4437
|
-
try {
|
|
4438
|
-
const taggerConfig = JSON.parse(body);
|
|
4439
|
-
const config2 = loadConfig(reqRoot);
|
|
4440
|
-
config2.taggers = taggerConfig;
|
|
4441
|
-
const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
|
|
4442
|
-
import_node_fs17.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
4443
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4444
|
-
res.end(JSON.stringify({ ok: true }));
|
|
4445
|
-
} catch (err) {
|
|
4446
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4447
|
-
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
4448
|
-
}
|
|
4449
|
-
});
|
|
4450
|
-
return;
|
|
4451
|
-
}
|
|
4452
|
-
if (req.method === "GET" && url2.pathname === "/api/tags") {
|
|
4453
|
-
const store = readTagStore(reqRoot);
|
|
4454
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4455
|
-
res.end(JSON.stringify(store));
|
|
4456
|
-
return;
|
|
4457
|
-
}
|
|
4458
|
-
if (req.method === "POST" && url2.pathname === "/api/tags") {
|
|
4459
|
-
let body = "";
|
|
4460
|
-
req.on("data", (chunk) => {
|
|
4461
|
-
body += chunk.toString();
|
|
4462
|
-
});
|
|
4463
|
-
req.on("end", () => {
|
|
4464
|
-
try {
|
|
4465
|
-
const { nodeId, key, value } = JSON.parse(body);
|
|
4466
|
-
if (!nodeId || !key || !value) {
|
|
4467
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4468
|
-
res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
|
|
4469
|
-
return;
|
|
4470
|
-
}
|
|
4471
|
-
setTag(reqRoot, nodeId, key, value);
|
|
4472
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4473
|
-
res.end(JSON.stringify({ ok: true }));
|
|
4474
|
-
} catch (err) {
|
|
4475
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4476
|
-
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
4477
|
-
}
|
|
4478
|
-
});
|
|
4479
|
-
return;
|
|
4480
|
-
}
|
|
4481
|
-
if (req.method === "DELETE" && url2.pathname === "/api/tags") {
|
|
4482
|
-
let body = "";
|
|
4483
|
-
req.on("data", (chunk) => {
|
|
4484
|
-
body += chunk.toString();
|
|
4485
|
-
});
|
|
4486
|
-
req.on("end", () => {
|
|
4487
|
-
try {
|
|
4488
|
-
const { nodeId, key } = JSON.parse(body);
|
|
4489
|
-
if (!nodeId || !key) {
|
|
4490
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4491
|
-
res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
|
|
4492
|
-
return;
|
|
4493
|
-
}
|
|
4494
|
-
removeTag(reqRoot, nodeId, key);
|
|
4495
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4496
|
-
res.end(JSON.stringify({ ok: true }));
|
|
4497
|
-
} catch (err) {
|
|
4498
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4499
|
-
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
4500
|
-
}
|
|
4501
|
-
});
|
|
4502
|
-
return;
|
|
4503
|
-
}
|
|
4504
|
-
if (req.method === "GET" && url2.pathname === "/api/detected-paths") {
|
|
4505
|
-
const config2 = loadConfig(reqRoot);
|
|
4506
|
-
const paths = resolveProjectPaths(reqRoot, config2);
|
|
4507
|
-
const overrides = {
|
|
4508
|
-
appDir: !!config2.paths?.appDir,
|
|
4509
|
-
dbDir: !!config2.paths?.dbDir
|
|
4510
|
-
};
|
|
4511
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4512
|
-
res.end(JSON.stringify({
|
|
4513
|
-
projectRoot: reqRoot,
|
|
4514
|
-
detected: paths ? {
|
|
4515
|
-
srcDir: import_node_path19.default.relative(reqRoot, paths.srcDir) || ".",
|
|
4516
|
-
appDir: import_node_path19.default.relative(reqRoot, paths.appDir),
|
|
4517
|
-
apiDir: import_node_path19.default.relative(reqRoot, paths.apiDir),
|
|
4518
|
-
dbDir: paths.dbDir ? import_node_path19.default.relative(reqRoot, paths.dbDir) : null
|
|
4519
|
-
} : null,
|
|
4520
|
-
overrides,
|
|
4521
|
-
isOverride: overrides.appDir
|
|
4522
|
-
}));
|
|
4523
|
-
return;
|
|
4524
|
-
}
|
|
4525
|
-
if (req.method === "GET" && url2.pathname === "/api/browse-dir") {
|
|
4526
|
-
const browsePath = url2.searchParams.get("path") || projectRoot;
|
|
4527
|
-
const abs = import_node_path19.default.resolve(browsePath);
|
|
4528
|
-
const twoUp = import_node_path19.default.resolve(projectRoot, "..", "..");
|
|
4529
|
-
if (!abs.startsWith(twoUp)) {
|
|
4530
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
4531
|
-
res.end(JSON.stringify({ ok: false, error: "Path outside allowed range" }));
|
|
4532
|
-
return;
|
|
4533
|
-
}
|
|
4534
|
-
try {
|
|
4535
|
-
const entries = import_node_fs17.default.readdirSync(abs, { withFileTypes: true });
|
|
4536
|
-
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "dist" && e.name !== ".next").map((e) => e.name).sort();
|
|
4537
|
-
const parent = abs !== twoUp ? import_node_path19.default.dirname(abs) : null;
|
|
4538
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4539
|
-
res.end(JSON.stringify({ current: abs, parent, dirs, relative: import_node_path19.default.relative(projectRoot, abs) || "." }));
|
|
4540
|
-
} catch (err) {
|
|
4541
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4542
|
-
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
4543
|
-
}
|
|
4544
|
-
return;
|
|
4545
|
-
}
|
|
4546
|
-
if (req.method === "GET" && url2.pathname === "/api/audit") {
|
|
4547
|
-
const layer = url2.searchParams.get("layer") ?? "all";
|
|
4548
|
-
const check = url2.searchParams.get("check") ?? void 0;
|
|
4549
|
-
const reports = runAudit(reqRoot, layer, check);
|
|
4550
|
-
const prompt = formatAsPrompt(reports);
|
|
4551
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4552
|
-
res.end(JSON.stringify({ reports, prompt, checks: getAvailableChecks() }));
|
|
4553
|
-
return;
|
|
4554
|
-
}
|
|
4555
|
-
if (req.method === "POST" && url2.pathname === "/api/projects") {
|
|
4556
|
-
let body = "";
|
|
4557
|
-
req.on("data", (chunk) => {
|
|
4558
|
-
body += chunk.toString();
|
|
4559
|
-
});
|
|
4560
|
-
req.on("end", () => {
|
|
4561
|
-
try {
|
|
4562
|
-
const { projects: newProjects } = JSON.parse(body);
|
|
4563
|
-
const config2 = loadConfig(projectRoot);
|
|
4564
|
-
config2.projects = newProjects.length > 0 ? newProjects : void 0;
|
|
4565
|
-
const configPath = import_node_path19.default.join(projectRoot, ".launchchart.json");
|
|
4566
|
-
import_node_fs17.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
4567
|
-
projects.length = 0;
|
|
4568
|
-
if (config2.projects) projects.push(...config2.projects);
|
|
4569
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4570
|
-
res.end(JSON.stringify({ ok: true }));
|
|
4571
|
-
} catch (err) {
|
|
4572
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4573
|
-
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
4574
|
-
}
|
|
4575
|
-
});
|
|
4576
|
-
return;
|
|
4577
|
-
}
|
|
4578
|
-
if (url2.pathname !== "/") {
|
|
4579
|
-
const staticPath = import_node_path19.default.join(clientDir, url2.pathname);
|
|
4580
|
-
if (serveStatic(res, staticPath)) return;
|
|
4581
|
-
}
|
|
4582
|
-
serveIndex(res, clientDir);
|
|
4583
|
-
} catch (err) {
|
|
4584
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
4585
|
-
res.end(JSON.stringify({ error: String(err) }));
|
|
4586
|
-
}
|
|
4587
|
-
});
|
|
4588
|
-
const config = loadConfig(projectRoot);
|
|
4589
|
-
const startPort = opts.port ?? config.port ?? randomPort();
|
|
4590
|
-
const port = await bindWithFallback(server, startPort);
|
|
4591
|
-
const url = `http://localhost:${port}`;
|
|
4592
|
-
writeLock({
|
|
4593
|
-
pid: process.pid,
|
|
4594
|
-
port,
|
|
4595
|
-
cwd,
|
|
4596
|
-
url,
|
|
4597
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4598
|
-
}, projectRoot);
|
|
4599
|
-
const cleanup = () => {
|
|
4600
|
-
clearLock(projectRoot);
|
|
4601
|
-
server.close();
|
|
4602
|
-
};
|
|
4603
|
-
process.once("SIGINT", () => {
|
|
4604
|
-
cleanup();
|
|
4605
|
-
process.exit(0);
|
|
4606
|
-
});
|
|
4607
|
-
process.once("SIGTERM", () => {
|
|
4608
|
-
cleanup();
|
|
4609
|
-
process.exit(0);
|
|
4610
|
-
});
|
|
4611
|
-
process.once("exit", cleanup);
|
|
4612
|
-
if (!opts.quiet) {
|
|
4613
|
-
process.stderr.write(`[launch-chart] serving ${url}
|
|
4614
|
-
`);
|
|
4615
|
-
process.stderr.write(`[launch-chart] project root: ${projectRoot}
|
|
4616
|
-
`);
|
|
4617
|
-
if (projects.length > 0) {
|
|
4618
|
-
process.stderr.write(`[launch-chart] multi-project mode: ${projects.length} projects
|
|
4619
|
-
`);
|
|
4620
|
-
}
|
|
4621
|
-
}
|
|
4622
|
-
return { port, url };
|
|
4623
|
-
}
|
|
4624
|
-
function runServeCli(argv) {
|
|
4625
|
-
let port;
|
|
4626
|
-
for (let i = 0; i < argv.length; i++) {
|
|
4627
|
-
if (argv[i] === "--port" && argv[i + 1]) {
|
|
4628
|
-
port = parseInt(argv[++i], 10);
|
|
4629
|
-
} else if (argv[i].startsWith("--port=")) {
|
|
4630
|
-
port = parseInt(argv[i].slice("--port=".length), 10);
|
|
4631
|
-
}
|
|
4632
|
-
}
|
|
4633
|
-
startChartServer({ port }).catch((err) => {
|
|
4634
|
-
process.stderr.write(`[launch-chart] failed to start: ${err}
|
|
4635
|
-
`);
|
|
4636
|
-
process.exit(1);
|
|
4637
|
-
});
|
|
4638
|
-
}
|
|
4639
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
4640
|
-
0 && (module.exports = {
|
|
4641
|
-
runServeCli,
|
|
4642
|
-
startChartServer
|
|
4643
|
-
});
|