@launchsecure/launch-kit 0.0.25 → 0.0.27
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-CJ4mgRRF.css +1 -0
- package/dist/chart-client/assets/{index-C8ANseEa.js → index-Ccy-DpI-.js} +82 -73
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-DI5qSR_w.css +32 -0
- package/dist/client/assets/index-Dp0_okva.js +294 -0
- package/dist/client/index.html +2 -2
- package/dist/council-client/assets/index-C_-vAM9L.css +1 -0
- package/dist/council-client/assets/{index-Dc41S-R2.js → index-Dt4zWKSj.js} +14 -14
- package/dist/council-client/index.html +2 -2
- package/dist/deck-client/assets/{_baseUniq-2gclQXo7.js → _baseUniq-W2JQDmje.js} +1 -1
- package/dist/deck-client/assets/{arc-DcMY5Wm0.js → arc-DIBWAId9.js} +1 -1
- package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-B8iirmmJ.js → architectureDiagram-Q4EWVU46-CAIRMvJK.js} +1 -1
- package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-B4JBLjmJ.js → blockDiagram-DXYQGD6D-BeNaNiOi.js} +1 -1
- package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-CojrJAk8.js → c4Diagram-AHTNJAMY-B9Ozi62h.js} +1 -1
- package/dist/deck-client/assets/channel-CRdozqbp.js +1 -0
- package/dist/deck-client/assets/{chunk-4BX2VUAB-Bmb_BMDo.js → chunk-4BX2VUAB-D7AZ47dt.js} +1 -1
- package/dist/deck-client/assets/{chunk-4TB4RGXK-CumBy8qe.js → chunk-4TB4RGXK-DnVnNPcI.js} +1 -1
- package/dist/deck-client/assets/{chunk-55IACEB6-Ka8Hb1wD.js → chunk-55IACEB6-UKYs-YNd.js} +1 -1
- package/dist/deck-client/assets/{chunk-EDXVE4YY-B3sIPiQo.js → chunk-EDXVE4YY-D43b-SKn.js} +1 -1
- package/dist/deck-client/assets/{chunk-FMBD7UC4-C1tYkaqu.js → chunk-FMBD7UC4-QzBAoyyW.js} +1 -1
- package/dist/deck-client/assets/{chunk-OYMX7WX6-D7Wacbky.js → chunk-OYMX7WX6-Cjif4r6W.js} +1 -1
- package/dist/deck-client/assets/{chunk-QZHKN3VN-ChXI0vO3.js → chunk-QZHKN3VN-CqLDirEI.js} +1 -1
- package/dist/deck-client/assets/{chunk-YZCP3GAM-BXhiqf8u.js → chunk-YZCP3GAM-_FQvmMs4.js} +1 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-lIZMp57W.js +1 -0
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-lIZMp57W.js +1 -0
- package/dist/deck-client/assets/clone-BtWeSTyJ.js +1 -0
- package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-Bqp3p68D.js → cose-bilkent-S5V4N54A-rfrocesE.js} +1 -1
- package/dist/deck-client/assets/{dagre-KV5264BT-BS-rtyhZ.js → dagre-KV5264BT-Bv_7DJat.js} +1 -1
- package/dist/deck-client/assets/{diagram-5BDNPKRD-BIrj9YGI.js → diagram-5BDNPKRD-4F1414G5.js} +1 -1
- package/dist/deck-client/assets/{diagram-G4DWMVQ6-noHWPIg4.js → diagram-G4DWMVQ6-C4-Pszqm.js} +1 -1
- package/dist/deck-client/assets/{diagram-MMDJMWI5-C2qHxvqV.js → diagram-MMDJMWI5-B647TIx9.js} +1 -1
- package/dist/deck-client/assets/{diagram-TYMM5635-BytnGQr-.js → diagram-TYMM5635-BFAqpezd.js} +1 -1
- package/dist/deck-client/assets/{erDiagram-SMLLAGMA-BfK5m2YQ.js → erDiagram-SMLLAGMA-BfBfrJOC.js} +1 -1
- package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-Cq925G1Z.js → flowDiagram-DWJPFMVM-DX9YAYes.js} +1 -1
- package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-DhhHPAmj.js → ganttDiagram-T4ZO3ILL-DCuiy7wF.js} +1 -1
- package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-B3Lc0h9q.js → gitGraphDiagram-UUTBAWPF-CGp1IXUh.js} +1 -1
- package/dist/deck-client/assets/{graph-RTawgVWm.js → graph-B7g8aoxv.js} +1 -1
- package/dist/deck-client/assets/{index-BfIfJXmS.js → index-Dg1r-WSN.js} +68 -68
- package/dist/deck-client/assets/index-DsIZ3LqL.css +1 -0
- package/dist/deck-client/assets/{infoDiagram-42DDH7IO-BlR584kX.js → infoDiagram-42DDH7IO-L3fahMkF.js} +1 -1
- package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DygKoNGY.js → ishikawaDiagram-UXIWVN3A-aS_EjWBZ.js} +1 -1
- package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-BnaiYp9N.js → journeyDiagram-VCZTEJTY-djTSQZF9.js} +1 -1
- package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-BQBUBzJC.js → kanban-definition-6JOO6SKY-CcTHo4CM.js} +1 -1
- package/dist/deck-client/assets/{layout-DeZ8HI1T.js → layout-mEJiadb7.js} +1 -1
- package/dist/deck-client/assets/{linear-C6roLi_9.js → linear-XgTKqyRu.js} +1 -1
- package/dist/deck-client/assets/{min-CbUksbuI.js → min-Ct9jZdpd.js} +1 -1
- package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-iNxV62yN.js → mindmap-definition-QFDTVHPH-BaFxCGNU.js} +1 -1
- package/dist/deck-client/assets/{pieDiagram-DEJITSTG-DHVA0jaG.js → pieDiagram-DEJITSTG-CIbYYjtw.js} +1 -1
- package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-DBeKKLUQ.js → quadrantDiagram-34T5L4WZ-D9EtCOvh.js} +1 -1
- package/dist/deck-client/assets/{requirementDiagram-MS252O5E-CBwITx7p.js → requirementDiagram-MS252O5E-xeni9eVG.js} +1 -1
- package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-BtE-1YTU.js → sankeyDiagram-XADWPNL6-LYeknz9h.js} +1 -1
- package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-DN96yPP2.js → sequenceDiagram-FGHM5R23-RDbsKFZf.js} +1 -1
- package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-VUkKC2uJ.js → stateDiagram-FHFEXIEX-BH1Zjglk.js} +1 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-BrV78NDR.js +1 -0
- package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-oUeZhRns.js → timeline-definition-GMOUNBTQ-IFXxKptt.js} +1 -1
- package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-D87fK90n.js → vennDiagram-DHZGUBPP-D-sLkQs9.js} +1 -1
- package/dist/deck-client/assets/wardley-RL74JXVD-C010F8l4.js +162 -0
- package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-Ca_i0QRA.js → wardleyDiagram-NUSXRM2D-BTjjuDU3.js} +1 -1
- package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-CUOJVIvq.js → xychartDiagram-5P7HB3ND-AYbv92n-.js} +1 -1
- package/dist/deck-client/index.html +2 -2
- package/dist/server/chart-serve.js +4524 -3564
- package/dist/server/cli.js +27351 -5398
- package/dist/server/council-entry.js +17 -5
- package/dist/server/council-serve.js +8 -3
- package/dist/server/deck-mcp-entry.js +354 -13
- package/dist/server/deck-serve.js +298 -7
- 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 +5943 -4361
- package/dist/server/init-entry.js +609 -0
- package/dist/server/orbit-entry.js +2272 -0
- package/dist/server/{server/chart-serve.js → parse-worker-entry.js} +1900 -1822
- package/dist/server/recall-entry.js +1450 -0
- package/package.json +40 -8
- package/scaffolds/migrate-safety/.github/workflows/backup-on-migration.yml +72 -0
- package/scaffolds/migrate-safety/docs/migrations-runbook.md +172 -0
- package/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +294 -0
- package/dist/chart-client/assets/index--120d9P9.css +0 -1
- package/dist/client/assets/index-Bf8zdL3x.css +0 -32
- package/dist/client/assets/index-Ds9UP_cj.js +0 -291
- 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/cli.js +0 -13360
- package/dist/server/server/fb-wizard.js +0 -136
- package/dist/server/server/graph-mcp-entry.js +0 -6776
|
@@ -30,41 +30,171 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
));
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
|
-
// src/server/
|
|
34
|
-
var
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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 {};
|
|
33
|
+
// src/server/launch-kit-paths.ts
|
|
34
|
+
var LAUNCHSECURE_DIR, LAUNCHCHART_CONFIG_FILE;
|
|
35
|
+
var init_launch_kit_paths = __esm({
|
|
36
|
+
"src/server/launch-kit-paths.ts"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
LAUNCHSECURE_DIR = ".launchsecure";
|
|
39
|
+
LAUNCHCHART_CONFIG_FILE = ".launchchart.json";
|
|
45
40
|
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// src/server/graph/core/parse-failure-cache.ts
|
|
44
|
+
function hashContent(content) {
|
|
45
|
+
return "sha256:" + (0, import_node_crypto.createHash)("sha256").update(content).digest("hex");
|
|
46
|
+
}
|
|
47
|
+
function setActiveFailureCache(cache) {
|
|
48
|
+
activeCache = cache;
|
|
46
49
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
function getActiveFailureCache() {
|
|
51
|
+
return activeCache;
|
|
52
|
+
}
|
|
53
|
+
var import_node_crypto, import_node_fs4, import_node_path4, SCHEMA_VERSION, ParseFailureCache, activeCache;
|
|
54
|
+
var init_parse_failure_cache = __esm({
|
|
55
|
+
"src/server/graph/core/parse-failure-cache.ts"() {
|
|
50
56
|
"use strict";
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
import_node_crypto = require("node:crypto");
|
|
58
|
+
import_node_fs4 = require("node:fs");
|
|
59
|
+
import_node_path4 = require("node:path");
|
|
60
|
+
init_launch_kit_paths();
|
|
61
|
+
SCHEMA_VERSION = 1;
|
|
62
|
+
ParseFailureCache = class {
|
|
63
|
+
constructor(rootDir, treeSitterFingerprint) {
|
|
64
|
+
this.failures = /* @__PURE__ */ new Map();
|
|
65
|
+
this.dirty = false;
|
|
66
|
+
this.cacheFilePath = (0, import_node_path4.join)(rootDir, LAUNCHSECURE_DIR, "graphs", ".failures.json");
|
|
67
|
+
this.fingerprint = treeSitterFingerprint;
|
|
68
|
+
this.load();
|
|
69
|
+
}
|
|
70
|
+
/** Path the cache persists to. Exposed so tier 3 can audit it. */
|
|
71
|
+
get filePath() {
|
|
72
|
+
return this.cacheFilePath;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Return the entry for absPath IF the cached entry's contentHash matches the
|
|
76
|
+
* current bytes. Otherwise null (caller should attempt a real parse).
|
|
77
|
+
*/
|
|
78
|
+
shouldSkip(absPath, content) {
|
|
79
|
+
const cached = this.failures.get(absPath);
|
|
80
|
+
if (!cached) return null;
|
|
81
|
+
if (cached.contentHash !== hashContent(content)) return null;
|
|
82
|
+
return cached;
|
|
83
|
+
}
|
|
84
|
+
/** Record a parse failure. Increments consecutiveAttempts when hash matches. */
|
|
85
|
+
recordFailure(absPath, content, error) {
|
|
86
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
87
|
+
const hash = hashContent(content);
|
|
88
|
+
const prior = this.failures.get(absPath);
|
|
89
|
+
const entry = {
|
|
90
|
+
contentHash: hash,
|
|
91
|
+
failedAt: prior && prior.contentHash === hash ? prior.failedAt : now,
|
|
92
|
+
lastAttemptAt: now,
|
|
93
|
+
lastError: error,
|
|
94
|
+
consecutiveAttempts: prior && prior.contentHash === hash ? prior.consecutiveAttempts + 1 : 1
|
|
95
|
+
};
|
|
96
|
+
this.failures.set(absPath, entry);
|
|
97
|
+
this.dirty = true;
|
|
98
|
+
}
|
|
99
|
+
/** Bump the lastAttemptAt timestamp on a skip without changing the failure record. */
|
|
100
|
+
bumpAttempt(absPath) {
|
|
101
|
+
const cached = this.failures.get(absPath);
|
|
102
|
+
if (!cached) return;
|
|
103
|
+
cached.lastAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
104
|
+
this.dirty = true;
|
|
105
|
+
}
|
|
106
|
+
/** Clear a single entry (call on successful parse). */
|
|
107
|
+
clear(absPath) {
|
|
108
|
+
if (!this.failures.has(absPath)) return false;
|
|
109
|
+
this.failures.delete(absPath);
|
|
110
|
+
this.dirty = true;
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
/** Wipe the entire cache (call after a successful tier-3 full regen). */
|
|
114
|
+
clearAll() {
|
|
115
|
+
if (this.failures.size === 0) return;
|
|
116
|
+
this.failures.clear();
|
|
117
|
+
this.dirty = true;
|
|
118
|
+
}
|
|
119
|
+
/** Snapshot for meta/status surfacing. */
|
|
120
|
+
list() {
|
|
121
|
+
const out = [];
|
|
122
|
+
for (const [path, entry] of this.failures) out.push({ path, ...entry });
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
size() {
|
|
126
|
+
return this.failures.size;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Atomically persist to disk. No-op if nothing has changed since last flush.
|
|
130
|
+
* Uses temp + rename so a torn write never corrupts the cache.
|
|
131
|
+
*/
|
|
132
|
+
flush() {
|
|
133
|
+
if (!this.dirty) return;
|
|
134
|
+
const payload = {
|
|
135
|
+
schemaVersion: SCHEMA_VERSION,
|
|
136
|
+
treeSitterFingerprint: this.fingerprint,
|
|
137
|
+
failures: Object.fromEntries(this.failures)
|
|
138
|
+
};
|
|
139
|
+
const dir = (0, import_node_path4.dirname)(this.cacheFilePath);
|
|
140
|
+
const tmp = this.cacheFilePath + ".tmp";
|
|
141
|
+
try {
|
|
142
|
+
require("node:fs").mkdirSync(dir, { recursive: true });
|
|
143
|
+
(0, import_node_fs4.writeFileSync)(tmp, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
144
|
+
(0, import_node_fs4.renameSync)(tmp, this.cacheFilePath);
|
|
145
|
+
this.dirty = false;
|
|
146
|
+
} catch (e) {
|
|
147
|
+
process.stderr.write(`[lc-failure-cache] flush failed for ${this.cacheFilePath}: ${e.message}
|
|
148
|
+
`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
load() {
|
|
152
|
+
if (!(0, import_node_fs4.existsSync)(this.cacheFilePath)) return;
|
|
153
|
+
let parsed;
|
|
154
|
+
try {
|
|
155
|
+
parsed = JSON.parse((0, import_node_fs4.readFileSync)(this.cacheFilePath, "utf-8"));
|
|
156
|
+
} catch (e) {
|
|
157
|
+
process.stderr.write(`[lc-failure-cache] cache file unreadable, starting fresh: ${e.message}
|
|
158
|
+
`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (!parsed || parsed.schemaVersion !== SCHEMA_VERSION) {
|
|
162
|
+
process.stderr.write(`[lc-failure-cache] schema version mismatch, discarding cache
|
|
163
|
+
`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (parsed.treeSitterFingerprint !== this.fingerprint) {
|
|
167
|
+
process.stderr.write(`[lc-failure-cache] tree-sitter fingerprint changed, retrying all cached failures
|
|
168
|
+
`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
for (const [path, entry] of Object.entries(parsed.failures)) {
|
|
172
|
+
this.failures.set(path, entry);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
activeCache = null;
|
|
54
177
|
}
|
|
55
178
|
});
|
|
56
179
|
|
|
57
180
|
// src/server/graph/core/ts-extractor.ts
|
|
58
181
|
var ts_extractor_exports = {};
|
|
59
182
|
__export(ts_extractor_exports, {
|
|
183
|
+
ParseCascadeError: () => ParseCascadeError,
|
|
60
184
|
classifyFile: () => classifyFile,
|
|
185
|
+
classifyRouteAgainstMiddleware: () => classifyRouteAgainstMiddleware,
|
|
61
186
|
createQuery: () => createQuery,
|
|
62
187
|
extractAuthWrappersTS: () => extractAuthWrappersTS,
|
|
63
188
|
extractDbCallsTS: () => extractDbCallsTS,
|
|
64
189
|
extractDeep: () => extractDeep,
|
|
190
|
+
extractEffects: () => extractEffects,
|
|
191
|
+
extractMiddlewareAuthTS: () => extractMiddlewareAuthTS,
|
|
192
|
+
getTreeSitterFingerprint: () => getTreeSitterFingerprint,
|
|
65
193
|
initTreeSitter: () => initTreeSitter,
|
|
194
|
+
middlewarePatternToRegex: () => middlewarePatternToRegex,
|
|
66
195
|
parseCodeTS: () => parseCodeTS,
|
|
67
196
|
parseFileTS: () => parseFileTS,
|
|
197
|
+
parseSource: () => parseSource,
|
|
68
198
|
setExtractorConfig: () => setExtractorConfig
|
|
69
199
|
});
|
|
70
200
|
async function initTreeSitter() {
|
|
@@ -75,7 +205,8 @@ async function initTreeSitter() {
|
|
|
75
205
|
await TreeSitter.init();
|
|
76
206
|
const wasmPath = require.resolve("tree-sitter-typescript/tree-sitter-tsx.wasm");
|
|
77
207
|
tsxLanguage = await TreeSitter.Language.load(wasmPath);
|
|
78
|
-
|
|
208
|
+
TreeSitterCtor = TreeSitter;
|
|
209
|
+
parserInstance = new TreeSitterCtor();
|
|
79
210
|
parserInstance.setLanguage(tsxLanguage);
|
|
80
211
|
initialized = true;
|
|
81
212
|
})();
|
|
@@ -86,20 +217,92 @@ function ensureInit() {
|
|
|
86
217
|
throw new Error("Tree-sitter not initialized. Call initTreeSitter() first.");
|
|
87
218
|
}
|
|
88
219
|
}
|
|
220
|
+
function getTreeSitterFingerprint() {
|
|
221
|
+
const read = (pkg) => {
|
|
222
|
+
try {
|
|
223
|
+
return String(require(`${pkg}/package.json`).version ?? "unknown");
|
|
224
|
+
} catch {
|
|
225
|
+
return "unknown";
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
return `web-tree-sitter@${read("web-tree-sitter")}+tree-sitter-typescript@${read("tree-sitter-typescript")}`;
|
|
229
|
+
}
|
|
89
230
|
function getQuery(name) {
|
|
90
231
|
ensureInit();
|
|
91
232
|
const cached = queryCache.get(name);
|
|
92
233
|
if (cached) return cached;
|
|
93
|
-
const scmPath = (0,
|
|
94
|
-
const scm = (0,
|
|
234
|
+
const scmPath = (0, import_node_path5.join)(queriesDir, `${name}.scm`);
|
|
235
|
+
const scm = (0, import_node_fs5.readFileSync)(scmPath, "utf-8");
|
|
95
236
|
const query = tsxLanguage.query(scm);
|
|
96
237
|
queryCache.set(name, query);
|
|
97
238
|
return query;
|
|
98
239
|
}
|
|
240
|
+
function recreateParser() {
|
|
241
|
+
if (!TreeSitterCtor || !tsxLanguage) {
|
|
242
|
+
throw new Error("recreateParser() called before initTreeSitter()");
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
parserInstance?.delete?.();
|
|
246
|
+
} catch {
|
|
247
|
+
}
|
|
248
|
+
parserInstance = new TreeSitterCtor();
|
|
249
|
+
parserInstance.setLanguage(tsxLanguage);
|
|
250
|
+
}
|
|
99
251
|
function parseSource(absPath) {
|
|
100
252
|
ensureInit();
|
|
101
|
-
|
|
102
|
-
|
|
253
|
+
let content;
|
|
254
|
+
try {
|
|
255
|
+
const stat = (0, import_node_fs5.statSync)(absPath);
|
|
256
|
+
if (stat.size > MAX_PARSEABLE_BYTES) {
|
|
257
|
+
process.stderr.write(`[lc-extractor] skipping ${absPath}: ${stat.size} bytes exceeds max ${MAX_PARSEABLE_BYTES}
|
|
258
|
+
`);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
content = (0, import_node_fs5.readFileSync)(absPath, "utf-8");
|
|
262
|
+
} catch (e) {
|
|
263
|
+
process.stderr.write(`[lc-extractor] read failed for ${absPath}: ${e instanceof Error ? e.message : String(e)}
|
|
264
|
+
`);
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const cache = getActiveFailureCache();
|
|
268
|
+
if (cache) {
|
|
269
|
+
const skipEntry = cache.shouldSkip(absPath, content);
|
|
270
|
+
if (skipEntry) {
|
|
271
|
+
cache.bumpAttempt(absPath);
|
|
272
|
+
process.stderr.write(`[lc-extractor] skipping ${absPath}: known-bad (${skipEntry.consecutiveAttempts} attempts, last error: ${skipEntry.lastError})
|
|
273
|
+
`);
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const tree = parserInstance.parse(content);
|
|
279
|
+
consecutiveParseFailures = 0;
|
|
280
|
+
cache?.clear(absPath);
|
|
281
|
+
return tree;
|
|
282
|
+
} catch (e) {
|
|
283
|
+
consecutiveParseFailures++;
|
|
284
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
285
|
+
process.stderr.write(
|
|
286
|
+
`[lc-extractor] parse failed for ${absPath}: ${msg} (consecutive failures: ${consecutiveParseFailures}/${MAX_CONSECUTIVE_PARSE_FAILURES})
|
|
287
|
+
`
|
|
288
|
+
);
|
|
289
|
+
cache?.recordFailure(absPath, content, msg);
|
|
290
|
+
try {
|
|
291
|
+
recreateParser();
|
|
292
|
+
} catch (recreateErr) {
|
|
293
|
+
const rmsg = recreateErr instanceof Error ? recreateErr.message : String(recreateErr);
|
|
294
|
+
process.stderr.write(`[lc-extractor] FATAL: recreateParser failed after ${absPath}: ${rmsg}
|
|
295
|
+
`);
|
|
296
|
+
consecutiveParseFailures = 0;
|
|
297
|
+
throw recreateErr;
|
|
298
|
+
}
|
|
299
|
+
if (consecutiveParseFailures >= MAX_CONSECUTIVE_PARSE_FAILURES) {
|
|
300
|
+
const tripCount = consecutiveParseFailures;
|
|
301
|
+
consecutiveParseFailures = 0;
|
|
302
|
+
throw new ParseCascadeError(absPath, tripCount);
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
103
306
|
}
|
|
104
307
|
function parseCodeTS(code) {
|
|
105
308
|
ensureInit();
|
|
@@ -139,8 +342,20 @@ function childrenOfType(node, type) {
|
|
|
139
342
|
function childOfType(node, type) {
|
|
140
343
|
return node.children.find((n) => n.type === type);
|
|
141
344
|
}
|
|
345
|
+
function emptyParsedFile(absPath) {
|
|
346
|
+
return {
|
|
347
|
+
name: absPath.split("/").pop() ?? absPath,
|
|
348
|
+
exports: [],
|
|
349
|
+
imports: [],
|
|
350
|
+
reExports: [],
|
|
351
|
+
jsxElements: /* @__PURE__ */ new Set(),
|
|
352
|
+
navigations: [],
|
|
353
|
+
fetchCalls: []
|
|
354
|
+
};
|
|
355
|
+
}
|
|
142
356
|
function parseFileTS(absPath) {
|
|
143
357
|
const tree = parseSource(absPath);
|
|
358
|
+
if (!tree) return emptyParsedFile(absPath);
|
|
144
359
|
const root = tree.rootNode;
|
|
145
360
|
const imports = [];
|
|
146
361
|
const importStatements = childrenOfType(root, "import_statement");
|
|
@@ -285,6 +500,12 @@ function parseFileTS(absPath) {
|
|
|
285
500
|
if (linkTemplate) {
|
|
286
501
|
navigations.push({ kind: "link-href", target: linkTemplate, isTemplate: true });
|
|
287
502
|
}
|
|
503
|
+
if (caps["nav.redirect.literal"]) {
|
|
504
|
+
navigations.push({ kind: "router-replace", target: caps["nav.redirect.literal"], isTemplate: false });
|
|
505
|
+
}
|
|
506
|
+
if (caps["nav.redirect.template"]) {
|
|
507
|
+
navigations.push({ kind: "router-replace", target: caps["nav.redirect.template"], isTemplate: true });
|
|
508
|
+
}
|
|
288
509
|
if (caps["nav.window.literal"]) {
|
|
289
510
|
navigations.push({ kind: "window-location", target: caps["nav.window.literal"], isTemplate: false });
|
|
290
511
|
}
|
|
@@ -329,6 +550,7 @@ function parseFileTS(absPath) {
|
|
|
329
550
|
}
|
|
330
551
|
function extractDbCallsTS(absPath) {
|
|
331
552
|
const tree = parseSource(absPath);
|
|
553
|
+
if (!tree) return [];
|
|
332
554
|
const root = tree.rootNode;
|
|
333
555
|
const dbQuery = getQuery("db-calls");
|
|
334
556
|
const matches = dbQuery.matches(root);
|
|
@@ -363,15 +585,25 @@ function extractDbCallsTS(absPath) {
|
|
|
363
585
|
const seen = /* @__PURE__ */ new Set();
|
|
364
586
|
for (const m of matches) {
|
|
365
587
|
const caps = captureMap(m);
|
|
588
|
+
const sbTable = caps["sb.table"];
|
|
589
|
+
const sbMethod = caps["sb.method"];
|
|
590
|
+
if (sbTable && sbMethod) {
|
|
591
|
+
const key2 = `sql:${sbTable}.${sbMethod}`;
|
|
592
|
+
if (seen.has(key2)) continue;
|
|
593
|
+
seen.add(key2);
|
|
594
|
+
const isMutation = SUPABASE_MUTATION_METHODS_BUILTIN.has(sbMethod) || extraMutationMethods.includes(sbMethod);
|
|
595
|
+
calls.push({ model: sbTable, method: sbMethod, isMutation, kind: "sql" });
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
366
598
|
const identifier = caps["db.identifier"];
|
|
367
599
|
const model = caps["db.model"];
|
|
368
600
|
const method = caps["db.method"];
|
|
369
601
|
if (!identifier || !model || !method) continue;
|
|
370
602
|
if (!dbIdentifiers.has(identifier)) continue;
|
|
371
|
-
const key =
|
|
603
|
+
const key = `orm:${model}.${method}`;
|
|
372
604
|
if (seen.has(key)) continue;
|
|
373
605
|
seen.add(key);
|
|
374
|
-
calls.push({ model, method, isMutation: getMutationMethods().has(method) });
|
|
606
|
+
calls.push({ model, method, isMutation: getMutationMethods().has(method), kind: "orm" });
|
|
375
607
|
}
|
|
376
608
|
return calls;
|
|
377
609
|
}
|
|
@@ -379,12 +611,17 @@ function classifyFile(absPath) {
|
|
|
379
611
|
const fileName = require("path").basename(absPath);
|
|
380
612
|
if (fileName.includes(".test.") || fileName.includes(".spec.") || fileName.includes("__test")) return "test";
|
|
381
613
|
if (fileName.includes(".stories.")) return "story";
|
|
614
|
+
if (fileName === "middleware.ts" || fileName === "middleware.tsx") return "middleware";
|
|
382
615
|
const tree = parseSource(absPath);
|
|
616
|
+
if (!tree) return "util";
|
|
383
617
|
const root = tree.rootNode;
|
|
384
618
|
const classifyQuery = getQuery("classify");
|
|
385
619
|
const captures = classifyQuery.captures(root);
|
|
386
620
|
const capNames = new Set(captures.map((c) => c.name));
|
|
387
621
|
if (capNames.has("http_export") || capNames.has("http_export_fn")) return "endpoint";
|
|
622
|
+
if (capNames.has("use_server_directive") && fileName !== "page.tsx" && fileName !== "layout.tsx") {
|
|
623
|
+
return "server-action";
|
|
624
|
+
}
|
|
388
625
|
if (fileName === "page.tsx" && capNames.has("has_jsx")) return "page";
|
|
389
626
|
if (fileName === "layout.tsx" && capNames.has("has_jsx")) return "layout";
|
|
390
627
|
if (capNames.has("has_create_context") || capNames.has("has_create_context_bare")) return "context";
|
|
@@ -395,6 +632,7 @@ function classifyFile(absPath) {
|
|
|
395
632
|
}
|
|
396
633
|
function extractAuthWrappersTS(absPath) {
|
|
397
634
|
const tree = parseSource(absPath);
|
|
635
|
+
if (!tree) return /* @__PURE__ */ new Set();
|
|
398
636
|
const root = tree.rootNode;
|
|
399
637
|
const wrapperQuery = getQuery("wrappers");
|
|
400
638
|
const matches = wrapperQuery.matches(root);
|
|
@@ -405,13 +643,386 @@ function extractAuthWrappersTS(absPath) {
|
|
|
405
643
|
wrappers.add(caps["wrapper.fn_name"]);
|
|
406
644
|
}
|
|
407
645
|
}
|
|
646
|
+
const inlineHelpers = /* @__PURE__ */ new Set();
|
|
647
|
+
for (const stmt of childrenOfType(root, "import_statement")) {
|
|
648
|
+
const sourceNode = childOfType(stmt, "string");
|
|
649
|
+
const frag = sourceNode ? childOfType(sourceNode, "string_fragment") : void 0;
|
|
650
|
+
if (!frag) continue;
|
|
651
|
+
const provider = INLINE_AUTH_IMPORTS.find((p) => p.module.test(frag.text));
|
|
652
|
+
if (!provider) continue;
|
|
653
|
+
const clause = childOfType(stmt, "import_clause");
|
|
654
|
+
if (!clause) continue;
|
|
655
|
+
const named = childOfType(clause, "named_imports");
|
|
656
|
+
if (!named) continue;
|
|
657
|
+
for (const specNode of childrenOfType(named, "import_specifier")) {
|
|
658
|
+
const ids = childrenOfType(specNode, "identifier");
|
|
659
|
+
const importedName = ids[0]?.text;
|
|
660
|
+
const localName = ids[ids.length - 1]?.text;
|
|
661
|
+
if (importedName && provider.helpers.includes(importedName) && localName) {
|
|
662
|
+
inlineHelpers.add(localName);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (inlineHelpers.size > 0) {
|
|
667
|
+
const text = root.text;
|
|
668
|
+
for (const name of inlineHelpers) {
|
|
669
|
+
const re = new RegExp(`\\b${name}\\s*\\(`);
|
|
670
|
+
if (re.test(text)) {
|
|
671
|
+
wrappers.add("inline");
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
408
676
|
return wrappers;
|
|
409
677
|
}
|
|
678
|
+
function inferIntentFromName(name) {
|
|
679
|
+
if (TRUST_AS_PROTECT_KEYS.has(name)) {
|
|
680
|
+
return { intent: "protect", hint: `Next.js config.${name} \u2014 paths matched run through middleware` };
|
|
681
|
+
}
|
|
682
|
+
for (const re of EXEMPT_NAME_PATTERNS) {
|
|
683
|
+
if (re.test(name)) return { intent: "exempt", hint: `name "${name}" matches /${re.source}/` };
|
|
684
|
+
}
|
|
685
|
+
for (const re of PROTECT_NAME_PATTERNS) {
|
|
686
|
+
if (re.test(name)) return { intent: "protect", hint: `name "${name}" matches /${re.source}/` };
|
|
687
|
+
}
|
|
688
|
+
return { intent: "ambiguous", hint: `name "${name}" has no exempt/protect signal` };
|
|
689
|
+
}
|
|
690
|
+
function looksLikeRoutePattern(s) {
|
|
691
|
+
return s.startsWith("/") && !s.startsWith("//");
|
|
692
|
+
}
|
|
693
|
+
function collectStringsFromArray(arrNode) {
|
|
694
|
+
const out = [];
|
|
695
|
+
for (const child of arrNode.children) {
|
|
696
|
+
if (child.type !== "string") continue;
|
|
697
|
+
const frag = childOfType(child, "string_fragment");
|
|
698
|
+
if (frag) out.push(frag.text);
|
|
699
|
+
}
|
|
700
|
+
return out;
|
|
701
|
+
}
|
|
702
|
+
function findArrayInValue(valueNode) {
|
|
703
|
+
if (!valueNode) return null;
|
|
704
|
+
if (valueNode.type === "array") return valueNode;
|
|
705
|
+
if (valueNode.type === "call_expression") {
|
|
706
|
+
const args = childOfType(valueNode, "arguments");
|
|
707
|
+
if (!args) return null;
|
|
708
|
+
for (const c of args.children) {
|
|
709
|
+
if (c.type === "array") return c;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
function extractMatcherFromDeclarator(decl) {
|
|
715
|
+
const nameNode = childOfType(decl, "identifier");
|
|
716
|
+
if (!nameNode) return null;
|
|
717
|
+
let valueNode;
|
|
718
|
+
for (const c of decl.children) {
|
|
719
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
720
|
+
valueNode = c;
|
|
721
|
+
}
|
|
722
|
+
const arr = findArrayInValue(valueNode);
|
|
723
|
+
if (!arr) return null;
|
|
724
|
+
const strings = collectStringsFromArray(arr);
|
|
725
|
+
const routes = strings.filter(looksLikeRoutePattern);
|
|
726
|
+
if (routes.length === 0) return null;
|
|
727
|
+
const { intent, hint } = inferIntentFromName(nameNode.text);
|
|
728
|
+
return { name: nameNode.text, patterns: routes, intent, hint };
|
|
729
|
+
}
|
|
730
|
+
function extractMatchersFromObject(objNode) {
|
|
731
|
+
const out = [];
|
|
732
|
+
for (const pair of childrenOfType(objNode, "pair")) {
|
|
733
|
+
const key = childOfType(pair, "property_identifier");
|
|
734
|
+
if (!key) continue;
|
|
735
|
+
const arr = pair.children.find((c) => c.type === "array");
|
|
736
|
+
if (!arr) continue;
|
|
737
|
+
const routes = collectStringsFromArray(arr).filter(looksLikeRoutePattern);
|
|
738
|
+
if (routes.length === 0) continue;
|
|
739
|
+
const { intent, hint } = inferIntentFromName(key.text);
|
|
740
|
+
out.push({ name: key.text, patterns: routes, intent, hint });
|
|
741
|
+
}
|
|
742
|
+
return out;
|
|
743
|
+
}
|
|
744
|
+
function detectFallthroughProtect(root) {
|
|
745
|
+
const text = root.text;
|
|
746
|
+
const signals = [
|
|
747
|
+
/\bauth\.protect\s*\(/,
|
|
748
|
+
/\bauth\(\)\.protect\s*\(/,
|
|
749
|
+
/\bredirect\s*\(\s*['"`]\/(sign-?in|log-?in|auth)/i,
|
|
750
|
+
/\bNextResponse\.redirect\s*\(/,
|
|
751
|
+
/\bthrow\s+new\s+\w*Unauthorized/i
|
|
752
|
+
];
|
|
753
|
+
return signals.some((re) => re.test(text));
|
|
754
|
+
}
|
|
755
|
+
function extractMiddlewareAuthTS(absPath) {
|
|
756
|
+
if (!require("node:fs").existsSync(absPath)) return null;
|
|
757
|
+
const tree = parseSource(absPath);
|
|
758
|
+
if (!tree) return null;
|
|
759
|
+
const root = tree.rootNode;
|
|
760
|
+
const matchers = [];
|
|
761
|
+
for (const stmt of root.children) {
|
|
762
|
+
if (stmt.type !== "lexical_declaration" && stmt.type !== "variable_declaration") continue;
|
|
763
|
+
for (const decl of childrenOfType(stmt, "variable_declarator")) {
|
|
764
|
+
const m = extractMatcherFromDeclarator(decl);
|
|
765
|
+
if (m) matchers.push(m);
|
|
766
|
+
let valueNode;
|
|
767
|
+
for (const c of decl.children) {
|
|
768
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
769
|
+
valueNode = c;
|
|
770
|
+
}
|
|
771
|
+
if (valueNode?.type === "object") {
|
|
772
|
+
for (const inner of extractMatchersFromObject(valueNode)) matchers.push(inner);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
for (const stmt of root.children) {
|
|
777
|
+
if (stmt.type !== "export_statement") continue;
|
|
778
|
+
const decl = childOfType(stmt, "lexical_declaration") ?? childOfType(stmt, "variable_declaration");
|
|
779
|
+
if (!decl) continue;
|
|
780
|
+
for (const d of childrenOfType(decl, "variable_declarator")) {
|
|
781
|
+
let valueNode;
|
|
782
|
+
for (const c of d.children) {
|
|
783
|
+
if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
|
|
784
|
+
valueNode = c;
|
|
785
|
+
}
|
|
786
|
+
if (valueNode?.type === "object") {
|
|
787
|
+
for (const inner of extractMatchersFromObject(valueNode)) matchers.push(inner);
|
|
788
|
+
}
|
|
789
|
+
const m = extractMatcherFromDeclarator(d);
|
|
790
|
+
if (m && !matchers.some((x) => x.name === m.name && x.patterns.join() === m.patterns.join())) {
|
|
791
|
+
matchers.push(m);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
file: absPath,
|
|
797
|
+
matchers,
|
|
798
|
+
hasFallthroughProtect: detectFallthroughProtect(root)
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function middlewarePatternToRegex(pattern) {
|
|
802
|
+
const negLookaheadMatch = /^(\/?)\(\(\?\!([^)]+)\)(\.\*|\.\+)\)$/.exec(pattern);
|
|
803
|
+
if (negLookaheadMatch) {
|
|
804
|
+
const [, lead, altsRaw, body] = negLookaheadMatch;
|
|
805
|
+
const escapedAlts = altsRaw.split("|").map((alt) => alt.trim().replace(/[.+*?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
806
|
+
try {
|
|
807
|
+
return new RegExp(`^${lead}(?!${escapedAlts})${body}$`);
|
|
808
|
+
} catch {
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (/\(\?\!/.test(pattern)) return null;
|
|
813
|
+
if (pattern.startsWith("(")) return null;
|
|
814
|
+
let src = "^";
|
|
815
|
+
let i = 0;
|
|
816
|
+
while (i < pattern.length) {
|
|
817
|
+
const ch = pattern[i];
|
|
818
|
+
if (ch === "(" && pattern.slice(i, i + 4) === "(.*)") {
|
|
819
|
+
src += ".*";
|
|
820
|
+
i += 4;
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (ch === ":") {
|
|
824
|
+
i++;
|
|
825
|
+
while (i < pattern.length && /[a-zA-Z0-9_]/.test(pattern[i])) i++;
|
|
826
|
+
src += "[^/]+";
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
if (ch === "*") {
|
|
830
|
+
i++;
|
|
831
|
+
while (i < pattern.length && /[a-zA-Z0-9_]/.test(pattern[i])) i++;
|
|
832
|
+
if (pattern[i] === "?") {
|
|
833
|
+
i++;
|
|
834
|
+
src += ".*";
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
src += ".+";
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
if (/[.\\+?^${}()|[\]]/.test(ch)) {
|
|
841
|
+
src += "\\" + ch;
|
|
842
|
+
i++;
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
src += ch;
|
|
846
|
+
i++;
|
|
847
|
+
}
|
|
848
|
+
src += "$";
|
|
849
|
+
try {
|
|
850
|
+
return new RegExp(src);
|
|
851
|
+
} catch {
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
function classifyRouteAgainstMiddleware(routePath, info) {
|
|
856
|
+
for (const m of info.matchers) {
|
|
857
|
+
if (m.intent === "ambiguous") continue;
|
|
858
|
+
for (const pat of m.patterns) {
|
|
859
|
+
const re = middlewarePatternToRegex(pat);
|
|
860
|
+
if (!re) continue;
|
|
861
|
+
if (re.test(routePath)) return { intent: m.intent, matcher: m.name, pattern: pat };
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
410
866
|
function trunc(s, max = 120) {
|
|
411
867
|
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
412
868
|
}
|
|
869
|
+
function dedup(arr) {
|
|
870
|
+
return Array.from(new Set(arr));
|
|
871
|
+
}
|
|
872
|
+
function firstStringArg(callExpr) {
|
|
873
|
+
const args = callExpr.childForFieldName("arguments");
|
|
874
|
+
if (!args) return null;
|
|
875
|
+
for (const c of args.namedChildren) {
|
|
876
|
+
if (c.type === "string") {
|
|
877
|
+
const frag = c.namedChildren.find((cc) => cc.type === "string_fragment");
|
|
878
|
+
return frag ? frag.text : c.text.replace(/^['"`]|['"`]$/g, "");
|
|
879
|
+
}
|
|
880
|
+
if (c.type === "template_string") {
|
|
881
|
+
return c.text.replace(/^`|`$/g, "");
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
function isFunctionLike(node) {
|
|
887
|
+
return node.type === "arrow_function" || node.type === "function_expression" || node.type === "function_declaration" || node.type === "method_definition" || node.type === "generator_function" || node.type === "generator_function_declaration";
|
|
888
|
+
}
|
|
889
|
+
function stringLiteralValue(n) {
|
|
890
|
+
if (n.type !== "string") return null;
|
|
891
|
+
const frag = n.namedChildren.find((c) => c.type === "string_fragment");
|
|
892
|
+
if (frag) return frag.text;
|
|
893
|
+
return n.text.replace(/^['"`]|['"`]$/g, "");
|
|
894
|
+
}
|
|
895
|
+
function nthStringArg(callExpr, idx) {
|
|
896
|
+
const args = callExpr.childForFieldName("arguments");
|
|
897
|
+
if (!args) return null;
|
|
898
|
+
const child = args.namedChildren[idx];
|
|
899
|
+
return child ? stringLiteralValue(child) : null;
|
|
900
|
+
}
|
|
901
|
+
function objectArgIdValues(callExpr) {
|
|
902
|
+
const args = callExpr.childForFieldName("arguments");
|
|
903
|
+
if (!args) return [];
|
|
904
|
+
const out = [];
|
|
905
|
+
for (const arg of args.namedChildren) {
|
|
906
|
+
if (arg.type !== "object") continue;
|
|
907
|
+
for (const pair of arg.namedChildren) {
|
|
908
|
+
if (pair.type !== "pair") continue;
|
|
909
|
+
const key = pair.childForFieldName("key");
|
|
910
|
+
const val = pair.childForFieldName("value");
|
|
911
|
+
if (!key || !val) continue;
|
|
912
|
+
const keyText = key.type === "property_identifier" ? key.text : stringLiteralValue(key) ?? key.text;
|
|
913
|
+
if (keyText !== "id") continue;
|
|
914
|
+
const strVal = stringLiteralValue(val);
|
|
915
|
+
if (strVal) out.push(strVal);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return out;
|
|
919
|
+
}
|
|
920
|
+
function extractEffects(node, moduleOnly = false) {
|
|
921
|
+
const calls = [];
|
|
922
|
+
const dom_writes = [];
|
|
923
|
+
const subscribes = [];
|
|
924
|
+
const timers = [];
|
|
925
|
+
const persists = [];
|
|
926
|
+
const fetches = [];
|
|
927
|
+
const globals = [];
|
|
928
|
+
const dom_ids = [];
|
|
929
|
+
const window_events = [];
|
|
930
|
+
const storage_keys = [];
|
|
931
|
+
const fetch_urls = [];
|
|
932
|
+
function visit(n, depth) {
|
|
933
|
+
if (moduleOnly && depth > 0 && isFunctionLike(n)) return;
|
|
934
|
+
if (n.type === "call_expression") {
|
|
935
|
+
const fn = n.childForFieldName("function");
|
|
936
|
+
if (fn) {
|
|
937
|
+
const fnText = fn.text;
|
|
938
|
+
calls.push(fnText);
|
|
939
|
+
for (const id of objectArgIdValues(n)) dom_ids.push(id);
|
|
940
|
+
if (fn.type === "identifier") {
|
|
941
|
+
if (TIMER_FNS.has(fnText)) timers.push(fnText);
|
|
942
|
+
if (fnText === "fetch") {
|
|
943
|
+
const url = firstStringArg(n);
|
|
944
|
+
fetches.push(url ? `fetch("${url}")` : "fetch(...)");
|
|
945
|
+
if (url) fetch_urls.push(url);
|
|
946
|
+
}
|
|
947
|
+
} else if (fn.type === "member_expression") {
|
|
948
|
+
const obj = fn.childForFieldName("object");
|
|
949
|
+
const prop = fn.childForFieldName("property");
|
|
950
|
+
const objText = obj?.text ?? "";
|
|
951
|
+
const propText = prop?.text ?? "";
|
|
952
|
+
if (STORAGE_OBJECTS.has(objText)) {
|
|
953
|
+
const key = firstStringArg(n);
|
|
954
|
+
persists.push(key ? `${objText}.${propText}("${key}")` : `${objText}.${propText}(\u2026)`);
|
|
955
|
+
if (key) storage_keys.push(key);
|
|
956
|
+
} else if (propText === "addEventListener") {
|
|
957
|
+
const event = firstStringArg(n);
|
|
958
|
+
subscribes.push(event ? `${objText}.addEventListener("${event}")` : `${objText}.addEventListener(\u2026)`);
|
|
959
|
+
if (event && (objText === "window" || objText === "document")) {
|
|
960
|
+
window_events.push(`${objText}:${event}`);
|
|
961
|
+
}
|
|
962
|
+
} else if (propText === "removeEventListener") {
|
|
963
|
+
const event = firstStringArg(n);
|
|
964
|
+
subscribes.push(event ? `${objText}.removeEventListener("${event}")` : `${objText}.removeEventListener(\u2026)`);
|
|
965
|
+
} else if (propText === "subscribe" || propText === "on") {
|
|
966
|
+
subscribes.push(`${objText}.${propText}(\u2026)`);
|
|
967
|
+
} else if (DOM_METHOD_NAMES.has(propText)) {
|
|
968
|
+
dom_writes.push(`${objText}.${propText}(\u2026)`);
|
|
969
|
+
if (propText === "setAttribute" && nthStringArg(n, 0) === "id") {
|
|
970
|
+
const idVal = nthStringArg(n, 1);
|
|
971
|
+
if (idVal) dom_ids.push(idVal);
|
|
972
|
+
}
|
|
973
|
+
} else if (objText.endsWith(".classList") && CLASSLIST_METHODS.has(propText)) {
|
|
974
|
+
dom_writes.push(`${objText}.${propText}(\u2026)`);
|
|
975
|
+
} else if (objText === "history" && HISTORY_METHODS.has(propText)) {
|
|
976
|
+
globals.push(`history.${propText}(\u2026)`);
|
|
977
|
+
} else if (objText === "location" && LOCATION_METHODS.has(propText)) {
|
|
978
|
+
globals.push(`location.${propText}(\u2026)`);
|
|
979
|
+
} else if (objText === "window" && propText === "matchMedia") {
|
|
980
|
+
globals.push("window.matchMedia(\u2026)");
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
} else if (n.type === "assignment_expression") {
|
|
985
|
+
const lhs = n.children.find((c) => c.type !== "=");
|
|
986
|
+
const rhs = n.childForFieldName("right");
|
|
987
|
+
if (lhs?.type === "member_expression") {
|
|
988
|
+
const prop = lhs.childForFieldName("property");
|
|
989
|
+
const propText = prop?.text ?? "";
|
|
990
|
+
const lhsText = lhs.text;
|
|
991
|
+
if (lhsText.startsWith("document.cookie")) {
|
|
992
|
+
persists.push("document.cookie =");
|
|
993
|
+
} else if (lhsText.startsWith("document.title") || lhsText.startsWith("location.")) {
|
|
994
|
+
globals.push(`${lhsText} =`);
|
|
995
|
+
} else if (ASSIGN_DOM_PROPS.has(propText) || lhsText.includes(".style.") || lhsText.includes(".dataset.")) {
|
|
996
|
+
dom_writes.push(`${lhsText} =`);
|
|
997
|
+
if (propText === "id" && rhs) {
|
|
998
|
+
const idVal = stringLiteralValue(rhs);
|
|
999
|
+
if (idVal) dom_ids.push(idVal);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
for (const child of n.children) visit(child, depth + 1);
|
|
1005
|
+
}
|
|
1006
|
+
visit(node, 0);
|
|
1007
|
+
const out = {};
|
|
1008
|
+
if (calls.length) out.calls = dedup(calls);
|
|
1009
|
+
if (dom_writes.length) out.dom_writes = dedup(dom_writes);
|
|
1010
|
+
if (subscribes.length) out.subscribes = dedup(subscribes);
|
|
1011
|
+
if (timers.length) out.timers = dedup(timers);
|
|
1012
|
+
if (persists.length) out.persists = dedup(persists);
|
|
1013
|
+
if (fetches.length) out.fetches = dedup(fetches);
|
|
1014
|
+
if (globals.length) out.globals = dedup(globals);
|
|
1015
|
+
if (dom_ids.length) out.dom_ids = dedup(dom_ids);
|
|
1016
|
+
if (window_events.length) out.window_events = dedup(window_events);
|
|
1017
|
+
if (storage_keys.length) out.storage_keys = dedup(storage_keys);
|
|
1018
|
+
if (fetch_urls.length) out.fetch_urls = dedup(fetch_urls);
|
|
1019
|
+
return out;
|
|
1020
|
+
}
|
|
413
1021
|
function extractDeep(absPath) {
|
|
414
1022
|
const tree = parseSource(absPath);
|
|
1023
|
+
if (!tree) {
|
|
1024
|
+
return { elements: [], stateVars: [], conditions: [], variables: [], responses: [], params: [] };
|
|
1025
|
+
}
|
|
415
1026
|
const root = tree.rootNode;
|
|
416
1027
|
const elements = [];
|
|
417
1028
|
const elQuery = getQuery("deep/jsx-semantic");
|
|
@@ -504,12 +1115,18 @@ function extractDeep(absPath) {
|
|
|
504
1115
|
const caps = captureMap(m);
|
|
505
1116
|
const declNode = m.captures.find((c) => c.name === "var.decl" || c.name === "var.destructured" || c.name === "var.array")?.node;
|
|
506
1117
|
const kind = declNode?.children.find((n) => n.type === "const" || n.type === "let" || n.type === "var")?.type ?? "const";
|
|
1118
|
+
const initNode = m.captures.find((c) => c.name === "var.init")?.node;
|
|
507
1119
|
if (caps["var.name"] && caps["var.init"]) {
|
|
508
|
-
|
|
1120
|
+
const variable = {
|
|
509
1121
|
name: caps["var.name"],
|
|
510
1122
|
kind,
|
|
511
1123
|
init: trunc(caps["var.init"], 100)
|
|
512
|
-
}
|
|
1124
|
+
};
|
|
1125
|
+
if (initNode && isFunctionLike(initNode)) {
|
|
1126
|
+
const eff = extractEffects(initNode);
|
|
1127
|
+
if (Object.keys(eff).length > 0) variable.effects = eff;
|
|
1128
|
+
}
|
|
1129
|
+
variables.push(variable);
|
|
513
1130
|
}
|
|
514
1131
|
if (caps["var.destructured.obj"]) {
|
|
515
1132
|
variables.push({
|
|
@@ -561,21 +1178,49 @@ function extractDeep(absPath) {
|
|
|
561
1178
|
params.push({ name: caps["param.body"], source: "body" });
|
|
562
1179
|
}
|
|
563
1180
|
}
|
|
564
|
-
|
|
1181
|
+
const fileEffects = extractEffects(
|
|
1182
|
+
root,
|
|
1183
|
+
/* moduleOnly */
|
|
1184
|
+
false
|
|
1185
|
+
);
|
|
1186
|
+
const hasEffects = Object.keys(fileEffects).length > 0;
|
|
1187
|
+
return {
|
|
1188
|
+
elements,
|
|
1189
|
+
stateVars,
|
|
1190
|
+
conditions,
|
|
1191
|
+
variables,
|
|
1192
|
+
responses,
|
|
1193
|
+
params,
|
|
1194
|
+
...hasEffects ? { effects: fileEffects } : {}
|
|
1195
|
+
};
|
|
565
1196
|
}
|
|
566
|
-
var
|
|
1197
|
+
var import_node_fs5, import_node_path5, tsxLanguage, parserInstance, TreeSitterCtor, initPromise, initialized, queriesDir, queryCache, MAX_PARSEABLE_BYTES, MAX_CONSECUTIVE_PARSE_FAILURES, consecutiveParseFailures, ParseCascadeError, PRISMA_MUTATION_METHODS_BUILTIN, SUPABASE_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods, INLINE_AUTH_IMPORTS, EXEMPT_NAME_PATTERNS, PROTECT_NAME_PATTERNS, TRUST_AS_PROTECT_KEYS, TIMER_FNS, DOM_METHOD_NAMES, CLASSLIST_METHODS, STORAGE_OBJECTS, HISTORY_METHODS, LOCATION_METHODS, ASSIGN_DOM_PROPS;
|
|
567
1198
|
var init_ts_extractor = __esm({
|
|
568
1199
|
"src/server/graph/core/ts-extractor.ts"() {
|
|
569
1200
|
"use strict";
|
|
570
|
-
|
|
571
|
-
|
|
1201
|
+
import_node_fs5 = require("node:fs");
|
|
1202
|
+
import_node_path5 = require("node:path");
|
|
1203
|
+
init_parse_failure_cache();
|
|
572
1204
|
initialized = false;
|
|
573
1205
|
queriesDir = (() => {
|
|
574
|
-
const srcPath = (0,
|
|
1206
|
+
const srcPath = (0, import_node_path5.join)((0, import_node_path5.dirname)(__filename), "..", "queries");
|
|
575
1207
|
if (require("fs").existsSync(srcPath)) return srcPath;
|
|
576
|
-
return (0,
|
|
1208
|
+
return (0, import_node_path5.join)((0, import_node_path5.dirname)(__filename), "graph", "queries");
|
|
577
1209
|
})();
|
|
578
1210
|
queryCache = /* @__PURE__ */ new Map();
|
|
1211
|
+
MAX_PARSEABLE_BYTES = 2 * 1024 * 1024;
|
|
1212
|
+
MAX_CONSECUTIVE_PARSE_FAILURES = 10;
|
|
1213
|
+
consecutiveParseFailures = 0;
|
|
1214
|
+
ParseCascadeError = class extends Error {
|
|
1215
|
+
constructor(lastPath, failureCount) {
|
|
1216
|
+
super(
|
|
1217
|
+
`tree-sitter parse cascade: ${failureCount} consecutive WASM failures (last file: ${lastPath}). The shared Parser's WASM heap is likely corrupted; aborting regen so the graph isn't silently degraded. Restart the chart server to reinitialize, then re-run generate_graph.`
|
|
1218
|
+
);
|
|
1219
|
+
this.lastPath = lastPath;
|
|
1220
|
+
this.failureCount = failureCount;
|
|
1221
|
+
this.name = "ParseCascadeError";
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
579
1224
|
PRISMA_MUTATION_METHODS_BUILTIN = [
|
|
580
1225
|
"create",
|
|
581
1226
|
"createMany",
|
|
@@ -587,148 +1232,411 @@ var init_ts_extractor = __esm({
|
|
|
587
1232
|
"delete",
|
|
588
1233
|
"deleteMany"
|
|
589
1234
|
];
|
|
1235
|
+
SUPABASE_MUTATION_METHODS_BUILTIN = /* @__PURE__ */ new Set([
|
|
1236
|
+
"insert",
|
|
1237
|
+
"update",
|
|
1238
|
+
"delete",
|
|
1239
|
+
"upsert"
|
|
1240
|
+
]);
|
|
590
1241
|
DB_IDENTIFIERS_FALLBACK = ["db", "prisma"];
|
|
591
1242
|
extraDbIdentifiers = [];
|
|
592
1243
|
extraMutationMethods = [];
|
|
1244
|
+
INLINE_AUTH_IMPORTS = [
|
|
1245
|
+
{ module: /^@clerk\/nextjs(\/server)?$/, helpers: ["auth", "currentUser"] },
|
|
1246
|
+
{ module: /^next-auth(\/.+)?$/, helpers: ["auth", "getServerSession"] },
|
|
1247
|
+
{ module: /^@auth\//, helpers: ["auth"] },
|
|
1248
|
+
{ module: /^@supabase\/auth-helpers/, helpers: ["createServerClient"] }
|
|
1249
|
+
];
|
|
1250
|
+
EXEMPT_NAME_PATTERNS = [
|
|
1251
|
+
/^is_?public/i,
|
|
1252
|
+
/^public_?routes?/i,
|
|
1253
|
+
/^public_?paths?/i,
|
|
1254
|
+
/^whitelist/i,
|
|
1255
|
+
/^allowlist/i,
|
|
1256
|
+
/^unauthenticated/i,
|
|
1257
|
+
/^anonymous/i,
|
|
1258
|
+
/^guest/i,
|
|
1259
|
+
/^skip_?auth/i,
|
|
1260
|
+
/^bypass/i
|
|
1261
|
+
];
|
|
1262
|
+
PROTECT_NAME_PATTERNS = [
|
|
1263
|
+
/^is_?protected/i,
|
|
1264
|
+
/^protected_?routes?/i,
|
|
1265
|
+
/^protected_?paths?/i,
|
|
1266
|
+
/^require_?auth/i,
|
|
1267
|
+
/^auth_?required/i,
|
|
1268
|
+
/^private_?routes?/i,
|
|
1269
|
+
/^is_?admin/i,
|
|
1270
|
+
/^admin_?routes?/i,
|
|
1271
|
+
/^secured/i
|
|
1272
|
+
];
|
|
1273
|
+
TRUST_AS_PROTECT_KEYS = /* @__PURE__ */ new Set(["matcher", "matchers"]);
|
|
1274
|
+
TIMER_FNS = /* @__PURE__ */ new Set([
|
|
1275
|
+
"setInterval",
|
|
1276
|
+
"setTimeout",
|
|
1277
|
+
"clearInterval",
|
|
1278
|
+
"clearTimeout",
|
|
1279
|
+
"requestAnimationFrame",
|
|
1280
|
+
"cancelAnimationFrame",
|
|
1281
|
+
"queueMicrotask"
|
|
1282
|
+
]);
|
|
1283
|
+
DOM_METHOD_NAMES = /* @__PURE__ */ new Set([
|
|
1284
|
+
"setAttribute",
|
|
1285
|
+
"removeAttribute",
|
|
1286
|
+
"appendChild",
|
|
1287
|
+
"removeChild",
|
|
1288
|
+
"replaceChildren",
|
|
1289
|
+
"replaceWith",
|
|
1290
|
+
"insertBefore",
|
|
1291
|
+
"append",
|
|
1292
|
+
"prepend",
|
|
1293
|
+
"remove",
|
|
1294
|
+
"before",
|
|
1295
|
+
"after",
|
|
1296
|
+
"insertAdjacentHTML",
|
|
1297
|
+
"insertAdjacentElement"
|
|
1298
|
+
]);
|
|
1299
|
+
CLASSLIST_METHODS = /* @__PURE__ */ new Set(["add", "remove", "toggle", "replace"]);
|
|
1300
|
+
STORAGE_OBJECTS = /* @__PURE__ */ new Set(["localStorage", "sessionStorage"]);
|
|
1301
|
+
HISTORY_METHODS = /* @__PURE__ */ new Set(["pushState", "replaceState", "back", "forward", "go"]);
|
|
1302
|
+
LOCATION_METHODS = /* @__PURE__ */ new Set(["assign", "replace", "reload"]);
|
|
1303
|
+
ASSIGN_DOM_PROPS = /* @__PURE__ */ new Set([
|
|
1304
|
+
"textContent",
|
|
1305
|
+
"innerHTML",
|
|
1306
|
+
"innerText",
|
|
1307
|
+
"value",
|
|
1308
|
+
"src",
|
|
1309
|
+
"href",
|
|
1310
|
+
"className",
|
|
1311
|
+
"id",
|
|
1312
|
+
"checked",
|
|
1313
|
+
"selected",
|
|
1314
|
+
"disabled"
|
|
1315
|
+
]);
|
|
593
1316
|
}
|
|
594
1317
|
});
|
|
595
1318
|
|
|
596
|
-
// src/server/
|
|
597
|
-
var
|
|
598
|
-
|
|
599
|
-
|
|
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"));
|
|
1319
|
+
// src/server/parse-worker-entry.ts
|
|
1320
|
+
var parse_worker_entry_exports = {};
|
|
1321
|
+
module.exports = __toCommonJS(parse_worker_entry_exports);
|
|
1322
|
+
var import_node_worker_threads = require("node:worker_threads");
|
|
606
1323
|
|
|
607
|
-
// src/server/graph/
|
|
608
|
-
var
|
|
609
|
-
var
|
|
1324
|
+
// src/server/graph/core/config.ts
|
|
1325
|
+
var import_node_fs = require("node:fs");
|
|
1326
|
+
var import_node_path = require("node:path");
|
|
1327
|
+
init_launch_kit_paths();
|
|
1328
|
+
function loadConfig(rootDir) {
|
|
1329
|
+
const configPath = (0, import_node_path.join)(rootDir, LAUNCHCHART_CONFIG_FILE);
|
|
1330
|
+
if (!(0, import_node_fs.existsSync)(configPath)) return {};
|
|
1331
|
+
try {
|
|
1332
|
+
return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
|
|
1333
|
+
} catch {
|
|
1334
|
+
return {};
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
610
1337
|
|
|
611
1338
|
// src/server/graph/core/graph-builder.ts
|
|
612
|
-
var
|
|
613
|
-
var
|
|
614
|
-
|
|
1339
|
+
var import_node_fs13 = require("node:fs");
|
|
1340
|
+
var import_node_path14 = require("node:path");
|
|
1341
|
+
init_launch_kit_paths();
|
|
615
1342
|
|
|
616
1343
|
// src/server/graph/core/parser-registry.ts
|
|
617
|
-
var
|
|
1344
|
+
var import_node_path13 = require("node:path");
|
|
618
1345
|
|
|
619
1346
|
// src/server/graph/parsers/ts/typescript-project.ts
|
|
620
|
-
var
|
|
621
|
-
var
|
|
622
|
-
init_config();
|
|
1347
|
+
var import_node_fs6 = require("node:fs");
|
|
1348
|
+
var import_node_path6 = require("node:path");
|
|
623
1349
|
|
|
624
1350
|
// src/server/graph/core/resolve-paths.ts
|
|
1351
|
+
var import_node_fs3 = require("node:fs");
|
|
1352
|
+
var import_node_path3 = require("node:path");
|
|
1353
|
+
|
|
1354
|
+
// src/server/graph/core/walk.ts
|
|
625
1355
|
var import_node_fs2 = require("node:fs");
|
|
626
1356
|
var import_node_path2 = require("node:path");
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
};
|
|
1357
|
+
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
1358
|
+
"node_modules",
|
|
1359
|
+
"dist",
|
|
1360
|
+
"build",
|
|
1361
|
+
"out",
|
|
1362
|
+
"coverage"
|
|
1363
|
+
]);
|
|
667
1364
|
function walk(dir, exts) {
|
|
668
1365
|
const results = [];
|
|
669
|
-
if (!(0,
|
|
670
|
-
for (const entry of (0,
|
|
671
|
-
const full = (0,
|
|
1366
|
+
if (!(0, import_node_fs2.existsSync)(dir)) return results;
|
|
1367
|
+
for (const entry of (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true })) {
|
|
1368
|
+
const full = (0, import_node_path2.join)(dir, entry.name);
|
|
672
1369
|
if (entry.isDirectory()) {
|
|
673
1370
|
results.push(...walk(full, exts));
|
|
674
|
-
} else if (exts.includes((0,
|
|
1371
|
+
} else if (exts.includes((0, import_node_path2.extname)(entry.name))) {
|
|
675
1372
|
results.push(full);
|
|
676
1373
|
}
|
|
677
1374
|
}
|
|
678
1375
|
return results;
|
|
679
1376
|
}
|
|
680
|
-
function walkWithIgnore(dir, exts,
|
|
1377
|
+
function walkWithIgnore(dir, exts, opts = {}) {
|
|
681
1378
|
const results = [];
|
|
682
|
-
if (!(0,
|
|
683
|
-
|
|
1379
|
+
if (!(0, import_node_fs2.existsSync)(dir)) return results;
|
|
1380
|
+
const skip = opts.extraIgnore ? /* @__PURE__ */ new Set([...DEFAULT_IGNORE_DIRS, ...opts.extraIgnore]) : DEFAULT_IGNORE_DIRS;
|
|
1381
|
+
for (const entry of (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true })) {
|
|
684
1382
|
if (entry.isDirectory()) {
|
|
685
|
-
if (
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1383
|
+
if (entry.name.startsWith(".")) continue;
|
|
1384
|
+
if (skip.has(entry.name)) continue;
|
|
1385
|
+
results.push(...walkWithIgnore((0, import_node_path2.join)(dir, entry.name), exts, opts));
|
|
1386
|
+
} else if (exts.includes((0, import_node_path2.extname)(entry.name))) {
|
|
1387
|
+
results.push((0, import_node_path2.join)(dir, entry.name));
|
|
689
1388
|
}
|
|
690
1389
|
}
|
|
691
1390
|
return results;
|
|
692
1391
|
}
|
|
693
|
-
|
|
694
|
-
|
|
1392
|
+
|
|
1393
|
+
// src/server/graph/core/resolve-paths.ts
|
|
1394
|
+
function hasSqlFiles(dir) {
|
|
1395
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return false;
|
|
1396
|
+
try {
|
|
1397
|
+
return (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true }).some(
|
|
1398
|
+
(e) => e.isFile() && e.name.endsWith(".sql")
|
|
1399
|
+
);
|
|
1400
|
+
} catch {
|
|
1401
|
+
return false;
|
|
1402
|
+
}
|
|
695
1403
|
}
|
|
696
|
-
function
|
|
697
|
-
if (!
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
1404
|
+
function hasNestedMigrationSql(dir) {
|
|
1405
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return false;
|
|
1406
|
+
try {
|
|
1407
|
+
return (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true }).some(
|
|
1408
|
+
(e) => e.isDirectory() && (0, import_node_fs3.existsSync)((0, import_node_path3.join)(dir, e.name, "migration.sql"))
|
|
1409
|
+
);
|
|
1410
|
+
} catch {
|
|
1411
|
+
return false;
|
|
702
1412
|
}
|
|
703
|
-
return null;
|
|
704
1413
|
}
|
|
705
|
-
function
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1414
|
+
function resolveDbFromDir(dir) {
|
|
1415
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return { kind: "none", schemaPath: null, migrationsDir: null };
|
|
1416
|
+
const schemaPath = (0, import_node_path3.join)(dir, "schema.prisma");
|
|
1417
|
+
if ((0, import_node_fs3.existsSync)(schemaPath)) {
|
|
1418
|
+
const migrationsDir2 = (0, import_node_path3.join)(dir, "migrations");
|
|
1419
|
+
return {
|
|
1420
|
+
kind: "prisma",
|
|
1421
|
+
schemaPath,
|
|
1422
|
+
migrationsDir: (0, import_node_fs3.existsSync)(migrationsDir2) ? migrationsDir2 : null
|
|
1423
|
+
};
|
|
709
1424
|
}
|
|
710
|
-
|
|
1425
|
+
const migrationsDir = (0, import_node_path3.join)(dir, "migrations");
|
|
1426
|
+
if (hasSqlFiles(migrationsDir) || hasNestedMigrationSql(migrationsDir)) {
|
|
1427
|
+
return { kind: "sql-migrations", migrationsDir, schemaPath: null };
|
|
1428
|
+
}
|
|
1429
|
+
if (hasSqlFiles(dir) || hasNestedMigrationSql(dir)) {
|
|
1430
|
+
return { kind: "sql-migrations", migrationsDir: dir, schemaPath: null };
|
|
1431
|
+
}
|
|
1432
|
+
return { kind: "none", schemaPath: null, migrationsDir: null };
|
|
711
1433
|
}
|
|
712
|
-
function
|
|
713
|
-
|
|
714
|
-
|
|
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;
|
|
1434
|
+
function detectDbConfig(rootDir, config) {
|
|
1435
|
+
if (config.paths?.dbDir) {
|
|
1436
|
+
return resolveDbFromDir((0, import_node_path3.join)(rootDir, config.paths.dbDir));
|
|
723
1437
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
if (
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
1438
|
+
const candidates = ["prisma", "supabase", "drizzle", (0, import_node_path3.join)("db", "migrations"), "migrations"];
|
|
1439
|
+
for (const c of candidates) {
|
|
1440
|
+
const dir = (0, import_node_path3.join)(rootDir, c);
|
|
1441
|
+
const resolved = resolveDbFromDir(dir);
|
|
1442
|
+
if (resolved.kind !== "none") return resolved;
|
|
1443
|
+
}
|
|
1444
|
+
return { kind: "none", schemaPath: null, migrationsDir: null };
|
|
1445
|
+
}
|
|
1446
|
+
function detectDbDir(rootDir, config, dbConfig) {
|
|
1447
|
+
if (config.paths?.dbDir) return (0, import_node_path3.join)(rootDir, config.paths.dbDir);
|
|
1448
|
+
if (dbConfig.kind === "prisma") return (0, import_node_path3.dirname)(dbConfig.schemaPath);
|
|
1449
|
+
if (dbConfig.kind === "sql-migrations") return dbConfig.migrationsDir;
|
|
1450
|
+
return null;
|
|
1451
|
+
}
|
|
1452
|
+
var NON_SOURCE_DIRS = /* @__PURE__ */ new Set([
|
|
1453
|
+
...DEFAULT_IGNORE_DIRS,
|
|
1454
|
+
// DB conventions (handled by db parsers)
|
|
1455
|
+
"prisma",
|
|
1456
|
+
"supabase",
|
|
1457
|
+
"drizzle",
|
|
1458
|
+
"migrations",
|
|
1459
|
+
// Web assets
|
|
1460
|
+
"public",
|
|
1461
|
+
"static",
|
|
1462
|
+
"assets",
|
|
1463
|
+
// Docs
|
|
1464
|
+
"docs",
|
|
1465
|
+
"documentation",
|
|
1466
|
+
// Test dirs (project tests aren't part of the structural graph)
|
|
1467
|
+
"tests",
|
|
1468
|
+
"__tests__",
|
|
1469
|
+
"e2e",
|
|
1470
|
+
"playwright",
|
|
1471
|
+
"cypress",
|
|
1472
|
+
// Monorepo workspace roots — separate graph projects per .launchchart.json
|
|
1473
|
+
"packages",
|
|
1474
|
+
"apps",
|
|
1475
|
+
"services",
|
|
1476
|
+
"libs"
|
|
1477
|
+
]);
|
|
1478
|
+
function dirHasTSFiles(dir) {
|
|
1479
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return false;
|
|
1480
|
+
try {
|
|
1481
|
+
const stack = [dir];
|
|
1482
|
+
while (stack.length > 0) {
|
|
1483
|
+
const cur = stack.pop();
|
|
1484
|
+
const entries = (0, import_node_fs3.readdirSync)(cur, { withFileTypes: true });
|
|
1485
|
+
for (const e of entries) {
|
|
1486
|
+
if (e.isFile() && (e.name.endsWith(".ts") || e.name.endsWith(".tsx"))) return true;
|
|
1487
|
+
if (e.isDirectory() && !e.name.startsWith(".") && !DEFAULT_IGNORE_DIRS.has(e.name)) {
|
|
1488
|
+
stack.push((0, import_node_path3.join)(cur, e.name));
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
} catch {
|
|
1493
|
+
}
|
|
1494
|
+
return false;
|
|
1495
|
+
}
|
|
1496
|
+
function collectCodeBearingChildren(dir, extraSkip) {
|
|
1497
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return [];
|
|
1498
|
+
const out = [];
|
|
1499
|
+
try {
|
|
1500
|
+
for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
1501
|
+
if (!entry.isDirectory()) continue;
|
|
1502
|
+
if (entry.name.startsWith(".")) continue;
|
|
1503
|
+
if (NON_SOURCE_DIRS.has(entry.name)) continue;
|
|
1504
|
+
if (extraSkip?.has(entry.name)) continue;
|
|
1505
|
+
const full = (0, import_node_path3.join)(dir, entry.name);
|
|
1506
|
+
if (dirHasTSFiles(full)) out.push(full);
|
|
1507
|
+
}
|
|
1508
|
+
} catch {
|
|
1509
|
+
}
|
|
1510
|
+
return out;
|
|
1511
|
+
}
|
|
1512
|
+
function detectSrcRoots(rootDir, srcDir, appDir, config) {
|
|
1513
|
+
if (config.paths?.srcRoots && config.paths.srcRoots.length > 0) {
|
|
1514
|
+
const roots2 = /* @__PURE__ */ new Set();
|
|
1515
|
+
roots2.add(appDir);
|
|
1516
|
+
for (const r of config.paths.srcRoots) {
|
|
1517
|
+
const abs = (0, import_node_path3.isAbsolute)(r) ? r : (0, import_node_path3.resolve)(rootDir, r);
|
|
1518
|
+
roots2.add(abs);
|
|
1519
|
+
}
|
|
1520
|
+
return [...roots2];
|
|
1521
|
+
}
|
|
1522
|
+
const roots = /* @__PURE__ */ new Set();
|
|
1523
|
+
roots.add(appDir);
|
|
1524
|
+
for (const c of collectCodeBearingChildren(srcDir)) roots.add(c);
|
|
1525
|
+
if (srcDir !== rootDir) {
|
|
1526
|
+
const skipSrcWrapper = /* @__PURE__ */ new Set([(0, import_node_path3.basename)(srcDir)]);
|
|
1527
|
+
for (const c of collectCodeBearingChildren(rootDir, skipSrcWrapper)) roots.add(c);
|
|
1528
|
+
}
|
|
1529
|
+
return [...roots];
|
|
1530
|
+
}
|
|
1531
|
+
var CONVENTION_NAMES = ["middleware.ts", "middleware.tsx", "instrumentation.ts", "instrumentation.tsx"];
|
|
1532
|
+
function detectConventionFiles(rootDir, srcDir) {
|
|
1533
|
+
const out = [];
|
|
1534
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1535
|
+
const dirs = srcDir === rootDir ? [rootDir] : [srcDir, rootDir];
|
|
1536
|
+
for (const dir of dirs) {
|
|
1537
|
+
for (const name of CONVENTION_NAMES) {
|
|
1538
|
+
const full = (0, import_node_path3.join)(dir, name);
|
|
1539
|
+
if (!seen.has(full) && (0, import_node_fs3.existsSync)(full)) {
|
|
1540
|
+
try {
|
|
1541
|
+
if ((0, import_node_fs3.statSync)(full).isFile()) {
|
|
1542
|
+
seen.add(full);
|
|
1543
|
+
out.push(full);
|
|
1544
|
+
}
|
|
1545
|
+
} catch {
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return out;
|
|
1551
|
+
}
|
|
1552
|
+
function resolveProjectPaths(rootDir, config) {
|
|
1553
|
+
let srcDir;
|
|
1554
|
+
let appDir;
|
|
1555
|
+
if (config.paths?.appDir) {
|
|
1556
|
+
appDir = (0, import_node_path3.join)(rootDir, config.paths.appDir);
|
|
1557
|
+
srcDir = config.paths.srcDir ? (0, import_node_path3.join)(rootDir, config.paths.srcDir) : (0, import_node_path3.dirname)(appDir);
|
|
1558
|
+
} else {
|
|
1559
|
+
const srcApp = (0, import_node_path3.join)(rootDir, "src", "app");
|
|
1560
|
+
const rootApp = (0, import_node_path3.join)(rootDir, "app");
|
|
1561
|
+
if ((0, import_node_fs3.existsSync)(srcApp)) {
|
|
1562
|
+
srcDir = (0, import_node_path3.join)(rootDir, "src");
|
|
1563
|
+
appDir = srcApp;
|
|
1564
|
+
} else if ((0, import_node_fs3.existsSync)(rootApp)) {
|
|
1565
|
+
srcDir = rootDir;
|
|
1566
|
+
appDir = rootApp;
|
|
1567
|
+
} else {
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
const apiDir = (0, import_node_path3.join)(appDir, "api");
|
|
1572
|
+
const dbConfig = detectDbConfig(rootDir, config);
|
|
1573
|
+
const dbDir = detectDbDir(rootDir, config, dbConfig);
|
|
1574
|
+
const srcRoots = detectSrcRoots(rootDir, srcDir, appDir, config);
|
|
1575
|
+
const conventionFiles = detectConventionFiles(rootDir, srcDir);
|
|
1576
|
+
return { srcDir, appDir, apiDir, dbDir, srcRoots, conventionFiles, dbConfig };
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/server/graph/parsers/ts/typescript-project.ts
|
|
1580
|
+
init_ts_extractor();
|
|
1581
|
+
var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
1582
|
+
var CLASSIFICATION_TO_LAYER = {
|
|
1583
|
+
endpoint: "api",
|
|
1584
|
+
"server-action": "api",
|
|
1585
|
+
page: "ui",
|
|
1586
|
+
layout: "ui",
|
|
1587
|
+
component: "ui",
|
|
1588
|
+
ui: "ui",
|
|
1589
|
+
hook: "ui",
|
|
1590
|
+
context: "ui",
|
|
1591
|
+
config: "ui",
|
|
1592
|
+
lib: "ui",
|
|
1593
|
+
"mcp-tool": "ui",
|
|
1594
|
+
middleware: "ui",
|
|
1595
|
+
external: "ui"
|
|
1596
|
+
};
|
|
1597
|
+
function toNodeId(srcDir, rootDir, absPath) {
|
|
1598
|
+
const relFromSrc = (0, import_node_path6.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
1599
|
+
if (relFromSrc.startsWith("..")) {
|
|
1600
|
+
return (0, import_node_path6.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1601
|
+
}
|
|
1602
|
+
return relFromSrc;
|
|
1603
|
+
}
|
|
1604
|
+
function resolveImport(srcDir, specifier) {
|
|
1605
|
+
if (!specifier.startsWith("@/")) return null;
|
|
1606
|
+
const rel = specifier.slice(2);
|
|
1607
|
+
const base = (0, import_node_path6.join)(srcDir, rel);
|
|
1608
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path6.join)(base, "index.ts"), (0, import_node_path6.join)(base, "index.tsx")]) {
|
|
1609
|
+
if ((0, import_node_fs6.existsSync)(c) && (0, import_node_fs6.statSync)(c).isFile()) return c;
|
|
1610
|
+
}
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
function resolveRelativeImport(fromFile, specifier) {
|
|
1614
|
+
const base = (0, import_node_path6.join)((0, import_node_path6.dirname)(fromFile), specifier);
|
|
1615
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path6.join)(base, "index.ts"), (0, import_node_path6.join)(base, "index.tsx")]) {
|
|
1616
|
+
if ((0, import_node_fs6.existsSync)(c) && (0, import_node_fs6.statSync)(c).isFile()) return c;
|
|
1617
|
+
}
|
|
1618
|
+
return null;
|
|
1619
|
+
}
|
|
1620
|
+
function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
|
|
1621
|
+
const cached = memo.get(barrelAbsPath);
|
|
1622
|
+
if (cached) return cached;
|
|
1623
|
+
if (visiting.has(barrelAbsPath)) return /* @__PURE__ */ new Map();
|
|
1624
|
+
visiting.add(barrelAbsPath);
|
|
1625
|
+
const parsed = parsedByPath.get(barrelAbsPath);
|
|
1626
|
+
const map = /* @__PURE__ */ new Map();
|
|
1627
|
+
if (!parsed) {
|
|
1628
|
+
visiting.delete(barrelAbsPath);
|
|
1629
|
+
memo.set(barrelAbsPath, map);
|
|
1630
|
+
return map;
|
|
1631
|
+
}
|
|
1632
|
+
for (const re of parsed.reExports) {
|
|
1633
|
+
if (!re.from.startsWith(".")) continue;
|
|
1634
|
+
const resolved = resolveRelativeImport(barrelAbsPath, re.from);
|
|
1635
|
+
if (!resolved) continue;
|
|
1636
|
+
if (re.isWildcard) {
|
|
1637
|
+
const targetBn = (0, import_node_path6.basename)(resolved);
|
|
1638
|
+
const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
|
|
1639
|
+
if (targetIsBarrel) {
|
|
732
1640
|
const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
|
|
733
1641
|
for (const [name, target] of nested) {
|
|
734
1642
|
if (!map.has(name)) map.set(name, target);
|
|
@@ -753,12 +1661,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
|
753
1661
|
const barrels = /* @__PURE__ */ new Map();
|
|
754
1662
|
const memo = /* @__PURE__ */ new Map();
|
|
755
1663
|
for (const [absPath, parsed] of parsedByPath) {
|
|
756
|
-
const bn = (0,
|
|
1664
|
+
const bn = (0, import_node_path6.basename)(absPath);
|
|
757
1665
|
if (bn !== "index.ts" && bn !== "index.tsx") continue;
|
|
758
1666
|
if (parsed.reExports.length === 0) continue;
|
|
759
1667
|
const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
|
|
760
1668
|
if (map.size > 0) {
|
|
761
|
-
const barrelId = (0,
|
|
1669
|
+
const barrelId = (0, import_node_path6.relative)(srcDir, (0, import_node_path6.dirname)(absPath)).replace(/\\/g, "/");
|
|
762
1670
|
barrels.set(barrelId, map);
|
|
763
1671
|
}
|
|
764
1672
|
}
|
|
@@ -767,26 +1675,32 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
|
767
1675
|
function classifyType(absPath, id) {
|
|
768
1676
|
const contentType = classifyFile(absPath);
|
|
769
1677
|
if (contentType === "lib" && id.startsWith("server/mcp/")) return "mcp-tool";
|
|
1678
|
+
if (/^app\/(.+\/)?page\.tsx$/.test(id)) return "page";
|
|
1679
|
+
if (/^app\/(.+\/)?layout\.tsx$/.test(id)) return "layout";
|
|
770
1680
|
return contentType;
|
|
771
1681
|
}
|
|
772
1682
|
function extractRoute(id) {
|
|
773
1683
|
if (!id.endsWith("/page.tsx")) return null;
|
|
774
1684
|
let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
|
|
775
1685
|
route = route.replace(/\/\([^)]+\)/g, "");
|
|
1686
|
+
route = route.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "*$1?");
|
|
1687
|
+
route = route.replace(/\[\.\.\.([^\]]+)\]/g, "*$1");
|
|
776
1688
|
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
777
1689
|
route = route.replace(/\/+/g, "/");
|
|
778
1690
|
if (!route.startsWith("/")) route = "/" + route;
|
|
779
1691
|
return route || "/";
|
|
780
1692
|
}
|
|
781
1693
|
function nameFromFilename(absPath) {
|
|
782
|
-
return (0,
|
|
1694
|
+
return (0, import_node_path6.basename)(absPath, (0, import_node_path6.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
|
|
783
1695
|
}
|
|
784
|
-
function
|
|
785
|
-
let route = "/" + (0,
|
|
1696
|
+
function filePathToAppRoute(appDir, absPath) {
|
|
1697
|
+
let route = ("/" + (0, import_node_path6.relative)(appDir, absPath).replace(/\\/g, "/")).replace(/\/route\.tsx?$/, "");
|
|
1698
|
+
route = route.replace(/\/\([^)]+\)/g, "");
|
|
1699
|
+
route = route.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "*$1?");
|
|
1700
|
+
route = route.replace(/\[\.\.\.([^\]]+)\]/g, "*$1");
|
|
786
1701
|
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
787
1702
|
route = route.replace(/\/+/g, "/");
|
|
788
|
-
|
|
789
|
-
return "/api" + route;
|
|
1703
|
+
return route === "" ? "/" : route;
|
|
790
1704
|
}
|
|
791
1705
|
function camelToPascal(s) {
|
|
792
1706
|
if (!s) return s;
|
|
@@ -831,25 +1745,52 @@ function resolveTemplateLiteralRoute(template, routeToNodeId) {
|
|
|
831
1745
|
function routeMatchScore(candidate, known) {
|
|
832
1746
|
const segsA = candidate.split("/");
|
|
833
1747
|
const segsB = known.split("/");
|
|
834
|
-
if (segsA.length !== segsB.length) return -1;
|
|
835
1748
|
let score = 0;
|
|
836
|
-
|
|
837
|
-
|
|
1749
|
+
let i = 0, j = 0;
|
|
1750
|
+
while (i < segsA.length && j < segsB.length) {
|
|
1751
|
+
const a = segsA[i], b = segsB[j];
|
|
1752
|
+
if (b.startsWith("*") && b.endsWith("?")) {
|
|
1753
|
+
score += 1;
|
|
1754
|
+
return score;
|
|
1755
|
+
}
|
|
1756
|
+
if (b.startsWith("*")) {
|
|
1757
|
+
const remaining = segsA.length - i;
|
|
1758
|
+
if (remaining < 1) return -1;
|
|
1759
|
+
score += 1 + remaining;
|
|
1760
|
+
return score;
|
|
1761
|
+
}
|
|
838
1762
|
if (a === b) {
|
|
839
1763
|
score += 3;
|
|
1764
|
+
i++;
|
|
1765
|
+
j++;
|
|
840
1766
|
continue;
|
|
841
1767
|
}
|
|
842
1768
|
if (a.startsWith(":") && b.startsWith(":")) {
|
|
843
1769
|
score += 2;
|
|
1770
|
+
i++;
|
|
1771
|
+
j++;
|
|
844
1772
|
continue;
|
|
845
1773
|
}
|
|
846
1774
|
if (a.startsWith(":") || b.startsWith(":")) {
|
|
847
|
-
|
|
1775
|
+
i++;
|
|
1776
|
+
j++;
|
|
848
1777
|
continue;
|
|
849
1778
|
}
|
|
850
1779
|
return -1;
|
|
851
1780
|
}
|
|
852
|
-
|
|
1781
|
+
if (i === segsA.length) {
|
|
1782
|
+
while (j < segsB.length) {
|
|
1783
|
+
const b = segsB[j];
|
|
1784
|
+
if (b.startsWith("*") && b.endsWith("?")) {
|
|
1785
|
+
score += 1;
|
|
1786
|
+
j++;
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
return -1;
|
|
1790
|
+
}
|
|
1791
|
+
return score;
|
|
1792
|
+
}
|
|
1793
|
+
return -1;
|
|
853
1794
|
}
|
|
854
1795
|
function templateToRoute(template) {
|
|
855
1796
|
return template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
@@ -871,7 +1812,7 @@ function matchRouteToPage(route, routeToNodeId) {
|
|
|
871
1812
|
if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
|
|
872
1813
|
return null;
|
|
873
1814
|
}
|
|
874
|
-
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps, routeToNodeId) {
|
|
1815
|
+
function extractEdges(srcDir, rootDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps, routeToNodeId) {
|
|
875
1816
|
const edges = [];
|
|
876
1817
|
const flagged = [];
|
|
877
1818
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -884,7 +1825,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps,
|
|
|
884
1825
|
edges.push(edge);
|
|
885
1826
|
}
|
|
886
1827
|
function edgeTypeFor(isTypeOnlyImport, importedNames) {
|
|
887
|
-
if (isTypeOnlyImport) return "
|
|
1828
|
+
if (isTypeOnlyImport) return "imports_type";
|
|
888
1829
|
const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
|
|
889
1830
|
if (anyRendered) return "renders";
|
|
890
1831
|
return "imports";
|
|
@@ -899,7 +1840,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps,
|
|
|
899
1840
|
for (const name of names) {
|
|
900
1841
|
const targetAbs = barrelMap.get(name);
|
|
901
1842
|
if (targetAbs) {
|
|
902
|
-
const targetId = toNodeId(srcDir, targetAbs);
|
|
1843
|
+
const targetId = toNodeId(srcDir, rootDir, targetAbs);
|
|
903
1844
|
if (nodeIdSet.has(targetId)) {
|
|
904
1845
|
if (!byTarget.has(targetId)) byTarget.set(targetId, []);
|
|
905
1846
|
byTarget.get(targetId).push(name);
|
|
@@ -913,18 +1854,20 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps,
|
|
|
913
1854
|
} else {
|
|
914
1855
|
const resolved = resolveImport(srcDir, specifier);
|
|
915
1856
|
if (resolved) {
|
|
916
|
-
const targetId = toNodeId(srcDir, resolved);
|
|
1857
|
+
const targetId = toNodeId(srcDir, rootDir, resolved);
|
|
917
1858
|
if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
|
|
918
|
-
|
|
1859
|
+
const allType = isTypeOnly || names.length > 0 && names.every((n) => typeNames.has(n));
|
|
1860
|
+
addEdge(targetId, edgeTypeFor(allType, names));
|
|
919
1861
|
}
|
|
920
1862
|
}
|
|
921
1863
|
}
|
|
922
1864
|
} else if (specifier.startsWith(".")) {
|
|
923
1865
|
const resolved = resolveRelativeImport(absPath, specifier);
|
|
924
1866
|
if (resolved) {
|
|
925
|
-
const targetId = toNodeId(srcDir, resolved);
|
|
1867
|
+
const targetId = toNodeId(srcDir, rootDir, resolved);
|
|
926
1868
|
if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
|
|
927
|
-
|
|
1869
|
+
const allType = isTypeOnly || names.length > 0 && names.every((n) => typeNames.has(n));
|
|
1870
|
+
addEdge(targetId, edgeTypeFor(allType, names));
|
|
928
1871
|
}
|
|
929
1872
|
}
|
|
930
1873
|
}
|
|
@@ -966,24 +1909,30 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps,
|
|
|
966
1909
|
}
|
|
967
1910
|
return { edges, flagged };
|
|
968
1911
|
}
|
|
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
1912
|
function detect(rootDir) {
|
|
973
1913
|
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
974
|
-
return paths !== null
|
|
1914
|
+
return paths !== null;
|
|
975
1915
|
}
|
|
976
1916
|
function generate(rootDir) {
|
|
977
1917
|
const config = loadConfig(rootDir);
|
|
978
1918
|
const paths = resolveProjectPaths(rootDir, config);
|
|
979
1919
|
const srcDir = paths.srcDir;
|
|
980
|
-
const
|
|
981
|
-
const
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1920
|
+
const allDiscovered = [];
|
|
1921
|
+
const discoveredSet = /* @__PURE__ */ new Set();
|
|
1922
|
+
for (const root of paths.srcRoots) {
|
|
1923
|
+
for (const f of walk(root, [".tsx", ".ts"])) {
|
|
1924
|
+
if (!discoveredSet.has(f)) {
|
|
1925
|
+
discoveredSet.add(f);
|
|
1926
|
+
allDiscovered.push(f);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
for (const conv of paths.conventionFiles) {
|
|
1931
|
+
if (!discoveredSet.has(conv)) {
|
|
1932
|
+
discoveredSet.add(conv);
|
|
1933
|
+
allDiscovered.push(conv);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
987
1936
|
const parsedByPath = /* @__PURE__ */ new Map();
|
|
988
1937
|
for (const absPath of allDiscovered) {
|
|
989
1938
|
parsedByPath.set(absPath, parseFileTS(absPath));
|
|
@@ -993,9 +1942,9 @@ function generate(rootDir) {
|
|
|
993
1942
|
const apiNodes = [];
|
|
994
1943
|
const nodeIdSet = /* @__PURE__ */ new Set();
|
|
995
1944
|
const routeToNodeId = /* @__PURE__ */ new Map();
|
|
996
|
-
const fileSet = allDiscovered.filter((f) => !(0,
|
|
1945
|
+
const fileSet = allDiscovered.filter((f) => !(0, import_node_path6.basename)(f).startsWith("index."));
|
|
997
1946
|
for (const absPath of fileSet) {
|
|
998
|
-
const id = toNodeId(srcDir, absPath);
|
|
1947
|
+
const id = toNodeId(srcDir, rootDir, absPath);
|
|
999
1948
|
const type = classifyType(absPath, id);
|
|
1000
1949
|
if (type === "test" || type === "story") continue;
|
|
1001
1950
|
const parsed = parsedByPath.get(absPath);
|
|
@@ -1003,26 +1952,34 @@ function generate(rootDir) {
|
|
|
1003
1952
|
const layer = CLASSIFICATION_TO_LAYER[type] ?? "ui";
|
|
1004
1953
|
nodeIdSet.add(id);
|
|
1005
1954
|
if (layer === "api") {
|
|
1006
|
-
const methods = [];
|
|
1007
|
-
for (const exp of parsed.exports) {
|
|
1008
|
-
if (HTTP_METHODS.has(exp)) methods.push(exp);
|
|
1009
|
-
}
|
|
1010
1955
|
const dbCalls = extractDbCallsTS(absPath);
|
|
1011
1956
|
const authWrappers = extractAuthWrappersTS(absPath);
|
|
1012
1957
|
const deep = extractDeep(absPath);
|
|
1013
|
-
const routePath = (0, import_node_fs4.existsSync)(apiDir) ? filePathToApiRoute(apiDir, absPath) : `/api/${id.replace(/\/route\.tsx?$/, "")}`;
|
|
1014
1958
|
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
1015
1959
|
const mutates = mutations.length > 0;
|
|
1016
1960
|
const authStrategy = [...authWrappers];
|
|
1961
|
+
const isServerAction = type === "server-action";
|
|
1962
|
+
const methods = [];
|
|
1963
|
+
if (!isServerAction) {
|
|
1964
|
+
for (const exp of parsed.exports) {
|
|
1965
|
+
if (HTTP_METHODS.has(exp)) methods.push(exp);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
const routePath = isServerAction ? null : filePathToAppRoute(paths.appDir, absPath);
|
|
1969
|
+
const actions = isServerAction ? parsed.exports.filter((e) => !HTTP_METHODS.has(e)) : [];
|
|
1017
1970
|
apiNodes.push({
|
|
1018
1971
|
id,
|
|
1019
|
-
type: "endpoint",
|
|
1020
|
-
name:
|
|
1972
|
+
type: isServerAction ? "server-action" : "endpoint",
|
|
1973
|
+
// For HTTP routes: name = URL path. For Server Actions: name = file id +
|
|
1974
|
+
// exported action names — the callable surface, not a URL.
|
|
1975
|
+
name: isServerAction ? actions.length > 0 ? `${id} (${actions.join(", ")})` : id : routePath,
|
|
1021
1976
|
layer: "api",
|
|
1022
1977
|
path: routePath,
|
|
1978
|
+
// null for Server Actions
|
|
1023
1979
|
methods,
|
|
1024
1980
|
handler: id,
|
|
1025
1981
|
mutates,
|
|
1982
|
+
...isServerAction ? { actions } : {},
|
|
1026
1983
|
auth: authStrategy.length > 0 ? authStrategy : ["public"],
|
|
1027
1984
|
db_models: [...new Set(dbCalls.map((c) => c.model))],
|
|
1028
1985
|
db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
|
|
@@ -1030,12 +1987,15 @@ function generate(rootDir) {
|
|
|
1030
1987
|
variables: deep.variables,
|
|
1031
1988
|
responses: deep.responses,
|
|
1032
1989
|
params: deep.params,
|
|
1990
|
+
...deep.effects ? { effects: deep.effects } : {},
|
|
1033
1991
|
_dbCalls: dbCalls
|
|
1034
1992
|
// temp: used for cross-ref building below
|
|
1035
1993
|
});
|
|
1036
1994
|
} else {
|
|
1037
1995
|
const route = extractRoute(id);
|
|
1038
1996
|
const deep = extractDeep(absPath);
|
|
1997
|
+
const dbCalls = extractDbCallsTS(absPath);
|
|
1998
|
+
const authWrappers = type === "page" || type === "layout" ? [...extractAuthWrappersTS(absPath)] : [];
|
|
1039
1999
|
uiNodes.push({
|
|
1040
2000
|
id,
|
|
1041
2001
|
type,
|
|
@@ -1046,19 +2006,29 @@ function generate(rootDir) {
|
|
|
1046
2006
|
elements: deep.elements,
|
|
1047
2007
|
stateVars: deep.stateVars,
|
|
1048
2008
|
conditions: deep.conditions,
|
|
1049
|
-
variables: deep.variables
|
|
2009
|
+
variables: deep.variables,
|
|
2010
|
+
...deep.effects ? { effects: deep.effects } : {},
|
|
2011
|
+
...authWrappers.length > 0 ? { auth: authWrappers } : {},
|
|
2012
|
+
...dbCalls.length > 0 ? { _dbCalls: dbCalls } : {}
|
|
1050
2013
|
});
|
|
1051
|
-
if (route)
|
|
2014
|
+
if (route) {
|
|
2015
|
+
routeToNodeId.set(route, id);
|
|
2016
|
+
const trimmed = route.replace(/\/\*[^/]+\?$/, "");
|
|
2017
|
+
if (trimmed && trimmed !== route && !routeToNodeId.has(trimmed)) {
|
|
2018
|
+
routeToNodeId.set(trimmed, id);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
1052
2021
|
}
|
|
1053
2022
|
}
|
|
1054
2023
|
const uiEdges = [];
|
|
1055
2024
|
const uiFlagged = [];
|
|
1056
2025
|
for (const absPath of fileSet) {
|
|
1057
|
-
const id = toNodeId(srcDir, absPath);
|
|
2026
|
+
const id = toNodeId(srcDir, rootDir, absPath);
|
|
1058
2027
|
if (!nodeIdSet.has(id)) continue;
|
|
1059
2028
|
const parsed = parsedByPath.get(absPath);
|
|
1060
2029
|
const { edges, flagged } = extractEdges(
|
|
1061
2030
|
srcDir,
|
|
2031
|
+
rootDir,
|
|
1062
2032
|
absPath,
|
|
1063
2033
|
id,
|
|
1064
2034
|
parsed,
|
|
@@ -1069,9 +2039,32 @@ function generate(rootDir) {
|
|
|
1069
2039
|
uiEdges.push(...edges);
|
|
1070
2040
|
uiFlagged.push(...flagged);
|
|
1071
2041
|
}
|
|
2042
|
+
const layoutsById = /* @__PURE__ */ new Set();
|
|
2043
|
+
for (const n of uiNodes) {
|
|
2044
|
+
if (n.type === "layout") layoutsById.add(n.id);
|
|
2045
|
+
}
|
|
2046
|
+
function findClosestLayout(pageId) {
|
|
2047
|
+
let dir = pageId.replace(/\/page\.tsx$/, "");
|
|
2048
|
+
while (dir.length > 0) {
|
|
2049
|
+
const candidate = `${dir}/layout.tsx`;
|
|
2050
|
+
if (layoutsById.has(candidate)) return candidate;
|
|
2051
|
+
const slash = dir.lastIndexOf("/");
|
|
2052
|
+
if (slash < 0) break;
|
|
2053
|
+
dir = dir.slice(0, slash);
|
|
2054
|
+
}
|
|
2055
|
+
if (layoutsById.has("app/layout.tsx")) return "app/layout.tsx";
|
|
2056
|
+
return null;
|
|
2057
|
+
}
|
|
2058
|
+
for (const n of uiNodes) {
|
|
2059
|
+
if (n.type !== "page") continue;
|
|
2060
|
+
const layoutId = findClosestLayout(n.id);
|
|
2061
|
+
if (layoutId && layoutId !== n.id) {
|
|
2062
|
+
uiEdges.push({ source: layoutId, target: n.id, type: "wraps" });
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
1072
2065
|
const fetchCallEntries = [];
|
|
1073
2066
|
for (const absPath of fileSet) {
|
|
1074
|
-
const sourceId = toNodeId(srcDir, absPath);
|
|
2067
|
+
const sourceId = toNodeId(srcDir, rootDir, absPath);
|
|
1075
2068
|
if (!nodeIdSet.has(sourceId)) continue;
|
|
1076
2069
|
const parsed = parsedByPath.get(absPath);
|
|
1077
2070
|
if (parsed.fetchCalls.length === 0) continue;
|
|
@@ -1087,20 +2080,7 @@ function generate(rootDir) {
|
|
|
1087
2080
|
});
|
|
1088
2081
|
}
|
|
1089
2082
|
const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
|
|
1090
|
-
const
|
|
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);
|
|
2083
|
+
const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], { extraIgnore: /* @__PURE__ */ new Set(["src"]) });
|
|
1104
2084
|
for (const absPath of externalCandidates) {
|
|
1105
2085
|
const normalized = absPath.replace(/\\/g, "/");
|
|
1106
2086
|
if (externalScanned.has(normalized)) continue;
|
|
@@ -1110,7 +2090,7 @@ function generate(rootDir) {
|
|
|
1110
2090
|
} catch {
|
|
1111
2091
|
continue;
|
|
1112
2092
|
}
|
|
1113
|
-
const externalId = (0,
|
|
2093
|
+
const externalId = (0, import_node_path6.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1114
2094
|
const edgesFromThis = [];
|
|
1115
2095
|
const seen = /* @__PURE__ */ new Set();
|
|
1116
2096
|
for (const imp of parsed.imports) {
|
|
@@ -1123,7 +2103,7 @@ function generate(rootDir) {
|
|
|
1123
2103
|
for (const name of names) {
|
|
1124
2104
|
const targetAbs = barrelMap.get(name);
|
|
1125
2105
|
if (!targetAbs) continue;
|
|
1126
|
-
const targetId2 = toNodeId(srcDir, targetAbs);
|
|
2106
|
+
const targetId2 = toNodeId(srcDir, rootDir, targetAbs);
|
|
1127
2107
|
if (!nodeIdSet.has(targetId2)) continue;
|
|
1128
2108
|
const key2 = `${externalId}\u2192${targetId2}`;
|
|
1129
2109
|
if (seen.has(key2)) continue;
|
|
@@ -1137,7 +2117,7 @@ function generate(rootDir) {
|
|
|
1137
2117
|
resolved = resolveRelativeImport(absPath, specifier);
|
|
1138
2118
|
}
|
|
1139
2119
|
if (!resolved) continue;
|
|
1140
|
-
const targetId = toNodeId(srcDir, resolved);
|
|
2120
|
+
const targetId = toNodeId(srcDir, rootDir, resolved);
|
|
1141
2121
|
if (!nodeIdSet.has(targetId)) continue;
|
|
1142
2122
|
if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
|
|
1143
2123
|
const key = `${externalId}\u2192${targetId}`;
|
|
@@ -1157,23 +2137,169 @@ function generate(rootDir) {
|
|
|
1157
2137
|
nodeIdSet.add(externalId);
|
|
1158
2138
|
uiEdges.push(...edgesFromThis);
|
|
1159
2139
|
}
|
|
2140
|
+
const tablesByFile = /* @__PURE__ */ new Map();
|
|
2141
|
+
const allDbNodes = [...apiNodes, ...uiNodes];
|
|
2142
|
+
for (const node of allDbNodes) {
|
|
2143
|
+
const calls = node._dbCalls;
|
|
2144
|
+
if (!calls || calls.length === 0) continue;
|
|
2145
|
+
const map = /* @__PURE__ */ new Map();
|
|
2146
|
+
for (const c of calls) {
|
|
2147
|
+
const key = `${c.kind}:${c.model}:${c.isMutation ? "m" : "r"}`;
|
|
2148
|
+
if (!map.has(key)) {
|
|
2149
|
+
map.set(key, { model: c.model, method: c.method, isMutation: c.isMutation, kind: c.kind, via: [] });
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
tablesByFile.set(node.id, map);
|
|
2153
|
+
}
|
|
2154
|
+
const reverseRuntimeImports = /* @__PURE__ */ new Map();
|
|
2155
|
+
for (const edge of uiEdges) {
|
|
2156
|
+
if (edge.type !== "imports" && edge.type !== "renders") continue;
|
|
2157
|
+
if (!reverseRuntimeImports.has(edge.target)) {
|
|
2158
|
+
reverseRuntimeImports.set(edge.target, /* @__PURE__ */ new Set());
|
|
2159
|
+
}
|
|
2160
|
+
reverseRuntimeImports.get(edge.target).add(edge.source);
|
|
2161
|
+
}
|
|
2162
|
+
let changed = true;
|
|
2163
|
+
let iterations = 0;
|
|
2164
|
+
while (changed && iterations < 50) {
|
|
2165
|
+
changed = false;
|
|
2166
|
+
iterations++;
|
|
2167
|
+
for (const [target, tableMap] of [...tablesByFile]) {
|
|
2168
|
+
const importers = reverseRuntimeImports.get(target);
|
|
2169
|
+
if (!importers) continue;
|
|
2170
|
+
for (const importer of importers) {
|
|
2171
|
+
if (importer === target) continue;
|
|
2172
|
+
let importerMap = tablesByFile.get(importer);
|
|
2173
|
+
if (!importerMap) {
|
|
2174
|
+
importerMap = /* @__PURE__ */ new Map();
|
|
2175
|
+
tablesByFile.set(importer, importerMap);
|
|
2176
|
+
}
|
|
2177
|
+
for (const [key, call] of tableMap) {
|
|
2178
|
+
if (importerMap.has(key)) continue;
|
|
2179
|
+
importerMap.set(key, {
|
|
2180
|
+
model: call.model,
|
|
2181
|
+
method: null,
|
|
2182
|
+
isMutation: call.isMutation,
|
|
2183
|
+
kind: call.kind,
|
|
2184
|
+
via: [...call.via, target]
|
|
2185
|
+
});
|
|
2186
|
+
changed = true;
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
for (const node of apiNodes) {
|
|
2192
|
+
const map = tablesByFile.get(node.id);
|
|
2193
|
+
if (!map) continue;
|
|
2194
|
+
node.db_models = [...new Set([...map.values()].map((c) => c.model))];
|
|
2195
|
+
node.db_operations = [...new Set(
|
|
2196
|
+
[...map.values()].filter((c) => c.via.length === 0 && c.method).map((c) => `${c.model}.${c.method}`)
|
|
2197
|
+
)];
|
|
2198
|
+
node.mutates = [...map.values()].some((c) => c.isMutation);
|
|
2199
|
+
}
|
|
1160
2200
|
const apiCrossRefs = [];
|
|
1161
2201
|
for (const node of apiNodes) {
|
|
1162
|
-
const
|
|
1163
|
-
if (!
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
2202
|
+
const map = tablesByFile.get(node.id);
|
|
2203
|
+
if (!map) {
|
|
2204
|
+
delete node._dbCalls;
|
|
2205
|
+
continue;
|
|
2206
|
+
}
|
|
2207
|
+
const seenTargets = /* @__PURE__ */ new Set();
|
|
2208
|
+
for (const call of map.values()) {
|
|
2209
|
+
const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
|
|
2210
|
+
if (seenTargets.has(target)) continue;
|
|
2211
|
+
seenTargets.add(target);
|
|
2212
|
+
const isTransitive = call.via.length > 0;
|
|
2213
|
+
const ref = {
|
|
1169
2214
|
source: node.id,
|
|
1170
|
-
target
|
|
1171
|
-
type: call.isMutation ? "mutates" : "reads",
|
|
2215
|
+
target,
|
|
2216
|
+
type: call.isMutation ? isTransitive ? "mutates_via" : "mutates" : isTransitive ? "reads_via" : "reads",
|
|
1172
2217
|
layer: "db"
|
|
1173
|
-
}
|
|
2218
|
+
};
|
|
2219
|
+
if (isTransitive) ref.via = call.via;
|
|
2220
|
+
apiCrossRefs.push(ref);
|
|
1174
2221
|
}
|
|
1175
2222
|
delete node._dbCalls;
|
|
1176
2223
|
}
|
|
2224
|
+
const uiCrossRefs = [];
|
|
2225
|
+
for (const node of uiNodes) {
|
|
2226
|
+
const map = tablesByFile.get(node.id);
|
|
2227
|
+
if (!map) {
|
|
2228
|
+
if (node._dbCalls) delete node._dbCalls;
|
|
2229
|
+
continue;
|
|
2230
|
+
}
|
|
2231
|
+
const seenTargets = /* @__PURE__ */ new Set();
|
|
2232
|
+
for (const call of map.values()) {
|
|
2233
|
+
const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
|
|
2234
|
+
if (seenTargets.has(target)) continue;
|
|
2235
|
+
seenTargets.add(target);
|
|
2236
|
+
const isTransitive = call.via.length > 0;
|
|
2237
|
+
const ref = {
|
|
2238
|
+
source: node.id,
|
|
2239
|
+
target,
|
|
2240
|
+
type: call.isMutation ? isTransitive ? "mutates_via" : "mutates" : isTransitive ? "reads_via" : "reads",
|
|
2241
|
+
layer: "db"
|
|
2242
|
+
};
|
|
2243
|
+
if (isTransitive) ref.via = call.via;
|
|
2244
|
+
uiCrossRefs.push(ref);
|
|
2245
|
+
}
|
|
2246
|
+
if (node._dbCalls) delete node._dbCalls;
|
|
2247
|
+
}
|
|
2248
|
+
uiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
2249
|
+
const middlewareInfos = [];
|
|
2250
|
+
for (const conv of paths.conventionFiles) {
|
|
2251
|
+
if (!/middleware\.tsx?$/.test(conv)) continue;
|
|
2252
|
+
try {
|
|
2253
|
+
const info = extractMiddlewareAuthTS(conv);
|
|
2254
|
+
if (info && info.matchers.length > 0) middlewareInfos.push(info);
|
|
2255
|
+
} catch {
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
if (middlewareInfos.length > 0) {
|
|
2259
|
+
let setAuth2 = function(node, newTags, replaceAll) {
|
|
2260
|
+
const existing = node.auth ?? [];
|
|
2261
|
+
const meaningful = existing.filter((a) => a !== "public");
|
|
2262
|
+
const merged = replaceAll ? newTags : [.../* @__PURE__ */ new Set([...newTags, ...meaningful])];
|
|
2263
|
+
node.auth = merged.length > 0 ? merged : ["public"];
|
|
2264
|
+
}, applyMiddleware2 = function(node, routePath) {
|
|
2265
|
+
let resolved = null;
|
|
2266
|
+
let label = "";
|
|
2267
|
+
let hasAnyExemptMatcher = false;
|
|
2268
|
+
let hasAnyFallthrough = false;
|
|
2269
|
+
for (const info of middlewareInfos) {
|
|
2270
|
+
if (info.hasFallthroughProtect) hasAnyFallthrough = true;
|
|
2271
|
+
if (info.matchers.some((m) => m.intent === "exempt")) hasAnyExemptMatcher = true;
|
|
2272
|
+
const c = classifyRouteAgainstMiddleware(routePath, info);
|
|
2273
|
+
if (!c) continue;
|
|
2274
|
+
if (!resolved) {
|
|
2275
|
+
resolved = c.intent;
|
|
2276
|
+
label = c.matcher;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
if (resolved === "exempt") {
|
|
2280
|
+
setAuth2(node, ["public"], true);
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
if (resolved === "protect") {
|
|
2284
|
+
setAuth2(node, [`middleware:${label}`], false);
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
if (hasAnyExemptMatcher && hasAnyFallthrough) {
|
|
2288
|
+
setAuth2(node, ["middleware-protected"], false);
|
|
2289
|
+
}
|
|
2290
|
+
};
|
|
2291
|
+
var setAuth = setAuth2, applyMiddleware = applyMiddleware2;
|
|
2292
|
+
for (const node of apiNodes) {
|
|
2293
|
+
const routePath = node.path;
|
|
2294
|
+
if (!routePath) continue;
|
|
2295
|
+
applyMiddleware2(node, routePath);
|
|
2296
|
+
}
|
|
2297
|
+
for (const node of uiNodes) {
|
|
2298
|
+
const route = node.route;
|
|
2299
|
+
if (!route) continue;
|
|
2300
|
+
applyMiddleware2(node, route);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
1177
2303
|
const apiNodeIds = new Set(apiNodes.map((n) => n.id));
|
|
1178
2304
|
const apiEdges = [];
|
|
1179
2305
|
const uiOnlyEdges = [];
|
|
@@ -1235,7 +2361,7 @@ function generate(rootDir) {
|
|
|
1235
2361
|
},
|
|
1236
2362
|
nodes: stripLayer(uiNodes),
|
|
1237
2363
|
edges: uiOnlyEdges,
|
|
1238
|
-
cross_refs:
|
|
2364
|
+
cross_refs: uiCrossRefs,
|
|
1239
2365
|
contradictions: [],
|
|
1240
2366
|
warnings: [],
|
|
1241
2367
|
flagged_edges: dedupedFlagged,
|
|
@@ -1314,8 +2440,7 @@ var typescriptProjectParser = {
|
|
|
1314
2440
|
};
|
|
1315
2441
|
|
|
1316
2442
|
// src/server/graph/parsers/db/prisma-schema.ts
|
|
1317
|
-
var
|
|
1318
|
-
var import_node_path5 = require("node:path");
|
|
2443
|
+
var import_node_fs7 = require("node:fs");
|
|
1319
2444
|
function parseModels(content) {
|
|
1320
2445
|
const nodes = [];
|
|
1321
2446
|
const relations = [];
|
|
@@ -1406,11 +2531,25 @@ function parseEnums(content) {
|
|
|
1406
2531
|
return nodes;
|
|
1407
2532
|
}
|
|
1408
2533
|
function detect2(rootDir) {
|
|
1409
|
-
|
|
2534
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2535
|
+
return paths?.dbConfig.kind === "prisma" && (0, import_node_fs7.existsSync)(paths.dbConfig.schemaPath);
|
|
1410
2536
|
}
|
|
1411
2537
|
function generate2(rootDir) {
|
|
1412
|
-
const
|
|
1413
|
-
|
|
2538
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2539
|
+
if (paths.dbConfig.kind !== "prisma") {
|
|
2540
|
+
return {
|
|
2541
|
+
metadata: { generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), layer: "db", source: "none" },
|
|
2542
|
+
nodes: [],
|
|
2543
|
+
edges: [],
|
|
2544
|
+
cross_refs: [],
|
|
2545
|
+
contradictions: [],
|
|
2546
|
+
warnings: [],
|
|
2547
|
+
flagged_edges: [],
|
|
2548
|
+
patterns: { total_tables: 0, total_enums: 0, total_relations: 0 }
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
const schemaPath = paths.dbConfig.schemaPath;
|
|
2552
|
+
const content = (0, import_node_fs7.readFileSync)(schemaPath, "utf-8");
|
|
1414
2553
|
const { nodes: modelNodes, relations } = parseModels(content);
|
|
1415
2554
|
const enumNodes = parseEnums(content);
|
|
1416
2555
|
const allNodes = [...modelNodes, ...enumNodes];
|
|
@@ -1430,7 +2569,7 @@ function generate2(rootDir) {
|
|
|
1430
2569
|
metadata: {
|
|
1431
2570
|
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1432
2571
|
scope: "prisma-schema",
|
|
1433
|
-
source:
|
|
2572
|
+
source: schemaPath,
|
|
1434
2573
|
provider: "postgresql",
|
|
1435
2574
|
layer: "db",
|
|
1436
2575
|
total_models: modelNodes.length,
|
|
@@ -1467,8 +2606,8 @@ var prismaSchemaParser = {
|
|
|
1467
2606
|
};
|
|
1468
2607
|
|
|
1469
2608
|
// src/server/graph/parsers/db/sql-migrations.ts
|
|
1470
|
-
var
|
|
1471
|
-
var
|
|
2609
|
+
var import_node_fs8 = require("node:fs");
|
|
2610
|
+
var import_node_path7 = require("node:path");
|
|
1472
2611
|
var PG_TO_PRISMA = {
|
|
1473
2612
|
"TEXT": "String",
|
|
1474
2613
|
"VARCHAR": "String",
|
|
@@ -1499,51 +2638,149 @@ function pgTypeToPrisma(pgType) {
|
|
|
1499
2638
|
const upper = pgType.toUpperCase().trim();
|
|
1500
2639
|
return PG_TO_PRISMA[upper] ?? upper;
|
|
1501
2640
|
}
|
|
2641
|
+
var ID = `(?:"[\\w$]+"|[\\w$]+)`;
|
|
2642
|
+
var QID = `(?:${ID}\\.)?${ID}`;
|
|
2643
|
+
function bareName(captured) {
|
|
2644
|
+
const parts = captured.split(".");
|
|
2645
|
+
const last = parts[parts.length - 1];
|
|
2646
|
+
return last.replace(/^"(.*)"$/, "$1").trim();
|
|
2647
|
+
}
|
|
1502
2648
|
function parseCreateTable(sql, state) {
|
|
1503
|
-
const re =
|
|
2649
|
+
const re = new RegExp(
|
|
2650
|
+
`CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(${QID})\\s*\\(([\\s\\S]*?)\\);`,
|
|
2651
|
+
"gi"
|
|
2652
|
+
);
|
|
1504
2653
|
let m;
|
|
1505
2654
|
while ((m = re.exec(sql)) !== null) {
|
|
1506
|
-
const tableName = m[1];
|
|
2655
|
+
const tableName = bareName(m[1]);
|
|
1507
2656
|
const body = m[2];
|
|
1508
2657
|
const columns = /* @__PURE__ */ new Map();
|
|
1509
2658
|
let primaryCol = null;
|
|
1510
|
-
|
|
1511
|
-
|
|
2659
|
+
const inlineFks = [];
|
|
2660
|
+
const lines = splitTopLevelCommas(body);
|
|
2661
|
+
for (const raw of lines) {
|
|
2662
|
+
const trimmed = raw.trim().replace(/,\s*$/, "");
|
|
1512
2663
|
if (!trimmed || trimmed.startsWith("--")) continue;
|
|
1513
|
-
const
|
|
1514
|
-
if (
|
|
1515
|
-
primaryCol =
|
|
2664
|
+
const namedPk = trimmed.match(new RegExp(`^CONSTRAINT\\s+${ID}\\s+PRIMARY\\s+KEY\\s*\\(\\s*(${QID})`, "i"));
|
|
2665
|
+
if (namedPk) {
|
|
2666
|
+
primaryCol = bareName(namedPk[1]);
|
|
2667
|
+
continue;
|
|
2668
|
+
}
|
|
2669
|
+
const tablePk = trimmed.match(new RegExp(`^PRIMARY\\s+KEY\\s*\\(\\s*(${QID})`, "i"));
|
|
2670
|
+
if (tablePk) {
|
|
2671
|
+
primaryCol = bareName(tablePk[1]);
|
|
2672
|
+
continue;
|
|
2673
|
+
}
|
|
2674
|
+
if (/^UNIQUE\s*\(/i.test(trimmed)) continue;
|
|
2675
|
+
const namedFk = trimmed.match(new RegExp(
|
|
2676
|
+
`^CONSTRAINT\\s+(${ID})\\s+FOREIGN\\s+KEY\\s*\\(\\s*(${QID})\\s*\\)\\s+REFERENCES\\s+(${QID})\\s*\\(\\s*(${QID})\\s*\\)(?:\\s+ON\\s+DELETE\\s+(\\w+(?:\\s+\\w+)?))?`,
|
|
2677
|
+
"i"
|
|
2678
|
+
));
|
|
2679
|
+
if (namedFk) {
|
|
2680
|
+
inlineFks.push({
|
|
2681
|
+
constraintName: bareName(namedFk[1]),
|
|
2682
|
+
sourceTable: tableName,
|
|
2683
|
+
sourceColumn: bareName(namedFk[2]),
|
|
2684
|
+
targetTable: bareName(namedFk[3]),
|
|
2685
|
+
targetColumn: bareName(namedFk[4]),
|
|
2686
|
+
onDelete: namedFk[5] ?? null
|
|
2687
|
+
});
|
|
2688
|
+
continue;
|
|
2689
|
+
}
|
|
2690
|
+
const bareFk = trimmed.match(new RegExp(
|
|
2691
|
+
`^FOREIGN\\s+KEY\\s*\\(\\s*(${QID})\\s*\\)\\s+REFERENCES\\s+(${QID})\\s*\\(\\s*(${QID})\\s*\\)(?:\\s+ON\\s+DELETE\\s+(\\w+(?:\\s+\\w+)?))?`,
|
|
2692
|
+
"i"
|
|
2693
|
+
));
|
|
2694
|
+
if (bareFk) {
|
|
2695
|
+
inlineFks.push({
|
|
2696
|
+
constraintName: `${tableName}_${bareName(bareFk[1])}_fkey`,
|
|
2697
|
+
sourceTable: tableName,
|
|
2698
|
+
sourceColumn: bareName(bareFk[1]),
|
|
2699
|
+
targetTable: bareName(bareFk[2]),
|
|
2700
|
+
targetColumn: bareName(bareFk[3]),
|
|
2701
|
+
onDelete: bareFk[4] ?? null
|
|
2702
|
+
});
|
|
1516
2703
|
continue;
|
|
1517
2704
|
}
|
|
1518
|
-
if (
|
|
1519
|
-
const colMatch = trimmed.match(
|
|
2705
|
+
if (/^CONSTRAINT\s/i.test(trimmed)) continue;
|
|
2706
|
+
const colMatch = trimmed.match(new RegExp(`^(${ID})\\s+(.+)`, "i"));
|
|
1520
2707
|
if (!colMatch) continue;
|
|
1521
|
-
const colName = colMatch[1];
|
|
2708
|
+
const colName = bareName(colMatch[1]);
|
|
1522
2709
|
let rest = colMatch[2];
|
|
2710
|
+
const inlineRefMatch = rest.match(new RegExp(
|
|
2711
|
+
`\\bREFERENCES\\s+(${QID})\\s*\\(\\s*(${QID})\\s*\\)(?:\\s+ON\\s+DELETE\\s+(\\w+(?:\\s+\\w+)?))?`,
|
|
2712
|
+
"i"
|
|
2713
|
+
));
|
|
2714
|
+
if (inlineRefMatch) {
|
|
2715
|
+
inlineFks.push({
|
|
2716
|
+
constraintName: `${tableName}_${colName}_fkey`,
|
|
2717
|
+
sourceTable: tableName,
|
|
2718
|
+
sourceColumn: colName,
|
|
2719
|
+
targetTable: bareName(inlineRefMatch[1]),
|
|
2720
|
+
targetColumn: bareName(inlineRefMatch[2]),
|
|
2721
|
+
onDelete: inlineRefMatch[3] ?? null
|
|
2722
|
+
});
|
|
2723
|
+
rest = rest.replace(inlineRefMatch[0], "").trim();
|
|
2724
|
+
}
|
|
1523
2725
|
const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
|
|
2726
|
+
const isPrimaryKey = /\bPRIMARY\s+KEY\b/i.test(rest);
|
|
2727
|
+
const isUnique = /\bUNIQUE\b/i.test(rest);
|
|
1524
2728
|
const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)(?:\s*,?\s*$)/i);
|
|
1525
2729
|
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();
|
|
2730
|
+
let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bPRIMARY\s+KEY\b/gi, "").replace(/\bUNIQUE\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim().replace(/,\s*$/, "").trim();
|
|
1527
2731
|
columns.set(colName, {
|
|
1528
2732
|
name: colName,
|
|
1529
2733
|
type: colType,
|
|
1530
|
-
nullable: !isNotNull,
|
|
1531
|
-
primary:
|
|
1532
|
-
unique:
|
|
2734
|
+
nullable: !isNotNull && !isPrimaryKey,
|
|
2735
|
+
primary: isPrimaryKey,
|
|
2736
|
+
unique: isUnique,
|
|
1533
2737
|
default: defaultVal
|
|
1534
2738
|
});
|
|
2739
|
+
if (isPrimaryKey) primaryCol = colName;
|
|
1535
2740
|
}
|
|
1536
2741
|
if (primaryCol && columns.has(primaryCol)) {
|
|
1537
2742
|
columns.get(primaryCol).primary = true;
|
|
1538
2743
|
}
|
|
1539
2744
|
state.tables.set(tableName, { name: tableName, columns });
|
|
2745
|
+
state.fks.push(...inlineFks);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
function splitTopLevelCommas(body) {
|
|
2749
|
+
const out = [];
|
|
2750
|
+
let depth = 0;
|
|
2751
|
+
let buf = "";
|
|
2752
|
+
let inString = null;
|
|
2753
|
+
for (const ch of body) {
|
|
2754
|
+
if (inString) {
|
|
2755
|
+
buf += ch;
|
|
2756
|
+
if (ch === inString) inString = null;
|
|
2757
|
+
continue;
|
|
2758
|
+
}
|
|
2759
|
+
if (ch === "'" || ch === '"') {
|
|
2760
|
+
inString = ch;
|
|
2761
|
+
buf += ch;
|
|
2762
|
+
continue;
|
|
2763
|
+
}
|
|
2764
|
+
if (ch === "(") depth++;
|
|
2765
|
+
else if (ch === ")") depth--;
|
|
2766
|
+
if (ch === "," && depth === 0) {
|
|
2767
|
+
out.push(buf);
|
|
2768
|
+
buf = "";
|
|
2769
|
+
continue;
|
|
2770
|
+
}
|
|
2771
|
+
buf += ch;
|
|
1540
2772
|
}
|
|
2773
|
+
if (buf.trim()) out.push(buf);
|
|
2774
|
+
return out;
|
|
1541
2775
|
}
|
|
1542
2776
|
function parseCreateEnum(sql, state) {
|
|
1543
|
-
const re =
|
|
2777
|
+
const re = new RegExp(
|
|
2778
|
+
`CREATE\\s+TYPE\\s+(${QID})\\s+AS\\s+ENUM\\s*\\(([^)]+)\\)`,
|
|
2779
|
+
"gi"
|
|
2780
|
+
);
|
|
1544
2781
|
let m;
|
|
1545
2782
|
while ((m = re.exec(sql)) !== null) {
|
|
1546
|
-
const enumName = m[1];
|
|
2783
|
+
const enumName = bareName(m[1]);
|
|
1547
2784
|
const valuesStr = m[2];
|
|
1548
2785
|
const values = new Set(
|
|
1549
2786
|
valuesStr.split(",").map((v) => v.trim().replace(/^'(.*)'$/, "$1")).filter(Boolean)
|
|
@@ -1552,11 +2789,14 @@ function parseCreateEnum(sql, state) {
|
|
|
1552
2789
|
}
|
|
1553
2790
|
}
|
|
1554
2791
|
function parseAlterTable(sql, state) {
|
|
1555
|
-
const addColRe =
|
|
2792
|
+
const addColRe = new RegExp(
|
|
2793
|
+
`ALTER\\s+TABLE\\s+(${QID})\\s+ADD\\s+COLUMN\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(${QID})\\s+(.+?);`,
|
|
2794
|
+
"gi"
|
|
2795
|
+
);
|
|
1556
2796
|
let m;
|
|
1557
2797
|
while ((m = addColRe.exec(sql)) !== null) {
|
|
1558
|
-
const tableName = m[1];
|
|
1559
|
-
const colName = m[2];
|
|
2798
|
+
const tableName = bareName(m[1]);
|
|
2799
|
+
const colName = bareName(m[2]);
|
|
1560
2800
|
let rest = m[3];
|
|
1561
2801
|
const table = state.tables.get(tableName);
|
|
1562
2802
|
if (!table) continue;
|
|
@@ -1573,64 +2813,92 @@ function parseAlterTable(sql, state) {
|
|
|
1573
2813
|
default: defaultVal
|
|
1574
2814
|
});
|
|
1575
2815
|
}
|
|
1576
|
-
const dropColRe =
|
|
2816
|
+
const dropColRe = new RegExp(
|
|
2817
|
+
`ALTER\\s+TABLE\\s+(${QID})\\s+DROP\\s+COLUMN\\s+(?:IF\\s+EXISTS\\s+)?(${QID})`,
|
|
2818
|
+
"gi"
|
|
2819
|
+
);
|
|
1577
2820
|
while ((m = dropColRe.exec(sql)) !== null) {
|
|
1578
|
-
const table = state.tables.get(m[1]);
|
|
1579
|
-
if (table) table.columns.delete(m[2]);
|
|
2821
|
+
const table = state.tables.get(bareName(m[1]));
|
|
2822
|
+
if (table) table.columns.delete(bareName(m[2]));
|
|
1580
2823
|
}
|
|
1581
|
-
const fkRe =
|
|
2824
|
+
const fkRe = new RegExp(
|
|
2825
|
+
`ALTER\\s+TABLE\\s+(${QID})\\s+ADD\\s+CONSTRAINT\\s+(${ID})\\s+FOREIGN\\s+KEY\\s*\\(\\s*(${QID})\\s*\\)\\s+REFERENCES\\s+(${QID})\\s*\\(\\s*(${QID})\\s*\\)(?:\\s+ON\\s+DELETE\\s+(\\w+(?:\\s+\\w+)?))?`,
|
|
2826
|
+
"gi"
|
|
2827
|
+
);
|
|
1582
2828
|
while ((m = fkRe.exec(sql)) !== null) {
|
|
1583
2829
|
state.fks.push({
|
|
1584
|
-
constraintName: m[2],
|
|
1585
|
-
sourceTable: m[1],
|
|
1586
|
-
sourceColumn: m[3],
|
|
1587
|
-
targetTable: m[4],
|
|
1588
|
-
targetColumn: m[5],
|
|
2830
|
+
constraintName: bareName(m[2]),
|
|
2831
|
+
sourceTable: bareName(m[1]),
|
|
2832
|
+
sourceColumn: bareName(m[3]),
|
|
2833
|
+
targetTable: bareName(m[4]),
|
|
2834
|
+
targetColumn: bareName(m[5]),
|
|
1589
2835
|
onDelete: m[6] ?? null
|
|
1590
2836
|
});
|
|
1591
2837
|
}
|
|
1592
2838
|
}
|
|
1593
2839
|
function parseAlterEnum(sql, state) {
|
|
1594
|
-
const re =
|
|
2840
|
+
const re = new RegExp(
|
|
2841
|
+
`ALTER\\s+TYPE\\s+(${QID})\\s+ADD\\s+VALUE\\s+'([^']+)'`,
|
|
2842
|
+
"gi"
|
|
2843
|
+
);
|
|
1595
2844
|
let m;
|
|
1596
2845
|
while ((m = re.exec(sql)) !== null) {
|
|
1597
|
-
const en = state.enums.get(m[1]);
|
|
2846
|
+
const en = state.enums.get(bareName(m[1]));
|
|
1598
2847
|
if (en) en.values.add(m[2]);
|
|
1599
2848
|
}
|
|
1600
2849
|
}
|
|
1601
2850
|
function parseDropTable(sql, state) {
|
|
1602
|
-
const re =
|
|
2851
|
+
const re = new RegExp(
|
|
2852
|
+
`DROP\\s+TABLE\\s+(?:IF\\s+EXISTS\\s+)?(${QID})`,
|
|
2853
|
+
"gi"
|
|
2854
|
+
);
|
|
1603
2855
|
let m;
|
|
1604
2856
|
while ((m = re.exec(sql)) !== null) {
|
|
1605
|
-
|
|
1606
|
-
state.
|
|
2857
|
+
const dropped = bareName(m[1]);
|
|
2858
|
+
state.tables.delete(dropped);
|
|
2859
|
+
state.fks = state.fks.filter((fk) => fk.sourceTable !== dropped && fk.targetTable !== dropped);
|
|
1607
2860
|
}
|
|
1608
2861
|
}
|
|
1609
2862
|
function parseUniqueIndex(sql, state) {
|
|
1610
|
-
const re =
|
|
2863
|
+
const re = new RegExp(
|
|
2864
|
+
`CREATE\\s+UNIQUE\\s+INDEX\\s+(?:(?:IF\\s+NOT\\s+EXISTS\\s+)?(?:${ID}\\s+)?)?ON\\s+(${QID})\\s*\\(\\s*(${QID})\\s*\\)`,
|
|
2865
|
+
"gi"
|
|
2866
|
+
);
|
|
1611
2867
|
let m;
|
|
1612
2868
|
while ((m = re.exec(sql)) !== null) {
|
|
1613
|
-
const
|
|
1614
|
-
const
|
|
2869
|
+
const tableName = bareName(m[1]);
|
|
2870
|
+
const colName = bareName(m[2]);
|
|
2871
|
+
const table = state.tables.get(tableName);
|
|
2872
|
+
const col = table?.columns.get(colName);
|
|
1615
2873
|
if (col) col.unique = true;
|
|
1616
|
-
if (!state.uniqueIndexes.has(
|
|
1617
|
-
state.uniqueIndexes.get(
|
|
2874
|
+
if (!state.uniqueIndexes.has(tableName)) state.uniqueIndexes.set(tableName, /* @__PURE__ */ new Set());
|
|
2875
|
+
state.uniqueIndexes.get(tableName).add(colName);
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
function discoverMigrationFiles(migrationsDir) {
|
|
2879
|
+
if (!(0, import_node_fs8.existsSync)(migrationsDir)) return [];
|
|
2880
|
+
const out = [];
|
|
2881
|
+
const entries = (0, import_node_fs8.readdirSync)(migrationsDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
2882
|
+
for (const entry of entries) {
|
|
2883
|
+
if (entry.isDirectory()) {
|
|
2884
|
+
const sqlPath = (0, import_node_path7.join)(migrationsDir, entry.name, "migration.sql");
|
|
2885
|
+
if ((0, import_node_fs8.existsSync)(sqlPath)) out.push(sqlPath);
|
|
2886
|
+
} else if (entry.isFile() && entry.name.endsWith(".sql")) {
|
|
2887
|
+
out.push((0, import_node_path7.join)(migrationsDir, entry.name));
|
|
2888
|
+
}
|
|
1618
2889
|
}
|
|
2890
|
+
return out;
|
|
1619
2891
|
}
|
|
1620
|
-
function parseMigrations(
|
|
1621
|
-
const migrationsDir = (0, import_node_path6.join)(rootDir, "prisma", "migrations");
|
|
2892
|
+
function parseMigrations(migrationsDir) {
|
|
1622
2893
|
const state = {
|
|
1623
2894
|
tables: /* @__PURE__ */ new Map(),
|
|
1624
2895
|
enums: /* @__PURE__ */ new Map(),
|
|
1625
2896
|
fks: [],
|
|
1626
2897
|
uniqueIndexes: /* @__PURE__ */ new Map()
|
|
1627
2898
|
};
|
|
1628
|
-
if (!
|
|
1629
|
-
const
|
|
1630
|
-
|
|
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");
|
|
2899
|
+
if (!migrationsDir) return state;
|
|
2900
|
+
for (const sqlPath of discoverMigrationFiles(migrationsDir)) {
|
|
2901
|
+
const sql = (0, import_node_fs8.readFileSync)(sqlPath, "utf-8");
|
|
1634
2902
|
parseCreateEnum(sql, state);
|
|
1635
2903
|
parseCreateTable(sql, state);
|
|
1636
2904
|
parseAlterTable(sql, state);
|
|
@@ -1640,10 +2908,9 @@ function parseMigrations(rootDir) {
|
|
|
1640
2908
|
}
|
|
1641
2909
|
return state;
|
|
1642
2910
|
}
|
|
1643
|
-
function loadPrismaState(
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
const content = (0, import_node_fs6.readFileSync)(schemaPath, "utf-8");
|
|
2911
|
+
function loadPrismaState(schemaPath) {
|
|
2912
|
+
if (!schemaPath || !(0, import_node_fs8.existsSync)(schemaPath)) return null;
|
|
2913
|
+
const content = (0, import_node_fs8.readFileSync)(schemaPath, "utf-8");
|
|
1647
2914
|
const tables = /* @__PURE__ */ new Map();
|
|
1648
2915
|
const enums = /* @__PURE__ */ new Map();
|
|
1649
2916
|
const relations = [];
|
|
@@ -1807,14 +3074,28 @@ function verify(sqlState, prisma) {
|
|
|
1807
3074
|
}
|
|
1808
3075
|
return { contradictions, flaggedEdges };
|
|
1809
3076
|
}
|
|
3077
|
+
function migrationsDirFor(rootDir) {
|
|
3078
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
3079
|
+
if (!paths) return null;
|
|
3080
|
+
if (paths.dbConfig.kind === "prisma" || paths.dbConfig.kind === "sql-migrations") {
|
|
3081
|
+
return paths.dbConfig.migrationsDir;
|
|
3082
|
+
}
|
|
3083
|
+
return null;
|
|
3084
|
+
}
|
|
3085
|
+
function schemaPathFor(rootDir) {
|
|
3086
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
3087
|
+
if (!paths) return null;
|
|
3088
|
+
return paths.dbConfig.kind === "prisma" ? paths.dbConfig.schemaPath : null;
|
|
3089
|
+
}
|
|
1810
3090
|
function detect3(rootDir) {
|
|
1811
|
-
const
|
|
1812
|
-
if (!
|
|
1813
|
-
return (
|
|
3091
|
+
const dir = migrationsDirFor(rootDir);
|
|
3092
|
+
if (!dir) return false;
|
|
3093
|
+
return discoverMigrationFiles(dir).length > 0;
|
|
1814
3094
|
}
|
|
1815
3095
|
function generate3(rootDir) {
|
|
1816
|
-
const
|
|
1817
|
-
const
|
|
3096
|
+
const migrationsDir = migrationsDirFor(rootDir);
|
|
3097
|
+
const sqlState = parseMigrations(migrationsDir);
|
|
3098
|
+
const prisma = loadPrismaState(schemaPathFor(rootDir));
|
|
1818
3099
|
const prismaTableIds = prisma ? new Set(prisma.tables.keys()) : /* @__PURE__ */ new Set();
|
|
1819
3100
|
const prismaEnumIds = prisma ? new Set(prisma.enums.keys()) : /* @__PURE__ */ new Set();
|
|
1820
3101
|
const nodes = [];
|
|
@@ -1860,7 +3141,7 @@ function generate3(rootDir) {
|
|
|
1860
3141
|
metadata: {
|
|
1861
3142
|
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1862
3143
|
scope: "sql-migrations",
|
|
1863
|
-
source: "
|
|
3144
|
+
source: migrationsDir ?? "none",
|
|
1864
3145
|
layer: "db",
|
|
1865
3146
|
sql_tables: sqlState.tables.size,
|
|
1866
3147
|
sql_enums: sqlState.enums.size,
|
|
@@ -1885,15 +3166,16 @@ var sqlMigrationsParser = {
|
|
|
1885
3166
|
};
|
|
1886
3167
|
|
|
1887
3168
|
// src/server/graph/core/api-route-matching.ts
|
|
3169
|
+
init_launch_kit_paths();
|
|
1888
3170
|
function loadApiRoutesFromOutput(apiOutput) {
|
|
1889
3171
|
const routes = [];
|
|
1890
3172
|
for (const n of apiOutput.nodes) {
|
|
1891
|
-
const
|
|
1892
|
-
if (!
|
|
3173
|
+
const path = n.path;
|
|
3174
|
+
if (!path || typeof path !== "string") continue;
|
|
1893
3175
|
routes.push({
|
|
1894
|
-
path
|
|
3176
|
+
path,
|
|
1895
3177
|
nodeId: n.id,
|
|
1896
|
-
segments:
|
|
3178
|
+
segments: path.split("/").filter(Boolean)
|
|
1897
3179
|
});
|
|
1898
3180
|
}
|
|
1899
3181
|
return routes;
|
|
@@ -1902,6 +3184,10 @@ function buildApiPathMap(routes) {
|
|
|
1902
3184
|
const map = /* @__PURE__ */ new Map();
|
|
1903
3185
|
for (const r of routes) {
|
|
1904
3186
|
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
3187
|
+
const trimmed = r.path.replace(/\/\*[^/]+\?$/, "");
|
|
3188
|
+
if (trimmed && trimmed !== r.path && !map.has(trimmed)) {
|
|
3189
|
+
map.set(trimmed, r.nodeId);
|
|
3190
|
+
}
|
|
1905
3191
|
}
|
|
1906
3192
|
return map;
|
|
1907
3193
|
}
|
|
@@ -1924,26 +3210,54 @@ function normalizeFetchUrl(raw) {
|
|
|
1924
3210
|
return { path: s || "/", hadInterpolation };
|
|
1925
3211
|
}
|
|
1926
3212
|
function scoreApiRouteMatch(candidate, known) {
|
|
1927
|
-
if (candidate.length !== known.length) return -1;
|
|
1928
3213
|
let score = 0;
|
|
1929
|
-
|
|
3214
|
+
let i = 0, j = 0;
|
|
3215
|
+
while (i < candidate.length && j < known.length) {
|
|
1930
3216
|
const a = candidate[i];
|
|
1931
|
-
const b = known[
|
|
3217
|
+
const b = known[j];
|
|
3218
|
+
if (b.startsWith("*") && b.endsWith("?")) {
|
|
3219
|
+
score += 1;
|
|
3220
|
+
return score;
|
|
3221
|
+
}
|
|
3222
|
+
if (b.startsWith("*")) {
|
|
3223
|
+
const remaining = candidate.length - i;
|
|
3224
|
+
if (remaining < 1) return -1;
|
|
3225
|
+
score += 1 + remaining;
|
|
3226
|
+
return score;
|
|
3227
|
+
}
|
|
1932
3228
|
if (a === b) {
|
|
1933
3229
|
score += 3;
|
|
3230
|
+
i++;
|
|
3231
|
+
j++;
|
|
1934
3232
|
continue;
|
|
1935
3233
|
}
|
|
1936
3234
|
if (a.startsWith(":") && b.startsWith(":")) {
|
|
1937
3235
|
score += 2;
|
|
3236
|
+
i++;
|
|
3237
|
+
j++;
|
|
1938
3238
|
continue;
|
|
1939
3239
|
}
|
|
1940
3240
|
if (a.startsWith(":") || b.startsWith(":")) {
|
|
1941
3241
|
score += 1;
|
|
3242
|
+
i++;
|
|
3243
|
+
j++;
|
|
1942
3244
|
continue;
|
|
1943
3245
|
}
|
|
1944
3246
|
return -1;
|
|
1945
3247
|
}
|
|
1946
|
-
|
|
3248
|
+
if (i === candidate.length) {
|
|
3249
|
+
while (j < known.length) {
|
|
3250
|
+
const b = known[j];
|
|
3251
|
+
if (b.startsWith("*") && b.endsWith("?")) {
|
|
3252
|
+
score += 1;
|
|
3253
|
+
j++;
|
|
3254
|
+
continue;
|
|
3255
|
+
}
|
|
3256
|
+
return -1;
|
|
3257
|
+
}
|
|
3258
|
+
return score;
|
|
3259
|
+
}
|
|
3260
|
+
return -1;
|
|
1947
3261
|
}
|
|
1948
3262
|
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
1949
3263
|
const raw = call.url;
|
|
@@ -1953,16 +3267,16 @@ function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
|
1953
3267
|
if (call.isConcat) {
|
|
1954
3268
|
return { kind: "dynamic", normalizedUrl: raw };
|
|
1955
3269
|
}
|
|
1956
|
-
const { path
|
|
1957
|
-
if (!
|
|
1958
|
-
return { kind: "unresolved", normalizedUrl:
|
|
3270
|
+
const { path, hadInterpolation } = normalizeFetchUrl(raw);
|
|
3271
|
+
if (!path.startsWith("/")) {
|
|
3272
|
+
return { kind: "unresolved", normalizedUrl: path };
|
|
1959
3273
|
}
|
|
1960
|
-
const segs =
|
|
3274
|
+
const segs = path.split("/").filter(Boolean);
|
|
1961
3275
|
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
1962
|
-
return { kind: "dynamic", normalizedUrl:
|
|
3276
|
+
return { kind: "dynamic", normalizedUrl: path };
|
|
1963
3277
|
}
|
|
1964
|
-
const exact = apiPathMap.get(
|
|
1965
|
-
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl:
|
|
3278
|
+
const exact = apiPathMap.get(path);
|
|
3279
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path };
|
|
1966
3280
|
let bestScore = -1;
|
|
1967
3281
|
let bestId = null;
|
|
1968
3282
|
for (const r of apiRoutes) {
|
|
@@ -1973,21 +3287,21 @@ function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
|
1973
3287
|
}
|
|
1974
3288
|
}
|
|
1975
3289
|
if (bestId && bestScore > 0) {
|
|
1976
|
-
return { kind: "resolved", nodeId: bestId, normalizedUrl:
|
|
3290
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path };
|
|
1977
3291
|
}
|
|
1978
|
-
return { kind: "unresolved", normalizedUrl:
|
|
3292
|
+
return { kind: "unresolved", normalizedUrl: path };
|
|
1979
3293
|
}
|
|
1980
3294
|
function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
|
|
1981
|
-
const { path
|
|
1982
|
-
if (!
|
|
1983
|
-
return { kind: "unresolved", normalizedUrl:
|
|
3295
|
+
const { path, hadInterpolation } = normalizeFetchUrl(urlPath);
|
|
3296
|
+
if (!path.startsWith("/")) {
|
|
3297
|
+
return { kind: "unresolved", normalizedUrl: path };
|
|
1984
3298
|
}
|
|
1985
|
-
const segs =
|
|
3299
|
+
const segs = path.split("/").filter(Boolean);
|
|
1986
3300
|
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
1987
|
-
return { kind: "dynamic", normalizedUrl:
|
|
3301
|
+
return { kind: "dynamic", normalizedUrl: path };
|
|
1988
3302
|
}
|
|
1989
|
-
const exact = apiPathMap.get(
|
|
1990
|
-
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl:
|
|
3303
|
+
const exact = apiPathMap.get(path);
|
|
3304
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path };
|
|
1991
3305
|
let bestScore = -1;
|
|
1992
3306
|
let bestId = null;
|
|
1993
3307
|
for (const r of apiRoutes) {
|
|
@@ -1998,9 +3312,9 @@ function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
|
|
|
1998
3312
|
}
|
|
1999
3313
|
}
|
|
2000
3314
|
if (bestId && bestScore > 0) {
|
|
2001
|
-
return { kind: "resolved", nodeId: bestId, normalizedUrl:
|
|
3315
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path };
|
|
2002
3316
|
}
|
|
2003
|
-
return { kind: "unresolved", normalizedUrl:
|
|
3317
|
+
return { kind: "unresolved", normalizedUrl: path };
|
|
2004
3318
|
}
|
|
2005
3319
|
|
|
2006
3320
|
// src/server/graph/parsers/crosslayer/fetch-resolver.ts
|
|
@@ -2098,50 +3412,59 @@ var fetchResolverParser = {
|
|
|
2098
3412
|
};
|
|
2099
3413
|
|
|
2100
3414
|
// src/server/graph/parsers/crosslayer/api-annotations.ts
|
|
2101
|
-
var
|
|
2102
|
-
var
|
|
3415
|
+
var import_node_fs9 = require("node:fs");
|
|
3416
|
+
var import_node_path8 = require("node:path");
|
|
2103
3417
|
var API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
|
|
2104
|
-
function
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
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
|
-
}
|
|
3418
|
+
function toNodeId2(srcDir, rootDir, absPath) {
|
|
3419
|
+
const relFromSrc = (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
3420
|
+
if (relFromSrc.startsWith("..")) {
|
|
3421
|
+
return (0, import_node_path8.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
2115
3422
|
}
|
|
2116
|
-
return
|
|
2117
|
-
}
|
|
2118
|
-
function toNodeId2(srcDir, absPath) {
|
|
2119
|
-
return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
3423
|
+
return relFromSrc;
|
|
2120
3424
|
}
|
|
2121
3425
|
var apiAnnotationsParser = {
|
|
2122
3426
|
id: "api-annotations",
|
|
2123
3427
|
layer: "crosslayer",
|
|
2124
3428
|
concern: "api-binding",
|
|
2125
3429
|
detect(rootDir) {
|
|
2126
|
-
return (
|
|
3430
|
+
return resolveProjectPaths(rootDir, loadConfig(rootDir)) !== null;
|
|
2127
3431
|
},
|
|
2128
3432
|
generate(rootDir, layerOutputs) {
|
|
2129
3433
|
const apiOutput = layerOutputs.get("api");
|
|
2130
3434
|
if (!apiOutput) {
|
|
2131
3435
|
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2132
3436
|
}
|
|
3437
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
3438
|
+
if (!paths) {
|
|
3439
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
3440
|
+
}
|
|
2133
3441
|
const uiOutput = layerOutputs.get("ui");
|
|
2134
3442
|
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
2135
3443
|
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
2136
3444
|
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
2137
|
-
const srcDir =
|
|
2138
|
-
const
|
|
3445
|
+
const srcDir = paths.srcDir;
|
|
3446
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3447
|
+
const files = [];
|
|
3448
|
+
for (const root of paths.srcRoots) {
|
|
3449
|
+
for (const f of walk(root, [".ts", ".tsx"])) {
|
|
3450
|
+
if (!seen.has(f)) {
|
|
3451
|
+
seen.add(f);
|
|
3452
|
+
files.push(f);
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
for (const conv of paths.conventionFiles) {
|
|
3457
|
+
if (!seen.has(conv)) {
|
|
3458
|
+
seen.add(conv);
|
|
3459
|
+
files.push(conv);
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
2139
3462
|
const crossRefs = [];
|
|
2140
3463
|
const flaggedEdges = [];
|
|
2141
|
-
const
|
|
3464
|
+
const seenEdge = /* @__PURE__ */ new Set();
|
|
2142
3465
|
for (const absPath of files) {
|
|
2143
|
-
const content = (0,
|
|
2144
|
-
const sourceId = toNodeId2(srcDir, absPath);
|
|
3466
|
+
const content = (0, import_node_fs9.readFileSync)(absPath, "utf-8");
|
|
3467
|
+
const sourceId = toNodeId2(srcDir, rootDir, absPath);
|
|
2145
3468
|
if (!uiNodeIds.has(sourceId)) continue;
|
|
2146
3469
|
let match;
|
|
2147
3470
|
API_ANNOTATION_RE.lastIndex = 0;
|
|
@@ -2151,8 +3474,8 @@ var apiAnnotationsParser = {
|
|
|
2151
3474
|
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
2152
3475
|
if (result.kind === "resolved" && result.nodeId) {
|
|
2153
3476
|
const key = `${sourceId}|${result.nodeId}|calls_api`;
|
|
2154
|
-
if (
|
|
2155
|
-
|
|
3477
|
+
if (seenEdge.has(key)) continue;
|
|
3478
|
+
seenEdge.add(key);
|
|
2156
3479
|
crossRefs.push({
|
|
2157
3480
|
source: sourceId,
|
|
2158
3481
|
target: result.nodeId,
|
|
@@ -2184,26 +3507,15 @@ var apiAnnotationsParser = {
|
|
|
2184
3507
|
};
|
|
2185
3508
|
|
|
2186
3509
|
// src/server/graph/parsers/crosslayer/url-literal-scanner.ts
|
|
2187
|
-
var
|
|
2188
|
-
var
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
if (
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
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, "/");
|
|
3510
|
+
var import_node_fs10 = require("node:fs");
|
|
3511
|
+
var import_node_path9 = require("node:path");
|
|
3512
|
+
var URL_LITERAL_RE = /['"`](\/[a-zA-Z][^'"`\s]*?)['"`]/g;
|
|
3513
|
+
function toNodeId3(srcDir, rootDir, absPath) {
|
|
3514
|
+
const relFromSrc = (0, import_node_path9.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
3515
|
+
if (relFromSrc.startsWith("..")) {
|
|
3516
|
+
return (0, import_node_path9.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
3517
|
+
}
|
|
3518
|
+
return relFromSrc;
|
|
2207
3519
|
}
|
|
2208
3520
|
var urlLiteralScannerParser = {
|
|
2209
3521
|
id: "url-literal-scanner",
|
|
@@ -2224,17 +3536,28 @@ var urlLiteralScannerParser = {
|
|
|
2224
3536
|
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
2225
3537
|
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2226
3538
|
const srcDir = paths.srcDir;
|
|
2227
|
-
const
|
|
2228
|
-
const files = [
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
3539
|
+
const seenFile = /* @__PURE__ */ new Set();
|
|
3540
|
+
const files = [];
|
|
3541
|
+
for (const root of paths.srcRoots) {
|
|
3542
|
+
for (const f of walk(root, [".ts", ".tsx"])) {
|
|
3543
|
+
if (!seenFile.has(f)) {
|
|
3544
|
+
seenFile.add(f);
|
|
3545
|
+
files.push(f);
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
for (const conv of paths.conventionFiles) {
|
|
3550
|
+
if (!seenFile.has(conv)) {
|
|
3551
|
+
seenFile.add(conv);
|
|
3552
|
+
files.push(conv);
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
2232
3555
|
const crossRefs = [];
|
|
2233
3556
|
const seen = /* @__PURE__ */ new Set();
|
|
2234
3557
|
for (const absPath of files) {
|
|
2235
|
-
const sourceId = toNodeId3(srcDir, absPath);
|
|
3558
|
+
const sourceId = toNodeId3(srcDir, rootDir, absPath);
|
|
2236
3559
|
if (!uiNodeIds.has(sourceId)) continue;
|
|
2237
|
-
const content = (0,
|
|
3560
|
+
const content = (0, import_node_fs10.readFileSync)(absPath, "utf-8");
|
|
2238
3561
|
let match;
|
|
2239
3562
|
URL_LITERAL_RE.lastIndex = 0;
|
|
2240
3563
|
while ((match = URL_LITERAL_RE.exec(content)) !== null) {
|
|
@@ -2265,8 +3588,8 @@ var urlLiteralScannerParser = {
|
|
|
2265
3588
|
};
|
|
2266
3589
|
|
|
2267
3590
|
// src/server/graph/parsers/static/static-values.ts
|
|
2268
|
-
var
|
|
2269
|
-
var
|
|
3591
|
+
var import_node_fs11 = require("node:fs");
|
|
3592
|
+
var import_node_path10 = require("node:path");
|
|
2270
3593
|
var parseCode = null;
|
|
2271
3594
|
function tryLoadTreeSitter() {
|
|
2272
3595
|
if (parseCode) return true;
|
|
@@ -2298,20 +3621,25 @@ function classifyScope(source, model) {
|
|
|
2298
3621
|
function extractEnumValues(rootDir) {
|
|
2299
3622
|
const nodes = [];
|
|
2300
3623
|
const edges = [];
|
|
2301
|
-
const
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
3624
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
3625
|
+
const schemaPaths = [];
|
|
3626
|
+
if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath) {
|
|
3627
|
+
schemaPaths.push(paths.dbConfig.schemaPath);
|
|
3628
|
+
schemaPaths.push((0, import_node_path10.join)((0, import_node_path10.dirname)(paths.dbConfig.schemaPath), "schema"));
|
|
3629
|
+
} else {
|
|
3630
|
+
schemaPaths.push((0, import_node_path10.join)(rootDir, "prisma", "schema.prisma"));
|
|
3631
|
+
schemaPaths.push((0, import_node_path10.join)(rootDir, "prisma", "schema"));
|
|
3632
|
+
}
|
|
2305
3633
|
let content = "";
|
|
2306
3634
|
for (const p of schemaPaths) {
|
|
2307
|
-
if ((0,
|
|
3635
|
+
if ((0, import_node_fs11.existsSync)(p)) {
|
|
2308
3636
|
try {
|
|
2309
|
-
const stat = (0,
|
|
3637
|
+
const stat = (0, import_node_fs11.statSync)(p);
|
|
2310
3638
|
if (stat.isFile()) {
|
|
2311
|
-
content = (0,
|
|
3639
|
+
content = (0, import_node_fs11.readFileSync)(p, "utf-8");
|
|
2312
3640
|
} else if (stat.isDirectory()) {
|
|
2313
|
-
const files = (0,
|
|
2314
|
-
content = files.map((f) => (0,
|
|
3641
|
+
const files = (0, import_node_fs11.readdirSync)(p).filter((f) => f.endsWith(".prisma"));
|
|
3642
|
+
content = files.map((f) => (0, import_node_fs11.readFileSync)((0, import_node_path10.join)(p, f), "utf-8")).join("\n");
|
|
2315
3643
|
}
|
|
2316
3644
|
} catch {
|
|
2317
3645
|
continue;
|
|
@@ -2385,7 +3713,7 @@ function extractStringArrayFromNode(node) {
|
|
|
2385
3713
|
return values;
|
|
2386
3714
|
}
|
|
2387
3715
|
function findArrayDecl(root, varName) {
|
|
2388
|
-
function
|
|
3716
|
+
function walk2(node) {
|
|
2389
3717
|
if (node.type === "variable_declarator") {
|
|
2390
3718
|
const nameNode = node.childForFieldName("name");
|
|
2391
3719
|
const valueNode = node.childForFieldName("value");
|
|
@@ -2398,12 +3726,12 @@ function findArrayDecl(root, varName) {
|
|
|
2398
3726
|
}
|
|
2399
3727
|
}
|
|
2400
3728
|
for (const child of node.namedChildren) {
|
|
2401
|
-
const found =
|
|
3729
|
+
const found = walk2(child);
|
|
2402
3730
|
if (found) return found;
|
|
2403
3731
|
}
|
|
2404
3732
|
return null;
|
|
2405
3733
|
}
|
|
2406
|
-
return
|
|
3734
|
+
return walk2(root);
|
|
2407
3735
|
}
|
|
2408
3736
|
function extractObjectPropsRegex(objStr) {
|
|
2409
3737
|
const props = {};
|
|
@@ -2466,15 +3794,30 @@ function modelToNodeType(model) {
|
|
|
2466
3794
|
function extractSeedData(rootDir) {
|
|
2467
3795
|
const nodes = [];
|
|
2468
3796
|
const edges = [];
|
|
2469
|
-
const
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
(0,
|
|
2473
|
-
|
|
3797
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
3798
|
+
const candidates = [];
|
|
3799
|
+
if (paths?.dbDir) {
|
|
3800
|
+
candidates.push((0, import_node_path10.join)(paths.dbDir, "seed.ts"));
|
|
3801
|
+
candidates.push((0, import_node_path10.join)(paths.dbDir, "seed.js"));
|
|
3802
|
+
} else {
|
|
3803
|
+
candidates.push((0, import_node_path10.join)(rootDir, "prisma", "seed.ts"));
|
|
3804
|
+
candidates.push((0, import_node_path10.join)(rootDir, "prisma", "seed.js"));
|
|
3805
|
+
}
|
|
3806
|
+
const baseRoots = paths?.srcRoots ?? [(0, import_node_path10.join)(rootDir, "src")];
|
|
3807
|
+
for (const root of baseRoots) {
|
|
3808
|
+
candidates.push((0, import_node_path10.join)(root, "server", "lib", "system-tags.ts"));
|
|
3809
|
+
}
|
|
3810
|
+
const seedFiles = candidates.filter((p) => {
|
|
3811
|
+
try {
|
|
3812
|
+
return (0, import_node_fs11.existsSync)(p) && (0, import_node_fs11.statSync)(p).isFile();
|
|
3813
|
+
} catch {
|
|
3814
|
+
return false;
|
|
3815
|
+
}
|
|
3816
|
+
});
|
|
2474
3817
|
const useTreeSitter = tryLoadTreeSitter();
|
|
2475
3818
|
for (const filePath of seedFiles) {
|
|
2476
|
-
const content = (0,
|
|
2477
|
-
const relPath = (0,
|
|
3819
|
+
const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
|
|
3820
|
+
const relPath = (0, import_node_path10.relative)(rootDir, filePath);
|
|
2478
3821
|
const seeded = detectSeededArrays(content, relPath);
|
|
2479
3822
|
let astRoot = null;
|
|
2480
3823
|
if (useTreeSitter && parseCode) {
|
|
@@ -2568,11 +3911,11 @@ function extractSeedData(rootDir) {
|
|
|
2568
3911
|
return { nodes, edges };
|
|
2569
3912
|
}
|
|
2570
3913
|
function walkDir(dir, exts) {
|
|
2571
|
-
if (!(0,
|
|
3914
|
+
if (!(0, import_node_fs11.existsSync)(dir)) return [];
|
|
2572
3915
|
const results = [];
|
|
2573
|
-
for (const entry of (0,
|
|
3916
|
+
for (const entry of (0, import_node_fs11.readdirSync)(dir, { withFileTypes: true })) {
|
|
2574
3917
|
if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
|
|
2575
|
-
const full = (0,
|
|
3918
|
+
const full = (0, import_node_path10.join)(dir, entry.name);
|
|
2576
3919
|
if (entry.isDirectory()) results.push(...walkDir(full, exts));
|
|
2577
3920
|
else if (exts.some((ext) => entry.name.endsWith(ext))) results.push(full);
|
|
2578
3921
|
}
|
|
@@ -2580,11 +3923,22 @@ function walkDir(dir, exts) {
|
|
|
2580
3923
|
}
|
|
2581
3924
|
function extractConstants(rootDir) {
|
|
2582
3925
|
const nodes = [];
|
|
2583
|
-
const
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
3926
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
3927
|
+
const roots = paths?.srcRoots ?? [(0, import_node_path10.join)(rootDir, "src")];
|
|
3928
|
+
const seenFile = /* @__PURE__ */ new Set();
|
|
3929
|
+
const allFiles = [];
|
|
3930
|
+
for (const root of roots) {
|
|
3931
|
+
for (const f of walkDir(root, [".ts", ".tsx"])) {
|
|
3932
|
+
if (!seenFile.has(f)) {
|
|
3933
|
+
seenFile.add(f);
|
|
3934
|
+
allFiles.push(f);
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
if (allFiles.length === 0) return { nodes };
|
|
3939
|
+
for (const filePath of allFiles) {
|
|
3940
|
+
const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
|
|
3941
|
+
const relPath = (0, import_node_path10.relative)(rootDir, filePath);
|
|
2588
3942
|
const constArrayRe = /export\s+const\s+([A-Z][A-Z_0-9]+)\s*(?::[^=]+)?\s*=\s*\[/g;
|
|
2589
3943
|
let cm;
|
|
2590
3944
|
while ((cm = constArrayRe.exec(content)) !== null) {
|
|
@@ -2617,7 +3971,14 @@ function extractConstants(rootDir) {
|
|
|
2617
3971
|
return { nodes };
|
|
2618
3972
|
}
|
|
2619
3973
|
function detect4(rootDir) {
|
|
2620
|
-
|
|
3974
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
3975
|
+
if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath && (0, import_node_fs11.existsSync)(paths.dbConfig.schemaPath)) {
|
|
3976
|
+
return true;
|
|
3977
|
+
}
|
|
3978
|
+
if (paths?.dbDir) {
|
|
3979
|
+
if ((0, import_node_fs11.existsSync)((0, import_node_path10.join)(paths.dbDir, "seed.ts")) || (0, import_node_fs11.existsSync)((0, import_node_path10.join)(paths.dbDir, "seed.js"))) return true;
|
|
3980
|
+
}
|
|
3981
|
+
return (0, import_node_fs11.existsSync)((0, import_node_path10.join)(rootDir, "prisma", "schema.prisma")) || (0, import_node_fs11.existsSync)((0, import_node_path10.join)(rootDir, "prisma", "seed.ts"));
|
|
2621
3982
|
}
|
|
2622
3983
|
function generate4(rootDir) {
|
|
2623
3984
|
const enumResult = extractEnumValues(rootDir);
|
|
@@ -2692,26 +4053,8 @@ var staticValuesParser = {
|
|
|
2692
4053
|
};
|
|
2693
4054
|
|
|
2694
4055
|
// src/server/graph/parsers/crosslayer/static-ref-scanner.ts
|
|
2695
|
-
var
|
|
2696
|
-
var
|
|
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
|
-
}
|
|
4056
|
+
var import_node_fs12 = require("node:fs");
|
|
4057
|
+
var import_node_path11 = require("node:path");
|
|
2715
4058
|
var MIN_VALUE_LENGTH = 4;
|
|
2716
4059
|
var SKIP_VALUES = /* @__PURE__ */ new Set([
|
|
2717
4060
|
"true",
|
|
@@ -2827,7 +4170,7 @@ var staticRefScannerParser = {
|
|
|
2827
4170
|
},
|
|
2828
4171
|
generate(rootDir, layerOutputs) {
|
|
2829
4172
|
const staticOutput = layerOutputs.get("static");
|
|
2830
|
-
if (!staticOutput || staticOutput.nodes.length === 0) {
|
|
4173
|
+
if (!staticOutput || !Array.isArray(staticOutput.nodes) || staticOutput.nodes.length === 0) {
|
|
2831
4174
|
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2832
4175
|
}
|
|
2833
4176
|
const valueLookup = /* @__PURE__ */ new Map();
|
|
@@ -2853,13 +4196,22 @@ var staticRefScannerParser = {
|
|
|
2853
4196
|
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2854
4197
|
if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2855
4198
|
const srcDir = paths.srcDir;
|
|
2856
|
-
const
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
4199
|
+
const seenFile = /* @__PURE__ */ new Set();
|
|
4200
|
+
const files = [];
|
|
4201
|
+
for (const root of paths.srcRoots) {
|
|
4202
|
+
for (const f of walkWithIgnore(root, [".ts", ".tsx"])) {
|
|
4203
|
+
if (!seenFile.has(f)) {
|
|
4204
|
+
seenFile.add(f);
|
|
4205
|
+
files.push(f);
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
for (const conv of paths.conventionFiles) {
|
|
4210
|
+
if (!seenFile.has(conv)) {
|
|
4211
|
+
seenFile.add(conv);
|
|
4212
|
+
files.push(conv);
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
2863
4215
|
const uiOutput = layerOutputs.get("ui");
|
|
2864
4216
|
const apiOutput = layerOutputs.get("api");
|
|
2865
4217
|
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
@@ -2876,10 +4228,11 @@ var staticRefScannerParser = {
|
|
|
2876
4228
|
const seen = /* @__PURE__ */ new Set();
|
|
2877
4229
|
let filesScanned = 0;
|
|
2878
4230
|
for (const absPath of files) {
|
|
2879
|
-
const
|
|
4231
|
+
const relFromSrc = (0, import_node_path11.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
4232
|
+
const sourceId = relFromSrc.startsWith("..") ? (0, import_node_path11.relative)(rootDir, absPath).replace(/\\/g, "/") : relFromSrc;
|
|
2880
4233
|
const sourceLayer = uiNodeIds.has(sourceId) ? "ui" : apiNodeIds.has(sourceId) ? "api" : null;
|
|
2881
4234
|
if (!sourceLayer) continue;
|
|
2882
|
-
const content = (0,
|
|
4235
|
+
const content = (0, import_node_fs12.readFileSync)(absPath, "utf-8");
|
|
2883
4236
|
filesScanned++;
|
|
2884
4237
|
let fileRefs;
|
|
2885
4238
|
if (parseCode2) {
|
|
@@ -2920,6 +4273,111 @@ var staticRefScannerParser = {
|
|
|
2920
4273
|
}
|
|
2921
4274
|
};
|
|
2922
4275
|
|
|
4276
|
+
// src/server/graph/parsers/crosslayer/middleware-gates.ts
|
|
4277
|
+
var import_node_path12 = require("node:path");
|
|
4278
|
+
init_ts_extractor();
|
|
4279
|
+
function toNodeId4(srcDir, rootDir, absPath) {
|
|
4280
|
+
const relFromSrc = (0, import_node_path12.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
4281
|
+
if (relFromSrc.startsWith("..")) {
|
|
4282
|
+
return (0, import_node_path12.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
4283
|
+
}
|
|
4284
|
+
return relFromSrc;
|
|
4285
|
+
}
|
|
4286
|
+
function collectTargets(apiOutput, uiOutput) {
|
|
4287
|
+
const out = [];
|
|
4288
|
+
for (const n of apiOutput?.nodes ?? []) {
|
|
4289
|
+
if (n.type !== "endpoint") continue;
|
|
4290
|
+
const path = n.path;
|
|
4291
|
+
if (typeof path !== "string" || !path) continue;
|
|
4292
|
+
out.push({ id: n.id, route: path, layer: "api" });
|
|
4293
|
+
}
|
|
4294
|
+
for (const n of uiOutput?.nodes ?? []) {
|
|
4295
|
+
if (n.type !== "page") continue;
|
|
4296
|
+
const route = n.route;
|
|
4297
|
+
if (typeof route !== "string" || !route) continue;
|
|
4298
|
+
out.push({ id: n.id, route, layer: "ui" });
|
|
4299
|
+
}
|
|
4300
|
+
return out;
|
|
4301
|
+
}
|
|
4302
|
+
var middlewareGatesParser = {
|
|
4303
|
+
id: "middleware-gates",
|
|
4304
|
+
layer: "crosslayer",
|
|
4305
|
+
concern: "auth-gate",
|
|
4306
|
+
detect(rootDir) {
|
|
4307
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
4308
|
+
if (!paths) return false;
|
|
4309
|
+
return paths.conventionFiles.some((f) => /middleware\.tsx?$/.test(f));
|
|
4310
|
+
},
|
|
4311
|
+
generate(rootDir, layerOutputs) {
|
|
4312
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
4313
|
+
if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
4314
|
+
const middlewareFiles = paths.conventionFiles.filter((f) => /middleware\.tsx?$/.test(f));
|
|
4315
|
+
if (middlewareFiles.length === 0) {
|
|
4316
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
4317
|
+
}
|
|
4318
|
+
const apiOutput = layerOutputs.get("api");
|
|
4319
|
+
const uiOutput = layerOutputs.get("ui");
|
|
4320
|
+
const targets = collectTargets(apiOutput, uiOutput);
|
|
4321
|
+
const crossRefs = [];
|
|
4322
|
+
const flagged = [];
|
|
4323
|
+
const seenEdge = /* @__PURE__ */ new Set();
|
|
4324
|
+
const seenUnparseable = /* @__PURE__ */ new Set();
|
|
4325
|
+
let protectMatcherCount = 0;
|
|
4326
|
+
for (const file of middlewareFiles) {
|
|
4327
|
+
let info = null;
|
|
4328
|
+
try {
|
|
4329
|
+
info = extractMiddlewareAuthTS(file);
|
|
4330
|
+
} catch {
|
|
4331
|
+
}
|
|
4332
|
+
if (!info || info.matchers.length === 0) continue;
|
|
4333
|
+
const middlewareId = toNodeId4(paths.srcDir, rootDir, file);
|
|
4334
|
+
for (const matcher of info.matchers) {
|
|
4335
|
+
if (matcher.intent !== "protect") continue;
|
|
4336
|
+
protectMatcherCount += 1;
|
|
4337
|
+
for (const pattern of matcher.patterns) {
|
|
4338
|
+
const re = middlewarePatternToRegex(pattern);
|
|
4339
|
+
if (!re) {
|
|
4340
|
+
const key = `${middlewareId}|${pattern}`;
|
|
4341
|
+
if (seenUnparseable.has(key)) continue;
|
|
4342
|
+
seenUnparseable.add(key);
|
|
4343
|
+
flagged.push({
|
|
4344
|
+
source: middlewareId,
|
|
4345
|
+
target: "UNRESOLVED",
|
|
4346
|
+
type: "middleware_pattern_unparseable",
|
|
4347
|
+
label: `pattern "${pattern}" in matcher "${matcher.name}" \u2014 coverage unknown`,
|
|
4348
|
+
confidence: "high"
|
|
4349
|
+
});
|
|
4350
|
+
continue;
|
|
4351
|
+
}
|
|
4352
|
+
for (const target of targets) {
|
|
4353
|
+
if (!re.test(target.route)) continue;
|
|
4354
|
+
const key = `${middlewareId}|${target.id}|protects`;
|
|
4355
|
+
if (seenEdge.has(key)) continue;
|
|
4356
|
+
seenEdge.add(key);
|
|
4357
|
+
crossRefs.push({
|
|
4358
|
+
source: middlewareId,
|
|
4359
|
+
target: target.id,
|
|
4360
|
+
type: "protects",
|
|
4361
|
+
layer: target.layer
|
|
4362
|
+
});
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
return {
|
|
4368
|
+
cross_refs: crossRefs,
|
|
4369
|
+
flagged_edges: flagged,
|
|
4370
|
+
warnings: [],
|
|
4371
|
+
patterns: {
|
|
4372
|
+
middleware_files: middlewareFiles.length,
|
|
4373
|
+
protect_matchers: protectMatcherCount,
|
|
4374
|
+
protects_edges: crossRefs.length,
|
|
4375
|
+
unparseable_patterns: flagged.length
|
|
4376
|
+
}
|
|
4377
|
+
};
|
|
4378
|
+
}
|
|
4379
|
+
};
|
|
4380
|
+
|
|
2923
4381
|
// src/server/graph/core/parser-registry.ts
|
|
2924
4382
|
function isMultiLayerParser(p) {
|
|
2925
4383
|
return "layers" in p && Array.isArray(p.layers);
|
|
@@ -2985,7 +4443,8 @@ function registerBuiltins(registry, disabled) {
|
|
|
2985
4443
|
fetchResolverParser,
|
|
2986
4444
|
apiAnnotationsParser,
|
|
2987
4445
|
urlLiteralScannerParser,
|
|
2988
|
-
staticRefScannerParser
|
|
4446
|
+
staticRefScannerParser,
|
|
4447
|
+
middlewareGatesParser
|
|
2989
4448
|
];
|
|
2990
4449
|
for (const parser of builtins) {
|
|
2991
4450
|
if (disabled.has(parser.id)) continue;
|
|
@@ -2995,7 +4454,7 @@ function registerBuiltins(registry, disabled) {
|
|
|
2995
4454
|
function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
2996
4455
|
for (const entry of config.parsers?.custom ?? []) {
|
|
2997
4456
|
try {
|
|
2998
|
-
const absPath = (0,
|
|
4457
|
+
const absPath = (0, import_node_path13.resolve)(rootDir, entry.path);
|
|
2999
4458
|
const mod = require(absPath);
|
|
3000
4459
|
const parser = "default" in mod ? mod.default : mod;
|
|
3001
4460
|
if (disabled.has(parser.id)) continue;
|
|
@@ -3125,10 +4584,10 @@ function applyCrossLayerResults(uiOutput, results) {
|
|
|
3125
4584
|
|
|
3126
4585
|
// src/server/graph/core/graph-builder.ts
|
|
3127
4586
|
function readGraphFromDisk(rootDir, layer) {
|
|
3128
|
-
const filePath = (0,
|
|
3129
|
-
if (!(0,
|
|
4587
|
+
const filePath = (0, import_node_path14.join)(rootDir, LAUNCHSECURE_DIR, "graphs", `${layer}.json`);
|
|
4588
|
+
if (!(0, import_node_fs13.existsSync)(filePath)) return null;
|
|
3130
4589
|
try {
|
|
3131
|
-
return JSON.parse((0,
|
|
4590
|
+
return JSON.parse((0, import_node_fs13.readFileSync)(filePath, "utf-8"));
|
|
3132
4591
|
} catch {
|
|
3133
4592
|
return null;
|
|
3134
4593
|
}
|
|
@@ -3225,1419 +4684,38 @@ function generateAll(rootDir) {
|
|
|
3225
4684
|
return [...wellKnownOrder, ...extras].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
3226
4685
|
}
|
|
3227
4686
|
|
|
3228
|
-
// src/server/
|
|
3229
|
-
|
|
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
|
|
4687
|
+
// src/server/parse-worker-entry.ts
|
|
4688
|
+
init_parse_failure_cache();
|
|
3652
4689
|
init_ts_extractor();
|
|
3653
|
-
|
|
3654
|
-
|
|
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
|
-
}
|
|
4690
|
+
if (!import_node_worker_threads.parentPort) {
|
|
4691
|
+
throw new Error("parse-worker-entry must be spawned as a worker thread");
|
|
3805
4692
|
}
|
|
3806
|
-
function
|
|
4693
|
+
async function run(req) {
|
|
3807
4694
|
try {
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
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
|
|
4695
|
+
await initTreeSitter();
|
|
4696
|
+
const config = loadConfig(req.rootDir);
|
|
4697
|
+
setExtractorConfig({
|
|
4698
|
+
dbIdentifiers: config.parsers?.patterns?.dbIdentifiers,
|
|
4699
|
+
mutationMethods: config.parsers?.patterns?.mutationMethods
|
|
3902
4700
|
});
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
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;
|
|
4701
|
+
const failureCache = new ParseFailureCache(req.rootDir, getTreeSitterFingerprint());
|
|
4702
|
+
if (!req.layer) failureCache.clearAll();
|
|
4703
|
+
setActiveFailureCache(failureCache);
|
|
4704
|
+
let results;
|
|
4260
4705
|
try {
|
|
4261
|
-
|
|
4262
|
-
}
|
|
4263
|
-
|
|
4264
|
-
|
|
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);
|
|
4706
|
+
results = req.layer ? [generateLayer(req.rootDir, req.layer)].filter((r) => r !== null) : generateAll(req.rootDir);
|
|
4707
|
+
} finally {
|
|
4708
|
+
failureCache.flush();
|
|
4709
|
+
setActiveFailureCache(null);
|
|
4631
4710
|
}
|
|
4711
|
+
const failedFiles = failureCache.list().map((f) => f.path);
|
|
4712
|
+
return { ok: true, results, failedFiles };
|
|
4713
|
+
} catch (e) {
|
|
4714
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
4715
|
+
return { ok: false, error: { name: err.name, message: err.message, stack: err.stack } };
|
|
4632
4716
|
}
|
|
4633
|
-
startChartServer({ port }).catch((err) => {
|
|
4634
|
-
process.stderr.write(`[launch-chart] failed to start: ${err}
|
|
4635
|
-
`);
|
|
4636
|
-
process.exit(1);
|
|
4637
|
-
});
|
|
4638
4717
|
}
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
startChartServer
|
|
4718
|
+
import_node_worker_threads.parentPort.once("message", async (msg) => {
|
|
4719
|
+
const reply = await run(msg);
|
|
4720
|
+
import_node_worker_threads.parentPort.postMessage(reply);
|
|
4643
4721
|
});
|