@launchsecure/launch-kit 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chart-client/assets/index-BN_N_I08.js +379 -0
- package/dist/chart-client/assets/index-DJRXEWQm.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-CCpAvTkG.css +32 -0
- package/dist/client/assets/{index-D9e81rsq.js → index-DldfczJ1.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +1388 -377
- package/dist/server/cli.js +1522 -406
- package/dist/server/graph-mcp-entry.js +1741 -415
- package/package.json +1 -1
- package/dist/chart-client/assets/index-BPR5akxH.js +0 -323
- package/dist/chart-client/assets/index-DhNl1aFF.css +0 -1
- package/dist/client/assets/index-nR-HgoHH.css +0 -32
|
@@ -29,17 +29,36 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
29
29
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
30
|
mod
|
|
31
31
|
));
|
|
32
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
33
|
|
|
33
34
|
// src/server/lockfile.ts
|
|
34
|
-
function lockDir() {
|
|
35
|
+
function lockDir(projectRoot) {
|
|
36
|
+
if (projectRoot) {
|
|
37
|
+
return (0, import_node_path.join)(projectRoot, ".launchsecure");
|
|
38
|
+
}
|
|
35
39
|
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".launchsecure");
|
|
36
40
|
}
|
|
37
|
-
function lockPath() {
|
|
38
|
-
return (0, import_node_path.join)(lockDir(), "launch-chart.lock");
|
|
41
|
+
function lockPath(projectRoot) {
|
|
42
|
+
return (0, import_node_path.join)(lockDir(projectRoot), "launch-chart.lock");
|
|
39
43
|
}
|
|
40
|
-
function readLock() {
|
|
41
|
-
const
|
|
42
|
-
|
|
44
|
+
function readLock(projectRoot) {
|
|
45
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
46
|
+
const p = lockPath(root);
|
|
47
|
+
if (!(0, import_node_fs.existsSync)(p)) {
|
|
48
|
+
if (root) {
|
|
49
|
+
const globalP = lockPath();
|
|
50
|
+
if ((0, import_node_fs.existsSync)(globalP)) {
|
|
51
|
+
try {
|
|
52
|
+
const data = JSON.parse((0, import_node_fs.readFileSync)(globalP, "utf-8"));
|
|
53
|
+
if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
|
|
54
|
+
return data;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
43
62
|
try {
|
|
44
63
|
const data = JSON.parse((0, import_node_fs.readFileSync)(p, "utf-8"));
|
|
45
64
|
if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
|
|
@@ -70,31 +89,35 @@ function getListenerPid(port) {
|
|
|
70
89
|
return null;
|
|
71
90
|
}
|
|
72
91
|
}
|
|
73
|
-
function getLiveLock() {
|
|
74
|
-
const
|
|
92
|
+
function getLiveLock(projectRoot) {
|
|
93
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
94
|
+
const lock = readLock(root);
|
|
75
95
|
if (!lock) return null;
|
|
76
96
|
const listenerPid = getListenerPid(lock.port);
|
|
77
97
|
const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
|
|
78
98
|
if (!live) {
|
|
79
99
|
try {
|
|
80
|
-
(0, import_node_fs.unlinkSync)(lockPath());
|
|
100
|
+
(0, import_node_fs.unlinkSync)(lockPath(root));
|
|
81
101
|
} catch {
|
|
82
102
|
}
|
|
83
103
|
return null;
|
|
84
104
|
}
|
|
85
105
|
return lock;
|
|
86
106
|
}
|
|
87
|
-
function writeLock(data) {
|
|
88
|
-
|
|
89
|
-
(0, import_node_fs.
|
|
107
|
+
function writeLock(data, projectRoot) {
|
|
108
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
109
|
+
(0, import_node_fs.mkdirSync)(lockDir(root), { recursive: true });
|
|
110
|
+
(0, import_node_fs.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
111
|
+
if (root) _activeProjectRoot = root;
|
|
90
112
|
}
|
|
91
|
-
function clearLock() {
|
|
113
|
+
function clearLock(projectRoot) {
|
|
114
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
92
115
|
try {
|
|
93
|
-
(0, import_node_fs.unlinkSync)(lockPath());
|
|
116
|
+
(0, import_node_fs.unlinkSync)(lockPath(root));
|
|
94
117
|
} catch {
|
|
95
118
|
}
|
|
96
119
|
}
|
|
97
|
-
var import_node_child_process, import_node_fs, import_node_os, import_node_path;
|
|
120
|
+
var import_node_child_process, import_node_fs, import_node_os, import_node_path, _activeProjectRoot;
|
|
98
121
|
var init_lockfile = __esm({
|
|
99
122
|
"src/server/lockfile.ts"() {
|
|
100
123
|
"use strict";
|
|
@@ -105,6 +128,30 @@ var init_lockfile = __esm({
|
|
|
105
128
|
}
|
|
106
129
|
});
|
|
107
130
|
|
|
131
|
+
// src/server/graph/core/config.ts
|
|
132
|
+
var config_exports = {};
|
|
133
|
+
__export(config_exports, {
|
|
134
|
+
loadConfig: () => loadConfig
|
|
135
|
+
});
|
|
136
|
+
function loadConfig(rootDir) {
|
|
137
|
+
const configPath = (0, import_node_path2.join)(rootDir, CONFIG_FILENAME);
|
|
138
|
+
if (!(0, import_node_fs2.existsSync)(configPath)) return {};
|
|
139
|
+
try {
|
|
140
|
+
return JSON.parse((0, import_node_fs2.readFileSync)(configPath, "utf-8"));
|
|
141
|
+
} catch {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
var import_node_fs2, import_node_path2, CONFIG_FILENAME;
|
|
146
|
+
var init_config = __esm({
|
|
147
|
+
"src/server/graph/core/config.ts"() {
|
|
148
|
+
"use strict";
|
|
149
|
+
import_node_fs2 = require("node:fs");
|
|
150
|
+
import_node_path2 = require("node:path");
|
|
151
|
+
CONFIG_FILENAME = ".launchchart.json";
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
108
155
|
// src/server/graph/core/ast-helpers.ts
|
|
109
156
|
function getTs() {
|
|
110
157
|
if (!tsModule) {
|
|
@@ -114,8 +161,8 @@ function getTs() {
|
|
|
114
161
|
}
|
|
115
162
|
function parseFile(absPath) {
|
|
116
163
|
const ts = getTs();
|
|
117
|
-
const content = (0,
|
|
118
|
-
const ext = (0,
|
|
164
|
+
const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
|
|
165
|
+
const ext = (0, import_node_path3.extname)(absPath);
|
|
119
166
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
|
|
120
167
|
const sourceFile = ts.createSourceFile(
|
|
121
168
|
absPath,
|
|
@@ -382,8 +429,8 @@ function parseFile(absPath) {
|
|
|
382
429
|
}
|
|
383
430
|
function extractDbCalls(absPath) {
|
|
384
431
|
const ts = getTs();
|
|
385
|
-
const content = (0,
|
|
386
|
-
const ext = (0,
|
|
432
|
+
const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
|
|
433
|
+
const ext = (0, import_node_path3.extname)(absPath);
|
|
387
434
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
388
435
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
389
436
|
const calls = [];
|
|
@@ -412,8 +459,8 @@ function extractDbCalls(absPath) {
|
|
|
412
459
|
}
|
|
413
460
|
function extractAuthWrappers(absPath) {
|
|
414
461
|
const ts = getTs();
|
|
415
|
-
const content = (0,
|
|
416
|
-
const ext = (0,
|
|
462
|
+
const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
|
|
463
|
+
const ext = (0, import_node_path3.extname)(absPath);
|
|
417
464
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
418
465
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
419
466
|
const wrappers = /* @__PURE__ */ new Set();
|
|
@@ -429,12 +476,12 @@ function extractAuthWrappers(absPath) {
|
|
|
429
476
|
visit(sourceFile);
|
|
430
477
|
return wrappers;
|
|
431
478
|
}
|
|
432
|
-
var
|
|
479
|
+
var import_node_fs3, import_node_path3, tsModule, HTTP_METHODS, MUTATION_METHODS;
|
|
433
480
|
var init_ast_helpers = __esm({
|
|
434
481
|
"src/server/graph/core/ast-helpers.ts"() {
|
|
435
482
|
"use strict";
|
|
436
|
-
|
|
437
|
-
|
|
483
|
+
import_node_fs3 = require("node:fs");
|
|
484
|
+
import_node_path3 = require("node:path");
|
|
438
485
|
HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
439
486
|
MUTATION_METHODS = /* @__PURE__ */ new Set([
|
|
440
487
|
"create",
|
|
@@ -453,12 +500,12 @@ var init_ast_helpers = __esm({
|
|
|
453
500
|
// src/server/graph/parsers/ui/react-nextjs.ts
|
|
454
501
|
function walk(dir, exts) {
|
|
455
502
|
const results = [];
|
|
456
|
-
if (!(0,
|
|
457
|
-
for (const entry of (0,
|
|
458
|
-
const full = (0,
|
|
503
|
+
if (!(0, import_node_fs4.existsSync)(dir)) return results;
|
|
504
|
+
for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
505
|
+
const full = (0, import_node_path4.join)(dir, entry.name);
|
|
459
506
|
if (entry.isDirectory()) {
|
|
460
507
|
results.push(...walk(full, exts));
|
|
461
|
-
} else if (exts.includes((0,
|
|
508
|
+
} else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
|
|
462
509
|
results.push(full);
|
|
463
510
|
}
|
|
464
511
|
}
|
|
@@ -466,33 +513,33 @@ function walk(dir, exts) {
|
|
|
466
513
|
}
|
|
467
514
|
function walkWithIgnore(dir, exts, ignoreDirs) {
|
|
468
515
|
const results = [];
|
|
469
|
-
if (!(0,
|
|
470
|
-
for (const entry of (0,
|
|
516
|
+
if (!(0, import_node_fs4.existsSync)(dir)) return results;
|
|
517
|
+
for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
471
518
|
if (entry.isDirectory()) {
|
|
472
519
|
if (ignoreDirs.has(entry.name)) continue;
|
|
473
|
-
results.push(...walkWithIgnore((0,
|
|
474
|
-
} else if (exts.includes((0,
|
|
475
|
-
results.push((0,
|
|
520
|
+
results.push(...walkWithIgnore((0, import_node_path4.join)(dir, entry.name), exts, ignoreDirs));
|
|
521
|
+
} else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
|
|
522
|
+
results.push((0, import_node_path4.join)(dir, entry.name));
|
|
476
523
|
}
|
|
477
524
|
}
|
|
478
525
|
return results;
|
|
479
526
|
}
|
|
480
527
|
function toNodeId(srcDir, absPath) {
|
|
481
|
-
return (0,
|
|
528
|
+
return (0, import_node_path4.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
482
529
|
}
|
|
483
530
|
function resolveImport(srcDir, specifier) {
|
|
484
531
|
if (!specifier.startsWith("@/")) return null;
|
|
485
532
|
const rel = specifier.slice(2);
|
|
486
|
-
const base = (0,
|
|
487
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
488
|
-
if ((0,
|
|
533
|
+
const base = (0, import_node_path4.join)(srcDir, rel);
|
|
534
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
|
|
535
|
+
if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
|
|
489
536
|
}
|
|
490
537
|
return null;
|
|
491
538
|
}
|
|
492
539
|
function resolveRelativeImport(fromFile, specifier) {
|
|
493
|
-
const base = (0,
|
|
494
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
495
|
-
if ((0,
|
|
540
|
+
const base = (0, import_node_path4.join)((0, import_node_path4.dirname)(fromFile), specifier);
|
|
541
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
|
|
542
|
+
if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
|
|
496
543
|
}
|
|
497
544
|
return null;
|
|
498
545
|
}
|
|
@@ -513,7 +560,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
|
|
|
513
560
|
const resolved = resolveRelativeImport(barrelAbsPath, re.from);
|
|
514
561
|
if (!resolved) continue;
|
|
515
562
|
if (re.isWildcard) {
|
|
516
|
-
const targetBn = (0,
|
|
563
|
+
const targetBn = (0, import_node_path4.basename)(resolved);
|
|
517
564
|
const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
|
|
518
565
|
if (targetIsBarrel) {
|
|
519
566
|
const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
|
|
@@ -540,12 +587,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
|
540
587
|
const barrels = /* @__PURE__ */ new Map();
|
|
541
588
|
const memo = /* @__PURE__ */ new Map();
|
|
542
589
|
for (const [absPath, parsed] of parsedByPath) {
|
|
543
|
-
const bn = (0,
|
|
590
|
+
const bn = (0, import_node_path4.basename)(absPath);
|
|
544
591
|
if (bn !== "index.ts" && bn !== "index.tsx") continue;
|
|
545
592
|
if (parsed.reExports.length === 0) continue;
|
|
546
593
|
const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
|
|
547
594
|
if (map.size > 0) {
|
|
548
|
-
const barrelId = (0,
|
|
595
|
+
const barrelId = (0, import_node_path4.relative)(srcDir, (0, import_node_path4.dirname)(absPath)).replace(/\\/g, "/");
|
|
549
596
|
barrels.set(barrelId, map);
|
|
550
597
|
}
|
|
551
598
|
}
|
|
@@ -566,34 +613,6 @@ function classifyType(id) {
|
|
|
566
613
|
if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
|
|
567
614
|
return "component";
|
|
568
615
|
}
|
|
569
|
-
function classifyModule(id) {
|
|
570
|
-
if (/app\/\(auth\)\//.test(id)) return "auth";
|
|
571
|
-
if (/app\/\(admin\)\//.test(id)) return "admin";
|
|
572
|
-
if (/app\/\(settings\)\//.test(id)) return "settings";
|
|
573
|
-
if (/app\/\(app\)\/\[orgSlug\]\/\(project-pages\)\//.test(id)) return "project";
|
|
574
|
-
if (/app\/\(app\)\/\[orgSlug\]\/\(org-pages\)\//.test(id)) return "org";
|
|
575
|
-
if (/app\/\(app\)\/\[orgSlug\]\//.test(id)) return "org";
|
|
576
|
-
if (id.startsWith("app/integrations/")) return "integrations";
|
|
577
|
-
if (id.startsWith("app/docs/")) return "admin";
|
|
578
|
-
if (id.startsWith("client/components/ui/")) return "shared-ui";
|
|
579
|
-
if (id.startsWith("client/components/layout/") || /client\/lib\/navigation/.test(id)) return "layout";
|
|
580
|
-
if (/client\/components\/auth\//.test(id) || /client\/lib\/auth-/.test(id) || /client\/lib\/github-oauth/.test(id) || /client\/lib\/permission-service/.test(id) || /client\/hooks\/use-permissions/.test(id)) return "auth";
|
|
581
|
-
if (/client\/components\/prd-/.test(id) || /client\/hooks\/use-admin/.test(id)) return "admin";
|
|
582
|
-
if (/client\/components\/org-/.test(id) || /client\/hooks\/use-org-/.test(id) || /client\/hooks\/use-provider-def/.test(id)) return "org";
|
|
583
|
-
if (/client\/components\/project/.test(id) || /client\/hooks\/use-project-/.test(id) || /client\/hooks\/use-pipeline/.test(id) || /client\/hooks\/use-databases/.test(id) || /client\/hooks\/use-provider-env/.test(id) || /client\/hooks\/use-role-assign/.test(id) || /client\/components\/pipeline/.test(id) || /client\/components\/deployments/.test(id)) return "project";
|
|
584
|
-
if (/client\/hooks\/use-(profile|sessions|organizations|notification)/.test(id)) return "settings";
|
|
585
|
-
if (id.startsWith("server/auth/")) return "auth";
|
|
586
|
-
if (id.startsWith("server/mcp/")) return "mcp";
|
|
587
|
-
if (id.startsWith("server/lib/")) return "server-lib";
|
|
588
|
-
if (id.startsWith("server/middleware")) return "middleware";
|
|
589
|
-
if (id.startsWith("server/services/")) return "services";
|
|
590
|
-
if (id.startsWith("server/db")) return "db";
|
|
591
|
-
if (id.startsWith("server/errors")) return "errors";
|
|
592
|
-
if (id.startsWith("server/")) return "server-lib";
|
|
593
|
-
if (id.startsWith("config/")) return "config";
|
|
594
|
-
if (id.startsWith("lib/")) return "lib";
|
|
595
|
-
return "root";
|
|
596
|
-
}
|
|
597
616
|
function extractRoute(id) {
|
|
598
617
|
if (!id.endsWith("/page.tsx")) return null;
|
|
599
618
|
let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
|
|
@@ -604,7 +623,7 @@ function extractRoute(id) {
|
|
|
604
623
|
return route || "/";
|
|
605
624
|
}
|
|
606
625
|
function nameFromFilename(absPath) {
|
|
607
|
-
return (0,
|
|
626
|
+
return (0, import_node_path4.basename)(absPath, (0, import_node_path4.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
|
|
608
627
|
}
|
|
609
628
|
function resolveTemplateLiteralRoute(template, routeToNodeId) {
|
|
610
629
|
const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
@@ -685,105 +704,6 @@ function matchRouteToPage(route, routeToNodeId) {
|
|
|
685
704
|
if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
|
|
686
705
|
return null;
|
|
687
706
|
}
|
|
688
|
-
function loadApiRoutes(rootDir) {
|
|
689
|
-
const apiJsonPath = (0, import_node_path3.join)(rootDir, ".launchsecure", "graphs", "api.json");
|
|
690
|
-
if (!(0, import_node_fs3.existsSync)(apiJsonPath)) return [];
|
|
691
|
-
try {
|
|
692
|
-
const parsed = JSON.parse((0, import_node_fs3.readFileSync)(apiJsonPath, "utf-8"));
|
|
693
|
-
const routes = [];
|
|
694
|
-
for (const n of parsed.nodes ?? []) {
|
|
695
|
-
const path3 = n.path;
|
|
696
|
-
if (!path3 || typeof path3 !== "string") continue;
|
|
697
|
-
routes.push({
|
|
698
|
-
path: path3,
|
|
699
|
-
nodeId: n.id,
|
|
700
|
-
segments: path3.split("/").filter(Boolean)
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
return routes;
|
|
704
|
-
} catch {
|
|
705
|
-
return [];
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
function buildApiPathMap(routes) {
|
|
709
|
-
const map = /* @__PURE__ */ new Map();
|
|
710
|
-
for (const r of routes) {
|
|
711
|
-
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
712
|
-
}
|
|
713
|
-
return map;
|
|
714
|
-
}
|
|
715
|
-
function normalizeFetchUrl(raw) {
|
|
716
|
-
let s = raw.replace(/^`|`$/g, "");
|
|
717
|
-
const qIdx = s.indexOf("?");
|
|
718
|
-
if (qIdx >= 0) s = s.slice(0, qIdx);
|
|
719
|
-
const hIdx = s.indexOf("#");
|
|
720
|
-
if (hIdx >= 0) s = s.slice(0, hIdx);
|
|
721
|
-
let hadInterpolation = false;
|
|
722
|
-
s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
723
|
-
hadInterpolation = true;
|
|
724
|
-
const cleaned = expr.trim();
|
|
725
|
-
const last = cleaned.split(".").pop() ?? cleaned;
|
|
726
|
-
const name = last.replace(/[^\w]/g, "") || "param";
|
|
727
|
-
return ":" + name;
|
|
728
|
-
});
|
|
729
|
-
s = s.replace(/\/+/g, "/");
|
|
730
|
-
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
731
|
-
return { path: s || "/", hadInterpolation };
|
|
732
|
-
}
|
|
733
|
-
function scoreApiRouteMatch(candidate, known) {
|
|
734
|
-
if (candidate.length !== known.length) return -1;
|
|
735
|
-
let score = 0;
|
|
736
|
-
for (let i = 0; i < candidate.length; i++) {
|
|
737
|
-
const a = candidate[i];
|
|
738
|
-
const b = known[i];
|
|
739
|
-
if (a === b) {
|
|
740
|
-
score += 3;
|
|
741
|
-
continue;
|
|
742
|
-
}
|
|
743
|
-
if (a.startsWith(":") && b.startsWith(":")) {
|
|
744
|
-
score += 2;
|
|
745
|
-
continue;
|
|
746
|
-
}
|
|
747
|
-
if (a.startsWith(":") || b.startsWith(":")) {
|
|
748
|
-
score += 1;
|
|
749
|
-
continue;
|
|
750
|
-
}
|
|
751
|
-
return -1;
|
|
752
|
-
}
|
|
753
|
-
return score;
|
|
754
|
-
}
|
|
755
|
-
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
756
|
-
const raw = call.url;
|
|
757
|
-
if (/^(https?:)?\/\//i.test(raw)) {
|
|
758
|
-
return { kind: "external", normalizedUrl: raw };
|
|
759
|
-
}
|
|
760
|
-
if (call.isConcat) {
|
|
761
|
-
return { kind: "dynamic", normalizedUrl: raw };
|
|
762
|
-
}
|
|
763
|
-
const { path: path3, hadInterpolation } = normalizeFetchUrl(raw);
|
|
764
|
-
if (!path3.startsWith("/")) {
|
|
765
|
-
return { kind: "unresolved", normalizedUrl: path3 };
|
|
766
|
-
}
|
|
767
|
-
const segs = path3.split("/").filter(Boolean);
|
|
768
|
-
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
769
|
-
return { kind: "dynamic", normalizedUrl: path3 };
|
|
770
|
-
}
|
|
771
|
-
const exact = apiPathMap.get(path3);
|
|
772
|
-
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
|
|
773
|
-
let bestScore = -1;
|
|
774
|
-
let bestId = null;
|
|
775
|
-
for (const r of apiRoutes) {
|
|
776
|
-
const score = scoreApiRouteMatch(segs, r.segments);
|
|
777
|
-
if (score > bestScore) {
|
|
778
|
-
bestScore = score;
|
|
779
|
-
bestId = r.nodeId;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
if (bestId && bestScore > 0) {
|
|
783
|
-
return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
|
|
784
|
-
}
|
|
785
|
-
return { kind: "unresolved", normalizedUrl: path3 };
|
|
786
|
-
}
|
|
787
707
|
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
|
|
788
708
|
const edges = [];
|
|
789
709
|
const flagged = [];
|
|
@@ -883,26 +803,26 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
883
803
|
return { edges, flagged };
|
|
884
804
|
}
|
|
885
805
|
function detect(rootDir) {
|
|
886
|
-
return (0,
|
|
806
|
+
return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app")) && (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"));
|
|
887
807
|
}
|
|
888
808
|
function generate(rootDir) {
|
|
889
|
-
const srcDir = (0,
|
|
890
|
-
const appFiles = walk((0,
|
|
891
|
-
(f) => (0,
|
|
809
|
+
const srcDir = (0, import_node_path4.join)(rootDir, "src");
|
|
810
|
+
const appFiles = walk((0, import_node_path4.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
|
|
811
|
+
(f) => (0, import_node_path4.basename)(f) !== "route.ts" && (0, import_node_path4.basename)(f) !== "route.tsx"
|
|
892
812
|
);
|
|
893
|
-
const clientFiles = walk((0,
|
|
894
|
-
const serverFiles = walk((0,
|
|
895
|
-
(f) => (0,
|
|
813
|
+
const clientFiles = walk((0, import_node_path4.join)(srcDir, "client"), [".tsx", ".ts"]);
|
|
814
|
+
const serverFiles = walk((0, import_node_path4.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
|
|
815
|
+
(f) => (0, import_node_path4.basename)(f) !== "route.ts" && (0, import_node_path4.basename)(f) !== "route.tsx"
|
|
896
816
|
);
|
|
897
|
-
const libFiles = walk((0,
|
|
898
|
-
const configFiles = walk((0,
|
|
817
|
+
const libFiles = walk((0, import_node_path4.join)(srcDir, "lib"), [".ts", ".tsx"]);
|
|
818
|
+
const configFiles = walk((0, import_node_path4.join)(srcDir, "config"), [".ts", ".tsx"]);
|
|
899
819
|
const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
|
|
900
820
|
const parsedByPath = /* @__PURE__ */ new Map();
|
|
901
821
|
for (const absPath of allDiscovered) {
|
|
902
822
|
parsedByPath.set(absPath, parseFile(absPath));
|
|
903
823
|
}
|
|
904
824
|
const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
|
|
905
|
-
const fileSet = allDiscovered.filter((f) => !(0,
|
|
825
|
+
const fileSet = allDiscovered.filter((f) => !(0, import_node_path4.basename)(f).startsWith("index."));
|
|
906
826
|
const nodes = [];
|
|
907
827
|
const nodeIdSet = /* @__PURE__ */ new Set();
|
|
908
828
|
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
@@ -913,15 +833,13 @@ function generate(rootDir) {
|
|
|
913
833
|
const parsed = parsedByPath.get(absPath);
|
|
914
834
|
const name = parsed.name || nameFromFilename(absPath);
|
|
915
835
|
const route = extractRoute(id);
|
|
916
|
-
|
|
917
|
-
nodes.push({ id, type, name, route, module: module_, exports: parsed.exports });
|
|
836
|
+
nodes.push({ id, type, name, route, exports: parsed.exports });
|
|
918
837
|
nodeIdSet.add(id);
|
|
919
838
|
nodeTypeMap.set(id, type);
|
|
920
839
|
if (route) routeToNodeId.set(route, id);
|
|
921
840
|
}
|
|
922
841
|
const allEdges = [];
|
|
923
842
|
const allFlagged = [];
|
|
924
|
-
const crossRefs = [];
|
|
925
843
|
for (const absPath of fileSet) {
|
|
926
844
|
const sourceId = toNodeId(srcDir, absPath);
|
|
927
845
|
const parsed = parsedByPath.get(absPath);
|
|
@@ -938,66 +856,21 @@ function generate(rootDir) {
|
|
|
938
856
|
allEdges.push(...edges);
|
|
939
857
|
allFlagged.push(...flagged);
|
|
940
858
|
}
|
|
941
|
-
const
|
|
942
|
-
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
943
|
-
const includeExternalFetches = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
|
|
944
|
-
const fetchSeen = /* @__PURE__ */ new Set();
|
|
945
|
-
let fetchResolvedCount = 0;
|
|
946
|
-
let fetchDynamicCount = 0;
|
|
947
|
-
let fetchUnresolvedCount = 0;
|
|
948
|
-
let fetchExternalCount = 0;
|
|
859
|
+
const fetchCallEntries = [];
|
|
949
860
|
for (const absPath of fileSet) {
|
|
950
861
|
const sourceId = toNodeId(srcDir, absPath);
|
|
951
862
|
const parsed = parsedByPath.get(absPath);
|
|
952
863
|
if (parsed.fetchCalls.length === 0) continue;
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
type: "calls_api",
|
|
964
|
-
layer: "api"
|
|
965
|
-
});
|
|
966
|
-
fetchResolvedCount++;
|
|
967
|
-
continue;
|
|
968
|
-
}
|
|
969
|
-
if (result.kind === "dynamic") {
|
|
970
|
-
fetchDynamicCount++;
|
|
971
|
-
allFlagged.push({
|
|
972
|
-
source: sourceId,
|
|
973
|
-
target: "DYNAMIC",
|
|
974
|
-
type: "calls_api",
|
|
975
|
-
label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
|
|
976
|
-
confidence: call.isConcat ? "low" : "medium"
|
|
977
|
-
});
|
|
978
|
-
continue;
|
|
979
|
-
}
|
|
980
|
-
if (result.kind === "external") {
|
|
981
|
-
fetchExternalCount++;
|
|
982
|
-
if (!includeExternalFetches) continue;
|
|
983
|
-
allFlagged.push({
|
|
984
|
-
source: sourceId,
|
|
985
|
-
target: "EXTERNAL",
|
|
986
|
-
type: "calls_external",
|
|
987
|
-
label: `${methodTag} external fetch: ${call.url}`,
|
|
988
|
-
confidence: "high"
|
|
989
|
-
});
|
|
990
|
-
continue;
|
|
991
|
-
}
|
|
992
|
-
fetchUnresolvedCount++;
|
|
993
|
-
allFlagged.push({
|
|
994
|
-
source: sourceId,
|
|
995
|
-
target: "UNRESOLVED",
|
|
996
|
-
type: "calls_api",
|
|
997
|
-
label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
|
|
998
|
-
confidence: "medium"
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
864
|
+
fetchCallEntries.push({
|
|
865
|
+
nodeId: sourceId,
|
|
866
|
+
calls: parsed.fetchCalls.map((c) => ({
|
|
867
|
+
url: c.url,
|
|
868
|
+
method: c.method,
|
|
869
|
+
isTemplate: c.isTemplate,
|
|
870
|
+
isConcat: c.isConcat,
|
|
871
|
+
kind: c.kind
|
|
872
|
+
}))
|
|
873
|
+
});
|
|
1001
874
|
}
|
|
1002
875
|
const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
|
|
1003
876
|
const IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -1023,7 +896,7 @@ function generate(rootDir) {
|
|
|
1023
896
|
} catch {
|
|
1024
897
|
continue;
|
|
1025
898
|
}
|
|
1026
|
-
const externalId = (0,
|
|
899
|
+
const externalId = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1027
900
|
const edgesFromThis = [];
|
|
1028
901
|
const seen = /* @__PURE__ */ new Set();
|
|
1029
902
|
for (const imp of parsed.imports) {
|
|
@@ -1064,7 +937,6 @@ function generate(rootDir) {
|
|
|
1064
937
|
type: "external",
|
|
1065
938
|
name: parsed.name || nameFromFilename(absPath),
|
|
1066
939
|
route: null,
|
|
1067
|
-
module: "external",
|
|
1068
940
|
exports: parsed.exports
|
|
1069
941
|
});
|
|
1070
942
|
nodeIdSet.add(externalId);
|
|
@@ -1114,20 +986,11 @@ function generate(rootDir) {
|
|
|
1114
986
|
layer: "ui",
|
|
1115
987
|
parser: "react-nextjs-ast",
|
|
1116
988
|
...stats,
|
|
1117
|
-
api_call_detection: {
|
|
1118
|
-
includeExternalFetches,
|
|
1119
|
-
includeConcatFetches: process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1",
|
|
1120
|
-
apiRoutesLoaded: apiRoutes.length,
|
|
1121
|
-
resolved: fetchResolvedCount,
|
|
1122
|
-
dynamic: fetchDynamicCount,
|
|
1123
|
-
unresolved: fetchUnresolvedCount,
|
|
1124
|
-
external: fetchExternalCount
|
|
1125
|
-
},
|
|
1126
989
|
notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
|
|
1127
990
|
},
|
|
1128
991
|
nodes,
|
|
1129
992
|
edges: allEdges,
|
|
1130
|
-
cross_refs:
|
|
993
|
+
cross_refs: [],
|
|
1131
994
|
contradictions: [],
|
|
1132
995
|
warnings: [],
|
|
1133
996
|
flagged_edges: dedupedFlagged,
|
|
@@ -1138,16 +1001,17 @@ function generate(rootDir) {
|
|
|
1138
1001
|
renders: allEdges.filter((e) => e.type === "renders").length,
|
|
1139
1002
|
imports: allEdges.filter((e) => e.type === "imports").length,
|
|
1140
1003
|
navigates: allEdges.filter((e) => e.type === "navigates").length
|
|
1141
|
-
}
|
|
1004
|
+
},
|
|
1005
|
+
fetch_calls: fetchCallEntries
|
|
1142
1006
|
}
|
|
1143
1007
|
};
|
|
1144
1008
|
}
|
|
1145
|
-
var
|
|
1009
|
+
var import_node_fs4, import_node_path4, RENDER_TYPES, reactNextjsParser;
|
|
1146
1010
|
var init_react_nextjs = __esm({
|
|
1147
1011
|
"src/server/graph/parsers/ui/react-nextjs.ts"() {
|
|
1148
1012
|
"use strict";
|
|
1149
|
-
|
|
1150
|
-
|
|
1013
|
+
import_node_fs4 = require("node:fs");
|
|
1014
|
+
import_node_path4 = require("node:path");
|
|
1151
1015
|
init_ast_helpers();
|
|
1152
1016
|
RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
|
|
1153
1017
|
reactNextjsParser = {
|
|
@@ -1162,9 +1026,9 @@ var init_react_nextjs = __esm({
|
|
|
1162
1026
|
// src/server/graph/parsers/api/nextjs-routes.ts
|
|
1163
1027
|
function walk2(dir) {
|
|
1164
1028
|
const results = [];
|
|
1165
|
-
if (!(0,
|
|
1166
|
-
for (const entry of (0,
|
|
1167
|
-
const full = (0,
|
|
1029
|
+
if (!(0, import_node_fs5.existsSync)(dir)) return results;
|
|
1030
|
+
for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
|
|
1031
|
+
const full = (0, import_node_path5.join)(dir, entry.name);
|
|
1168
1032
|
if (entry.isDirectory()) {
|
|
1169
1033
|
results.push(...walk2(full));
|
|
1170
1034
|
} else if (entry.name === "route.ts" || entry.name === "route.tsx") {
|
|
@@ -1174,7 +1038,7 @@ function walk2(dir) {
|
|
|
1174
1038
|
return results;
|
|
1175
1039
|
}
|
|
1176
1040
|
function filePathToRoute(apiDir, absPath) {
|
|
1177
|
-
let route = "/" + (0,
|
|
1041
|
+
let route = "/" + (0, import_node_path5.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
1178
1042
|
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
1179
1043
|
route = route.replace(/\/+/g, "/");
|
|
1180
1044
|
if (route === "/") return "/api";
|
|
@@ -1185,10 +1049,10 @@ function camelToPascal(s) {
|
|
|
1185
1049
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1186
1050
|
}
|
|
1187
1051
|
function detect2(rootDir) {
|
|
1188
|
-
return (0,
|
|
1052
|
+
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "src", "app", "api"));
|
|
1189
1053
|
}
|
|
1190
1054
|
function generate2(rootDir) {
|
|
1191
|
-
const apiDir = (0,
|
|
1055
|
+
const apiDir = (0, import_node_path5.join)(rootDir, "src", "app", "api");
|
|
1192
1056
|
const routeFiles = walk2(apiDir);
|
|
1193
1057
|
const nodes = [];
|
|
1194
1058
|
const edges = [];
|
|
@@ -1206,7 +1070,7 @@ function generate2(rootDir) {
|
|
|
1206
1070
|
if (HTTP_METHODS2.has(exp)) methods.push(exp);
|
|
1207
1071
|
}
|
|
1208
1072
|
const routePath = filePathToRoute(apiDir, absPath);
|
|
1209
|
-
const relPath = (0,
|
|
1073
|
+
const relPath = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1210
1074
|
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
1211
1075
|
const reads = dbCalls.filter((c) => !c.isMutation);
|
|
1212
1076
|
const mutates = mutations.length > 0;
|
|
@@ -1283,12 +1147,12 @@ function generate2(rootDir) {
|
|
|
1283
1147
|
}
|
|
1284
1148
|
};
|
|
1285
1149
|
}
|
|
1286
|
-
var
|
|
1150
|
+
var import_node_fs5, import_node_path5, HTTP_METHODS2, nextjsRoutesParser;
|
|
1287
1151
|
var init_nextjs_routes = __esm({
|
|
1288
1152
|
"src/server/graph/parsers/api/nextjs-routes.ts"() {
|
|
1289
1153
|
"use strict";
|
|
1290
|
-
|
|
1291
|
-
|
|
1154
|
+
import_node_fs5 = require("node:fs");
|
|
1155
|
+
import_node_path5 = require("node:path");
|
|
1292
1156
|
init_ast_helpers();
|
|
1293
1157
|
HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
1294
1158
|
nextjsRoutesParser = {
|
|
@@ -1391,11 +1255,11 @@ function parseEnums(content) {
|
|
|
1391
1255
|
return nodes;
|
|
1392
1256
|
}
|
|
1393
1257
|
function detect3(rootDir) {
|
|
1394
|
-
return (0,
|
|
1258
|
+
return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "prisma", "schema.prisma"));
|
|
1395
1259
|
}
|
|
1396
1260
|
function generate3(rootDir) {
|
|
1397
|
-
const schemaPath = (0,
|
|
1398
|
-
const content = (0,
|
|
1261
|
+
const schemaPath = (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
|
|
1262
|
+
const content = (0, import_node_fs6.readFileSync)(schemaPath, "utf-8");
|
|
1399
1263
|
const { nodes: modelNodes, relations } = parseModels(content);
|
|
1400
1264
|
const enumNodes = parseEnums(content);
|
|
1401
1265
|
const allNodes = [...modelNodes, ...enumNodes];
|
|
@@ -1444,12 +1308,12 @@ function generate3(rootDir) {
|
|
|
1444
1308
|
}
|
|
1445
1309
|
};
|
|
1446
1310
|
}
|
|
1447
|
-
var
|
|
1311
|
+
var import_node_fs6, import_node_path6, prismaSchemaParser;
|
|
1448
1312
|
var init_prisma_schema = __esm({
|
|
1449
1313
|
"src/server/graph/parsers/db/prisma-schema.ts"() {
|
|
1450
1314
|
"use strict";
|
|
1451
|
-
|
|
1452
|
-
|
|
1315
|
+
import_node_fs6 = require("node:fs");
|
|
1316
|
+
import_node_path6 = require("node:path");
|
|
1453
1317
|
prismaSchemaParser = {
|
|
1454
1318
|
id: "prisma-schema",
|
|
1455
1319
|
layer: "db",
|
|
@@ -1459,99 +1323,1161 @@ var init_prisma_schema = __esm({
|
|
|
1459
1323
|
}
|
|
1460
1324
|
});
|
|
1461
1325
|
|
|
1462
|
-
// src/server/graph/core/
|
|
1463
|
-
function
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
output,
|
|
1474
|
-
nodeCount: output.nodes.length,
|
|
1475
|
-
edgeCount: output.edges.length
|
|
1476
|
-
};
|
|
1477
|
-
}
|
|
1478
|
-
function generateAll(rootDir) {
|
|
1479
|
-
const layers = ["api", "db", "ui"];
|
|
1480
|
-
const results = [];
|
|
1481
|
-
for (const layer of layers) {
|
|
1482
|
-
const result = generateLayer(rootDir, layer);
|
|
1483
|
-
if (result) results.push(result);
|
|
1326
|
+
// src/server/graph/core/api-route-matching.ts
|
|
1327
|
+
function loadApiRoutesFromOutput(apiOutput) {
|
|
1328
|
+
const routes = [];
|
|
1329
|
+
for (const n of apiOutput.nodes) {
|
|
1330
|
+
const path3 = n.path;
|
|
1331
|
+
if (!path3 || typeof path3 !== "string") continue;
|
|
1332
|
+
routes.push({
|
|
1333
|
+
path: path3,
|
|
1334
|
+
nodeId: n.id,
|
|
1335
|
+
segments: path3.split("/").filter(Boolean)
|
|
1336
|
+
});
|
|
1484
1337
|
}
|
|
1485
|
-
|
|
1486
|
-
return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
1338
|
+
return routes;
|
|
1487
1339
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
init_react_nextjs();
|
|
1493
|
-
init_nextjs_routes();
|
|
1494
|
-
init_prisma_schema();
|
|
1495
|
-
ALL_PARSERS = [
|
|
1496
|
-
reactNextjsParser,
|
|
1497
|
-
nextjsRoutesParser,
|
|
1498
|
-
prismaSchemaParser
|
|
1499
|
-
];
|
|
1340
|
+
function buildApiPathMap(routes) {
|
|
1341
|
+
const map = /* @__PURE__ */ new Map();
|
|
1342
|
+
for (const r of routes) {
|
|
1343
|
+
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
1500
1344
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
// src/server/graph/index.ts
|
|
1504
|
-
function graphsDir(rootDir) {
|
|
1505
|
-
return (0, import_node_path6.join)(rootDir, GRAPHS_DIR);
|
|
1506
|
-
}
|
|
1507
|
-
function graphFilePath(rootDir, layer) {
|
|
1508
|
-
return (0, import_node_path6.join)(graphsDir(rootDir), `${layer}.json`);
|
|
1345
|
+
return map;
|
|
1509
1346
|
}
|
|
1510
|
-
function
|
|
1511
|
-
|
|
1347
|
+
function normalizeFetchUrl(raw) {
|
|
1348
|
+
let s = raw.replace(/^`|`$/g, "");
|
|
1349
|
+
const qIdx = s.indexOf("?");
|
|
1350
|
+
if (qIdx >= 0) s = s.slice(0, qIdx);
|
|
1351
|
+
const hIdx = s.indexOf("#");
|
|
1352
|
+
if (hIdx >= 0) s = s.slice(0, hIdx);
|
|
1353
|
+
let hadInterpolation = false;
|
|
1354
|
+
s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
1355
|
+
hadInterpolation = true;
|
|
1356
|
+
const cleaned = expr.trim();
|
|
1357
|
+
const last = cleaned.split(".").pop() ?? cleaned;
|
|
1358
|
+
const name = last.replace(/[^\w]/g, "") || "param";
|
|
1359
|
+
return ":" + name;
|
|
1360
|
+
});
|
|
1361
|
+
s = s.replace(/\/+/g, "/");
|
|
1362
|
+
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
1363
|
+
return { path: s || "/", hadInterpolation };
|
|
1512
1364
|
}
|
|
1513
|
-
function
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1365
|
+
function scoreApiRouteMatch(candidate, known) {
|
|
1366
|
+
if (candidate.length !== known.length) return -1;
|
|
1367
|
+
let score = 0;
|
|
1368
|
+
for (let i = 0; i < candidate.length; i++) {
|
|
1369
|
+
const a = candidate[i];
|
|
1370
|
+
const b = known[i];
|
|
1371
|
+
if (a === b) {
|
|
1372
|
+
score += 3;
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
if (a.startsWith(":") && b.startsWith(":")) {
|
|
1376
|
+
score += 2;
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
if (a.startsWith(":") || b.startsWith(":")) {
|
|
1380
|
+
score += 1;
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
return -1;
|
|
1520
1384
|
}
|
|
1521
|
-
|
|
1522
|
-
const graph = JSON.parse(content);
|
|
1523
|
-
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
1524
|
-
return graph;
|
|
1385
|
+
return score;
|
|
1525
1386
|
}
|
|
1526
|
-
function
|
|
1527
|
-
const
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
if (graph) result[layer] = graph;
|
|
1387
|
+
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
1388
|
+
const raw = call.url;
|
|
1389
|
+
if (/^(https?:)?\/\//i.test(raw)) {
|
|
1390
|
+
return { kind: "external", normalizedUrl: raw };
|
|
1531
1391
|
}
|
|
1532
|
-
|
|
1533
|
-
}
|
|
1534
|
-
function generateGraph(rootDir, layer) {
|
|
1535
|
-
const dir = graphsDir(rootDir);
|
|
1536
|
-
(0, import_node_fs6.mkdirSync)(dir, { recursive: true });
|
|
1537
|
-
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
1538
|
-
for (const result of results) {
|
|
1539
|
-
const filePath = graphFilePath(rootDir, result.layer);
|
|
1540
|
-
(0, import_node_fs6.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
1541
|
-
invalidateCache(filePath);
|
|
1392
|
+
if (call.isConcat) {
|
|
1393
|
+
return { kind: "dynamic", normalizedUrl: raw };
|
|
1542
1394
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
"
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1395
|
+
const { path: path3, hadInterpolation } = normalizeFetchUrl(raw);
|
|
1396
|
+
if (!path3.startsWith("/")) {
|
|
1397
|
+
return { kind: "unresolved", normalizedUrl: path3 };
|
|
1398
|
+
}
|
|
1399
|
+
const segs = path3.split("/").filter(Boolean);
|
|
1400
|
+
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
1401
|
+
return { kind: "dynamic", normalizedUrl: path3 };
|
|
1402
|
+
}
|
|
1403
|
+
const exact = apiPathMap.get(path3);
|
|
1404
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
|
|
1405
|
+
let bestScore = -1;
|
|
1406
|
+
let bestId = null;
|
|
1407
|
+
for (const r of apiRoutes) {
|
|
1408
|
+
const score = scoreApiRouteMatch(segs, r.segments);
|
|
1409
|
+
if (score > bestScore) {
|
|
1410
|
+
bestScore = score;
|
|
1411
|
+
bestId = r.nodeId;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
if (bestId && bestScore > 0) {
|
|
1415
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
|
|
1416
|
+
}
|
|
1417
|
+
return { kind: "unresolved", normalizedUrl: path3 };
|
|
1418
|
+
}
|
|
1419
|
+
function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
|
|
1420
|
+
const { path: path3, hadInterpolation } = normalizeFetchUrl(urlPath);
|
|
1421
|
+
if (!path3.startsWith("/")) {
|
|
1422
|
+
return { kind: "unresolved", normalizedUrl: path3 };
|
|
1423
|
+
}
|
|
1424
|
+
const segs = path3.split("/").filter(Boolean);
|
|
1425
|
+
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
1426
|
+
return { kind: "dynamic", normalizedUrl: path3 };
|
|
1427
|
+
}
|
|
1428
|
+
const exact = apiPathMap.get(path3);
|
|
1429
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
|
|
1430
|
+
let bestScore = -1;
|
|
1431
|
+
let bestId = null;
|
|
1432
|
+
for (const r of apiRoutes) {
|
|
1433
|
+
const score = scoreApiRouteMatch(segs, r.segments);
|
|
1434
|
+
if (score > bestScore) {
|
|
1435
|
+
bestScore = score;
|
|
1436
|
+
bestId = r.nodeId;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
if (bestId && bestScore > 0) {
|
|
1440
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
|
|
1441
|
+
}
|
|
1442
|
+
return { kind: "unresolved", normalizedUrl: path3 };
|
|
1443
|
+
}
|
|
1444
|
+
var init_api_route_matching = __esm({
|
|
1445
|
+
"src/server/graph/core/api-route-matching.ts"() {
|
|
1446
|
+
"use strict";
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
// src/server/graph/parsers/crosslayer/fetch-resolver.ts
|
|
1451
|
+
var fetchResolverParser;
|
|
1452
|
+
var init_fetch_resolver = __esm({
|
|
1453
|
+
"src/server/graph/parsers/crosslayer/fetch-resolver.ts"() {
|
|
1454
|
+
"use strict";
|
|
1455
|
+
init_api_route_matching();
|
|
1456
|
+
fetchResolverParser = {
|
|
1457
|
+
id: "fetch-resolver",
|
|
1458
|
+
layer: "crosslayer",
|
|
1459
|
+
detect(_rootDir) {
|
|
1460
|
+
return true;
|
|
1461
|
+
},
|
|
1462
|
+
generate(_rootDir, layerOutputs) {
|
|
1463
|
+
const uiOutput = layerOutputs.get("ui");
|
|
1464
|
+
const apiOutput = layerOutputs.get("api");
|
|
1465
|
+
if (!uiOutput || !apiOutput) {
|
|
1466
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1467
|
+
}
|
|
1468
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1469
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1470
|
+
const fetchCallEntries = uiOutput.patterns?.fetch_calls ?? [];
|
|
1471
|
+
if (fetchCallEntries.length === 0) {
|
|
1472
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1473
|
+
}
|
|
1474
|
+
const includeExternal = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
|
|
1475
|
+
const crossRefs = [];
|
|
1476
|
+
const flaggedEdges = [];
|
|
1477
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1478
|
+
let resolvedCount = 0;
|
|
1479
|
+
let dynamicCount = 0;
|
|
1480
|
+
let unresolvedCount = 0;
|
|
1481
|
+
let externalCount = 0;
|
|
1482
|
+
for (const entry of fetchCallEntries) {
|
|
1483
|
+
for (const call of entry.calls) {
|
|
1484
|
+
const result = resolveFetchCall(call, apiPathMap, apiRoutes);
|
|
1485
|
+
const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
|
|
1486
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
1487
|
+
const key = `${entry.nodeId}\u2192${result.nodeId}\u2192calls_api`;
|
|
1488
|
+
if (seen.has(key)) continue;
|
|
1489
|
+
seen.add(key);
|
|
1490
|
+
crossRefs.push({
|
|
1491
|
+
source: entry.nodeId,
|
|
1492
|
+
target: result.nodeId,
|
|
1493
|
+
type: "calls_api",
|
|
1494
|
+
layer: "api"
|
|
1495
|
+
});
|
|
1496
|
+
resolvedCount++;
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
if (result.kind === "dynamic") {
|
|
1500
|
+
dynamicCount++;
|
|
1501
|
+
flaggedEdges.push({
|
|
1502
|
+
source: entry.nodeId,
|
|
1503
|
+
target: "DYNAMIC",
|
|
1504
|
+
type: "calls_api",
|
|
1505
|
+
label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
|
|
1506
|
+
confidence: call.isConcat ? "low" : "medium"
|
|
1507
|
+
});
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
if (result.kind === "external") {
|
|
1511
|
+
externalCount++;
|
|
1512
|
+
if (!includeExternal) continue;
|
|
1513
|
+
flaggedEdges.push({
|
|
1514
|
+
source: entry.nodeId,
|
|
1515
|
+
target: "EXTERNAL",
|
|
1516
|
+
type: "calls_external",
|
|
1517
|
+
label: `${methodTag} external fetch: ${call.url}`,
|
|
1518
|
+
confidence: "high"
|
|
1519
|
+
});
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
unresolvedCount++;
|
|
1523
|
+
flaggedEdges.push({
|
|
1524
|
+
source: entry.nodeId,
|
|
1525
|
+
target: "UNRESOLVED",
|
|
1526
|
+
type: "calls_api",
|
|
1527
|
+
label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
|
|
1528
|
+
confidence: "medium"
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
return {
|
|
1533
|
+
cross_refs: crossRefs,
|
|
1534
|
+
flagged_edges: flaggedEdges,
|
|
1535
|
+
warnings: [],
|
|
1536
|
+
patterns: {
|
|
1537
|
+
api_call_detection: {
|
|
1538
|
+
resolved: resolvedCount,
|
|
1539
|
+
dynamic: dynamicCount,
|
|
1540
|
+
unresolved: unresolvedCount,
|
|
1541
|
+
external: externalCount
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
// src/server/graph/parsers/crosslayer/api-annotations.ts
|
|
1551
|
+
function walk3(dir, exts) {
|
|
1552
|
+
if (!(0, import_node_fs7.existsSync)(dir)) return [];
|
|
1553
|
+
const results = [];
|
|
1554
|
+
for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
|
|
1555
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1556
|
+
const full = (0, import_node_path7.join)(dir, entry.name);
|
|
1557
|
+
if (entry.isDirectory()) {
|
|
1558
|
+
results.push(...walk3(full, exts));
|
|
1559
|
+
} else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
|
|
1560
|
+
results.push(full);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return results;
|
|
1564
|
+
}
|
|
1565
|
+
function toNodeId2(srcDir, absPath) {
|
|
1566
|
+
return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
1567
|
+
}
|
|
1568
|
+
var import_node_fs7, import_node_path7, API_ANNOTATION_RE, apiAnnotationsParser;
|
|
1569
|
+
var init_api_annotations = __esm({
|
|
1570
|
+
"src/server/graph/parsers/crosslayer/api-annotations.ts"() {
|
|
1571
|
+
"use strict";
|
|
1572
|
+
import_node_fs7 = require("node:fs");
|
|
1573
|
+
import_node_path7 = require("node:path");
|
|
1574
|
+
init_api_route_matching();
|
|
1575
|
+
API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
|
|
1576
|
+
apiAnnotationsParser = {
|
|
1577
|
+
id: "api-annotations",
|
|
1578
|
+
layer: "crosslayer",
|
|
1579
|
+
detect(rootDir) {
|
|
1580
|
+
return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
|
|
1581
|
+
},
|
|
1582
|
+
generate(rootDir, layerOutputs) {
|
|
1583
|
+
const apiOutput = layerOutputs.get("api");
|
|
1584
|
+
if (!apiOutput) {
|
|
1585
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1586
|
+
}
|
|
1587
|
+
const uiOutput = layerOutputs.get("ui");
|
|
1588
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
1589
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1590
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1591
|
+
const srcDir = (0, import_node_path7.join)(rootDir, "src");
|
|
1592
|
+
const files = walk3(srcDir, [".ts", ".tsx"]);
|
|
1593
|
+
const crossRefs = [];
|
|
1594
|
+
const flaggedEdges = [];
|
|
1595
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1596
|
+
for (const absPath of files) {
|
|
1597
|
+
const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
|
|
1598
|
+
const sourceId = toNodeId2(srcDir, absPath);
|
|
1599
|
+
if (!uiNodeIds.has(sourceId)) continue;
|
|
1600
|
+
let match;
|
|
1601
|
+
API_ANNOTATION_RE.lastIndex = 0;
|
|
1602
|
+
while ((match = API_ANNOTATION_RE.exec(content)) !== null) {
|
|
1603
|
+
const method = match[1];
|
|
1604
|
+
const urlPath = match[2];
|
|
1605
|
+
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
1606
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
1607
|
+
const key = `${sourceId}|${result.nodeId}|calls_api`;
|
|
1608
|
+
if (seen.has(key)) continue;
|
|
1609
|
+
seen.add(key);
|
|
1610
|
+
crossRefs.push({
|
|
1611
|
+
source: sourceId,
|
|
1612
|
+
target: result.nodeId,
|
|
1613
|
+
type: "calls_api",
|
|
1614
|
+
layer: "api"
|
|
1615
|
+
});
|
|
1616
|
+
} else {
|
|
1617
|
+
flaggedEdges.push({
|
|
1618
|
+
source: sourceId,
|
|
1619
|
+
target: "UNRESOLVED",
|
|
1620
|
+
type: "annotation_unresolved",
|
|
1621
|
+
label: `@api ${method} ${urlPath} \u2014 no matching API route found`,
|
|
1622
|
+
confidence: "high"
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
return {
|
|
1628
|
+
cross_refs: crossRefs,
|
|
1629
|
+
flagged_edges: flaggedEdges,
|
|
1630
|
+
warnings: [],
|
|
1631
|
+
patterns: {
|
|
1632
|
+
annotations_found: crossRefs.length + flaggedEdges.length,
|
|
1633
|
+
annotations_resolved: crossRefs.length,
|
|
1634
|
+
annotations_unresolved: flaggedEdges.length
|
|
1635
|
+
}
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
// src/server/graph/parsers/crosslayer/url-literal-scanner.ts
|
|
1643
|
+
function walk4(dir, exts) {
|
|
1644
|
+
if (!(0, import_node_fs8.existsSync)(dir)) return [];
|
|
1645
|
+
const results = [];
|
|
1646
|
+
for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
|
|
1647
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1648
|
+
const full = (0, import_node_path8.join)(dir, entry.name);
|
|
1649
|
+
if (entry.isDirectory()) {
|
|
1650
|
+
results.push(...walk4(full, exts));
|
|
1651
|
+
} else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
|
|
1652
|
+
results.push(full);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return results;
|
|
1656
|
+
}
|
|
1657
|
+
function toNodeId3(srcDir, absPath) {
|
|
1658
|
+
return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
1659
|
+
}
|
|
1660
|
+
var import_node_fs8, import_node_path8, URL_LITERAL_RE, urlLiteralScannerParser;
|
|
1661
|
+
var init_url_literal_scanner = __esm({
|
|
1662
|
+
"src/server/graph/parsers/crosslayer/url-literal-scanner.ts"() {
|
|
1663
|
+
"use strict";
|
|
1664
|
+
import_node_fs8 = require("node:fs");
|
|
1665
|
+
import_node_path8 = require("node:path");
|
|
1666
|
+
init_api_route_matching();
|
|
1667
|
+
URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
|
|
1668
|
+
urlLiteralScannerParser = {
|
|
1669
|
+
id: "url-literal-scanner",
|
|
1670
|
+
layer: "crosslayer",
|
|
1671
|
+
detect(rootDir) {
|
|
1672
|
+
return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(rootDir, "src"));
|
|
1673
|
+
},
|
|
1674
|
+
generate(rootDir, layerOutputs) {
|
|
1675
|
+
const apiOutput = layerOutputs.get("api");
|
|
1676
|
+
if (!apiOutput) {
|
|
1677
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1678
|
+
}
|
|
1679
|
+
const uiOutput = layerOutputs.get("ui");
|
|
1680
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
1681
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1682
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1683
|
+
const srcDir = (0, import_node_path8.join)(rootDir, "src");
|
|
1684
|
+
const clientDir = (0, import_node_path8.join)(srcDir, "client");
|
|
1685
|
+
const appDir = (0, import_node_path8.join)(srcDir, "app");
|
|
1686
|
+
const files = [
|
|
1687
|
+
...walk4(clientDir, [".ts", ".tsx"]),
|
|
1688
|
+
...walk4(appDir, [".ts", ".tsx"])
|
|
1689
|
+
];
|
|
1690
|
+
const crossRefs = [];
|
|
1691
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1692
|
+
for (const absPath of files) {
|
|
1693
|
+
const sourceId = toNodeId3(srcDir, absPath);
|
|
1694
|
+
if (!uiNodeIds.has(sourceId)) continue;
|
|
1695
|
+
const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
|
|
1696
|
+
let match;
|
|
1697
|
+
URL_LITERAL_RE.lastIndex = 0;
|
|
1698
|
+
while ((match = URL_LITERAL_RE.exec(content)) !== null) {
|
|
1699
|
+
const urlPath = match[1];
|
|
1700
|
+
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
1701
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
1702
|
+
const key = `${sourceId}|${result.nodeId}|references_api`;
|
|
1703
|
+
if (seen.has(key)) continue;
|
|
1704
|
+
seen.add(key);
|
|
1705
|
+
crossRefs.push({
|
|
1706
|
+
source: sourceId,
|
|
1707
|
+
target: result.nodeId,
|
|
1708
|
+
type: "references_api",
|
|
1709
|
+
layer: "api"
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
return {
|
|
1715
|
+
cross_refs: crossRefs,
|
|
1716
|
+
flagged_edges: [],
|
|
1717
|
+
warnings: [],
|
|
1718
|
+
patterns: {
|
|
1719
|
+
url_literals_resolved: crossRefs.length
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
// src/server/graph/core/parser-registry.ts
|
|
1728
|
+
function registerBuiltins(registry, disabled) {
|
|
1729
|
+
const builtins = [
|
|
1730
|
+
reactNextjsParser,
|
|
1731
|
+
nextjsRoutesParser,
|
|
1732
|
+
prismaSchemaParser,
|
|
1733
|
+
fetchResolverParser,
|
|
1734
|
+
apiAnnotationsParser,
|
|
1735
|
+
urlLiteralScannerParser
|
|
1736
|
+
];
|
|
1737
|
+
for (const parser of builtins) {
|
|
1738
|
+
if (disabled.has(parser.id)) continue;
|
|
1739
|
+
registry.register(parser);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
1743
|
+
for (const entry of config.parsers?.custom ?? []) {
|
|
1744
|
+
try {
|
|
1745
|
+
const absPath = (0, import_node_path9.resolve)(rootDir, entry.path);
|
|
1746
|
+
const mod = require(absPath);
|
|
1747
|
+
const parser = "default" in mod ? mod.default : mod;
|
|
1748
|
+
if (disabled.has(parser.id)) continue;
|
|
1749
|
+
if (parser.layer !== entry.layer) {
|
|
1750
|
+
process.stderr.write(
|
|
1751
|
+
`[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
|
|
1752
|
+
`
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
registry.register(parser);
|
|
1756
|
+
} catch (err2) {
|
|
1757
|
+
process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err2}
|
|
1758
|
+
`);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
function createRegistry(config, rootDir) {
|
|
1763
|
+
const registry = new ParserRegistry();
|
|
1764
|
+
const disabled = new Set(config.parsers?.disabled ?? []);
|
|
1765
|
+
registerBuiltins(registry, disabled);
|
|
1766
|
+
loadCustomParsers(registry, config, rootDir, disabled);
|
|
1767
|
+
return registry;
|
|
1768
|
+
}
|
|
1769
|
+
var import_node_path9, ParserRegistry;
|
|
1770
|
+
var init_parser_registry = __esm({
|
|
1771
|
+
"src/server/graph/core/parser-registry.ts"() {
|
|
1772
|
+
"use strict";
|
|
1773
|
+
import_node_path9 = require("node:path");
|
|
1774
|
+
init_react_nextjs();
|
|
1775
|
+
init_nextjs_routes();
|
|
1776
|
+
init_prisma_schema();
|
|
1777
|
+
init_fetch_resolver();
|
|
1778
|
+
init_api_annotations();
|
|
1779
|
+
init_url_literal_scanner();
|
|
1780
|
+
ParserRegistry = class {
|
|
1781
|
+
constructor() {
|
|
1782
|
+
this.parsers = /* @__PURE__ */ new Map();
|
|
1783
|
+
this.ids = /* @__PURE__ */ new Set();
|
|
1784
|
+
}
|
|
1785
|
+
register(parser) {
|
|
1786
|
+
if (this.ids.has(parser.id)) {
|
|
1787
|
+
throw new Error(`Duplicate parser id: ${parser.id}`);
|
|
1788
|
+
}
|
|
1789
|
+
this.ids.add(parser.id);
|
|
1790
|
+
const list = this.parsers.get(parser.layer) ?? [];
|
|
1791
|
+
list.push(parser);
|
|
1792
|
+
this.parsers.set(parser.layer, list);
|
|
1793
|
+
}
|
|
1794
|
+
getParsers(layer) {
|
|
1795
|
+
return this.parsers.get(layer) ?? [];
|
|
1796
|
+
}
|
|
1797
|
+
getCrossLayerParsers() {
|
|
1798
|
+
return this.parsers.get("crosslayer") ?? [];
|
|
1799
|
+
}
|
|
1800
|
+
getAll() {
|
|
1801
|
+
const all = [];
|
|
1802
|
+
for (const list of this.parsers.values()) all.push(...list);
|
|
1803
|
+
return all;
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1809
|
+
// src/server/graph/core/merge.ts
|
|
1810
|
+
function mergeGraphOutputs(outputs, layer) {
|
|
1811
|
+
if (outputs.length === 0) {
|
|
1812
|
+
return {
|
|
1813
|
+
metadata: { generated: (/* @__PURE__ */ new Date()).toISOString(), scope: "", layer },
|
|
1814
|
+
nodes: [],
|
|
1815
|
+
edges: [],
|
|
1816
|
+
cross_refs: [],
|
|
1817
|
+
contradictions: [],
|
|
1818
|
+
warnings: [],
|
|
1819
|
+
flagged_edges: []
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
if (outputs.length === 1) return outputs[0];
|
|
1823
|
+
const seenNodes = /* @__PURE__ */ new Set();
|
|
1824
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
1825
|
+
const seenCrossRefs = /* @__PURE__ */ new Set();
|
|
1826
|
+
const mergedNodes = [];
|
|
1827
|
+
const mergedEdges = [];
|
|
1828
|
+
const mergedCrossRefs = [];
|
|
1829
|
+
const mergedContradictions = [];
|
|
1830
|
+
const mergedWarnings = [];
|
|
1831
|
+
const mergedFlagged = [];
|
|
1832
|
+
const parserIds = [];
|
|
1833
|
+
for (const output of outputs) {
|
|
1834
|
+
if (output.metadata.parser) {
|
|
1835
|
+
parserIds.push(String(output.metadata.parser));
|
|
1836
|
+
}
|
|
1837
|
+
for (const node of output.nodes) {
|
|
1838
|
+
if (seenNodes.has(node.id)) {
|
|
1839
|
+
mergedWarnings.push({
|
|
1840
|
+
type: "merge_conflict",
|
|
1841
|
+
detail: `Node "${node.id}" produced by multiple parsers; keeping first`
|
|
1842
|
+
});
|
|
1843
|
+
continue;
|
|
1844
|
+
}
|
|
1845
|
+
seenNodes.add(node.id);
|
|
1846
|
+
mergedNodes.push(node);
|
|
1847
|
+
}
|
|
1848
|
+
for (const edge of output.edges) {
|
|
1849
|
+
const key = `${edge.source}|${edge.target}|${edge.type}`;
|
|
1850
|
+
if (seenEdges.has(key)) continue;
|
|
1851
|
+
seenEdges.add(key);
|
|
1852
|
+
mergedEdges.push(edge);
|
|
1853
|
+
}
|
|
1854
|
+
for (const ref of output.cross_refs) {
|
|
1855
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
1856
|
+
if (seenCrossRefs.has(key)) continue;
|
|
1857
|
+
seenCrossRefs.add(key);
|
|
1858
|
+
mergedCrossRefs.push(ref);
|
|
1859
|
+
}
|
|
1860
|
+
mergedContradictions.push(...output.contradictions);
|
|
1861
|
+
mergedWarnings.push(...output.warnings);
|
|
1862
|
+
mergedFlagged.push(...output.flagged_edges);
|
|
1863
|
+
}
|
|
1864
|
+
const metadata = {
|
|
1865
|
+
...outputs[0].metadata,
|
|
1866
|
+
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1867
|
+
parsers: parserIds
|
|
1868
|
+
};
|
|
1869
|
+
return {
|
|
1870
|
+
metadata,
|
|
1871
|
+
nodes: mergedNodes,
|
|
1872
|
+
edges: mergedEdges,
|
|
1873
|
+
cross_refs: mergedCrossRefs,
|
|
1874
|
+
contradictions: mergedContradictions,
|
|
1875
|
+
warnings: mergedWarnings,
|
|
1876
|
+
flagged_edges: mergedFlagged,
|
|
1877
|
+
patterns: outputs[0].patterns
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
function dedupCrossRefs(refs) {
|
|
1881
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1882
|
+
const result = [];
|
|
1883
|
+
for (const ref of refs) {
|
|
1884
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
1885
|
+
if (seen.has(key)) continue;
|
|
1886
|
+
seen.add(key);
|
|
1887
|
+
result.push(ref);
|
|
1888
|
+
}
|
|
1889
|
+
return result;
|
|
1890
|
+
}
|
|
1891
|
+
function applyCrossLayerResults(uiOutput, results, primaryId) {
|
|
1892
|
+
const allCrossRefs = [...uiOutput.cross_refs];
|
|
1893
|
+
const allFlagged = [...uiOutput.flagged_edges];
|
|
1894
|
+
const allWarnings = [...uiOutput.warnings];
|
|
1895
|
+
const primaryResult = results.find((r) => r.parserId === primaryId);
|
|
1896
|
+
const secondaryResults = results.filter((r) => r.parserId !== primaryId);
|
|
1897
|
+
if (primaryResult) {
|
|
1898
|
+
allCrossRefs.push(...primaryResult.output.cross_refs);
|
|
1899
|
+
allFlagged.push(...primaryResult.output.flagged_edges);
|
|
1900
|
+
allWarnings.push(...primaryResult.output.warnings);
|
|
1901
|
+
}
|
|
1902
|
+
const primarySet = new Set(
|
|
1903
|
+
(primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
|
|
1904
|
+
);
|
|
1905
|
+
for (const sec of secondaryResults) {
|
|
1906
|
+
for (const ref of sec.output.cross_refs) {
|
|
1907
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
1908
|
+
if (primarySet.has(key)) {
|
|
1909
|
+
allCrossRefs.push(ref);
|
|
1910
|
+
} else {
|
|
1911
|
+
allFlagged.push({
|
|
1912
|
+
source: ref.source,
|
|
1913
|
+
target: ref.target,
|
|
1914
|
+
type: "out_of_pattern",
|
|
1915
|
+
label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
|
|
1916
|
+
confidence: "medium"
|
|
1917
|
+
});
|
|
1918
|
+
allCrossRefs.push(ref);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
allFlagged.push(...sec.output.flagged_edges);
|
|
1922
|
+
allWarnings.push(...sec.output.warnings);
|
|
1923
|
+
}
|
|
1924
|
+
return {
|
|
1925
|
+
...uiOutput,
|
|
1926
|
+
cross_refs: dedupCrossRefs(allCrossRefs),
|
|
1927
|
+
flagged_edges: allFlagged,
|
|
1928
|
+
warnings: allWarnings
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
var init_merge = __esm({
|
|
1932
|
+
"src/server/graph/core/merge.ts"() {
|
|
1933
|
+
"use strict";
|
|
1934
|
+
}
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
// src/server/graph/core/graph-builder.ts
|
|
1938
|
+
function readGraphFromDisk(rootDir, layer) {
|
|
1939
|
+
const filePath = (0, import_node_path10.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
1940
|
+
if (!(0, import_node_fs9.existsSync)(filePath)) return null;
|
|
1941
|
+
try {
|
|
1942
|
+
return JSON.parse((0, import_node_fs9.readFileSync)(filePath, "utf-8"));
|
|
1943
|
+
} catch {
|
|
1944
|
+
return null;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
function generateLayer(rootDir, layer) {
|
|
1948
|
+
const config = loadConfig(rootDir);
|
|
1949
|
+
const registry = createRegistry(config, rootDir);
|
|
1950
|
+
const parsers = registry.getParsers(layer);
|
|
1951
|
+
const outputs = [];
|
|
1952
|
+
for (const parser of parsers) {
|
|
1953
|
+
if (!parser.detect(rootDir)) continue;
|
|
1954
|
+
outputs.push(parser.generate(rootDir));
|
|
1955
|
+
}
|
|
1956
|
+
if (outputs.length === 0) return null;
|
|
1957
|
+
let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
1958
|
+
if (layer === "ui") {
|
|
1959
|
+
const layerOutputs = /* @__PURE__ */ new Map();
|
|
1960
|
+
layerOutputs.set("ui", merged);
|
|
1961
|
+
for (const otherLayer of ["api", "db"]) {
|
|
1962
|
+
const existing = readGraphFromDisk(rootDir, otherLayer);
|
|
1963
|
+
if (existing) layerOutputs.set(otherLayer, existing);
|
|
1964
|
+
}
|
|
1965
|
+
const crossParsers = registry.getCrossLayerParsers();
|
|
1966
|
+
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
1967
|
+
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
1968
|
+
if (crossResults.length > 0) {
|
|
1969
|
+
merged = applyCrossLayerResults(merged, crossResults, primaryId);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
return {
|
|
1973
|
+
layer,
|
|
1974
|
+
output: merged,
|
|
1975
|
+
nodeCount: merged.nodes.length,
|
|
1976
|
+
edgeCount: merged.edges.length
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
function generateAll(rootDir) {
|
|
1980
|
+
const config = loadConfig(rootDir);
|
|
1981
|
+
const registry = createRegistry(config, rootDir);
|
|
1982
|
+
const layerOrder = ["api", "db", "ui"];
|
|
1983
|
+
const layerOutputs = /* @__PURE__ */ new Map();
|
|
1984
|
+
const results = [];
|
|
1985
|
+
for (const layer of layerOrder) {
|
|
1986
|
+
const parsers = registry.getParsers(layer);
|
|
1987
|
+
const outputs = [];
|
|
1988
|
+
for (const parser of parsers) {
|
|
1989
|
+
if (!parser.detect(rootDir)) continue;
|
|
1990
|
+
outputs.push(parser.generate(rootDir));
|
|
1991
|
+
}
|
|
1992
|
+
if (outputs.length === 0) continue;
|
|
1993
|
+
const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
1994
|
+
layerOutputs.set(layer, merged);
|
|
1995
|
+
results.push({
|
|
1996
|
+
layer,
|
|
1997
|
+
output: merged,
|
|
1998
|
+
nodeCount: merged.nodes.length,
|
|
1999
|
+
edgeCount: merged.edges.length
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
const crossParsers = registry.getCrossLayerParsers();
|
|
2003
|
+
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
2004
|
+
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
2005
|
+
if (crossResults.length > 0 && layerOutputs.has("ui")) {
|
|
2006
|
+
const uiOutput = layerOutputs.get("ui");
|
|
2007
|
+
const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
|
|
2008
|
+
layerOutputs.set("ui", merged);
|
|
2009
|
+
const uiResult = results.find((r) => r.layer === "ui");
|
|
2010
|
+
if (uiResult) {
|
|
2011
|
+
uiResult.output = merged;
|
|
2012
|
+
uiResult.nodeCount = merged.nodes.length;
|
|
2013
|
+
uiResult.edgeCount = merged.edges.length;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
const byLayer = new Map(results.map((r) => [r.layer, r]));
|
|
2017
|
+
return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
2018
|
+
}
|
|
2019
|
+
var import_node_fs9, import_node_path10;
|
|
2020
|
+
var init_graph_builder = __esm({
|
|
2021
|
+
"src/server/graph/core/graph-builder.ts"() {
|
|
2022
|
+
"use strict";
|
|
2023
|
+
import_node_fs9 = require("node:fs");
|
|
2024
|
+
import_node_path10 = require("node:path");
|
|
2025
|
+
init_config();
|
|
2026
|
+
init_parser_registry();
|
|
2027
|
+
init_merge();
|
|
2028
|
+
}
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
// src/server/graph/taggers/module-tagger.ts
|
|
2032
|
+
function matchGlob(pattern, id) {
|
|
2033
|
+
const patParts = pattern.split("/");
|
|
2034
|
+
const idParts = id.split("/");
|
|
2035
|
+
return matchParts(patParts, 0, idParts, 0);
|
|
2036
|
+
}
|
|
2037
|
+
function matchParts(pat, pi, id, ii) {
|
|
2038
|
+
while (pi < pat.length && ii < id.length) {
|
|
2039
|
+
const p = pat[pi];
|
|
2040
|
+
if (p === "**") {
|
|
2041
|
+
for (let skip = ii; skip <= id.length; skip++) {
|
|
2042
|
+
if (matchParts(pat, pi + 1, id, skip)) return true;
|
|
2043
|
+
}
|
|
2044
|
+
return false;
|
|
2045
|
+
}
|
|
2046
|
+
if (p === "*") {
|
|
2047
|
+
pi++;
|
|
2048
|
+
ii++;
|
|
2049
|
+
continue;
|
|
2050
|
+
}
|
|
2051
|
+
if (p !== id[ii]) return false;
|
|
2052
|
+
pi++;
|
|
2053
|
+
ii++;
|
|
2054
|
+
}
|
|
2055
|
+
while (pi < pat.length && pat[pi] === "**") pi++;
|
|
2056
|
+
return pi === pat.length && ii === id.length;
|
|
2057
|
+
}
|
|
2058
|
+
function detectConventionDirs(rootDir) {
|
|
2059
|
+
const result = /* @__PURE__ */ new Map();
|
|
2060
|
+
const searchDirs = [
|
|
2061
|
+
rootDir,
|
|
2062
|
+
(0, import_node_path11.join)(rootDir, "src"),
|
|
2063
|
+
(0, import_node_path11.join)(rootDir, "app"),
|
|
2064
|
+
(0, import_node_path11.join)(rootDir, "lib")
|
|
2065
|
+
];
|
|
2066
|
+
for (const base of searchDirs) {
|
|
2067
|
+
for (const convention of CONVENTION_DIRS) {
|
|
2068
|
+
const dir = (0, import_node_path11.join)(base, convention);
|
|
2069
|
+
if (!(0, import_node_fs10.existsSync)(dir)) continue;
|
|
2070
|
+
try {
|
|
2071
|
+
const stat = (0, import_node_fs10.statSync)(dir);
|
|
2072
|
+
if (!stat.isDirectory()) continue;
|
|
2073
|
+
const entries = (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
2074
|
+
if (entries.length > 0) {
|
|
2075
|
+
const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
|
|
2076
|
+
result.set(relPath, entries);
|
|
2077
|
+
}
|
|
2078
|
+
} catch {
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
return result;
|
|
2083
|
+
}
|
|
2084
|
+
function extractRouteGroups(id) {
|
|
2085
|
+
const groups = [];
|
|
2086
|
+
const re = /\(([^)]+)\)/g;
|
|
2087
|
+
let m;
|
|
2088
|
+
while ((m = re.exec(id)) !== null) {
|
|
2089
|
+
groups.push(m[1]);
|
|
2090
|
+
}
|
|
2091
|
+
return groups;
|
|
2092
|
+
}
|
|
2093
|
+
function isRouteGroup(segment) {
|
|
2094
|
+
return segment.startsWith("(") && segment.endsWith(")");
|
|
2095
|
+
}
|
|
2096
|
+
function isDynamicSegment(segment) {
|
|
2097
|
+
return segment.startsWith("[") || segment.startsWith(":");
|
|
2098
|
+
}
|
|
2099
|
+
function isDomainDir(segment) {
|
|
2100
|
+
return segment.includes(".") && !segment.endsWith(".tsx") && !segment.endsWith(".ts") && !segment.endsWith(".js") && !segment.endsWith(".jsx") && !segment.endsWith(".vue");
|
|
2101
|
+
}
|
|
2102
|
+
function isTrivialGroup(name) {
|
|
2103
|
+
if (TRIVIAL_GROUPS.has(name)) return true;
|
|
2104
|
+
const lower = name.toLowerCase();
|
|
2105
|
+
const wrapperPatterns = [
|
|
2106
|
+
/^.*-?wrapper$/,
|
|
2107
|
+
// "page-wrapper", "use-page-wrapper"
|
|
2108
|
+
/^.*-?layout$/,
|
|
2109
|
+
// "admin-layout", "settings-layout"
|
|
2110
|
+
/^use-/,
|
|
2111
|
+
// "use-page-wrapper"
|
|
2112
|
+
/^default$/
|
|
2113
|
+
];
|
|
2114
|
+
return wrapperPatterns.some((p) => p.test(lower));
|
|
2115
|
+
}
|
|
2116
|
+
function normalizeGroupName(name) {
|
|
2117
|
+
return name.replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
|
|
2118
|
+
}
|
|
2119
|
+
function extractModuleFromPath(id) {
|
|
2120
|
+
const segments = id.split("/");
|
|
2121
|
+
const routeGroups = extractRouteGroups(id);
|
|
2122
|
+
const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g)).map(normalizeGroupName);
|
|
2123
|
+
if (moduleGroups.length > 0) {
|
|
2124
|
+
return moduleGroups[moduleGroups.length - 1];
|
|
2125
|
+
}
|
|
2126
|
+
const meaningful = [];
|
|
2127
|
+
for (const seg of segments) {
|
|
2128
|
+
if (seg.includes(".")) continue;
|
|
2129
|
+
if (isRouteGroup(seg)) continue;
|
|
2130
|
+
if (isDynamicSegment(seg)) continue;
|
|
2131
|
+
if (isDomainDir(seg)) continue;
|
|
2132
|
+
if (SKIP_SEGMENTS.has(seg)) continue;
|
|
2133
|
+
meaningful.push(seg);
|
|
2134
|
+
}
|
|
2135
|
+
if (meaningful.length > 0) {
|
|
2136
|
+
return meaningful[0];
|
|
2137
|
+
}
|
|
2138
|
+
return "root";
|
|
2139
|
+
}
|
|
2140
|
+
var import_node_fs10, import_node_path11, CONVENTION_DIRS, SKIP_SEGMENTS, TRIVIAL_GROUPS, cachedRootDir, cachedConventionDirs, moduleTagger;
|
|
2141
|
+
var init_module_tagger = __esm({
|
|
2142
|
+
"src/server/graph/taggers/module-tagger.ts"() {
|
|
2143
|
+
"use strict";
|
|
2144
|
+
import_node_fs10 = require("node:fs");
|
|
2145
|
+
import_node_path11 = require("node:path");
|
|
2146
|
+
CONVENTION_DIRS = ["features", "modules", "domains", "areas"];
|
|
2147
|
+
SKIP_SEGMENTS = /* @__PURE__ */ new Set([
|
|
2148
|
+
"src",
|
|
2149
|
+
"app",
|
|
2150
|
+
"client",
|
|
2151
|
+
"server",
|
|
2152
|
+
"lib",
|
|
2153
|
+
"config"
|
|
2154
|
+
]);
|
|
2155
|
+
TRIVIAL_GROUPS = /* @__PURE__ */ new Set([
|
|
2156
|
+
"app",
|
|
2157
|
+
"all",
|
|
2158
|
+
"ee",
|
|
2159
|
+
"home",
|
|
2160
|
+
"root"
|
|
2161
|
+
]);
|
|
2162
|
+
cachedRootDir = null;
|
|
2163
|
+
cachedConventionDirs = /* @__PURE__ */ new Map();
|
|
2164
|
+
moduleTagger = {
|
|
2165
|
+
id: "module",
|
|
2166
|
+
tagKey: "module",
|
|
2167
|
+
trackUntagged: true,
|
|
2168
|
+
layers: null,
|
|
2169
|
+
// applies to all layers
|
|
2170
|
+
tag(nodes, layer, rootDir) {
|
|
2171
|
+
if (cachedRootDir !== rootDir) {
|
|
2172
|
+
cachedConventionDirs = detectConventionDirs(rootDir);
|
|
2173
|
+
cachedRootDir = rootDir;
|
|
2174
|
+
}
|
|
2175
|
+
let configRules = [];
|
|
2176
|
+
try {
|
|
2177
|
+
const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
|
|
2178
|
+
const config = loadConfig2(rootDir);
|
|
2179
|
+
configRules = config.taggers?.module?.rules ?? [];
|
|
2180
|
+
} catch {
|
|
2181
|
+
}
|
|
2182
|
+
const result = /* @__PURE__ */ new Map();
|
|
2183
|
+
for (const node of nodes) {
|
|
2184
|
+
const id = node.id;
|
|
2185
|
+
let matched = false;
|
|
2186
|
+
for (const rule of configRules) {
|
|
2187
|
+
if (matchGlob(rule.match, id)) {
|
|
2188
|
+
result.set(id, rule.module);
|
|
2189
|
+
matched = true;
|
|
2190
|
+
break;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
if (matched) continue;
|
|
2194
|
+
matched = false;
|
|
2195
|
+
for (const [convDir, moduleNames] of cachedConventionDirs) {
|
|
2196
|
+
if (id.startsWith(convDir + "/")) {
|
|
2197
|
+
const rest = id.slice(convDir.length + 1);
|
|
2198
|
+
const firstSeg = rest.split("/")[0];
|
|
2199
|
+
if (moduleNames.includes(firstSeg)) {
|
|
2200
|
+
result.set(id, firstSeg);
|
|
2201
|
+
matched = true;
|
|
2202
|
+
break;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
if (matched) continue;
|
|
2207
|
+
const module2 = extractModuleFromPath(id);
|
|
2208
|
+
result.set(id, module2);
|
|
2209
|
+
}
|
|
2210
|
+
return result;
|
|
2211
|
+
}
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
});
|
|
2215
|
+
|
|
2216
|
+
// src/server/graph/taggers/screen-tagger.ts
|
|
2217
|
+
var SCREEN_TYPES, screenTagger;
|
|
2218
|
+
var init_screen_tagger = __esm({
|
|
2219
|
+
"src/server/graph/taggers/screen-tagger.ts"() {
|
|
2220
|
+
"use strict";
|
|
2221
|
+
SCREEN_TYPES = /* @__PURE__ */ new Set(["page", "layout"]);
|
|
2222
|
+
screenTagger = {
|
|
2223
|
+
id: "screen",
|
|
2224
|
+
tagKey: "screen",
|
|
2225
|
+
trackUntagged: true,
|
|
2226
|
+
layers: ["ui"],
|
|
2227
|
+
tag(nodes, layer) {
|
|
2228
|
+
if (layer !== "ui") return /* @__PURE__ */ new Map();
|
|
2229
|
+
const result = /* @__PURE__ */ new Map();
|
|
2230
|
+
for (const node of nodes) {
|
|
2231
|
+
if (SCREEN_TYPES.has(node.type)) {
|
|
2232
|
+
result.set(node.id, "true");
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
return result;
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
|
|
2241
|
+
// src/server/graph/core/tagger-registry.ts
|
|
2242
|
+
function registerBuiltins2(registry, disabled, config) {
|
|
2243
|
+
for (const tagger of BUILTIN_TAGGERS) {
|
|
2244
|
+
if (disabled.has(tagger.id)) continue;
|
|
2245
|
+
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
2246
|
+
if (override !== void 0) {
|
|
2247
|
+
tagger.trackUntagged = override;
|
|
2248
|
+
}
|
|
2249
|
+
registry.register(tagger);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
function loadCustomTaggers(registry, config, rootDir, disabled) {
|
|
2253
|
+
for (const entry of config.taggers?.custom ?? []) {
|
|
2254
|
+
if (disabled.has(entry.id)) continue;
|
|
2255
|
+
try {
|
|
2256
|
+
const absPath = (0, import_node_path12.resolve)(rootDir, entry.path);
|
|
2257
|
+
const mod = require(absPath);
|
|
2258
|
+
const tagger = "default" in mod ? mod.default : mod;
|
|
2259
|
+
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
2260
|
+
if (override !== void 0) {
|
|
2261
|
+
tagger.trackUntagged = override;
|
|
2262
|
+
}
|
|
2263
|
+
registry.register(tagger);
|
|
2264
|
+
} catch (err2) {
|
|
2265
|
+
process.stderr.write(`[launch-chart] failed to load custom tagger from ${entry.path}: ${err2}
|
|
2266
|
+
`);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
function createTaggerRegistry(config, rootDir) {
|
|
2271
|
+
const registry = new TaggerRegistry();
|
|
2272
|
+
const disabled = new Set(config.taggers?.disabled ?? []);
|
|
2273
|
+
registerBuiltins2(registry, disabled, config);
|
|
2274
|
+
loadCustomTaggers(registry, config, rootDir, disabled);
|
|
2275
|
+
return registry;
|
|
2276
|
+
}
|
|
2277
|
+
var import_node_path12, TaggerRegistry, BUILTIN_TAGGERS;
|
|
2278
|
+
var init_tagger_registry = __esm({
|
|
2279
|
+
"src/server/graph/core/tagger-registry.ts"() {
|
|
2280
|
+
"use strict";
|
|
2281
|
+
import_node_path12 = require("node:path");
|
|
2282
|
+
init_module_tagger();
|
|
2283
|
+
init_screen_tagger();
|
|
2284
|
+
TaggerRegistry = class {
|
|
2285
|
+
constructor() {
|
|
2286
|
+
this.taggers = [];
|
|
2287
|
+
this.ids = /* @__PURE__ */ new Set();
|
|
2288
|
+
}
|
|
2289
|
+
register(tagger) {
|
|
2290
|
+
if (this.ids.has(tagger.id)) {
|
|
2291
|
+
throw new Error(`Duplicate tagger id: ${tagger.id}`);
|
|
2292
|
+
}
|
|
2293
|
+
this.ids.add(tagger.id);
|
|
2294
|
+
this.taggers.push(tagger);
|
|
2295
|
+
}
|
|
2296
|
+
getAll() {
|
|
2297
|
+
return this.taggers;
|
|
2298
|
+
}
|
|
2299
|
+
getForLayer(layer) {
|
|
2300
|
+
return this.taggers.filter((t) => t.layers === null || t.layers.includes(layer));
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2303
|
+
BUILTIN_TAGGERS = [moduleTagger, screenTagger];
|
|
2304
|
+
}
|
|
2305
|
+
});
|
|
2306
|
+
|
|
2307
|
+
// src/server/graph/core/tag-store.ts
|
|
2308
|
+
function tagsFilePath(rootDir) {
|
|
2309
|
+
return (0, import_node_path13.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
|
|
2310
|
+
}
|
|
2311
|
+
function readTagStore(rootDir) {
|
|
2312
|
+
const filePath = tagsFilePath(rootDir);
|
|
2313
|
+
if (!(0, import_node_fs11.existsSync)(filePath)) return {};
|
|
2314
|
+
const stat = (0, import_node_fs11.statSync)(filePath);
|
|
2315
|
+
const cached = tagCache.get(filePath);
|
|
2316
|
+
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
2317
|
+
return cached.store;
|
|
2318
|
+
}
|
|
2319
|
+
try {
|
|
2320
|
+
const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
|
|
2321
|
+
const store = JSON.parse(content);
|
|
2322
|
+
tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
|
|
2323
|
+
return store;
|
|
2324
|
+
} catch {
|
|
2325
|
+
return {};
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
function writeTagStore(rootDir, store) {
|
|
2329
|
+
const filePath = tagsFilePath(rootDir);
|
|
2330
|
+
const dir = (0, import_node_path13.dirname)(filePath);
|
|
2331
|
+
(0, import_node_fs11.mkdirSync)(dir, { recursive: true });
|
|
2332
|
+
const cleaned = {};
|
|
2333
|
+
for (const [nodeId, tags] of Object.entries(store)) {
|
|
2334
|
+
if (Object.keys(tags).length > 0) {
|
|
2335
|
+
cleaned[nodeId] = tags;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
(0, import_node_fs11.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
|
|
2339
|
+
tagCache.delete(filePath);
|
|
2340
|
+
}
|
|
2341
|
+
function setTag(rootDir, nodeId, key, value) {
|
|
2342
|
+
const store = readTagStore(rootDir);
|
|
2343
|
+
if (!store[nodeId]) store[nodeId] = {};
|
|
2344
|
+
store[nodeId][key] = value;
|
|
2345
|
+
writeTagStore(rootDir, store);
|
|
2346
|
+
}
|
|
2347
|
+
function removeTag(rootDir, nodeId, key) {
|
|
2348
|
+
const store = readTagStore(rootDir);
|
|
2349
|
+
if (!store[nodeId]) return;
|
|
2350
|
+
delete store[nodeId][key];
|
|
2351
|
+
if (Object.keys(store[nodeId]).length === 0) {
|
|
2352
|
+
delete store[nodeId];
|
|
2353
|
+
}
|
|
2354
|
+
writeTagStore(rootDir, store);
|
|
2355
|
+
}
|
|
2356
|
+
var import_node_fs11, import_node_path13, TAGS_FILENAME, GRAPHS_DIR, tagCache;
|
|
2357
|
+
var init_tag_store = __esm({
|
|
2358
|
+
"src/server/graph/core/tag-store.ts"() {
|
|
2359
|
+
"use strict";
|
|
2360
|
+
import_node_fs11 = require("node:fs");
|
|
2361
|
+
import_node_path13 = require("node:path");
|
|
2362
|
+
TAGS_FILENAME = "tags.json";
|
|
1552
2363
|
GRAPHS_DIR = ".launchsecure/graphs";
|
|
2364
|
+
tagCache = /* @__PURE__ */ new Map();
|
|
2365
|
+
}
|
|
2366
|
+
});
|
|
2367
|
+
|
|
2368
|
+
// src/server/graph/index.ts
|
|
2369
|
+
function graphsDir(rootDir) {
|
|
2370
|
+
return (0, import_node_path14.join)(rootDir, GRAPHS_DIR2);
|
|
2371
|
+
}
|
|
2372
|
+
function graphFilePath(rootDir, layer) {
|
|
2373
|
+
return (0, import_node_path14.join)(graphsDir(rootDir), `${layer}.json`);
|
|
2374
|
+
}
|
|
2375
|
+
function tagsFilePath2(rootDir) {
|
|
2376
|
+
return (0, import_node_path14.join)(graphsDir(rootDir), "tags.json");
|
|
2377
|
+
}
|
|
2378
|
+
function getMtimeMs(filePath) {
|
|
2379
|
+
if (!(0, import_node_fs12.existsSync)(filePath)) return 0;
|
|
2380
|
+
return (0, import_node_fs12.statSync)(filePath).mtimeMs;
|
|
2381
|
+
}
|
|
2382
|
+
function invalidateCache(filePath) {
|
|
2383
|
+
graphCache.delete(filePath);
|
|
2384
|
+
}
|
|
2385
|
+
function invalidateTaggedCache(rootDir, layer) {
|
|
2386
|
+
taggedCache.delete(`${rootDir}:${layer}`);
|
|
2387
|
+
}
|
|
2388
|
+
function applyTags(graph, layer, rootDir) {
|
|
2389
|
+
const config = loadConfig(rootDir);
|
|
2390
|
+
const registry = createTaggerRegistry(config, rootDir);
|
|
2391
|
+
const manualTags = readTagStore(rootDir);
|
|
2392
|
+
const taggedNodes = graph.nodes.map((n) => ({ ...n }));
|
|
2393
|
+
const taggers = registry.getForLayer(layer);
|
|
2394
|
+
for (const tagger of taggers) {
|
|
2395
|
+
const assignments = tagger.tag(taggedNodes, layer, rootDir);
|
|
2396
|
+
for (const node of taggedNodes) {
|
|
2397
|
+
if (!node.tags) node.tags = {};
|
|
2398
|
+
const tags = node.tags;
|
|
2399
|
+
const value = assignments.get(node.id);
|
|
2400
|
+
if (value !== void 0) {
|
|
2401
|
+
tags[tagger.tagKey] = value;
|
|
2402
|
+
} else if (tagger.trackUntagged) {
|
|
2403
|
+
tags[tagger.tagKey] = "untagged";
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
for (const node of taggedNodes) {
|
|
2408
|
+
const manual = manualTags[node.id];
|
|
2409
|
+
if (manual) {
|
|
2410
|
+
if (!node.tags) node.tags = {};
|
|
2411
|
+
const tags = node.tags;
|
|
2412
|
+
Object.assign(tags, manual);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
return { ...graph, nodes: taggedNodes };
|
|
2416
|
+
}
|
|
2417
|
+
function readGraphRaw(rootDir, layer) {
|
|
2418
|
+
const filePath = graphFilePath(rootDir, layer);
|
|
2419
|
+
if (!(0, import_node_fs12.existsSync)(filePath)) return null;
|
|
2420
|
+
const stat = (0, import_node_fs12.statSync)(filePath);
|
|
2421
|
+
const cached = graphCache.get(filePath);
|
|
2422
|
+
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
2423
|
+
return cached.graph;
|
|
2424
|
+
}
|
|
2425
|
+
const content = (0, import_node_fs12.readFileSync)(filePath, "utf-8");
|
|
2426
|
+
const graph = JSON.parse(content);
|
|
2427
|
+
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
2428
|
+
return graph;
|
|
2429
|
+
}
|
|
2430
|
+
function readGraph(rootDir, layer) {
|
|
2431
|
+
const rawFilePath = graphFilePath(rootDir, layer);
|
|
2432
|
+
if (!(0, import_node_fs12.existsSync)(rawFilePath)) return null;
|
|
2433
|
+
const rawMtime = getMtimeMs(rawFilePath);
|
|
2434
|
+
const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
|
|
2435
|
+
const cacheKey = `${rootDir}:${layer}`;
|
|
2436
|
+
const cached = taggedCache.get(cacheKey);
|
|
2437
|
+
if (cached && cached.rawMtimeMs === rawMtime && cached.tagsMtimeMs === tagsMtime) {
|
|
2438
|
+
return cached.graph;
|
|
2439
|
+
}
|
|
2440
|
+
const raw = readGraphRaw(rootDir, layer);
|
|
2441
|
+
if (!raw) return null;
|
|
2442
|
+
const tagged = applyTags(raw, layer, rootDir);
|
|
2443
|
+
taggedCache.set(cacheKey, { rawMtimeMs: rawMtime, tagsMtimeMs: tagsMtime, graph: tagged });
|
|
2444
|
+
return tagged;
|
|
2445
|
+
}
|
|
2446
|
+
function readAllGraphs(rootDir) {
|
|
2447
|
+
const result = {};
|
|
2448
|
+
for (const layer of LAYERS) {
|
|
2449
|
+
const graph = readGraph(rootDir, layer);
|
|
2450
|
+
if (graph) result[layer] = graph;
|
|
2451
|
+
}
|
|
2452
|
+
return result;
|
|
2453
|
+
}
|
|
2454
|
+
function generateGraph(rootDir, layer) {
|
|
2455
|
+
const dir = graphsDir(rootDir);
|
|
2456
|
+
(0, import_node_fs12.mkdirSync)(dir, { recursive: true });
|
|
2457
|
+
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
2458
|
+
for (const result of results) {
|
|
2459
|
+
const filePath = graphFilePath(rootDir, result.layer);
|
|
2460
|
+
(0, import_node_fs12.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
2461
|
+
invalidateCache(filePath);
|
|
2462
|
+
invalidateTaggedCache(rootDir, result.layer);
|
|
2463
|
+
}
|
|
2464
|
+
return results;
|
|
2465
|
+
}
|
|
2466
|
+
var import_node_fs12, import_node_path14, GRAPHS_DIR2, LAYERS, graphCache, taggedCache;
|
|
2467
|
+
var init_graph = __esm({
|
|
2468
|
+
"src/server/graph/index.ts"() {
|
|
2469
|
+
"use strict";
|
|
2470
|
+
import_node_fs12 = require("node:fs");
|
|
2471
|
+
import_node_path14 = require("node:path");
|
|
2472
|
+
init_graph_builder();
|
|
2473
|
+
init_config();
|
|
2474
|
+
init_tagger_registry();
|
|
2475
|
+
init_tag_store();
|
|
2476
|
+
init_tag_store();
|
|
2477
|
+
GRAPHS_DIR2 = ".launchsecure/graphs";
|
|
1553
2478
|
LAYERS = ["ui", "api", "db"];
|
|
1554
2479
|
graphCache = /* @__PURE__ */ new Map();
|
|
2480
|
+
taggedCache = /* @__PURE__ */ new Map();
|
|
1555
2481
|
}
|
|
1556
2482
|
});
|
|
1557
2483
|
|
|
@@ -1561,19 +2487,22 @@ __export(chart_serve_exports, {
|
|
|
1561
2487
|
runServeCli: () => runServeCli,
|
|
1562
2488
|
startChartServer: () => startChartServer
|
|
1563
2489
|
});
|
|
2490
|
+
function randomPort() {
|
|
2491
|
+
return 49152 + Math.floor(Math.random() * (65535 - 49152));
|
|
2492
|
+
}
|
|
1564
2493
|
function findProjectRoot(startDir) {
|
|
1565
2494
|
let dir = startDir;
|
|
1566
2495
|
for (let i = 0; i < 8; i++) {
|
|
1567
|
-
const graphsDir2 =
|
|
1568
|
-
if (
|
|
1569
|
-
const parent =
|
|
2496
|
+
const graphsDir2 = import_node_path15.default.join(dir, ".launchsecure", "graphs");
|
|
2497
|
+
if (import_node_fs13.default.existsSync(import_node_path15.default.join(graphsDir2, "ui.json")) || import_node_fs13.default.existsSync(import_node_path15.default.join(graphsDir2, "api.json")) || import_node_fs13.default.existsSync(import_node_path15.default.join(graphsDir2, "db.json"))) return dir;
|
|
2498
|
+
const parent = import_node_path15.default.dirname(dir);
|
|
1570
2499
|
if (parent === dir) break;
|
|
1571
2500
|
dir = parent;
|
|
1572
2501
|
}
|
|
1573
2502
|
dir = startDir;
|
|
1574
2503
|
for (let i = 0; i < 8; i++) {
|
|
1575
|
-
if (
|
|
1576
|
-
const parent =
|
|
2504
|
+
if (import_node_fs13.default.existsSync(import_node_path15.default.join(dir, ".git"))) return dir;
|
|
2505
|
+
const parent = import_node_path15.default.dirname(dir);
|
|
1577
2506
|
if (parent === dir) break;
|
|
1578
2507
|
dir = parent;
|
|
1579
2508
|
}
|
|
@@ -1625,16 +2554,16 @@ function buildMergedGraph(projectRoot) {
|
|
|
1625
2554
|
};
|
|
1626
2555
|
}
|
|
1627
2556
|
function serveStatic(res, filePath) {
|
|
1628
|
-
if (!
|
|
1629
|
-
const ext =
|
|
2557
|
+
if (!import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) return false;
|
|
2558
|
+
const ext = import_node_path15.default.extname(filePath).toLowerCase();
|
|
1630
2559
|
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
1631
2560
|
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
1632
|
-
|
|
2561
|
+
import_node_fs13.default.createReadStream(filePath).pipe(res);
|
|
1633
2562
|
return true;
|
|
1634
2563
|
}
|
|
1635
2564
|
function serveIndex(res, clientDir) {
|
|
1636
|
-
const indexPath =
|
|
1637
|
-
if (!
|
|
2565
|
+
const indexPath = import_node_path15.default.join(clientDir, "index.html");
|
|
2566
|
+
if (!import_node_fs13.default.existsSync(indexPath)) {
|
|
1638
2567
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1639
2568
|
res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
|
|
1640
2569
|
return;
|
|
@@ -1642,14 +2571,14 @@ function serveIndex(res, clientDir) {
|
|
|
1642
2571
|
serveStatic(res, indexPath);
|
|
1643
2572
|
}
|
|
1644
2573
|
function tryListen(server, port) {
|
|
1645
|
-
return new Promise((
|
|
2574
|
+
return new Promise((resolve3, reject) => {
|
|
1646
2575
|
const onError = (err2) => {
|
|
1647
2576
|
server.off("listening", onListening);
|
|
1648
2577
|
reject(err2);
|
|
1649
2578
|
};
|
|
1650
2579
|
const onListening = () => {
|
|
1651
2580
|
server.off("error", onError);
|
|
1652
|
-
|
|
2581
|
+
resolve3(port);
|
|
1653
2582
|
};
|
|
1654
2583
|
server.once("error", onError);
|
|
1655
2584
|
server.once("listening", onListening);
|
|
@@ -1676,7 +2605,7 @@ async function bindWithFallback(server, startPort) {
|
|
|
1676
2605
|
async function startChartServer(opts = {}) {
|
|
1677
2606
|
const cwd = opts.cwd ?? process.cwd();
|
|
1678
2607
|
const projectRoot = findProjectRoot(cwd);
|
|
1679
|
-
const existing = getLiveLock();
|
|
2608
|
+
const existing = getLiveLock(projectRoot);
|
|
1680
2609
|
if (existing) {
|
|
1681
2610
|
if (!opts.quiet) {
|
|
1682
2611
|
process.stderr.write(
|
|
@@ -1686,7 +2615,7 @@ async function startChartServer(opts = {}) {
|
|
|
1686
2615
|
}
|
|
1687
2616
|
return { port: existing.port, url: existing.url };
|
|
1688
2617
|
}
|
|
1689
|
-
const clientDir = opts.clientDir ??
|
|
2618
|
+
const clientDir = opts.clientDir ?? import_node_path15.default.join(__dirname, "..", "chart-client");
|
|
1690
2619
|
const server = import_node_http.default.createServer((req, res) => {
|
|
1691
2620
|
try {
|
|
1692
2621
|
const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
@@ -1724,13 +2653,154 @@ async function startChartServer(opts = {}) {
|
|
|
1724
2653
|
}
|
|
1725
2654
|
return;
|
|
1726
2655
|
}
|
|
2656
|
+
if (req.method === "GET" && url2.pathname === "/api/file-content") {
|
|
2657
|
+
const relPath = url2.searchParams.get("path");
|
|
2658
|
+
if (!relPath || relPath.includes("..") || import_node_path15.default.isAbsolute(relPath)) {
|
|
2659
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2660
|
+
res.end(JSON.stringify({ error: "Invalid path" }));
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
const filePath = import_node_path15.default.join(projectRoot, relPath);
|
|
2664
|
+
if (!filePath.startsWith(projectRoot) || !import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) {
|
|
2665
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2666
|
+
res.end(JSON.stringify({ error: "File not found" }));
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
const ext = import_node_path15.default.extname(filePath).toLowerCase();
|
|
2670
|
+
const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
|
|
2671
|
+
const content = import_node_fs13.default.readFileSync(filePath, "utf-8");
|
|
2672
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2673
|
+
res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
1727
2676
|
if (req.method === "GET" && url2.pathname === "/api/health") {
|
|
1728
2677
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1729
2678
|
res.end(JSON.stringify({ ok: true, projectRoot }));
|
|
1730
2679
|
return;
|
|
1731
2680
|
}
|
|
2681
|
+
if (req.method === "GET" && url2.pathname === "/api/parser-config") {
|
|
2682
|
+
const config = loadConfig(projectRoot);
|
|
2683
|
+
const detection = [
|
|
2684
|
+
{ id: "react-nextjs", layer: "ui", label: "React + Next.js", detected: reactNextjsParser.detect(projectRoot) },
|
|
2685
|
+
{ id: "nextjs-routes", layer: "api", label: "Next.js API Routes", detected: nextjsRoutesParser.detect(projectRoot) },
|
|
2686
|
+
{ id: "prisma-schema", layer: "db", label: "Prisma Schema", detected: prismaSchemaParser.detect(projectRoot) }
|
|
2687
|
+
];
|
|
2688
|
+
const crosslayerParsers = [
|
|
2689
|
+
{ id: "fetch-resolver", label: "Fetch / api.method() calls" },
|
|
2690
|
+
{ id: "api-annotations", label: "@api annotations" },
|
|
2691
|
+
{ id: "url-literal-scanner", label: "/api/... URL literals" }
|
|
2692
|
+
];
|
|
2693
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2694
|
+
res.end(JSON.stringify({ config, detection, crosslayerParsers }));
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
if (req.method === "POST" && url2.pathname === "/api/parser-config") {
|
|
2698
|
+
let body = "";
|
|
2699
|
+
req.on("data", (chunk) => {
|
|
2700
|
+
body += chunk.toString();
|
|
2701
|
+
});
|
|
2702
|
+
req.on("end", () => {
|
|
2703
|
+
try {
|
|
2704
|
+
const newConfig = JSON.parse(body);
|
|
2705
|
+
const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
|
|
2706
|
+
import_node_fs13.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
|
|
2707
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2708
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2709
|
+
} catch (err2) {
|
|
2710
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2711
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
2712
|
+
}
|
|
2713
|
+
});
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
|
|
2717
|
+
const config = loadConfig(projectRoot);
|
|
2718
|
+
const builtinTaggers = [
|
|
2719
|
+
{ id: "module", tagKey: "module", trackUntagged: config.taggers?.trackUntagged?.module ?? true },
|
|
2720
|
+
{ id: "screen", tagKey: "screen", trackUntagged: config.taggers?.trackUntagged?.screen ?? true }
|
|
2721
|
+
];
|
|
2722
|
+
const disabled = config.taggers?.disabled ?? [];
|
|
2723
|
+
const customTaggers = config.taggers?.custom ?? [];
|
|
2724
|
+
const moduleRules = config.taggers?.module?.rules ?? [];
|
|
2725
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2726
|
+
res.end(JSON.stringify({ builtinTaggers, disabled, customTaggers, moduleRules }));
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
if (req.method === "POST" && url2.pathname === "/api/tagger-config") {
|
|
2730
|
+
let body = "";
|
|
2731
|
+
req.on("data", (chunk) => {
|
|
2732
|
+
body += chunk.toString();
|
|
2733
|
+
});
|
|
2734
|
+
req.on("end", () => {
|
|
2735
|
+
try {
|
|
2736
|
+
const taggerConfig = JSON.parse(body);
|
|
2737
|
+
const config = loadConfig(projectRoot);
|
|
2738
|
+
config.taggers = taggerConfig;
|
|
2739
|
+
const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
|
|
2740
|
+
import_node_fs13.default.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2741
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2742
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2743
|
+
} catch (err2) {
|
|
2744
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2745
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
2746
|
+
}
|
|
2747
|
+
});
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
if (req.method === "GET" && url2.pathname === "/api/tags") {
|
|
2751
|
+
const store = readTagStore(projectRoot);
|
|
2752
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2753
|
+
res.end(JSON.stringify(store));
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
if (req.method === "POST" && url2.pathname === "/api/tags") {
|
|
2757
|
+
let body = "";
|
|
2758
|
+
req.on("data", (chunk) => {
|
|
2759
|
+
body += chunk.toString();
|
|
2760
|
+
});
|
|
2761
|
+
req.on("end", () => {
|
|
2762
|
+
try {
|
|
2763
|
+
const { nodeId, key, value } = JSON.parse(body);
|
|
2764
|
+
if (!nodeId || !key || !value) {
|
|
2765
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2766
|
+
res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
setTag(projectRoot, nodeId, key, value);
|
|
2770
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2771
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2772
|
+
} catch (err2) {
|
|
2773
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2774
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
2775
|
+
}
|
|
2776
|
+
});
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
if (req.method === "DELETE" && url2.pathname === "/api/tags") {
|
|
2780
|
+
let body = "";
|
|
2781
|
+
req.on("data", (chunk) => {
|
|
2782
|
+
body += chunk.toString();
|
|
2783
|
+
});
|
|
2784
|
+
req.on("end", () => {
|
|
2785
|
+
try {
|
|
2786
|
+
const { nodeId, key } = JSON.parse(body);
|
|
2787
|
+
if (!nodeId || !key) {
|
|
2788
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2789
|
+
res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
removeTag(projectRoot, nodeId, key);
|
|
2793
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2794
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2795
|
+
} catch (err2) {
|
|
2796
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2797
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
2798
|
+
}
|
|
2799
|
+
});
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
1732
2802
|
if (url2.pathname !== "/") {
|
|
1733
|
-
const staticPath =
|
|
2803
|
+
const staticPath = import_node_path15.default.join(clientDir, url2.pathname);
|
|
1734
2804
|
if (serveStatic(res, staticPath)) return;
|
|
1735
2805
|
}
|
|
1736
2806
|
serveIndex(res, clientDir);
|
|
@@ -1739,7 +2809,8 @@ async function startChartServer(opts = {}) {
|
|
|
1739
2809
|
res.end(JSON.stringify({ error: String(err2) }));
|
|
1740
2810
|
}
|
|
1741
2811
|
});
|
|
1742
|
-
const
|
|
2812
|
+
const startPort = opts.port ?? randomPort();
|
|
2813
|
+
const port = await bindWithFallback(server, startPort);
|
|
1743
2814
|
const url = `http://localhost:${port}`;
|
|
1744
2815
|
writeLock({
|
|
1745
2816
|
pid: process.pid,
|
|
@@ -1747,9 +2818,9 @@ async function startChartServer(opts = {}) {
|
|
|
1747
2818
|
cwd,
|
|
1748
2819
|
url,
|
|
1749
2820
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1750
|
-
});
|
|
2821
|
+
}, projectRoot);
|
|
1751
2822
|
const cleanup = () => {
|
|
1752
|
-
clearLock();
|
|
2823
|
+
clearLock(projectRoot);
|
|
1753
2824
|
server.close();
|
|
1754
2825
|
};
|
|
1755
2826
|
process.once("SIGINT", () => {
|
|
@@ -1784,17 +2855,20 @@ function runServeCli(argv) {
|
|
|
1784
2855
|
process.exit(1);
|
|
1785
2856
|
});
|
|
1786
2857
|
}
|
|
1787
|
-
var import_node_http,
|
|
2858
|
+
var import_node_http, import_node_fs13, import_node_path15, MAX_PORT_SCAN, MIME_TYPES;
|
|
1788
2859
|
var init_chart_serve = __esm({
|
|
1789
2860
|
"src/server/chart-serve.ts"() {
|
|
1790
2861
|
"use strict";
|
|
1791
2862
|
import_node_http = __toESM(require("node:http"));
|
|
1792
|
-
|
|
1793
|
-
|
|
2863
|
+
import_node_fs13 = __toESM(require("node:fs"));
|
|
2864
|
+
import_node_path15 = __toESM(require("node:path"));
|
|
1794
2865
|
init_graph();
|
|
1795
2866
|
init_lockfile();
|
|
1796
|
-
|
|
1797
|
-
|
|
2867
|
+
init_config();
|
|
2868
|
+
init_react_nextjs();
|
|
2869
|
+
init_nextjs_routes();
|
|
2870
|
+
init_prisma_schema();
|
|
2871
|
+
MAX_PORT_SCAN = 3;
|
|
1798
2872
|
MIME_TYPES = {
|
|
1799
2873
|
".html": "text/html; charset=utf-8",
|
|
1800
2874
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -1825,7 +2899,7 @@ function matchesSearch(node, query) {
|
|
|
1825
2899
|
function toMinimal(nodes) {
|
|
1826
2900
|
return nodes.map((n) => {
|
|
1827
2901
|
const out = { id: n.id, type: n.type, name: n.name };
|
|
1828
|
-
if (n.
|
|
2902
|
+
if (n.tags != null) out.tags = n.tags;
|
|
1829
2903
|
if (n.route != null) out.route = n.route;
|
|
1830
2904
|
if (n.methods != null) out.methods = n.methods;
|
|
1831
2905
|
return out;
|
|
@@ -1833,11 +2907,13 @@ function toMinimal(nodes) {
|
|
|
1833
2907
|
}
|
|
1834
2908
|
function toCompactNode(n) {
|
|
1835
2909
|
const out = { i: n.id, t: n.type, n: n.name };
|
|
1836
|
-
|
|
2910
|
+
const tags = n.tags;
|
|
2911
|
+
if (tags?.module) out.m = tags.module;
|
|
1837
2912
|
if (n.route != null) out.r = n.route;
|
|
1838
2913
|
if (n.methods != null) out.mt = n.methods;
|
|
1839
2914
|
if (n.exports != null) out.x = n.exports;
|
|
1840
2915
|
if (n.columns != null) out.c = n.columns;
|
|
2916
|
+
if (tags != null) out.tg = tags;
|
|
1841
2917
|
for (const k of Object.keys(n)) {
|
|
1842
2918
|
if (!COMPACT_NODE_KNOWN_KEYS.has(k) && n[k] != null) out[k] = n[k];
|
|
1843
2919
|
}
|
|
@@ -1913,7 +2989,8 @@ function layerSummary(graph) {
|
|
|
1913
2989
|
const moduleCounts = {};
|
|
1914
2990
|
for (const n of graph.nodes) {
|
|
1915
2991
|
typeCounts[n.type] = (typeCounts[n.type] ?? 0) + 1;
|
|
1916
|
-
const
|
|
2992
|
+
const tags = n.tags;
|
|
2993
|
+
const mod = tags?.module;
|
|
1917
2994
|
if (mod) moduleCounts[mod] = (moduleCounts[mod] ?? 0) + 1;
|
|
1918
2995
|
}
|
|
1919
2996
|
const edgeTypeCounts = {};
|
|
@@ -1970,12 +3047,14 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
1970
3047
|
const search = args.search;
|
|
1971
3048
|
const type = args.type;
|
|
1972
3049
|
const module_ = args.module;
|
|
3050
|
+
const tagKey = args.tag_key;
|
|
3051
|
+
const tagValue = args.tag_value;
|
|
1973
3052
|
const nodeId = args.node_id;
|
|
1974
3053
|
const hops = args.hops ?? 1;
|
|
1975
3054
|
const layerIsDb = args.layer === "db";
|
|
1976
3055
|
const minimal = args.minimal ?? layerIsDb;
|
|
1977
3056
|
const includeEdges = args.include_edges;
|
|
1978
|
-
const hasFilter = !!(search || type || module_ || nodeId);
|
|
3057
|
+
const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
|
|
1979
3058
|
if (layer && !["ui", "api", "db"].includes(layer)) {
|
|
1980
3059
|
return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
|
|
1981
3060
|
}
|
|
@@ -2031,7 +3110,9 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
2031
3110
|
const matched = graph.nodes.filter((n) => {
|
|
2032
3111
|
if (search && !matchesSearch(n, search)) return false;
|
|
2033
3112
|
if (type && n.type !== type) return false;
|
|
2034
|
-
|
|
3113
|
+
const nodeTags = n.tags;
|
|
3114
|
+
if (module_ && nodeTags?.module !== module_) return false;
|
|
3115
|
+
if (tagKey && tagValue && nodeTags?.[tagKey] !== tagValue) return false;
|
|
2035
3116
|
return true;
|
|
2036
3117
|
});
|
|
2037
3118
|
const matchedIds = new Set(matched.map((n) => n.id));
|
|
@@ -2118,9 +3199,9 @@ function handleReadGraph(args) {
|
|
|
2118
3199
|
return okJson(result);
|
|
2119
3200
|
}
|
|
2120
3201
|
function nodeToFilePath(rootDir, layer, nodeId) {
|
|
2121
|
-
if (layer === "ui") return (0,
|
|
2122
|
-
if (layer === "api") return (0,
|
|
2123
|
-
if (layer === "db") return (0,
|
|
3202
|
+
if (layer === "ui") return (0, import_node_path16.join)(rootDir, "src", nodeId);
|
|
3203
|
+
if (layer === "api") return (0, import_node_path16.join)(rootDir, nodeId);
|
|
3204
|
+
if (layer === "db") return (0, import_node_path16.join)(rootDir, "prisma", "schema.prisma");
|
|
2124
3205
|
return null;
|
|
2125
3206
|
}
|
|
2126
3207
|
function handleGrepNodes(args) {
|
|
@@ -2180,11 +3261,11 @@ function handleGrepNodes(args) {
|
|
|
2180
3261
|
let filesSearched = 0;
|
|
2181
3262
|
let truncated = false;
|
|
2182
3263
|
for (const [filePath, nodeId] of filePaths) {
|
|
2183
|
-
if (!(0,
|
|
3264
|
+
if (!(0, import_node_fs14.existsSync)(filePath)) continue;
|
|
2184
3265
|
filesSearched++;
|
|
2185
3266
|
let content;
|
|
2186
3267
|
try {
|
|
2187
|
-
content = (0,
|
|
3268
|
+
content = (0, import_node_fs14.readFileSync)(filePath, "utf-8");
|
|
2188
3269
|
} catch {
|
|
2189
3270
|
continue;
|
|
2190
3271
|
}
|
|
@@ -2221,13 +3302,11 @@ function handleGrepNodes(args) {
|
|
|
2221
3302
|
truncated
|
|
2222
3303
|
});
|
|
2223
3304
|
}
|
|
2224
|
-
function
|
|
2225
|
-
const
|
|
3305
|
+
function handleChartServerStatus() {
|
|
3306
|
+
const rootDir = process.cwd();
|
|
3307
|
+
const lock = getLiveLock(rootDir);
|
|
2226
3308
|
if (!lock) {
|
|
2227
|
-
return okJson({
|
|
2228
|
-
running: false,
|
|
2229
|
-
hint: "No launch-chart UI server is currently running. Start one with `launch-chart serve`, or set LAUNCH_CHART_AUTOSERVE=1 in your MCP config to auto-start it alongside the MCP server."
|
|
2230
|
-
});
|
|
3309
|
+
return okJson({ running: false });
|
|
2231
3310
|
}
|
|
2232
3311
|
return okJson({
|
|
2233
3312
|
running: true,
|
|
@@ -2238,6 +3317,146 @@ function handleGetGraphUiUrl() {
|
|
|
2238
3317
|
startedAt: lock.startedAt
|
|
2239
3318
|
});
|
|
2240
3319
|
}
|
|
3320
|
+
function handleStartChartServer(args) {
|
|
3321
|
+
const rootDir = process.cwd();
|
|
3322
|
+
const lock = getLiveLock(rootDir);
|
|
3323
|
+
if (lock) {
|
|
3324
|
+
return okJson({
|
|
3325
|
+
started: false,
|
|
3326
|
+
reason: "already_running",
|
|
3327
|
+
url: lock.url,
|
|
3328
|
+
port: lock.port,
|
|
3329
|
+
pid: lock.pid
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
const entryPath = process.argv[1];
|
|
3333
|
+
const logDir = (0, import_node_path16.join)((0, import_node_os2.homedir)(), ".launchsecure");
|
|
3334
|
+
(0, import_node_fs14.mkdirSync)(logDir, { recursive: true });
|
|
3335
|
+
const logPath = (0, import_node_path16.join)(logDir, "launch-chart.log");
|
|
3336
|
+
const out = (0, import_node_fs14.openSync)(logPath, "a");
|
|
3337
|
+
const err2 = (0, import_node_fs14.openSync)(logPath, "a");
|
|
3338
|
+
const portArgs = args.port ? ["--port", String(args.port)] : [];
|
|
3339
|
+
const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
|
|
3340
|
+
detached: true,
|
|
3341
|
+
stdio: ["ignore", out, err2],
|
|
3342
|
+
env: { ...process.env, LAUNCH_CHART_AUTOSERVE: "" }
|
|
3343
|
+
});
|
|
3344
|
+
child.unref();
|
|
3345
|
+
return okJson({
|
|
3346
|
+
started: true,
|
|
3347
|
+
pid: child.pid,
|
|
3348
|
+
logPath
|
|
3349
|
+
});
|
|
3350
|
+
}
|
|
3351
|
+
function handleStopChartServer() {
|
|
3352
|
+
const rootDir = process.cwd();
|
|
3353
|
+
const lock = getLiveLock(rootDir);
|
|
3354
|
+
if (!lock) {
|
|
3355
|
+
return okJson({ stopped: false, reason: "not_running" });
|
|
3356
|
+
}
|
|
3357
|
+
try {
|
|
3358
|
+
process.kill(lock.pid, "SIGTERM");
|
|
3359
|
+
return okJson({ stopped: true, pid: lock.pid });
|
|
3360
|
+
} catch (e) {
|
|
3361
|
+
const code = e.code;
|
|
3362
|
+
if (code === "ESRCH") {
|
|
3363
|
+
clearLock(rootDir);
|
|
3364
|
+
return okJson({ stopped: true, pid: lock.pid, note: "process was already gone, lock cleaned up" });
|
|
3365
|
+
}
|
|
3366
|
+
return okJson({ stopped: false, reason: `kill failed: ${code ?? e}` });
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
function handleAddTag(args) {
|
|
3370
|
+
const rootDir = process.cwd();
|
|
3371
|
+
const nodeId = args.node_id;
|
|
3372
|
+
const key = args.key;
|
|
3373
|
+
const value = args.value;
|
|
3374
|
+
if (!nodeId) return err("node_id is required");
|
|
3375
|
+
if (!key) return err("key is required");
|
|
3376
|
+
if (!value) return err("value is required");
|
|
3377
|
+
const graphs = readAllGraphs(rootDir);
|
|
3378
|
+
let found = false;
|
|
3379
|
+
for (const graph of Object.values(graphs)) {
|
|
3380
|
+
if (graph && graph.nodes.some((n) => n.id === nodeId)) {
|
|
3381
|
+
found = true;
|
|
3382
|
+
break;
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
if (!found) {
|
|
3386
|
+
return err(`Node "${nodeId}" not found in any graph layer. Check the node_id.`);
|
|
3387
|
+
}
|
|
3388
|
+
setTag(rootDir, nodeId, key, value);
|
|
3389
|
+
return okJson({ ok: true, node_id: nodeId, tag: { [key]: value } });
|
|
3390
|
+
}
|
|
3391
|
+
function handleRemoveTag(args) {
|
|
3392
|
+
const rootDir = process.cwd();
|
|
3393
|
+
const nodeId = args.node_id;
|
|
3394
|
+
const key = args.key;
|
|
3395
|
+
if (!nodeId) return err("node_id is required");
|
|
3396
|
+
if (!key) return err("key is required");
|
|
3397
|
+
removeTag(rootDir, nodeId, key);
|
|
3398
|
+
return okJson({ ok: true, node_id: nodeId, removed_key: key });
|
|
3399
|
+
}
|
|
3400
|
+
function handleDetectProjectStack() {
|
|
3401
|
+
const rootDir = process.cwd();
|
|
3402
|
+
const parsers = [
|
|
3403
|
+
{ id: "react-nextjs", layer: "ui", detected: reactNextjsParser.detect(rootDir) },
|
|
3404
|
+
{ id: "nextjs-routes", layer: "api", detected: nextjsRoutesParser.detect(rootDir) },
|
|
3405
|
+
{ id: "prisma-schema", layer: "db", detected: prismaSchemaParser.detect(rootDir) }
|
|
3406
|
+
];
|
|
3407
|
+
const config = loadConfig(rootDir);
|
|
3408
|
+
let stats = { calls_api: 0, references_api: 0, out_of_pattern: 0, annotations: 0 };
|
|
3409
|
+
const uiGraph = readGraph(rootDir, "ui");
|
|
3410
|
+
if (uiGraph) {
|
|
3411
|
+
for (const ref of uiGraph.cross_refs ?? []) {
|
|
3412
|
+
if (ref.type === "calls_api") stats.calls_api++;
|
|
3413
|
+
if (ref.type === "references_api") stats.references_api++;
|
|
3414
|
+
}
|
|
3415
|
+
for (const f of uiGraph.flagged_edges ?? []) {
|
|
3416
|
+
if (f.type === "out_of_pattern") stats.out_of_pattern++;
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
const srcDir = (0, import_node_path16.join)(rootDir, "src");
|
|
3420
|
+
if ((0, import_node_fs14.existsSync)(srcDir)) {
|
|
3421
|
+
const scanDir = (dir) => {
|
|
3422
|
+
if (!(0, import_node_fs14.existsSync)(dir)) return;
|
|
3423
|
+
for (const entry of (0, import_node_fs14.readdirSync)(dir, { withFileTypes: true })) {
|
|
3424
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
3425
|
+
const full = (0, import_node_path16.join)(dir, entry.name);
|
|
3426
|
+
if (entry.isDirectory()) {
|
|
3427
|
+
scanDir(full);
|
|
3428
|
+
continue;
|
|
3429
|
+
}
|
|
3430
|
+
if (![".ts", ".tsx"].includes((0, import_node_path16.extname)(entry.name))) continue;
|
|
3431
|
+
try {
|
|
3432
|
+
const content = (0, import_node_fs14.readFileSync)(full, "utf-8");
|
|
3433
|
+
const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
|
|
3434
|
+
if (matches) stats.annotations += matches.length;
|
|
3435
|
+
} catch {
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
};
|
|
3439
|
+
scanDir(srcDir);
|
|
3440
|
+
}
|
|
3441
|
+
let recommendedPrimary = "fetch-resolver";
|
|
3442
|
+
if (stats.annotations > 0 && stats.annotations >= stats.calls_api) {
|
|
3443
|
+
recommendedPrimary = "api-annotations";
|
|
3444
|
+
} else if (stats.calls_api === 0 && stats.references_api > 0) {
|
|
3445
|
+
recommendedPrimary = "url-literal-scanner";
|
|
3446
|
+
}
|
|
3447
|
+
return okJson({
|
|
3448
|
+
parsers,
|
|
3449
|
+
crosslayer_parsers: [
|
|
3450
|
+
{ id: "fetch-resolver", description: "Detects direct fetch()/api.get() calls with inline URLs" },
|
|
3451
|
+
{ id: "api-annotations", description: "Scans for @api METHOD /path annotations in JSDoc/comments" },
|
|
3452
|
+
{ id: "url-literal-scanner", description: "Finds /api/... string literals as fallback detection" }
|
|
3453
|
+
],
|
|
3454
|
+
stats,
|
|
3455
|
+
recommended_primary: recommendedPrimary,
|
|
3456
|
+
current_config: Object.keys(config).length > 0 ? config : null,
|
|
3457
|
+
config_path: ".launchchart.json"
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
2241
3460
|
function send(msg) {
|
|
2242
3461
|
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
2243
3462
|
}
|
|
@@ -2281,8 +3500,28 @@ function handleMessage(msg) {
|
|
|
2281
3500
|
respond(id ?? null, handleGrepNodes(args));
|
|
2282
3501
|
return;
|
|
2283
3502
|
}
|
|
2284
|
-
if (toolName === "
|
|
2285
|
-
respond(id ?? null,
|
|
3503
|
+
if (toolName === "chart_server_status") {
|
|
3504
|
+
respond(id ?? null, handleChartServerStatus());
|
|
3505
|
+
return;
|
|
3506
|
+
}
|
|
3507
|
+
if (toolName === "start_chart_server") {
|
|
3508
|
+
respond(id ?? null, handleStartChartServer(args));
|
|
3509
|
+
return;
|
|
3510
|
+
}
|
|
3511
|
+
if (toolName === "stop_chart_server") {
|
|
3512
|
+
respond(id ?? null, handleStopChartServer());
|
|
3513
|
+
return;
|
|
3514
|
+
}
|
|
3515
|
+
if (toolName === "detect_project_stack") {
|
|
3516
|
+
respond(id ?? null, handleDetectProjectStack());
|
|
3517
|
+
return;
|
|
3518
|
+
}
|
|
3519
|
+
if (toolName === "add_tag") {
|
|
3520
|
+
respond(id ?? null, handleAddTag(args));
|
|
3521
|
+
return;
|
|
3522
|
+
}
|
|
3523
|
+
if (toolName === "remove_tag") {
|
|
3524
|
+
respond(id ?? null, handleRemoveTag(args));
|
|
2286
3525
|
return;
|
|
2287
3526
|
}
|
|
2288
3527
|
respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
|
|
@@ -2320,14 +3559,20 @@ function startGraphMcpServer() {
|
|
|
2320
3559
|
process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
|
|
2321
3560
|
`);
|
|
2322
3561
|
}
|
|
2323
|
-
var
|
|
3562
|
+
var import_node_fs14, import_node_path16, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
|
|
2324
3563
|
var init_graph_mcp = __esm({
|
|
2325
3564
|
"src/server/graph-mcp.ts"() {
|
|
2326
3565
|
"use strict";
|
|
2327
|
-
|
|
2328
|
-
|
|
3566
|
+
import_node_fs14 = require("node:fs");
|
|
3567
|
+
import_node_path16 = require("node:path");
|
|
3568
|
+
import_node_child_process2 = require("node:child_process");
|
|
3569
|
+
import_node_os2 = require("node:os");
|
|
2329
3570
|
init_graph();
|
|
2330
3571
|
init_lockfile();
|
|
3572
|
+
init_config();
|
|
3573
|
+
init_react_nextjs();
|
|
3574
|
+
init_nextjs_routes();
|
|
3575
|
+
init_prisma_schema();
|
|
2331
3576
|
SERVER_INFO = {
|
|
2332
3577
|
name: "launchsecure-graph",
|
|
2333
3578
|
version: "0.0.1"
|
|
@@ -2349,7 +3594,7 @@ var init_graph_mcp = __esm({
|
|
|
2349
3594
|
},
|
|
2350
3595
|
{
|
|
2351
3596
|
name: "read_graph",
|
|
2352
|
-
description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module (
|
|
3597
|
+
description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module tag (computed from directory structure, e.g. "auth", "admin", "settings")\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
|
|
2353
3598
|
inputSchema: {
|
|
2354
3599
|
type: "object",
|
|
2355
3600
|
properties: {
|
|
@@ -2368,7 +3613,15 @@ var init_graph_mcp = __esm({
|
|
|
2368
3613
|
},
|
|
2369
3614
|
module: {
|
|
2370
3615
|
type: "string",
|
|
2371
|
-
description: '
|
|
3616
|
+
description: 'Filter by module tag (e.g. "auth", "admin", "settings"). Works across all layers.'
|
|
3617
|
+
},
|
|
3618
|
+
tag_key: {
|
|
3619
|
+
type: "string",
|
|
3620
|
+
description: "Filter by arbitrary tag key. Must be used with tag_value."
|
|
3621
|
+
},
|
|
3622
|
+
tag_value: {
|
|
3623
|
+
type: "string",
|
|
3624
|
+
description: "Filter by tag value for the given tag_key."
|
|
2372
3625
|
},
|
|
2373
3626
|
node_id: {
|
|
2374
3627
|
type: "string",
|
|
@@ -2455,12 +3708,83 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
|
|
|
2455
3708
|
}
|
|
2456
3709
|
},
|
|
2457
3710
|
{
|
|
2458
|
-
name: "
|
|
2459
|
-
description:
|
|
3711
|
+
name: "chart_server_status",
|
|
3712
|
+
description: `Check whether the launch-chart UI server is running. Returns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }.
|
|
3713
|
+
|
|
3714
|
+
Use this when the user asks "is the chart running", "show me the project graph UI", "where's the chart", etc.`,
|
|
3715
|
+
inputSchema: {
|
|
3716
|
+
type: "object",
|
|
3717
|
+
properties: {}
|
|
3718
|
+
}
|
|
3719
|
+
},
|
|
3720
|
+
{
|
|
3721
|
+
name: "start_chart_server",
|
|
3722
|
+
description: 'Start the launch-chart UI server as a detached background process. The server serves the interactive project graph visualization at http://localhost:<port>. If the server is already running, returns the existing URL without spawning a duplicate. \n\nUse this when the user asks to "start the chart", "fire up charts", "open the graph UI", etc.',
|
|
3723
|
+
inputSchema: {
|
|
3724
|
+
type: "object",
|
|
3725
|
+
properties: {
|
|
3726
|
+
port: {
|
|
3727
|
+
type: "number",
|
|
3728
|
+
description: "Port to bind the server to. Defaults to 52819 with automatic fallback if in use."
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
},
|
|
3733
|
+
{
|
|
3734
|
+
name: "stop_chart_server",
|
|
3735
|
+
description: 'Stop the running launch-chart UI server. Sends SIGTERM to the server process and cleans up the lock file. If no server is running, returns a no-op response. \n\nUse this when the user asks to "stop the chart", "cool down charts", "kill the graph server", etc.',
|
|
2460
3736
|
inputSchema: {
|
|
2461
3737
|
type: "object",
|
|
2462
3738
|
properties: {}
|
|
2463
3739
|
}
|
|
3740
|
+
},
|
|
3741
|
+
{
|
|
3742
|
+
name: "detect_project_stack",
|
|
3743
|
+
description: "Detect project frameworks, available parsers, and recommend parser configuration. Scans the project to identify the tech stack (Next.js, Prisma, React, etc.), reports which built-in parsers are applicable, and provides cross-layer detection stats (fetch calls, @api annotations, URL literals). Returns a recommended primary parser and current .launchchart.json config if present. \n\nUse this when setting up launch-chart for a new project or reviewing parser configuration.",
|
|
3744
|
+
inputSchema: {
|
|
3745
|
+
type: "object",
|
|
3746
|
+
properties: {}
|
|
3747
|
+
}
|
|
3748
|
+
},
|
|
3749
|
+
{
|
|
3750
|
+
name: "add_tag",
|
|
3751
|
+
description: 'Tag a graph node with a key-value pair. Tags persist in .launchsecure/graphs/tags.json and survive graph regeneration. Use for annotating nodes with arbitrary metadata (e.g. "refactor_later", "owner", "priority"). Manual tags override computed tags (like module and screen) for the same key.',
|
|
3752
|
+
inputSchema: {
|
|
3753
|
+
type: "object",
|
|
3754
|
+
properties: {
|
|
3755
|
+
node_id: {
|
|
3756
|
+
type: "string",
|
|
3757
|
+
description: 'The node id to tag (e.g. "app/(auth)/login/page.tsx").'
|
|
3758
|
+
},
|
|
3759
|
+
key: {
|
|
3760
|
+
type: "string",
|
|
3761
|
+
description: 'Tag key (e.g. "module", "owner", "refactor_later").'
|
|
3762
|
+
},
|
|
3763
|
+
value: {
|
|
3764
|
+
type: "string",
|
|
3765
|
+
description: 'Tag value (e.g. "auth", "alice", "true").'
|
|
3766
|
+
}
|
|
3767
|
+
},
|
|
3768
|
+
required: ["node_id", "key", "value"]
|
|
3769
|
+
}
|
|
3770
|
+
},
|
|
3771
|
+
{
|
|
3772
|
+
name: "remove_tag",
|
|
3773
|
+
description: "Remove a manual tag from a graph node. Only removes tags from tags.json \u2014 computed tags (module, screen) cannot be removed (they are re-derived at read time).",
|
|
3774
|
+
inputSchema: {
|
|
3775
|
+
type: "object",
|
|
3776
|
+
properties: {
|
|
3777
|
+
node_id: {
|
|
3778
|
+
type: "string",
|
|
3779
|
+
description: "The node id to remove the tag from."
|
|
3780
|
+
},
|
|
3781
|
+
key: {
|
|
3782
|
+
type: "string",
|
|
3783
|
+
description: "Tag key to remove."
|
|
3784
|
+
}
|
|
3785
|
+
},
|
|
3786
|
+
required: ["node_id", "key"]
|
|
3787
|
+
}
|
|
2464
3788
|
}
|
|
2465
3789
|
];
|
|
2466
3790
|
COMPACT_SCHEMA = {
|
|
@@ -2468,11 +3792,12 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
|
|
|
2468
3792
|
i: "id",
|
|
2469
3793
|
t: "type",
|
|
2470
3794
|
n: "name",
|
|
2471
|
-
m: "module",
|
|
3795
|
+
m: "module (from tags)",
|
|
2472
3796
|
r: "route",
|
|
2473
3797
|
mt: "methods",
|
|
2474
3798
|
x: "exports",
|
|
2475
|
-
c: "columns"
|
|
3799
|
+
c: "columns",
|
|
3800
|
+
tg: "tags"
|
|
2476
3801
|
},
|
|
2477
3802
|
edges: {
|
|
2478
3803
|
s: "source_node_index",
|
|
@@ -2490,7 +3815,8 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
|
|
|
2490
3815
|
"route",
|
|
2491
3816
|
"methods",
|
|
2492
3817
|
"exports",
|
|
2493
|
-
"columns"
|
|
3818
|
+
"columns",
|
|
3819
|
+
"tags"
|
|
2494
3820
|
]);
|
|
2495
3821
|
EST_CHARS_PER_NODE_FULL = {
|
|
2496
3822
|
ui: 300,
|
|
@@ -2513,11 +3839,11 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
|
|
|
2513
3839
|
});
|
|
2514
3840
|
|
|
2515
3841
|
// src/server/graph-mcp-entry.ts
|
|
2516
|
-
var
|
|
2517
|
-
var
|
|
2518
|
-
var
|
|
2519
|
-
var
|
|
2520
|
-
var
|
|
3842
|
+
var import_node_child_process3 = require("node:child_process");
|
|
3843
|
+
var import_node_fs15 = require("node:fs");
|
|
3844
|
+
var import_node_path17 = __toESM(require("node:path"));
|
|
3845
|
+
var import_node_os3 = require("node:os");
|
|
3846
|
+
var import_node_fs16 = require("node:fs");
|
|
2521
3847
|
init_lockfile();
|
|
2522
3848
|
function logStderr(msg) {
|
|
2523
3849
|
process.stderr.write(`[launch-chart] ${msg}
|
|
@@ -2531,13 +3857,13 @@ function maybeAutoServe() {
|
|
|
2531
3857
|
return;
|
|
2532
3858
|
}
|
|
2533
3859
|
try {
|
|
2534
|
-
const logDir =
|
|
2535
|
-
(0,
|
|
2536
|
-
const logPath =
|
|
2537
|
-
const out = (0,
|
|
2538
|
-
const err2 = (0,
|
|
3860
|
+
const logDir = import_node_path17.default.join((0, import_node_os3.homedir)(), ".launchsecure");
|
|
3861
|
+
(0, import_node_fs16.mkdirSync)(logDir, { recursive: true });
|
|
3862
|
+
const logPath = import_node_path17.default.join(logDir, "launch-chart.log");
|
|
3863
|
+
const out = (0, import_node_fs15.openSync)(logPath, "a");
|
|
3864
|
+
const err2 = (0, import_node_fs15.openSync)(logPath, "a");
|
|
2539
3865
|
const entryPath = process.argv[1];
|
|
2540
|
-
const child = (0,
|
|
3866
|
+
const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
|
|
2541
3867
|
detached: true,
|
|
2542
3868
|
stdio: ["ignore", out, err2],
|
|
2543
3869
|
env: { ...process.env, LAUNCH_CHART_AUTOSERVE: "" }
|