@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
|
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
8
11
|
var __export = (target, all) => {
|
|
9
12
|
for (var name in all)
|
|
10
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -27,6 +30,30 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
30
|
));
|
|
28
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
32
|
|
|
33
|
+
// src/server/graph/core/config.ts
|
|
34
|
+
var config_exports = {};
|
|
35
|
+
__export(config_exports, {
|
|
36
|
+
loadConfig: () => loadConfig
|
|
37
|
+
});
|
|
38
|
+
function loadConfig(rootDir) {
|
|
39
|
+
const configPath = (0, import_node_path.join)(rootDir, CONFIG_FILENAME);
|
|
40
|
+
if (!(0, import_node_fs.existsSync)(configPath)) return {};
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
|
|
43
|
+
} catch {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
var import_node_fs, import_node_path, CONFIG_FILENAME;
|
|
48
|
+
var init_config = __esm({
|
|
49
|
+
"src/server/graph/core/config.ts"() {
|
|
50
|
+
"use strict";
|
|
51
|
+
import_node_fs = require("node:fs");
|
|
52
|
+
import_node_path = require("node:path");
|
|
53
|
+
CONFIG_FILENAME = ".launchchart.json";
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
30
57
|
// src/server/chart-serve.ts
|
|
31
58
|
var chart_serve_exports = {};
|
|
32
59
|
__export(chart_serve_exports, {
|
|
@@ -35,20 +62,28 @@ __export(chart_serve_exports, {
|
|
|
35
62
|
});
|
|
36
63
|
module.exports = __toCommonJS(chart_serve_exports);
|
|
37
64
|
var import_node_http = __toESM(require("node:http"));
|
|
38
|
-
var
|
|
39
|
-
var
|
|
65
|
+
var import_node_fs13 = __toESM(require("node:fs"));
|
|
66
|
+
var import_node_path15 = __toESM(require("node:path"));
|
|
40
67
|
|
|
41
68
|
// src/server/graph/index.ts
|
|
42
|
-
var
|
|
43
|
-
var
|
|
69
|
+
var import_node_fs11 = require("node:fs");
|
|
70
|
+
var import_node_path13 = require("node:path");
|
|
71
|
+
|
|
72
|
+
// src/server/graph/core/graph-builder.ts
|
|
73
|
+
var import_node_fs8 = require("node:fs");
|
|
74
|
+
var import_node_path9 = require("node:path");
|
|
75
|
+
init_config();
|
|
76
|
+
|
|
77
|
+
// src/server/graph/core/parser-registry.ts
|
|
78
|
+
var import_node_path8 = require("node:path");
|
|
44
79
|
|
|
45
80
|
// src/server/graph/parsers/ui/react-nextjs.ts
|
|
46
|
-
var
|
|
47
|
-
var
|
|
81
|
+
var import_node_fs3 = require("node:fs");
|
|
82
|
+
var import_node_path3 = require("node:path");
|
|
48
83
|
|
|
49
84
|
// src/server/graph/core/ast-helpers.ts
|
|
50
|
-
var
|
|
51
|
-
var
|
|
85
|
+
var import_node_fs2 = require("node:fs");
|
|
86
|
+
var import_node_path2 = require("node:path");
|
|
52
87
|
var tsModule;
|
|
53
88
|
function getTs() {
|
|
54
89
|
if (!tsModule) {
|
|
@@ -59,8 +94,8 @@ function getTs() {
|
|
|
59
94
|
var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
60
95
|
function parseFile(absPath) {
|
|
61
96
|
const ts = getTs();
|
|
62
|
-
const content = (0,
|
|
63
|
-
const ext = (0,
|
|
97
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
98
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
64
99
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
|
|
65
100
|
const sourceFile = ts.createSourceFile(
|
|
66
101
|
absPath,
|
|
@@ -338,8 +373,8 @@ var MUTATION_METHODS = /* @__PURE__ */ new Set([
|
|
|
338
373
|
]);
|
|
339
374
|
function extractDbCalls(absPath) {
|
|
340
375
|
const ts = getTs();
|
|
341
|
-
const content = (0,
|
|
342
|
-
const ext = (0,
|
|
376
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
377
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
343
378
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
344
379
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
345
380
|
const calls = [];
|
|
@@ -368,8 +403,8 @@ function extractDbCalls(absPath) {
|
|
|
368
403
|
}
|
|
369
404
|
function extractAuthWrappers(absPath) {
|
|
370
405
|
const ts = getTs();
|
|
371
|
-
const content = (0,
|
|
372
|
-
const ext = (0,
|
|
406
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
407
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
373
408
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
374
409
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
375
410
|
const wrappers = /* @__PURE__ */ new Set();
|
|
@@ -390,12 +425,12 @@ function extractAuthWrappers(absPath) {
|
|
|
390
425
|
var RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
|
|
391
426
|
function walk(dir, exts) {
|
|
392
427
|
const results = [];
|
|
393
|
-
if (!(0,
|
|
394
|
-
for (const entry of (0,
|
|
395
|
-
const full = (0,
|
|
428
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return results;
|
|
429
|
+
for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
430
|
+
const full = (0, import_node_path3.join)(dir, entry.name);
|
|
396
431
|
if (entry.isDirectory()) {
|
|
397
432
|
results.push(...walk(full, exts));
|
|
398
|
-
} else if (exts.includes((0,
|
|
433
|
+
} else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
|
|
399
434
|
results.push(full);
|
|
400
435
|
}
|
|
401
436
|
}
|
|
@@ -403,33 +438,33 @@ function walk(dir, exts) {
|
|
|
403
438
|
}
|
|
404
439
|
function walkWithIgnore(dir, exts, ignoreDirs) {
|
|
405
440
|
const results = [];
|
|
406
|
-
if (!(0,
|
|
407
|
-
for (const entry of (0,
|
|
441
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return results;
|
|
442
|
+
for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
408
443
|
if (entry.isDirectory()) {
|
|
409
444
|
if (ignoreDirs.has(entry.name)) continue;
|
|
410
|
-
results.push(...walkWithIgnore((0,
|
|
411
|
-
} else if (exts.includes((0,
|
|
412
|
-
results.push((0,
|
|
445
|
+
results.push(...walkWithIgnore((0, import_node_path3.join)(dir, entry.name), exts, ignoreDirs));
|
|
446
|
+
} else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
|
|
447
|
+
results.push((0, import_node_path3.join)(dir, entry.name));
|
|
413
448
|
}
|
|
414
449
|
}
|
|
415
450
|
return results;
|
|
416
451
|
}
|
|
417
452
|
function toNodeId(srcDir, absPath) {
|
|
418
|
-
return (0,
|
|
453
|
+
return (0, import_node_path3.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
419
454
|
}
|
|
420
455
|
function resolveImport(srcDir, specifier) {
|
|
421
456
|
if (!specifier.startsWith("@/")) return null;
|
|
422
457
|
const rel = specifier.slice(2);
|
|
423
|
-
const base = (0,
|
|
424
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
425
|
-
if ((0,
|
|
458
|
+
const base = (0, import_node_path3.join)(srcDir, rel);
|
|
459
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path3.join)(base, "index.ts"), (0, import_node_path3.join)(base, "index.tsx")]) {
|
|
460
|
+
if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
|
|
426
461
|
}
|
|
427
462
|
return null;
|
|
428
463
|
}
|
|
429
464
|
function resolveRelativeImport(fromFile, specifier) {
|
|
430
|
-
const base = (0,
|
|
431
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
432
|
-
if ((0,
|
|
465
|
+
const base = (0, import_node_path3.join)((0, import_node_path3.dirname)(fromFile), specifier);
|
|
466
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path3.join)(base, "index.ts"), (0, import_node_path3.join)(base, "index.tsx")]) {
|
|
467
|
+
if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
|
|
433
468
|
}
|
|
434
469
|
return null;
|
|
435
470
|
}
|
|
@@ -450,7 +485,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
|
|
|
450
485
|
const resolved = resolveRelativeImport(barrelAbsPath, re.from);
|
|
451
486
|
if (!resolved) continue;
|
|
452
487
|
if (re.isWildcard) {
|
|
453
|
-
const targetBn = (0,
|
|
488
|
+
const targetBn = (0, import_node_path3.basename)(resolved);
|
|
454
489
|
const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
|
|
455
490
|
if (targetIsBarrel) {
|
|
456
491
|
const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
|
|
@@ -477,12 +512,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
|
477
512
|
const barrels = /* @__PURE__ */ new Map();
|
|
478
513
|
const memo = /* @__PURE__ */ new Map();
|
|
479
514
|
for (const [absPath, parsed] of parsedByPath) {
|
|
480
|
-
const bn = (0,
|
|
515
|
+
const bn = (0, import_node_path3.basename)(absPath);
|
|
481
516
|
if (bn !== "index.ts" && bn !== "index.tsx") continue;
|
|
482
517
|
if (parsed.reExports.length === 0) continue;
|
|
483
518
|
const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
|
|
484
519
|
if (map.size > 0) {
|
|
485
|
-
const barrelId = (0,
|
|
520
|
+
const barrelId = (0, import_node_path3.relative)(srcDir, (0, import_node_path3.dirname)(absPath)).replace(/\\/g, "/");
|
|
486
521
|
barrels.set(barrelId, map);
|
|
487
522
|
}
|
|
488
523
|
}
|
|
@@ -503,34 +538,6 @@ function classifyType(id) {
|
|
|
503
538
|
if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
|
|
504
539
|
return "component";
|
|
505
540
|
}
|
|
506
|
-
function classifyModule(id) {
|
|
507
|
-
if (/app\/\(auth\)\//.test(id)) return "auth";
|
|
508
|
-
if (/app\/\(admin\)\//.test(id)) return "admin";
|
|
509
|
-
if (/app\/\(settings\)\//.test(id)) return "settings";
|
|
510
|
-
if (/app\/\(app\)\/\[orgSlug\]\/\(project-pages\)\//.test(id)) return "project";
|
|
511
|
-
if (/app\/\(app\)\/\[orgSlug\]\/\(org-pages\)\//.test(id)) return "org";
|
|
512
|
-
if (/app\/\(app\)\/\[orgSlug\]\//.test(id)) return "org";
|
|
513
|
-
if (id.startsWith("app/integrations/")) return "integrations";
|
|
514
|
-
if (id.startsWith("app/docs/")) return "admin";
|
|
515
|
-
if (id.startsWith("client/components/ui/")) return "shared-ui";
|
|
516
|
-
if (id.startsWith("client/components/layout/") || /client\/lib\/navigation/.test(id)) return "layout";
|
|
517
|
-
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";
|
|
518
|
-
if (/client\/components\/prd-/.test(id) || /client\/hooks\/use-admin/.test(id)) return "admin";
|
|
519
|
-
if (/client\/components\/org-/.test(id) || /client\/hooks\/use-org-/.test(id) || /client\/hooks\/use-provider-def/.test(id)) return "org";
|
|
520
|
-
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";
|
|
521
|
-
if (/client\/hooks\/use-(profile|sessions|organizations|notification)/.test(id)) return "settings";
|
|
522
|
-
if (id.startsWith("server/auth/")) return "auth";
|
|
523
|
-
if (id.startsWith("server/mcp/")) return "mcp";
|
|
524
|
-
if (id.startsWith("server/lib/")) return "server-lib";
|
|
525
|
-
if (id.startsWith("server/middleware")) return "middleware";
|
|
526
|
-
if (id.startsWith("server/services/")) return "services";
|
|
527
|
-
if (id.startsWith("server/db")) return "db";
|
|
528
|
-
if (id.startsWith("server/errors")) return "errors";
|
|
529
|
-
if (id.startsWith("server/")) return "server-lib";
|
|
530
|
-
if (id.startsWith("config/")) return "config";
|
|
531
|
-
if (id.startsWith("lib/")) return "lib";
|
|
532
|
-
return "root";
|
|
533
|
-
}
|
|
534
541
|
function extractRoute(id) {
|
|
535
542
|
if (!id.endsWith("/page.tsx")) return null;
|
|
536
543
|
let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
|
|
@@ -541,7 +548,7 @@ function extractRoute(id) {
|
|
|
541
548
|
return route || "/";
|
|
542
549
|
}
|
|
543
550
|
function nameFromFilename(absPath) {
|
|
544
|
-
return (0,
|
|
551
|
+
return (0, import_node_path3.basename)(absPath, (0, import_node_path3.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
|
|
545
552
|
}
|
|
546
553
|
function resolveTemplateLiteralRoute(template, routeToNodeId) {
|
|
547
554
|
const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
@@ -622,105 +629,6 @@ function matchRouteToPage(route, routeToNodeId) {
|
|
|
622
629
|
if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
|
|
623
630
|
return null;
|
|
624
631
|
}
|
|
625
|
-
function loadApiRoutes(rootDir) {
|
|
626
|
-
const apiJsonPath = (0, import_node_path2.join)(rootDir, ".launchsecure", "graphs", "api.json");
|
|
627
|
-
if (!(0, import_node_fs2.existsSync)(apiJsonPath)) return [];
|
|
628
|
-
try {
|
|
629
|
-
const parsed = JSON.parse((0, import_node_fs2.readFileSync)(apiJsonPath, "utf-8"));
|
|
630
|
-
const routes = [];
|
|
631
|
-
for (const n of parsed.nodes ?? []) {
|
|
632
|
-
const path2 = n.path;
|
|
633
|
-
if (!path2 || typeof path2 !== "string") continue;
|
|
634
|
-
routes.push({
|
|
635
|
-
path: path2,
|
|
636
|
-
nodeId: n.id,
|
|
637
|
-
segments: path2.split("/").filter(Boolean)
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
return routes;
|
|
641
|
-
} catch {
|
|
642
|
-
return [];
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
function buildApiPathMap(routes) {
|
|
646
|
-
const map = /* @__PURE__ */ new Map();
|
|
647
|
-
for (const r of routes) {
|
|
648
|
-
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
649
|
-
}
|
|
650
|
-
return map;
|
|
651
|
-
}
|
|
652
|
-
function normalizeFetchUrl(raw) {
|
|
653
|
-
let s = raw.replace(/^`|`$/g, "");
|
|
654
|
-
const qIdx = s.indexOf("?");
|
|
655
|
-
if (qIdx >= 0) s = s.slice(0, qIdx);
|
|
656
|
-
const hIdx = s.indexOf("#");
|
|
657
|
-
if (hIdx >= 0) s = s.slice(0, hIdx);
|
|
658
|
-
let hadInterpolation = false;
|
|
659
|
-
s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
660
|
-
hadInterpolation = true;
|
|
661
|
-
const cleaned = expr.trim();
|
|
662
|
-
const last = cleaned.split(".").pop() ?? cleaned;
|
|
663
|
-
const name = last.replace(/[^\w]/g, "") || "param";
|
|
664
|
-
return ":" + name;
|
|
665
|
-
});
|
|
666
|
-
s = s.replace(/\/+/g, "/");
|
|
667
|
-
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
668
|
-
return { path: s || "/", hadInterpolation };
|
|
669
|
-
}
|
|
670
|
-
function scoreApiRouteMatch(candidate, known) {
|
|
671
|
-
if (candidate.length !== known.length) return -1;
|
|
672
|
-
let score = 0;
|
|
673
|
-
for (let i = 0; i < candidate.length; i++) {
|
|
674
|
-
const a = candidate[i];
|
|
675
|
-
const b = known[i];
|
|
676
|
-
if (a === b) {
|
|
677
|
-
score += 3;
|
|
678
|
-
continue;
|
|
679
|
-
}
|
|
680
|
-
if (a.startsWith(":") && b.startsWith(":")) {
|
|
681
|
-
score += 2;
|
|
682
|
-
continue;
|
|
683
|
-
}
|
|
684
|
-
if (a.startsWith(":") || b.startsWith(":")) {
|
|
685
|
-
score += 1;
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
return -1;
|
|
689
|
-
}
|
|
690
|
-
return score;
|
|
691
|
-
}
|
|
692
|
-
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
693
|
-
const raw = call.url;
|
|
694
|
-
if (/^(https?:)?\/\//i.test(raw)) {
|
|
695
|
-
return { kind: "external", normalizedUrl: raw };
|
|
696
|
-
}
|
|
697
|
-
if (call.isConcat) {
|
|
698
|
-
return { kind: "dynamic", normalizedUrl: raw };
|
|
699
|
-
}
|
|
700
|
-
const { path: path2, hadInterpolation } = normalizeFetchUrl(raw);
|
|
701
|
-
if (!path2.startsWith("/")) {
|
|
702
|
-
return { kind: "unresolved", normalizedUrl: path2 };
|
|
703
|
-
}
|
|
704
|
-
const segs = path2.split("/").filter(Boolean);
|
|
705
|
-
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
706
|
-
return { kind: "dynamic", normalizedUrl: path2 };
|
|
707
|
-
}
|
|
708
|
-
const exact = apiPathMap.get(path2);
|
|
709
|
-
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path2 };
|
|
710
|
-
let bestScore = -1;
|
|
711
|
-
let bestId = null;
|
|
712
|
-
for (const r of apiRoutes) {
|
|
713
|
-
const score = scoreApiRouteMatch(segs, r.segments);
|
|
714
|
-
if (score > bestScore) {
|
|
715
|
-
bestScore = score;
|
|
716
|
-
bestId = r.nodeId;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
if (bestId && bestScore > 0) {
|
|
720
|
-
return { kind: "resolved", nodeId: bestId, normalizedUrl: path2 };
|
|
721
|
-
}
|
|
722
|
-
return { kind: "unresolved", normalizedUrl: path2 };
|
|
723
|
-
}
|
|
724
632
|
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
|
|
725
633
|
const edges = [];
|
|
726
634
|
const flagged = [];
|
|
@@ -820,26 +728,26 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
820
728
|
return { edges, flagged };
|
|
821
729
|
}
|
|
822
730
|
function detect(rootDir) {
|
|
823
|
-
return (0,
|
|
731
|
+
return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "src", "app")) && (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.ts")) || (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.js")) || (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.mjs"));
|
|
824
732
|
}
|
|
825
733
|
function generate(rootDir) {
|
|
826
|
-
const srcDir = (0,
|
|
827
|
-
const appFiles = walk((0,
|
|
828
|
-
(f) => (0,
|
|
734
|
+
const srcDir = (0, import_node_path3.join)(rootDir, "src");
|
|
735
|
+
const appFiles = walk((0, import_node_path3.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
|
|
736
|
+
(f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
|
|
829
737
|
);
|
|
830
|
-
const clientFiles = walk((0,
|
|
831
|
-
const serverFiles = walk((0,
|
|
832
|
-
(f) => (0,
|
|
738
|
+
const clientFiles = walk((0, import_node_path3.join)(srcDir, "client"), [".tsx", ".ts"]);
|
|
739
|
+
const serverFiles = walk((0, import_node_path3.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
|
|
740
|
+
(f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
|
|
833
741
|
);
|
|
834
|
-
const libFiles = walk((0,
|
|
835
|
-
const configFiles = walk((0,
|
|
742
|
+
const libFiles = walk((0, import_node_path3.join)(srcDir, "lib"), [".ts", ".tsx"]);
|
|
743
|
+
const configFiles = walk((0, import_node_path3.join)(srcDir, "config"), [".ts", ".tsx"]);
|
|
836
744
|
const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
|
|
837
745
|
const parsedByPath = /* @__PURE__ */ new Map();
|
|
838
746
|
for (const absPath of allDiscovered) {
|
|
839
747
|
parsedByPath.set(absPath, parseFile(absPath));
|
|
840
748
|
}
|
|
841
749
|
const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
|
|
842
|
-
const fileSet = allDiscovered.filter((f) => !(0,
|
|
750
|
+
const fileSet = allDiscovered.filter((f) => !(0, import_node_path3.basename)(f).startsWith("index."));
|
|
843
751
|
const nodes = [];
|
|
844
752
|
const nodeIdSet = /* @__PURE__ */ new Set();
|
|
845
753
|
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
@@ -850,15 +758,13 @@ function generate(rootDir) {
|
|
|
850
758
|
const parsed = parsedByPath.get(absPath);
|
|
851
759
|
const name = parsed.name || nameFromFilename(absPath);
|
|
852
760
|
const route = extractRoute(id);
|
|
853
|
-
|
|
854
|
-
nodes.push({ id, type, name, route, module: module_, exports: parsed.exports });
|
|
761
|
+
nodes.push({ id, type, name, route, exports: parsed.exports });
|
|
855
762
|
nodeIdSet.add(id);
|
|
856
763
|
nodeTypeMap.set(id, type);
|
|
857
764
|
if (route) routeToNodeId.set(route, id);
|
|
858
765
|
}
|
|
859
766
|
const allEdges = [];
|
|
860
767
|
const allFlagged = [];
|
|
861
|
-
const crossRefs = [];
|
|
862
768
|
for (const absPath of fileSet) {
|
|
863
769
|
const sourceId = toNodeId(srcDir, absPath);
|
|
864
770
|
const parsed = parsedByPath.get(absPath);
|
|
@@ -875,66 +781,21 @@ function generate(rootDir) {
|
|
|
875
781
|
allEdges.push(...edges);
|
|
876
782
|
allFlagged.push(...flagged);
|
|
877
783
|
}
|
|
878
|
-
const
|
|
879
|
-
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
880
|
-
const includeExternalFetches = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
|
|
881
|
-
const fetchSeen = /* @__PURE__ */ new Set();
|
|
882
|
-
let fetchResolvedCount = 0;
|
|
883
|
-
let fetchDynamicCount = 0;
|
|
884
|
-
let fetchUnresolvedCount = 0;
|
|
885
|
-
let fetchExternalCount = 0;
|
|
784
|
+
const fetchCallEntries = [];
|
|
886
785
|
for (const absPath of fileSet) {
|
|
887
786
|
const sourceId = toNodeId(srcDir, absPath);
|
|
888
787
|
const parsed = parsedByPath.get(absPath);
|
|
889
788
|
if (parsed.fetchCalls.length === 0) continue;
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
type: "calls_api",
|
|
901
|
-
layer: "api"
|
|
902
|
-
});
|
|
903
|
-
fetchResolvedCount++;
|
|
904
|
-
continue;
|
|
905
|
-
}
|
|
906
|
-
if (result.kind === "dynamic") {
|
|
907
|
-
fetchDynamicCount++;
|
|
908
|
-
allFlagged.push({
|
|
909
|
-
source: sourceId,
|
|
910
|
-
target: "DYNAMIC",
|
|
911
|
-
type: "calls_api",
|
|
912
|
-
label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
|
|
913
|
-
confidence: call.isConcat ? "low" : "medium"
|
|
914
|
-
});
|
|
915
|
-
continue;
|
|
916
|
-
}
|
|
917
|
-
if (result.kind === "external") {
|
|
918
|
-
fetchExternalCount++;
|
|
919
|
-
if (!includeExternalFetches) continue;
|
|
920
|
-
allFlagged.push({
|
|
921
|
-
source: sourceId,
|
|
922
|
-
target: "EXTERNAL",
|
|
923
|
-
type: "calls_external",
|
|
924
|
-
label: `${methodTag} external fetch: ${call.url}`,
|
|
925
|
-
confidence: "high"
|
|
926
|
-
});
|
|
927
|
-
continue;
|
|
928
|
-
}
|
|
929
|
-
fetchUnresolvedCount++;
|
|
930
|
-
allFlagged.push({
|
|
931
|
-
source: sourceId,
|
|
932
|
-
target: "UNRESOLVED",
|
|
933
|
-
type: "calls_api",
|
|
934
|
-
label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
|
|
935
|
-
confidence: "medium"
|
|
936
|
-
});
|
|
937
|
-
}
|
|
789
|
+
fetchCallEntries.push({
|
|
790
|
+
nodeId: sourceId,
|
|
791
|
+
calls: parsed.fetchCalls.map((c) => ({
|
|
792
|
+
url: c.url,
|
|
793
|
+
method: c.method,
|
|
794
|
+
isTemplate: c.isTemplate,
|
|
795
|
+
isConcat: c.isConcat,
|
|
796
|
+
kind: c.kind
|
|
797
|
+
}))
|
|
798
|
+
});
|
|
938
799
|
}
|
|
939
800
|
const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
|
|
940
801
|
const IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -960,7 +821,7 @@ function generate(rootDir) {
|
|
|
960
821
|
} catch {
|
|
961
822
|
continue;
|
|
962
823
|
}
|
|
963
|
-
const externalId = (0,
|
|
824
|
+
const externalId = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
964
825
|
const edgesFromThis = [];
|
|
965
826
|
const seen = /* @__PURE__ */ new Set();
|
|
966
827
|
for (const imp of parsed.imports) {
|
|
@@ -1001,7 +862,6 @@ function generate(rootDir) {
|
|
|
1001
862
|
type: "external",
|
|
1002
863
|
name: parsed.name || nameFromFilename(absPath),
|
|
1003
864
|
route: null,
|
|
1004
|
-
module: "external",
|
|
1005
865
|
exports: parsed.exports
|
|
1006
866
|
});
|
|
1007
867
|
nodeIdSet.add(externalId);
|
|
@@ -1051,20 +911,11 @@ function generate(rootDir) {
|
|
|
1051
911
|
layer: "ui",
|
|
1052
912
|
parser: "react-nextjs-ast",
|
|
1053
913
|
...stats,
|
|
1054
|
-
api_call_detection: {
|
|
1055
|
-
includeExternalFetches,
|
|
1056
|
-
includeConcatFetches: process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1",
|
|
1057
|
-
apiRoutesLoaded: apiRoutes.length,
|
|
1058
|
-
resolved: fetchResolvedCount,
|
|
1059
|
-
dynamic: fetchDynamicCount,
|
|
1060
|
-
unresolved: fetchUnresolvedCount,
|
|
1061
|
-
external: fetchExternalCount
|
|
1062
|
-
},
|
|
1063
914
|
notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
|
|
1064
915
|
},
|
|
1065
916
|
nodes,
|
|
1066
917
|
edges: allEdges,
|
|
1067
|
-
cross_refs:
|
|
918
|
+
cross_refs: [],
|
|
1068
919
|
contradictions: [],
|
|
1069
920
|
warnings: [],
|
|
1070
921
|
flagged_edges: dedupedFlagged,
|
|
@@ -1075,7 +926,8 @@ function generate(rootDir) {
|
|
|
1075
926
|
renders: allEdges.filter((e) => e.type === "renders").length,
|
|
1076
927
|
imports: allEdges.filter((e) => e.type === "imports").length,
|
|
1077
928
|
navigates: allEdges.filter((e) => e.type === "navigates").length
|
|
1078
|
-
}
|
|
929
|
+
},
|
|
930
|
+
fetch_calls: fetchCallEntries
|
|
1079
931
|
}
|
|
1080
932
|
};
|
|
1081
933
|
}
|
|
@@ -1087,14 +939,14 @@ var reactNextjsParser = {
|
|
|
1087
939
|
};
|
|
1088
940
|
|
|
1089
941
|
// src/server/graph/parsers/api/nextjs-routes.ts
|
|
1090
|
-
var
|
|
1091
|
-
var
|
|
942
|
+
var import_node_fs4 = require("node:fs");
|
|
943
|
+
var import_node_path4 = require("node:path");
|
|
1092
944
|
var HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
1093
945
|
function walk2(dir) {
|
|
1094
946
|
const results = [];
|
|
1095
|
-
if (!(0,
|
|
1096
|
-
for (const entry of (0,
|
|
1097
|
-
const full = (0,
|
|
947
|
+
if (!(0, import_node_fs4.existsSync)(dir)) return results;
|
|
948
|
+
for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
949
|
+
const full = (0, import_node_path4.join)(dir, entry.name);
|
|
1098
950
|
if (entry.isDirectory()) {
|
|
1099
951
|
results.push(...walk2(full));
|
|
1100
952
|
} else if (entry.name === "route.ts" || entry.name === "route.tsx") {
|
|
@@ -1104,7 +956,7 @@ function walk2(dir) {
|
|
|
1104
956
|
return results;
|
|
1105
957
|
}
|
|
1106
958
|
function filePathToRoute(apiDir, absPath) {
|
|
1107
|
-
let route = "/" + (0,
|
|
959
|
+
let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
1108
960
|
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
1109
961
|
route = route.replace(/\/+/g, "/");
|
|
1110
962
|
if (route === "/") return "/api";
|
|
@@ -1115,10 +967,10 @@ function camelToPascal(s) {
|
|
|
1115
967
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1116
968
|
}
|
|
1117
969
|
function detect2(rootDir) {
|
|
1118
|
-
return (0,
|
|
970
|
+
return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app", "api"));
|
|
1119
971
|
}
|
|
1120
972
|
function generate2(rootDir) {
|
|
1121
|
-
const apiDir = (0,
|
|
973
|
+
const apiDir = (0, import_node_path4.join)(rootDir, "src", "app", "api");
|
|
1122
974
|
const routeFiles = walk2(apiDir);
|
|
1123
975
|
const nodes = [];
|
|
1124
976
|
const edges = [];
|
|
@@ -1136,7 +988,7 @@ function generate2(rootDir) {
|
|
|
1136
988
|
if (HTTP_METHODS2.has(exp)) methods.push(exp);
|
|
1137
989
|
}
|
|
1138
990
|
const routePath = filePathToRoute(apiDir, absPath);
|
|
1139
|
-
const relPath = (0,
|
|
991
|
+
const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1140
992
|
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
1141
993
|
const reads = dbCalls.filter((c) => !c.isMutation);
|
|
1142
994
|
const mutates = mutations.length > 0;
|
|
@@ -1221,8 +1073,8 @@ var nextjsRoutesParser = {
|
|
|
1221
1073
|
};
|
|
1222
1074
|
|
|
1223
1075
|
// src/server/graph/parsers/db/prisma-schema.ts
|
|
1224
|
-
var
|
|
1225
|
-
var
|
|
1076
|
+
var import_node_fs5 = require("node:fs");
|
|
1077
|
+
var import_node_path5 = require("node:path");
|
|
1226
1078
|
function parseModels(content) {
|
|
1227
1079
|
const nodes = [];
|
|
1228
1080
|
const relations = [];
|
|
@@ -1313,11 +1165,11 @@ function parseEnums(content) {
|
|
|
1313
1165
|
return nodes;
|
|
1314
1166
|
}
|
|
1315
1167
|
function detect3(rootDir) {
|
|
1316
|
-
return (0,
|
|
1168
|
+
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
|
|
1317
1169
|
}
|
|
1318
1170
|
function generate3(rootDir) {
|
|
1319
|
-
const schemaPath = (0,
|
|
1320
|
-
const content = (0,
|
|
1171
|
+
const schemaPath = (0, import_node_path5.join)(rootDir, "prisma", "schema.prisma");
|
|
1172
|
+
const content = (0, import_node_fs5.readFileSync)(schemaPath, "utf-8");
|
|
1321
1173
|
const { nodes: modelNodes, relations } = parseModels(content);
|
|
1322
1174
|
const enumNodes = parseEnums(content);
|
|
1323
1175
|
const allNodes = [...modelNodes, ...enumNodes];
|
|
@@ -1373,106 +1225,1116 @@ var prismaSchemaParser = {
|
|
|
1373
1225
|
generate: generate3
|
|
1374
1226
|
};
|
|
1375
1227
|
|
|
1376
|
-
// src/server/graph/core/
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
if (!parser) return null;
|
|
1388
|
-
if (!parser.detect(rootDir)) return null;
|
|
1389
|
-
const output = parser.generate(rootDir);
|
|
1390
|
-
return {
|
|
1391
|
-
layer,
|
|
1392
|
-
output,
|
|
1393
|
-
nodeCount: output.nodes.length,
|
|
1394
|
-
edgeCount: output.edges.length
|
|
1395
|
-
};
|
|
1396
|
-
}
|
|
1397
|
-
function generateAll(rootDir) {
|
|
1398
|
-
const layers = ["api", "db", "ui"];
|
|
1399
|
-
const results = [];
|
|
1400
|
-
for (const layer of layers) {
|
|
1401
|
-
const result = generateLayer(rootDir, layer);
|
|
1402
|
-
if (result) results.push(result);
|
|
1228
|
+
// src/server/graph/core/api-route-matching.ts
|
|
1229
|
+
function loadApiRoutesFromOutput(apiOutput) {
|
|
1230
|
+
const routes = [];
|
|
1231
|
+
for (const n of apiOutput.nodes) {
|
|
1232
|
+
const path2 = n.path;
|
|
1233
|
+
if (!path2 || typeof path2 !== "string") continue;
|
|
1234
|
+
routes.push({
|
|
1235
|
+
path: path2,
|
|
1236
|
+
nodeId: n.id,
|
|
1237
|
+
segments: path2.split("/").filter(Boolean)
|
|
1238
|
+
});
|
|
1403
1239
|
}
|
|
1404
|
-
|
|
1405
|
-
return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
// src/server/graph/index.ts
|
|
1409
|
-
var GRAPHS_DIR = ".launchsecure/graphs";
|
|
1410
|
-
var LAYERS = ["ui", "api", "db"];
|
|
1411
|
-
var graphCache = /* @__PURE__ */ new Map();
|
|
1412
|
-
function graphsDir(rootDir) {
|
|
1413
|
-
return (0, import_node_path5.join)(rootDir, GRAPHS_DIR);
|
|
1414
|
-
}
|
|
1415
|
-
function graphFilePath(rootDir, layer) {
|
|
1416
|
-
return (0, import_node_path5.join)(graphsDir(rootDir), `${layer}.json`);
|
|
1417
|
-
}
|
|
1418
|
-
function invalidateCache(filePath) {
|
|
1419
|
-
graphCache.delete(filePath);
|
|
1240
|
+
return routes;
|
|
1420
1241
|
}
|
|
1421
|
-
function
|
|
1422
|
-
const
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
const cached = graphCache.get(filePath);
|
|
1426
|
-
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
1427
|
-
return cached.graph;
|
|
1242
|
+
function buildApiPathMap(routes) {
|
|
1243
|
+
const map = /* @__PURE__ */ new Map();
|
|
1244
|
+
for (const r of routes) {
|
|
1245
|
+
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
1428
1246
|
}
|
|
1429
|
-
|
|
1430
|
-
const graph = JSON.parse(content);
|
|
1431
|
-
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
1432
|
-
return graph;
|
|
1247
|
+
return map;
|
|
1433
1248
|
}
|
|
1434
|
-
function
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1249
|
+
function normalizeFetchUrl(raw) {
|
|
1250
|
+
let s = raw.replace(/^`|`$/g, "");
|
|
1251
|
+
const qIdx = s.indexOf("?");
|
|
1252
|
+
if (qIdx >= 0) s = s.slice(0, qIdx);
|
|
1253
|
+
const hIdx = s.indexOf("#");
|
|
1254
|
+
if (hIdx >= 0) s = s.slice(0, hIdx);
|
|
1255
|
+
let hadInterpolation = false;
|
|
1256
|
+
s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
1257
|
+
hadInterpolation = true;
|
|
1258
|
+
const cleaned = expr.trim();
|
|
1259
|
+
const last = cleaned.split(".").pop() ?? cleaned;
|
|
1260
|
+
const name = last.replace(/[^\w]/g, "") || "param";
|
|
1261
|
+
return ":" + name;
|
|
1262
|
+
});
|
|
1263
|
+
s = s.replace(/\/+/g, "/");
|
|
1264
|
+
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
1265
|
+
return { path: s || "/", hadInterpolation };
|
|
1441
1266
|
}
|
|
1442
|
-
function
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1267
|
+
function scoreApiRouteMatch(candidate, known) {
|
|
1268
|
+
if (candidate.length !== known.length) return -1;
|
|
1269
|
+
let score = 0;
|
|
1270
|
+
for (let i = 0; i < candidate.length; i++) {
|
|
1271
|
+
const a = candidate[i];
|
|
1272
|
+
const b = known[i];
|
|
1273
|
+
if (a === b) {
|
|
1274
|
+
score += 3;
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
if (a.startsWith(":") && b.startsWith(":")) {
|
|
1278
|
+
score += 2;
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
if (a.startsWith(":") || b.startsWith(":")) {
|
|
1282
|
+
score += 1;
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
return -1;
|
|
1450
1286
|
}
|
|
1451
|
-
return
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
// src/server/lockfile.ts
|
|
1455
|
-
var import_node_child_process = require("node:child_process");
|
|
1456
|
-
var import_node_fs6 = require("node:fs");
|
|
1457
|
-
var import_node_os = require("node:os");
|
|
1458
|
-
var import_node_path6 = require("node:path");
|
|
1459
|
-
function lockDir() {
|
|
1460
|
-
return (0, import_node_path6.join)((0, import_node_os.homedir)(), ".launchsecure");
|
|
1461
|
-
}
|
|
1462
|
-
function lockPath() {
|
|
1463
|
-
return (0, import_node_path6.join)(lockDir(), "launch-chart.lock");
|
|
1287
|
+
return score;
|
|
1464
1288
|
}
|
|
1465
|
-
function
|
|
1466
|
-
const
|
|
1467
|
-
if (
|
|
1468
|
-
|
|
1469
|
-
const data = JSON.parse((0, import_node_fs6.readFileSync)(p, "utf-8"));
|
|
1470
|
-
if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
|
|
1471
|
-
return data;
|
|
1472
|
-
} catch {
|
|
1473
|
-
return null;
|
|
1289
|
+
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
1290
|
+
const raw = call.url;
|
|
1291
|
+
if (/^(https?:)?\/\//i.test(raw)) {
|
|
1292
|
+
return { kind: "external", normalizedUrl: raw };
|
|
1474
1293
|
}
|
|
1475
|
-
|
|
1294
|
+
if (call.isConcat) {
|
|
1295
|
+
return { kind: "dynamic", normalizedUrl: raw };
|
|
1296
|
+
}
|
|
1297
|
+
const { path: path2, hadInterpolation } = normalizeFetchUrl(raw);
|
|
1298
|
+
if (!path2.startsWith("/")) {
|
|
1299
|
+
return { kind: "unresolved", normalizedUrl: path2 };
|
|
1300
|
+
}
|
|
1301
|
+
const segs = path2.split("/").filter(Boolean);
|
|
1302
|
+
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
1303
|
+
return { kind: "dynamic", normalizedUrl: path2 };
|
|
1304
|
+
}
|
|
1305
|
+
const exact = apiPathMap.get(path2);
|
|
1306
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path2 };
|
|
1307
|
+
let bestScore = -1;
|
|
1308
|
+
let bestId = null;
|
|
1309
|
+
for (const r of apiRoutes) {
|
|
1310
|
+
const score = scoreApiRouteMatch(segs, r.segments);
|
|
1311
|
+
if (score > bestScore) {
|
|
1312
|
+
bestScore = score;
|
|
1313
|
+
bestId = r.nodeId;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
if (bestId && bestScore > 0) {
|
|
1317
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path2 };
|
|
1318
|
+
}
|
|
1319
|
+
return { kind: "unresolved", normalizedUrl: path2 };
|
|
1320
|
+
}
|
|
1321
|
+
function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
|
|
1322
|
+
const { path: path2, hadInterpolation } = normalizeFetchUrl(urlPath);
|
|
1323
|
+
if (!path2.startsWith("/")) {
|
|
1324
|
+
return { kind: "unresolved", normalizedUrl: path2 };
|
|
1325
|
+
}
|
|
1326
|
+
const segs = path2.split("/").filter(Boolean);
|
|
1327
|
+
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
1328
|
+
return { kind: "dynamic", normalizedUrl: path2 };
|
|
1329
|
+
}
|
|
1330
|
+
const exact = apiPathMap.get(path2);
|
|
1331
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path2 };
|
|
1332
|
+
let bestScore = -1;
|
|
1333
|
+
let bestId = null;
|
|
1334
|
+
for (const r of apiRoutes) {
|
|
1335
|
+
const score = scoreApiRouteMatch(segs, r.segments);
|
|
1336
|
+
if (score > bestScore) {
|
|
1337
|
+
bestScore = score;
|
|
1338
|
+
bestId = r.nodeId;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
if (bestId && bestScore > 0) {
|
|
1342
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path2 };
|
|
1343
|
+
}
|
|
1344
|
+
return { kind: "unresolved", normalizedUrl: path2 };
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/server/graph/parsers/crosslayer/fetch-resolver.ts
|
|
1348
|
+
var fetchResolverParser = {
|
|
1349
|
+
id: "fetch-resolver",
|
|
1350
|
+
layer: "crosslayer",
|
|
1351
|
+
detect(_rootDir) {
|
|
1352
|
+
return true;
|
|
1353
|
+
},
|
|
1354
|
+
generate(_rootDir, layerOutputs) {
|
|
1355
|
+
const uiOutput = layerOutputs.get("ui");
|
|
1356
|
+
const apiOutput = layerOutputs.get("api");
|
|
1357
|
+
if (!uiOutput || !apiOutput) {
|
|
1358
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1359
|
+
}
|
|
1360
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1361
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1362
|
+
const fetchCallEntries = uiOutput.patterns?.fetch_calls ?? [];
|
|
1363
|
+
if (fetchCallEntries.length === 0) {
|
|
1364
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1365
|
+
}
|
|
1366
|
+
const includeExternal = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
|
|
1367
|
+
const crossRefs = [];
|
|
1368
|
+
const flaggedEdges = [];
|
|
1369
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1370
|
+
let resolvedCount = 0;
|
|
1371
|
+
let dynamicCount = 0;
|
|
1372
|
+
let unresolvedCount = 0;
|
|
1373
|
+
let externalCount = 0;
|
|
1374
|
+
for (const entry of fetchCallEntries) {
|
|
1375
|
+
for (const call of entry.calls) {
|
|
1376
|
+
const result = resolveFetchCall(call, apiPathMap, apiRoutes);
|
|
1377
|
+
const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
|
|
1378
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
1379
|
+
const key = `${entry.nodeId}\u2192${result.nodeId}\u2192calls_api`;
|
|
1380
|
+
if (seen.has(key)) continue;
|
|
1381
|
+
seen.add(key);
|
|
1382
|
+
crossRefs.push({
|
|
1383
|
+
source: entry.nodeId,
|
|
1384
|
+
target: result.nodeId,
|
|
1385
|
+
type: "calls_api",
|
|
1386
|
+
layer: "api"
|
|
1387
|
+
});
|
|
1388
|
+
resolvedCount++;
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
if (result.kind === "dynamic") {
|
|
1392
|
+
dynamicCount++;
|
|
1393
|
+
flaggedEdges.push({
|
|
1394
|
+
source: entry.nodeId,
|
|
1395
|
+
target: "DYNAMIC",
|
|
1396
|
+
type: "calls_api",
|
|
1397
|
+
label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
|
|
1398
|
+
confidence: call.isConcat ? "low" : "medium"
|
|
1399
|
+
});
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
if (result.kind === "external") {
|
|
1403
|
+
externalCount++;
|
|
1404
|
+
if (!includeExternal) continue;
|
|
1405
|
+
flaggedEdges.push({
|
|
1406
|
+
source: entry.nodeId,
|
|
1407
|
+
target: "EXTERNAL",
|
|
1408
|
+
type: "calls_external",
|
|
1409
|
+
label: `${methodTag} external fetch: ${call.url}`,
|
|
1410
|
+
confidence: "high"
|
|
1411
|
+
});
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
unresolvedCount++;
|
|
1415
|
+
flaggedEdges.push({
|
|
1416
|
+
source: entry.nodeId,
|
|
1417
|
+
target: "UNRESOLVED",
|
|
1418
|
+
type: "calls_api",
|
|
1419
|
+
label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
|
|
1420
|
+
confidence: "medium"
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
return {
|
|
1425
|
+
cross_refs: crossRefs,
|
|
1426
|
+
flagged_edges: flaggedEdges,
|
|
1427
|
+
warnings: [],
|
|
1428
|
+
patterns: {
|
|
1429
|
+
api_call_detection: {
|
|
1430
|
+
resolved: resolvedCount,
|
|
1431
|
+
dynamic: dynamicCount,
|
|
1432
|
+
unresolved: unresolvedCount,
|
|
1433
|
+
external: externalCount
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
};
|
|
1439
|
+
|
|
1440
|
+
// src/server/graph/parsers/crosslayer/api-annotations.ts
|
|
1441
|
+
var import_node_fs6 = require("node:fs");
|
|
1442
|
+
var import_node_path6 = require("node:path");
|
|
1443
|
+
var API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
|
|
1444
|
+
function walk3(dir, exts) {
|
|
1445
|
+
if (!(0, import_node_fs6.existsSync)(dir)) return [];
|
|
1446
|
+
const results = [];
|
|
1447
|
+
for (const entry of (0, import_node_fs6.readdirSync)(dir, { withFileTypes: true })) {
|
|
1448
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1449
|
+
const full = (0, import_node_path6.join)(dir, entry.name);
|
|
1450
|
+
if (entry.isDirectory()) {
|
|
1451
|
+
results.push(...walk3(full, exts));
|
|
1452
|
+
} else if (exts.includes((0, import_node_path6.extname)(entry.name))) {
|
|
1453
|
+
results.push(full);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
return results;
|
|
1457
|
+
}
|
|
1458
|
+
function toNodeId2(srcDir, absPath) {
|
|
1459
|
+
return (0, import_node_path6.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
1460
|
+
}
|
|
1461
|
+
var apiAnnotationsParser = {
|
|
1462
|
+
id: "api-annotations",
|
|
1463
|
+
layer: "crosslayer",
|
|
1464
|
+
detect(rootDir) {
|
|
1465
|
+
return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "src"));
|
|
1466
|
+
},
|
|
1467
|
+
generate(rootDir, layerOutputs) {
|
|
1468
|
+
const apiOutput = layerOutputs.get("api");
|
|
1469
|
+
if (!apiOutput) {
|
|
1470
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1471
|
+
}
|
|
1472
|
+
const uiOutput = layerOutputs.get("ui");
|
|
1473
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
1474
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1475
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1476
|
+
const srcDir = (0, import_node_path6.join)(rootDir, "src");
|
|
1477
|
+
const files = walk3(srcDir, [".ts", ".tsx"]);
|
|
1478
|
+
const crossRefs = [];
|
|
1479
|
+
const flaggedEdges = [];
|
|
1480
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1481
|
+
for (const absPath of files) {
|
|
1482
|
+
const content = (0, import_node_fs6.readFileSync)(absPath, "utf-8");
|
|
1483
|
+
const sourceId = toNodeId2(srcDir, absPath);
|
|
1484
|
+
if (!uiNodeIds.has(sourceId)) continue;
|
|
1485
|
+
let match;
|
|
1486
|
+
API_ANNOTATION_RE.lastIndex = 0;
|
|
1487
|
+
while ((match = API_ANNOTATION_RE.exec(content)) !== null) {
|
|
1488
|
+
const method = match[1];
|
|
1489
|
+
const urlPath = match[2];
|
|
1490
|
+
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
1491
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
1492
|
+
const key = `${sourceId}|${result.nodeId}|calls_api`;
|
|
1493
|
+
if (seen.has(key)) continue;
|
|
1494
|
+
seen.add(key);
|
|
1495
|
+
crossRefs.push({
|
|
1496
|
+
source: sourceId,
|
|
1497
|
+
target: result.nodeId,
|
|
1498
|
+
type: "calls_api",
|
|
1499
|
+
layer: "api"
|
|
1500
|
+
});
|
|
1501
|
+
} else {
|
|
1502
|
+
flaggedEdges.push({
|
|
1503
|
+
source: sourceId,
|
|
1504
|
+
target: "UNRESOLVED",
|
|
1505
|
+
type: "annotation_unresolved",
|
|
1506
|
+
label: `@api ${method} ${urlPath} \u2014 no matching API route found`,
|
|
1507
|
+
confidence: "high"
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
return {
|
|
1513
|
+
cross_refs: crossRefs,
|
|
1514
|
+
flagged_edges: flaggedEdges,
|
|
1515
|
+
warnings: [],
|
|
1516
|
+
patterns: {
|
|
1517
|
+
annotations_found: crossRefs.length + flaggedEdges.length,
|
|
1518
|
+
annotations_resolved: crossRefs.length,
|
|
1519
|
+
annotations_unresolved: flaggedEdges.length
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
// src/server/graph/parsers/crosslayer/url-literal-scanner.ts
|
|
1526
|
+
var import_node_fs7 = require("node:fs");
|
|
1527
|
+
var import_node_path7 = require("node:path");
|
|
1528
|
+
var URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
|
|
1529
|
+
function walk4(dir, exts) {
|
|
1530
|
+
if (!(0, import_node_fs7.existsSync)(dir)) return [];
|
|
1531
|
+
const results = [];
|
|
1532
|
+
for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
|
|
1533
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1534
|
+
const full = (0, import_node_path7.join)(dir, entry.name);
|
|
1535
|
+
if (entry.isDirectory()) {
|
|
1536
|
+
results.push(...walk4(full, exts));
|
|
1537
|
+
} else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
|
|
1538
|
+
results.push(full);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
return results;
|
|
1542
|
+
}
|
|
1543
|
+
function toNodeId3(srcDir, absPath) {
|
|
1544
|
+
return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
1545
|
+
}
|
|
1546
|
+
var urlLiteralScannerParser = {
|
|
1547
|
+
id: "url-literal-scanner",
|
|
1548
|
+
layer: "crosslayer",
|
|
1549
|
+
detect(rootDir) {
|
|
1550
|
+
return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
|
|
1551
|
+
},
|
|
1552
|
+
generate(rootDir, layerOutputs) {
|
|
1553
|
+
const apiOutput = layerOutputs.get("api");
|
|
1554
|
+
if (!apiOutput) {
|
|
1555
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1556
|
+
}
|
|
1557
|
+
const uiOutput = layerOutputs.get("ui");
|
|
1558
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
1559
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1560
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1561
|
+
const srcDir = (0, import_node_path7.join)(rootDir, "src");
|
|
1562
|
+
const clientDir = (0, import_node_path7.join)(srcDir, "client");
|
|
1563
|
+
const appDir = (0, import_node_path7.join)(srcDir, "app");
|
|
1564
|
+
const files = [
|
|
1565
|
+
...walk4(clientDir, [".ts", ".tsx"]),
|
|
1566
|
+
...walk4(appDir, [".ts", ".tsx"])
|
|
1567
|
+
];
|
|
1568
|
+
const crossRefs = [];
|
|
1569
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1570
|
+
for (const absPath of files) {
|
|
1571
|
+
const sourceId = toNodeId3(srcDir, absPath);
|
|
1572
|
+
if (!uiNodeIds.has(sourceId)) continue;
|
|
1573
|
+
const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
|
|
1574
|
+
let match;
|
|
1575
|
+
URL_LITERAL_RE.lastIndex = 0;
|
|
1576
|
+
while ((match = URL_LITERAL_RE.exec(content)) !== null) {
|
|
1577
|
+
const urlPath = match[1];
|
|
1578
|
+
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
1579
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
1580
|
+
const key = `${sourceId}|${result.nodeId}|references_api`;
|
|
1581
|
+
if (seen.has(key)) continue;
|
|
1582
|
+
seen.add(key);
|
|
1583
|
+
crossRefs.push({
|
|
1584
|
+
source: sourceId,
|
|
1585
|
+
target: result.nodeId,
|
|
1586
|
+
type: "references_api",
|
|
1587
|
+
layer: "api"
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return {
|
|
1593
|
+
cross_refs: crossRefs,
|
|
1594
|
+
flagged_edges: [],
|
|
1595
|
+
warnings: [],
|
|
1596
|
+
patterns: {
|
|
1597
|
+
url_literals_resolved: crossRefs.length
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
|
|
1603
|
+
// src/server/graph/core/parser-registry.ts
|
|
1604
|
+
var ParserRegistry = class {
|
|
1605
|
+
constructor() {
|
|
1606
|
+
this.parsers = /* @__PURE__ */ new Map();
|
|
1607
|
+
this.ids = /* @__PURE__ */ new Set();
|
|
1608
|
+
}
|
|
1609
|
+
register(parser) {
|
|
1610
|
+
if (this.ids.has(parser.id)) {
|
|
1611
|
+
throw new Error(`Duplicate parser id: ${parser.id}`);
|
|
1612
|
+
}
|
|
1613
|
+
this.ids.add(parser.id);
|
|
1614
|
+
const list = this.parsers.get(parser.layer) ?? [];
|
|
1615
|
+
list.push(parser);
|
|
1616
|
+
this.parsers.set(parser.layer, list);
|
|
1617
|
+
}
|
|
1618
|
+
getParsers(layer) {
|
|
1619
|
+
return this.parsers.get(layer) ?? [];
|
|
1620
|
+
}
|
|
1621
|
+
getCrossLayerParsers() {
|
|
1622
|
+
return this.parsers.get("crosslayer") ?? [];
|
|
1623
|
+
}
|
|
1624
|
+
getAll() {
|
|
1625
|
+
const all = [];
|
|
1626
|
+
for (const list of this.parsers.values()) all.push(...list);
|
|
1627
|
+
return all;
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
function registerBuiltins(registry, disabled) {
|
|
1631
|
+
const builtins = [
|
|
1632
|
+
reactNextjsParser,
|
|
1633
|
+
nextjsRoutesParser,
|
|
1634
|
+
prismaSchemaParser,
|
|
1635
|
+
fetchResolverParser,
|
|
1636
|
+
apiAnnotationsParser,
|
|
1637
|
+
urlLiteralScannerParser
|
|
1638
|
+
];
|
|
1639
|
+
for (const parser of builtins) {
|
|
1640
|
+
if (disabled.has(parser.id)) continue;
|
|
1641
|
+
registry.register(parser);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
1645
|
+
for (const entry of config.parsers?.custom ?? []) {
|
|
1646
|
+
try {
|
|
1647
|
+
const absPath = (0, import_node_path8.resolve)(rootDir, entry.path);
|
|
1648
|
+
const mod = require(absPath);
|
|
1649
|
+
const parser = "default" in mod ? mod.default : mod;
|
|
1650
|
+
if (disabled.has(parser.id)) continue;
|
|
1651
|
+
if (parser.layer !== entry.layer) {
|
|
1652
|
+
process.stderr.write(
|
|
1653
|
+
`[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
|
|
1654
|
+
`
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
registry.register(parser);
|
|
1658
|
+
} catch (err) {
|
|
1659
|
+
process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err}
|
|
1660
|
+
`);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
function createRegistry(config, rootDir) {
|
|
1665
|
+
const registry = new ParserRegistry();
|
|
1666
|
+
const disabled = new Set(config.parsers?.disabled ?? []);
|
|
1667
|
+
registerBuiltins(registry, disabled);
|
|
1668
|
+
loadCustomParsers(registry, config, rootDir, disabled);
|
|
1669
|
+
return registry;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// src/server/graph/core/merge.ts
|
|
1673
|
+
function mergeGraphOutputs(outputs, layer) {
|
|
1674
|
+
if (outputs.length === 0) {
|
|
1675
|
+
return {
|
|
1676
|
+
metadata: { generated: (/* @__PURE__ */ new Date()).toISOString(), scope: "", layer },
|
|
1677
|
+
nodes: [],
|
|
1678
|
+
edges: [],
|
|
1679
|
+
cross_refs: [],
|
|
1680
|
+
contradictions: [],
|
|
1681
|
+
warnings: [],
|
|
1682
|
+
flagged_edges: []
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
if (outputs.length === 1) return outputs[0];
|
|
1686
|
+
const seenNodes = /* @__PURE__ */ new Set();
|
|
1687
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
1688
|
+
const seenCrossRefs = /* @__PURE__ */ new Set();
|
|
1689
|
+
const mergedNodes = [];
|
|
1690
|
+
const mergedEdges = [];
|
|
1691
|
+
const mergedCrossRefs = [];
|
|
1692
|
+
const mergedContradictions = [];
|
|
1693
|
+
const mergedWarnings = [];
|
|
1694
|
+
const mergedFlagged = [];
|
|
1695
|
+
const parserIds = [];
|
|
1696
|
+
for (const output of outputs) {
|
|
1697
|
+
if (output.metadata.parser) {
|
|
1698
|
+
parserIds.push(String(output.metadata.parser));
|
|
1699
|
+
}
|
|
1700
|
+
for (const node of output.nodes) {
|
|
1701
|
+
if (seenNodes.has(node.id)) {
|
|
1702
|
+
mergedWarnings.push({
|
|
1703
|
+
type: "merge_conflict",
|
|
1704
|
+
detail: `Node "${node.id}" produced by multiple parsers; keeping first`
|
|
1705
|
+
});
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
seenNodes.add(node.id);
|
|
1709
|
+
mergedNodes.push(node);
|
|
1710
|
+
}
|
|
1711
|
+
for (const edge of output.edges) {
|
|
1712
|
+
const key = `${edge.source}|${edge.target}|${edge.type}`;
|
|
1713
|
+
if (seenEdges.has(key)) continue;
|
|
1714
|
+
seenEdges.add(key);
|
|
1715
|
+
mergedEdges.push(edge);
|
|
1716
|
+
}
|
|
1717
|
+
for (const ref of output.cross_refs) {
|
|
1718
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
1719
|
+
if (seenCrossRefs.has(key)) continue;
|
|
1720
|
+
seenCrossRefs.add(key);
|
|
1721
|
+
mergedCrossRefs.push(ref);
|
|
1722
|
+
}
|
|
1723
|
+
mergedContradictions.push(...output.contradictions);
|
|
1724
|
+
mergedWarnings.push(...output.warnings);
|
|
1725
|
+
mergedFlagged.push(...output.flagged_edges);
|
|
1726
|
+
}
|
|
1727
|
+
const metadata = {
|
|
1728
|
+
...outputs[0].metadata,
|
|
1729
|
+
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1730
|
+
parsers: parserIds
|
|
1731
|
+
};
|
|
1732
|
+
return {
|
|
1733
|
+
metadata,
|
|
1734
|
+
nodes: mergedNodes,
|
|
1735
|
+
edges: mergedEdges,
|
|
1736
|
+
cross_refs: mergedCrossRefs,
|
|
1737
|
+
contradictions: mergedContradictions,
|
|
1738
|
+
warnings: mergedWarnings,
|
|
1739
|
+
flagged_edges: mergedFlagged,
|
|
1740
|
+
patterns: outputs[0].patterns
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
function dedupCrossRefs(refs) {
|
|
1744
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1745
|
+
const result = [];
|
|
1746
|
+
for (const ref of refs) {
|
|
1747
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
1748
|
+
if (seen.has(key)) continue;
|
|
1749
|
+
seen.add(key);
|
|
1750
|
+
result.push(ref);
|
|
1751
|
+
}
|
|
1752
|
+
return result;
|
|
1753
|
+
}
|
|
1754
|
+
function applyCrossLayerResults(uiOutput, results, primaryId) {
|
|
1755
|
+
const allCrossRefs = [...uiOutput.cross_refs];
|
|
1756
|
+
const allFlagged = [...uiOutput.flagged_edges];
|
|
1757
|
+
const allWarnings = [...uiOutput.warnings];
|
|
1758
|
+
const primaryResult = results.find((r) => r.parserId === primaryId);
|
|
1759
|
+
const secondaryResults = results.filter((r) => r.parserId !== primaryId);
|
|
1760
|
+
if (primaryResult) {
|
|
1761
|
+
allCrossRefs.push(...primaryResult.output.cross_refs);
|
|
1762
|
+
allFlagged.push(...primaryResult.output.flagged_edges);
|
|
1763
|
+
allWarnings.push(...primaryResult.output.warnings);
|
|
1764
|
+
}
|
|
1765
|
+
const primarySet = new Set(
|
|
1766
|
+
(primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
|
|
1767
|
+
);
|
|
1768
|
+
for (const sec of secondaryResults) {
|
|
1769
|
+
for (const ref of sec.output.cross_refs) {
|
|
1770
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
1771
|
+
if (primarySet.has(key)) {
|
|
1772
|
+
allCrossRefs.push(ref);
|
|
1773
|
+
} else {
|
|
1774
|
+
allFlagged.push({
|
|
1775
|
+
source: ref.source,
|
|
1776
|
+
target: ref.target,
|
|
1777
|
+
type: "out_of_pattern",
|
|
1778
|
+
label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
|
|
1779
|
+
confidence: "medium"
|
|
1780
|
+
});
|
|
1781
|
+
allCrossRefs.push(ref);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
allFlagged.push(...sec.output.flagged_edges);
|
|
1785
|
+
allWarnings.push(...sec.output.warnings);
|
|
1786
|
+
}
|
|
1787
|
+
return {
|
|
1788
|
+
...uiOutput,
|
|
1789
|
+
cross_refs: dedupCrossRefs(allCrossRefs),
|
|
1790
|
+
flagged_edges: allFlagged,
|
|
1791
|
+
warnings: allWarnings
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// src/server/graph/core/graph-builder.ts
|
|
1796
|
+
function readGraphFromDisk(rootDir, layer) {
|
|
1797
|
+
const filePath = (0, import_node_path9.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
1798
|
+
if (!(0, import_node_fs8.existsSync)(filePath)) return null;
|
|
1799
|
+
try {
|
|
1800
|
+
return JSON.parse((0, import_node_fs8.readFileSync)(filePath, "utf-8"));
|
|
1801
|
+
} catch {
|
|
1802
|
+
return null;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
function generateLayer(rootDir, layer) {
|
|
1806
|
+
const config = loadConfig(rootDir);
|
|
1807
|
+
const registry = createRegistry(config, rootDir);
|
|
1808
|
+
const parsers = registry.getParsers(layer);
|
|
1809
|
+
const outputs = [];
|
|
1810
|
+
for (const parser of parsers) {
|
|
1811
|
+
if (!parser.detect(rootDir)) continue;
|
|
1812
|
+
outputs.push(parser.generate(rootDir));
|
|
1813
|
+
}
|
|
1814
|
+
if (outputs.length === 0) return null;
|
|
1815
|
+
let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
1816
|
+
if (layer === "ui") {
|
|
1817
|
+
const layerOutputs = /* @__PURE__ */ new Map();
|
|
1818
|
+
layerOutputs.set("ui", merged);
|
|
1819
|
+
for (const otherLayer of ["api", "db"]) {
|
|
1820
|
+
const existing = readGraphFromDisk(rootDir, otherLayer);
|
|
1821
|
+
if (existing) layerOutputs.set(otherLayer, existing);
|
|
1822
|
+
}
|
|
1823
|
+
const crossParsers = registry.getCrossLayerParsers();
|
|
1824
|
+
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
1825
|
+
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
1826
|
+
if (crossResults.length > 0) {
|
|
1827
|
+
merged = applyCrossLayerResults(merged, crossResults, primaryId);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
return {
|
|
1831
|
+
layer,
|
|
1832
|
+
output: merged,
|
|
1833
|
+
nodeCount: merged.nodes.length,
|
|
1834
|
+
edgeCount: merged.edges.length
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
function generateAll(rootDir) {
|
|
1838
|
+
const config = loadConfig(rootDir);
|
|
1839
|
+
const registry = createRegistry(config, rootDir);
|
|
1840
|
+
const layerOrder = ["api", "db", "ui"];
|
|
1841
|
+
const layerOutputs = /* @__PURE__ */ new Map();
|
|
1842
|
+
const results = [];
|
|
1843
|
+
for (const layer of layerOrder) {
|
|
1844
|
+
const parsers = registry.getParsers(layer);
|
|
1845
|
+
const outputs = [];
|
|
1846
|
+
for (const parser of parsers) {
|
|
1847
|
+
if (!parser.detect(rootDir)) continue;
|
|
1848
|
+
outputs.push(parser.generate(rootDir));
|
|
1849
|
+
}
|
|
1850
|
+
if (outputs.length === 0) continue;
|
|
1851
|
+
const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
1852
|
+
layerOutputs.set(layer, merged);
|
|
1853
|
+
results.push({
|
|
1854
|
+
layer,
|
|
1855
|
+
output: merged,
|
|
1856
|
+
nodeCount: merged.nodes.length,
|
|
1857
|
+
edgeCount: merged.edges.length
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
const crossParsers = registry.getCrossLayerParsers();
|
|
1861
|
+
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
1862
|
+
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
1863
|
+
if (crossResults.length > 0 && layerOutputs.has("ui")) {
|
|
1864
|
+
const uiOutput = layerOutputs.get("ui");
|
|
1865
|
+
const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
|
|
1866
|
+
layerOutputs.set("ui", merged);
|
|
1867
|
+
const uiResult = results.find((r) => r.layer === "ui");
|
|
1868
|
+
if (uiResult) {
|
|
1869
|
+
uiResult.output = merged;
|
|
1870
|
+
uiResult.nodeCount = merged.nodes.length;
|
|
1871
|
+
uiResult.edgeCount = merged.edges.length;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
const byLayer = new Map(results.map((r) => [r.layer, r]));
|
|
1875
|
+
return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// src/server/graph/index.ts
|
|
1879
|
+
init_config();
|
|
1880
|
+
|
|
1881
|
+
// src/server/graph/core/tagger-registry.ts
|
|
1882
|
+
var import_node_path11 = require("node:path");
|
|
1883
|
+
|
|
1884
|
+
// src/server/graph/taggers/module-tagger.ts
|
|
1885
|
+
var import_node_fs9 = require("node:fs");
|
|
1886
|
+
var import_node_path10 = require("node:path");
|
|
1887
|
+
function matchGlob(pattern, id) {
|
|
1888
|
+
const patParts = pattern.split("/");
|
|
1889
|
+
const idParts = id.split("/");
|
|
1890
|
+
return matchParts(patParts, 0, idParts, 0);
|
|
1891
|
+
}
|
|
1892
|
+
function matchParts(pat, pi, id, ii) {
|
|
1893
|
+
while (pi < pat.length && ii < id.length) {
|
|
1894
|
+
const p = pat[pi];
|
|
1895
|
+
if (p === "**") {
|
|
1896
|
+
for (let skip = ii; skip <= id.length; skip++) {
|
|
1897
|
+
if (matchParts(pat, pi + 1, id, skip)) return true;
|
|
1898
|
+
}
|
|
1899
|
+
return false;
|
|
1900
|
+
}
|
|
1901
|
+
if (p === "*") {
|
|
1902
|
+
pi++;
|
|
1903
|
+
ii++;
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
if (p !== id[ii]) return false;
|
|
1907
|
+
pi++;
|
|
1908
|
+
ii++;
|
|
1909
|
+
}
|
|
1910
|
+
while (pi < pat.length && pat[pi] === "**") pi++;
|
|
1911
|
+
return pi === pat.length && ii === id.length;
|
|
1912
|
+
}
|
|
1913
|
+
var CONVENTION_DIRS = ["features", "modules", "domains", "areas"];
|
|
1914
|
+
function detectConventionDirs(rootDir) {
|
|
1915
|
+
const result = /* @__PURE__ */ new Map();
|
|
1916
|
+
const searchDirs = [
|
|
1917
|
+
rootDir,
|
|
1918
|
+
(0, import_node_path10.join)(rootDir, "src"),
|
|
1919
|
+
(0, import_node_path10.join)(rootDir, "app"),
|
|
1920
|
+
(0, import_node_path10.join)(rootDir, "lib")
|
|
1921
|
+
];
|
|
1922
|
+
for (const base of searchDirs) {
|
|
1923
|
+
for (const convention of CONVENTION_DIRS) {
|
|
1924
|
+
const dir = (0, import_node_path10.join)(base, convention);
|
|
1925
|
+
if (!(0, import_node_fs9.existsSync)(dir)) continue;
|
|
1926
|
+
try {
|
|
1927
|
+
const stat = (0, import_node_fs9.statSync)(dir);
|
|
1928
|
+
if (!stat.isDirectory()) continue;
|
|
1929
|
+
const entries = (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
1930
|
+
if (entries.length > 0) {
|
|
1931
|
+
const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
|
|
1932
|
+
result.set(relPath, entries);
|
|
1933
|
+
}
|
|
1934
|
+
} catch {
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
return result;
|
|
1939
|
+
}
|
|
1940
|
+
function extractRouteGroups(id) {
|
|
1941
|
+
const groups = [];
|
|
1942
|
+
const re = /\(([^)]+)\)/g;
|
|
1943
|
+
let m;
|
|
1944
|
+
while ((m = re.exec(id)) !== null) {
|
|
1945
|
+
groups.push(m[1]);
|
|
1946
|
+
}
|
|
1947
|
+
return groups;
|
|
1948
|
+
}
|
|
1949
|
+
var SKIP_SEGMENTS = /* @__PURE__ */ new Set([
|
|
1950
|
+
"src",
|
|
1951
|
+
"app",
|
|
1952
|
+
"client",
|
|
1953
|
+
"server",
|
|
1954
|
+
"lib",
|
|
1955
|
+
"config"
|
|
1956
|
+
]);
|
|
1957
|
+
function isRouteGroup(segment) {
|
|
1958
|
+
return segment.startsWith("(") && segment.endsWith(")");
|
|
1959
|
+
}
|
|
1960
|
+
function isDynamicSegment(segment) {
|
|
1961
|
+
return segment.startsWith("[") || segment.startsWith(":");
|
|
1962
|
+
}
|
|
1963
|
+
function isDomainDir(segment) {
|
|
1964
|
+
return segment.includes(".") && !segment.endsWith(".tsx") && !segment.endsWith(".ts") && !segment.endsWith(".js") && !segment.endsWith(".jsx") && !segment.endsWith(".vue");
|
|
1965
|
+
}
|
|
1966
|
+
var TRIVIAL_GROUPS = /* @__PURE__ */ new Set([
|
|
1967
|
+
"app",
|
|
1968
|
+
"all",
|
|
1969
|
+
"ee",
|
|
1970
|
+
"home",
|
|
1971
|
+
"root"
|
|
1972
|
+
]);
|
|
1973
|
+
function isTrivialGroup(name) {
|
|
1974
|
+
if (TRIVIAL_GROUPS.has(name)) return true;
|
|
1975
|
+
const lower = name.toLowerCase();
|
|
1976
|
+
const wrapperPatterns = [
|
|
1977
|
+
/^.*-?wrapper$/,
|
|
1978
|
+
// "page-wrapper", "use-page-wrapper"
|
|
1979
|
+
/^.*-?layout$/,
|
|
1980
|
+
// "admin-layout", "settings-layout"
|
|
1981
|
+
/^use-/,
|
|
1982
|
+
// "use-page-wrapper"
|
|
1983
|
+
/^default$/
|
|
1984
|
+
];
|
|
1985
|
+
return wrapperPatterns.some((p) => p.test(lower));
|
|
1986
|
+
}
|
|
1987
|
+
function normalizeGroupName(name) {
|
|
1988
|
+
return name.replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
|
|
1989
|
+
}
|
|
1990
|
+
function extractModuleFromPath(id) {
|
|
1991
|
+
const segments = id.split("/");
|
|
1992
|
+
const routeGroups = extractRouteGroups(id);
|
|
1993
|
+
const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g)).map(normalizeGroupName);
|
|
1994
|
+
if (moduleGroups.length > 0) {
|
|
1995
|
+
return moduleGroups[moduleGroups.length - 1];
|
|
1996
|
+
}
|
|
1997
|
+
const meaningful = [];
|
|
1998
|
+
for (const seg of segments) {
|
|
1999
|
+
if (seg.includes(".")) continue;
|
|
2000
|
+
if (isRouteGroup(seg)) continue;
|
|
2001
|
+
if (isDynamicSegment(seg)) continue;
|
|
2002
|
+
if (isDomainDir(seg)) continue;
|
|
2003
|
+
if (SKIP_SEGMENTS.has(seg)) continue;
|
|
2004
|
+
meaningful.push(seg);
|
|
2005
|
+
}
|
|
2006
|
+
if (meaningful.length > 0) {
|
|
2007
|
+
return meaningful[0];
|
|
2008
|
+
}
|
|
2009
|
+
return "root";
|
|
2010
|
+
}
|
|
2011
|
+
var cachedRootDir = null;
|
|
2012
|
+
var cachedConventionDirs = /* @__PURE__ */ new Map();
|
|
2013
|
+
var moduleTagger = {
|
|
2014
|
+
id: "module",
|
|
2015
|
+
tagKey: "module",
|
|
2016
|
+
trackUntagged: true,
|
|
2017
|
+
layers: null,
|
|
2018
|
+
// applies to all layers
|
|
2019
|
+
tag(nodes, layer, rootDir) {
|
|
2020
|
+
if (cachedRootDir !== rootDir) {
|
|
2021
|
+
cachedConventionDirs = detectConventionDirs(rootDir);
|
|
2022
|
+
cachedRootDir = rootDir;
|
|
2023
|
+
}
|
|
2024
|
+
let configRules = [];
|
|
2025
|
+
try {
|
|
2026
|
+
const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
|
|
2027
|
+
const config = loadConfig2(rootDir);
|
|
2028
|
+
configRules = config.taggers?.module?.rules ?? [];
|
|
2029
|
+
} catch {
|
|
2030
|
+
}
|
|
2031
|
+
const result = /* @__PURE__ */ new Map();
|
|
2032
|
+
for (const node of nodes) {
|
|
2033
|
+
const id = node.id;
|
|
2034
|
+
let matched = false;
|
|
2035
|
+
for (const rule of configRules) {
|
|
2036
|
+
if (matchGlob(rule.match, id)) {
|
|
2037
|
+
result.set(id, rule.module);
|
|
2038
|
+
matched = true;
|
|
2039
|
+
break;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
if (matched) continue;
|
|
2043
|
+
matched = false;
|
|
2044
|
+
for (const [convDir, moduleNames] of cachedConventionDirs) {
|
|
2045
|
+
if (id.startsWith(convDir + "/")) {
|
|
2046
|
+
const rest = id.slice(convDir.length + 1);
|
|
2047
|
+
const firstSeg = rest.split("/")[0];
|
|
2048
|
+
if (moduleNames.includes(firstSeg)) {
|
|
2049
|
+
result.set(id, firstSeg);
|
|
2050
|
+
matched = true;
|
|
2051
|
+
break;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
if (matched) continue;
|
|
2056
|
+
const module2 = extractModuleFromPath(id);
|
|
2057
|
+
result.set(id, module2);
|
|
2058
|
+
}
|
|
2059
|
+
return result;
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
|
|
2063
|
+
// src/server/graph/taggers/screen-tagger.ts
|
|
2064
|
+
var SCREEN_TYPES = /* @__PURE__ */ new Set(["page", "layout"]);
|
|
2065
|
+
var screenTagger = {
|
|
2066
|
+
id: "screen",
|
|
2067
|
+
tagKey: "screen",
|
|
2068
|
+
trackUntagged: true,
|
|
2069
|
+
layers: ["ui"],
|
|
2070
|
+
tag(nodes, layer) {
|
|
2071
|
+
if (layer !== "ui") return /* @__PURE__ */ new Map();
|
|
2072
|
+
const result = /* @__PURE__ */ new Map();
|
|
2073
|
+
for (const node of nodes) {
|
|
2074
|
+
if (SCREEN_TYPES.has(node.type)) {
|
|
2075
|
+
result.set(node.id, "true");
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
return result;
|
|
2079
|
+
}
|
|
2080
|
+
};
|
|
2081
|
+
|
|
2082
|
+
// src/server/graph/core/tagger-registry.ts
|
|
2083
|
+
var TaggerRegistry = class {
|
|
2084
|
+
constructor() {
|
|
2085
|
+
this.taggers = [];
|
|
2086
|
+
this.ids = /* @__PURE__ */ new Set();
|
|
2087
|
+
}
|
|
2088
|
+
register(tagger) {
|
|
2089
|
+
if (this.ids.has(tagger.id)) {
|
|
2090
|
+
throw new Error(`Duplicate tagger id: ${tagger.id}`);
|
|
2091
|
+
}
|
|
2092
|
+
this.ids.add(tagger.id);
|
|
2093
|
+
this.taggers.push(tagger);
|
|
2094
|
+
}
|
|
2095
|
+
getAll() {
|
|
2096
|
+
return this.taggers;
|
|
2097
|
+
}
|
|
2098
|
+
getForLayer(layer) {
|
|
2099
|
+
return this.taggers.filter((t) => t.layers === null || t.layers.includes(layer));
|
|
2100
|
+
}
|
|
2101
|
+
};
|
|
2102
|
+
var BUILTIN_TAGGERS = [moduleTagger, screenTagger];
|
|
2103
|
+
function registerBuiltins2(registry, disabled, config) {
|
|
2104
|
+
for (const tagger of BUILTIN_TAGGERS) {
|
|
2105
|
+
if (disabled.has(tagger.id)) continue;
|
|
2106
|
+
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
2107
|
+
if (override !== void 0) {
|
|
2108
|
+
tagger.trackUntagged = override;
|
|
2109
|
+
}
|
|
2110
|
+
registry.register(tagger);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
function loadCustomTaggers(registry, config, rootDir, disabled) {
|
|
2114
|
+
for (const entry of config.taggers?.custom ?? []) {
|
|
2115
|
+
if (disabled.has(entry.id)) continue;
|
|
2116
|
+
try {
|
|
2117
|
+
const absPath = (0, import_node_path11.resolve)(rootDir, entry.path);
|
|
2118
|
+
const mod = require(absPath);
|
|
2119
|
+
const tagger = "default" in mod ? mod.default : mod;
|
|
2120
|
+
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
2121
|
+
if (override !== void 0) {
|
|
2122
|
+
tagger.trackUntagged = override;
|
|
2123
|
+
}
|
|
2124
|
+
registry.register(tagger);
|
|
2125
|
+
} catch (err) {
|
|
2126
|
+
process.stderr.write(`[launch-chart] failed to load custom tagger from ${entry.path}: ${err}
|
|
2127
|
+
`);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
function createTaggerRegistry(config, rootDir) {
|
|
2132
|
+
const registry = new TaggerRegistry();
|
|
2133
|
+
const disabled = new Set(config.taggers?.disabled ?? []);
|
|
2134
|
+
registerBuiltins2(registry, disabled, config);
|
|
2135
|
+
loadCustomTaggers(registry, config, rootDir, disabled);
|
|
2136
|
+
return registry;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
// src/server/graph/core/tag-store.ts
|
|
2140
|
+
var import_node_fs10 = require("node:fs");
|
|
2141
|
+
var import_node_path12 = require("node:path");
|
|
2142
|
+
var TAGS_FILENAME = "tags.json";
|
|
2143
|
+
var GRAPHS_DIR = ".launchsecure/graphs";
|
|
2144
|
+
var tagCache = /* @__PURE__ */ new Map();
|
|
2145
|
+
function tagsFilePath(rootDir) {
|
|
2146
|
+
return (0, import_node_path12.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
|
|
2147
|
+
}
|
|
2148
|
+
function readTagStore(rootDir) {
|
|
2149
|
+
const filePath = tagsFilePath(rootDir);
|
|
2150
|
+
if (!(0, import_node_fs10.existsSync)(filePath)) return {};
|
|
2151
|
+
const stat = (0, import_node_fs10.statSync)(filePath);
|
|
2152
|
+
const cached = tagCache.get(filePath);
|
|
2153
|
+
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
2154
|
+
return cached.store;
|
|
2155
|
+
}
|
|
2156
|
+
try {
|
|
2157
|
+
const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
|
|
2158
|
+
const store = JSON.parse(content);
|
|
2159
|
+
tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
|
|
2160
|
+
return store;
|
|
2161
|
+
} catch {
|
|
2162
|
+
return {};
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
function writeTagStore(rootDir, store) {
|
|
2166
|
+
const filePath = tagsFilePath(rootDir);
|
|
2167
|
+
const dir = (0, import_node_path12.dirname)(filePath);
|
|
2168
|
+
(0, import_node_fs10.mkdirSync)(dir, { recursive: true });
|
|
2169
|
+
const cleaned = {};
|
|
2170
|
+
for (const [nodeId, tags] of Object.entries(store)) {
|
|
2171
|
+
if (Object.keys(tags).length > 0) {
|
|
2172
|
+
cleaned[nodeId] = tags;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
(0, import_node_fs10.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
|
|
2176
|
+
tagCache.delete(filePath);
|
|
2177
|
+
}
|
|
2178
|
+
function setTag(rootDir, nodeId, key, value) {
|
|
2179
|
+
const store = readTagStore(rootDir);
|
|
2180
|
+
if (!store[nodeId]) store[nodeId] = {};
|
|
2181
|
+
store[nodeId][key] = value;
|
|
2182
|
+
writeTagStore(rootDir, store);
|
|
2183
|
+
}
|
|
2184
|
+
function removeTag(rootDir, nodeId, key) {
|
|
2185
|
+
const store = readTagStore(rootDir);
|
|
2186
|
+
if (!store[nodeId]) return;
|
|
2187
|
+
delete store[nodeId][key];
|
|
2188
|
+
if (Object.keys(store[nodeId]).length === 0) {
|
|
2189
|
+
delete store[nodeId];
|
|
2190
|
+
}
|
|
2191
|
+
writeTagStore(rootDir, store);
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
// src/server/graph/index.ts
|
|
2195
|
+
var GRAPHS_DIR2 = ".launchsecure/graphs";
|
|
2196
|
+
var LAYERS = ["ui", "api", "db"];
|
|
2197
|
+
var graphCache = /* @__PURE__ */ new Map();
|
|
2198
|
+
var taggedCache = /* @__PURE__ */ new Map();
|
|
2199
|
+
function graphsDir(rootDir) {
|
|
2200
|
+
return (0, import_node_path13.join)(rootDir, GRAPHS_DIR2);
|
|
2201
|
+
}
|
|
2202
|
+
function graphFilePath(rootDir, layer) {
|
|
2203
|
+
return (0, import_node_path13.join)(graphsDir(rootDir), `${layer}.json`);
|
|
2204
|
+
}
|
|
2205
|
+
function tagsFilePath2(rootDir) {
|
|
2206
|
+
return (0, import_node_path13.join)(graphsDir(rootDir), "tags.json");
|
|
2207
|
+
}
|
|
2208
|
+
function getMtimeMs(filePath) {
|
|
2209
|
+
if (!(0, import_node_fs11.existsSync)(filePath)) return 0;
|
|
2210
|
+
return (0, import_node_fs11.statSync)(filePath).mtimeMs;
|
|
2211
|
+
}
|
|
2212
|
+
function invalidateCache(filePath) {
|
|
2213
|
+
graphCache.delete(filePath);
|
|
2214
|
+
}
|
|
2215
|
+
function invalidateTaggedCache(rootDir, layer) {
|
|
2216
|
+
taggedCache.delete(`${rootDir}:${layer}`);
|
|
2217
|
+
}
|
|
2218
|
+
function applyTags(graph, layer, rootDir) {
|
|
2219
|
+
const config = loadConfig(rootDir);
|
|
2220
|
+
const registry = createTaggerRegistry(config, rootDir);
|
|
2221
|
+
const manualTags = readTagStore(rootDir);
|
|
2222
|
+
const taggedNodes = graph.nodes.map((n) => ({ ...n }));
|
|
2223
|
+
const taggers = registry.getForLayer(layer);
|
|
2224
|
+
for (const tagger of taggers) {
|
|
2225
|
+
const assignments = tagger.tag(taggedNodes, layer, rootDir);
|
|
2226
|
+
for (const node of taggedNodes) {
|
|
2227
|
+
if (!node.tags) node.tags = {};
|
|
2228
|
+
const tags = node.tags;
|
|
2229
|
+
const value = assignments.get(node.id);
|
|
2230
|
+
if (value !== void 0) {
|
|
2231
|
+
tags[tagger.tagKey] = value;
|
|
2232
|
+
} else if (tagger.trackUntagged) {
|
|
2233
|
+
tags[tagger.tagKey] = "untagged";
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
for (const node of taggedNodes) {
|
|
2238
|
+
const manual = manualTags[node.id];
|
|
2239
|
+
if (manual) {
|
|
2240
|
+
if (!node.tags) node.tags = {};
|
|
2241
|
+
const tags = node.tags;
|
|
2242
|
+
Object.assign(tags, manual);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
return { ...graph, nodes: taggedNodes };
|
|
2246
|
+
}
|
|
2247
|
+
function readGraphRaw(rootDir, layer) {
|
|
2248
|
+
const filePath = graphFilePath(rootDir, layer);
|
|
2249
|
+
if (!(0, import_node_fs11.existsSync)(filePath)) return null;
|
|
2250
|
+
const stat = (0, import_node_fs11.statSync)(filePath);
|
|
2251
|
+
const cached = graphCache.get(filePath);
|
|
2252
|
+
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
2253
|
+
return cached.graph;
|
|
2254
|
+
}
|
|
2255
|
+
const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
|
|
2256
|
+
const graph = JSON.parse(content);
|
|
2257
|
+
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
2258
|
+
return graph;
|
|
2259
|
+
}
|
|
2260
|
+
function readGraph(rootDir, layer) {
|
|
2261
|
+
const rawFilePath = graphFilePath(rootDir, layer);
|
|
2262
|
+
if (!(0, import_node_fs11.existsSync)(rawFilePath)) return null;
|
|
2263
|
+
const rawMtime = getMtimeMs(rawFilePath);
|
|
2264
|
+
const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
|
|
2265
|
+
const cacheKey = `${rootDir}:${layer}`;
|
|
2266
|
+
const cached = taggedCache.get(cacheKey);
|
|
2267
|
+
if (cached && cached.rawMtimeMs === rawMtime && cached.tagsMtimeMs === tagsMtime) {
|
|
2268
|
+
return cached.graph;
|
|
2269
|
+
}
|
|
2270
|
+
const raw = readGraphRaw(rootDir, layer);
|
|
2271
|
+
if (!raw) return null;
|
|
2272
|
+
const tagged = applyTags(raw, layer, rootDir);
|
|
2273
|
+
taggedCache.set(cacheKey, { rawMtimeMs: rawMtime, tagsMtimeMs: tagsMtime, graph: tagged });
|
|
2274
|
+
return tagged;
|
|
2275
|
+
}
|
|
2276
|
+
function readAllGraphs(rootDir) {
|
|
2277
|
+
const result = {};
|
|
2278
|
+
for (const layer of LAYERS) {
|
|
2279
|
+
const graph = readGraph(rootDir, layer);
|
|
2280
|
+
if (graph) result[layer] = graph;
|
|
2281
|
+
}
|
|
2282
|
+
return result;
|
|
2283
|
+
}
|
|
2284
|
+
function generateGraph(rootDir, layer) {
|
|
2285
|
+
const dir = graphsDir(rootDir);
|
|
2286
|
+
(0, import_node_fs11.mkdirSync)(dir, { recursive: true });
|
|
2287
|
+
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
2288
|
+
for (const result of results) {
|
|
2289
|
+
const filePath = graphFilePath(rootDir, result.layer);
|
|
2290
|
+
(0, import_node_fs11.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
2291
|
+
invalidateCache(filePath);
|
|
2292
|
+
invalidateTaggedCache(rootDir, result.layer);
|
|
2293
|
+
}
|
|
2294
|
+
return results;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
// src/server/lockfile.ts
|
|
2298
|
+
var import_node_child_process = require("node:child_process");
|
|
2299
|
+
var import_node_fs12 = require("node:fs");
|
|
2300
|
+
var import_node_os = require("node:os");
|
|
2301
|
+
var import_node_path14 = require("node:path");
|
|
2302
|
+
function lockDir(projectRoot) {
|
|
2303
|
+
if (projectRoot) {
|
|
2304
|
+
return (0, import_node_path14.join)(projectRoot, ".launchsecure");
|
|
2305
|
+
}
|
|
2306
|
+
return (0, import_node_path14.join)((0, import_node_os.homedir)(), ".launchsecure");
|
|
2307
|
+
}
|
|
2308
|
+
function lockPath(projectRoot) {
|
|
2309
|
+
return (0, import_node_path14.join)(lockDir(projectRoot), "launch-chart.lock");
|
|
2310
|
+
}
|
|
2311
|
+
var _activeProjectRoot;
|
|
2312
|
+
function readLock(projectRoot) {
|
|
2313
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
2314
|
+
const p = lockPath(root);
|
|
2315
|
+
if (!(0, import_node_fs12.existsSync)(p)) {
|
|
2316
|
+
if (root) {
|
|
2317
|
+
const globalP = lockPath();
|
|
2318
|
+
if ((0, import_node_fs12.existsSync)(globalP)) {
|
|
2319
|
+
try {
|
|
2320
|
+
const data = JSON.parse((0, import_node_fs12.readFileSync)(globalP, "utf-8"));
|
|
2321
|
+
if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
|
|
2322
|
+
return data;
|
|
2323
|
+
}
|
|
2324
|
+
} catch {
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
return null;
|
|
2329
|
+
}
|
|
2330
|
+
try {
|
|
2331
|
+
const data = JSON.parse((0, import_node_fs12.readFileSync)(p, "utf-8"));
|
|
2332
|
+
if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
|
|
2333
|
+
return data;
|
|
2334
|
+
} catch {
|
|
2335
|
+
return null;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
1476
2338
|
function isPidAlive(pid) {
|
|
1477
2339
|
try {
|
|
1478
2340
|
process.kill(pid, 0);
|
|
@@ -1495,34 +2357,41 @@ function getListenerPid(port) {
|
|
|
1495
2357
|
return null;
|
|
1496
2358
|
}
|
|
1497
2359
|
}
|
|
1498
|
-
function getLiveLock() {
|
|
1499
|
-
const
|
|
2360
|
+
function getLiveLock(projectRoot) {
|
|
2361
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
2362
|
+
const lock = readLock(root);
|
|
1500
2363
|
if (!lock) return null;
|
|
1501
2364
|
const listenerPid = getListenerPid(lock.port);
|
|
1502
2365
|
const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
|
|
1503
2366
|
if (!live) {
|
|
1504
2367
|
try {
|
|
1505
|
-
(0,
|
|
2368
|
+
(0, import_node_fs12.unlinkSync)(lockPath(root));
|
|
1506
2369
|
} catch {
|
|
1507
2370
|
}
|
|
1508
2371
|
return null;
|
|
1509
2372
|
}
|
|
1510
2373
|
return lock;
|
|
1511
2374
|
}
|
|
1512
|
-
function writeLock(data) {
|
|
1513
|
-
|
|
1514
|
-
(0,
|
|
2375
|
+
function writeLock(data, projectRoot) {
|
|
2376
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
2377
|
+
(0, import_node_fs12.mkdirSync)(lockDir(root), { recursive: true });
|
|
2378
|
+
(0, import_node_fs12.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
2379
|
+
if (root) _activeProjectRoot = root;
|
|
1515
2380
|
}
|
|
1516
|
-
function clearLock() {
|
|
2381
|
+
function clearLock(projectRoot) {
|
|
2382
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
1517
2383
|
try {
|
|
1518
|
-
(0,
|
|
2384
|
+
(0, import_node_fs12.unlinkSync)(lockPath(root));
|
|
1519
2385
|
} catch {
|
|
1520
2386
|
}
|
|
1521
2387
|
}
|
|
1522
2388
|
|
|
1523
2389
|
// src/server/chart-serve.ts
|
|
1524
|
-
|
|
1525
|
-
var MAX_PORT_SCAN =
|
|
2390
|
+
init_config();
|
|
2391
|
+
var MAX_PORT_SCAN = 3;
|
|
2392
|
+
function randomPort() {
|
|
2393
|
+
return 49152 + Math.floor(Math.random() * (65535 - 49152));
|
|
2394
|
+
}
|
|
1526
2395
|
var MIME_TYPES = {
|
|
1527
2396
|
".html": "text/html; charset=utf-8",
|
|
1528
2397
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -1537,16 +2406,16 @@ var MIME_TYPES = {
|
|
|
1537
2406
|
function findProjectRoot(startDir) {
|
|
1538
2407
|
let dir = startDir;
|
|
1539
2408
|
for (let i = 0; i < 8; i++) {
|
|
1540
|
-
const graphsDir2 =
|
|
1541
|
-
if (
|
|
1542
|
-
const parent =
|
|
2409
|
+
const graphsDir2 = import_node_path15.default.join(dir, ".launchsecure", "graphs");
|
|
2410
|
+
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;
|
|
2411
|
+
const parent = import_node_path15.default.dirname(dir);
|
|
1543
2412
|
if (parent === dir) break;
|
|
1544
2413
|
dir = parent;
|
|
1545
2414
|
}
|
|
1546
2415
|
dir = startDir;
|
|
1547
2416
|
for (let i = 0; i < 8; i++) {
|
|
1548
|
-
if (
|
|
1549
|
-
const parent =
|
|
2417
|
+
if (import_node_fs13.default.existsSync(import_node_path15.default.join(dir, ".git"))) return dir;
|
|
2418
|
+
const parent = import_node_path15.default.dirname(dir);
|
|
1550
2419
|
if (parent === dir) break;
|
|
1551
2420
|
dir = parent;
|
|
1552
2421
|
}
|
|
@@ -1598,16 +2467,16 @@ function buildMergedGraph(projectRoot) {
|
|
|
1598
2467
|
};
|
|
1599
2468
|
}
|
|
1600
2469
|
function serveStatic(res, filePath) {
|
|
1601
|
-
if (!
|
|
1602
|
-
const ext =
|
|
2470
|
+
if (!import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) return false;
|
|
2471
|
+
const ext = import_node_path15.default.extname(filePath).toLowerCase();
|
|
1603
2472
|
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
1604
2473
|
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
1605
|
-
|
|
2474
|
+
import_node_fs13.default.createReadStream(filePath).pipe(res);
|
|
1606
2475
|
return true;
|
|
1607
2476
|
}
|
|
1608
2477
|
function serveIndex(res, clientDir) {
|
|
1609
|
-
const indexPath =
|
|
1610
|
-
if (!
|
|
2478
|
+
const indexPath = import_node_path15.default.join(clientDir, "index.html");
|
|
2479
|
+
if (!import_node_fs13.default.existsSync(indexPath)) {
|
|
1611
2480
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1612
2481
|
res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
|
|
1613
2482
|
return;
|
|
@@ -1615,14 +2484,14 @@ function serveIndex(res, clientDir) {
|
|
|
1615
2484
|
serveStatic(res, indexPath);
|
|
1616
2485
|
}
|
|
1617
2486
|
function tryListen(server, port) {
|
|
1618
|
-
return new Promise((
|
|
2487
|
+
return new Promise((resolve3, reject) => {
|
|
1619
2488
|
const onError = (err) => {
|
|
1620
2489
|
server.off("listening", onListening);
|
|
1621
2490
|
reject(err);
|
|
1622
2491
|
};
|
|
1623
2492
|
const onListening = () => {
|
|
1624
2493
|
server.off("error", onError);
|
|
1625
|
-
|
|
2494
|
+
resolve3(port);
|
|
1626
2495
|
};
|
|
1627
2496
|
server.once("error", onError);
|
|
1628
2497
|
server.once("listening", onListening);
|
|
@@ -1649,7 +2518,7 @@ async function bindWithFallback(server, startPort) {
|
|
|
1649
2518
|
async function startChartServer(opts = {}) {
|
|
1650
2519
|
const cwd = opts.cwd ?? process.cwd();
|
|
1651
2520
|
const projectRoot = findProjectRoot(cwd);
|
|
1652
|
-
const existing = getLiveLock();
|
|
2521
|
+
const existing = getLiveLock(projectRoot);
|
|
1653
2522
|
if (existing) {
|
|
1654
2523
|
if (!opts.quiet) {
|
|
1655
2524
|
process.stderr.write(
|
|
@@ -1659,7 +2528,7 @@ async function startChartServer(opts = {}) {
|
|
|
1659
2528
|
}
|
|
1660
2529
|
return { port: existing.port, url: existing.url };
|
|
1661
2530
|
}
|
|
1662
|
-
const clientDir = opts.clientDir ??
|
|
2531
|
+
const clientDir = opts.clientDir ?? import_node_path15.default.join(__dirname, "..", "chart-client");
|
|
1663
2532
|
const server = import_node_http.default.createServer((req, res) => {
|
|
1664
2533
|
try {
|
|
1665
2534
|
const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
@@ -1697,13 +2566,154 @@ async function startChartServer(opts = {}) {
|
|
|
1697
2566
|
}
|
|
1698
2567
|
return;
|
|
1699
2568
|
}
|
|
2569
|
+
if (req.method === "GET" && url2.pathname === "/api/file-content") {
|
|
2570
|
+
const relPath = url2.searchParams.get("path");
|
|
2571
|
+
if (!relPath || relPath.includes("..") || import_node_path15.default.isAbsolute(relPath)) {
|
|
2572
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2573
|
+
res.end(JSON.stringify({ error: "Invalid path" }));
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
const filePath = import_node_path15.default.join(projectRoot, relPath);
|
|
2577
|
+
if (!filePath.startsWith(projectRoot) || !import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) {
|
|
2578
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2579
|
+
res.end(JSON.stringify({ error: "File not found" }));
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
const ext = import_node_path15.default.extname(filePath).toLowerCase();
|
|
2583
|
+
const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
|
|
2584
|
+
const content = import_node_fs13.default.readFileSync(filePath, "utf-8");
|
|
2585
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2586
|
+
res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
|
|
2587
|
+
return;
|
|
2588
|
+
}
|
|
1700
2589
|
if (req.method === "GET" && url2.pathname === "/api/health") {
|
|
1701
2590
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1702
2591
|
res.end(JSON.stringify({ ok: true, projectRoot }));
|
|
1703
2592
|
return;
|
|
1704
2593
|
}
|
|
2594
|
+
if (req.method === "GET" && url2.pathname === "/api/parser-config") {
|
|
2595
|
+
const config = loadConfig(projectRoot);
|
|
2596
|
+
const detection = [
|
|
2597
|
+
{ id: "react-nextjs", layer: "ui", label: "React + Next.js", detected: reactNextjsParser.detect(projectRoot) },
|
|
2598
|
+
{ id: "nextjs-routes", layer: "api", label: "Next.js API Routes", detected: nextjsRoutesParser.detect(projectRoot) },
|
|
2599
|
+
{ id: "prisma-schema", layer: "db", label: "Prisma Schema", detected: prismaSchemaParser.detect(projectRoot) }
|
|
2600
|
+
];
|
|
2601
|
+
const crosslayerParsers = [
|
|
2602
|
+
{ id: "fetch-resolver", label: "Fetch / api.method() calls" },
|
|
2603
|
+
{ id: "api-annotations", label: "@api annotations" },
|
|
2604
|
+
{ id: "url-literal-scanner", label: "/api/... URL literals" }
|
|
2605
|
+
];
|
|
2606
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2607
|
+
res.end(JSON.stringify({ config, detection, crosslayerParsers }));
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
if (req.method === "POST" && url2.pathname === "/api/parser-config") {
|
|
2611
|
+
let body = "";
|
|
2612
|
+
req.on("data", (chunk) => {
|
|
2613
|
+
body += chunk.toString();
|
|
2614
|
+
});
|
|
2615
|
+
req.on("end", () => {
|
|
2616
|
+
try {
|
|
2617
|
+
const newConfig = JSON.parse(body);
|
|
2618
|
+
const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
|
|
2619
|
+
import_node_fs13.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
|
|
2620
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2621
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2622
|
+
} catch (err) {
|
|
2623
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2624
|
+
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
2625
|
+
}
|
|
2626
|
+
});
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
|
|
2630
|
+
const config = loadConfig(projectRoot);
|
|
2631
|
+
const builtinTaggers = [
|
|
2632
|
+
{ id: "module", tagKey: "module", trackUntagged: config.taggers?.trackUntagged?.module ?? true },
|
|
2633
|
+
{ id: "screen", tagKey: "screen", trackUntagged: config.taggers?.trackUntagged?.screen ?? true }
|
|
2634
|
+
];
|
|
2635
|
+
const disabled = config.taggers?.disabled ?? [];
|
|
2636
|
+
const customTaggers = config.taggers?.custom ?? [];
|
|
2637
|
+
const moduleRules = config.taggers?.module?.rules ?? [];
|
|
2638
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2639
|
+
res.end(JSON.stringify({ builtinTaggers, disabled, customTaggers, moduleRules }));
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
if (req.method === "POST" && url2.pathname === "/api/tagger-config") {
|
|
2643
|
+
let body = "";
|
|
2644
|
+
req.on("data", (chunk) => {
|
|
2645
|
+
body += chunk.toString();
|
|
2646
|
+
});
|
|
2647
|
+
req.on("end", () => {
|
|
2648
|
+
try {
|
|
2649
|
+
const taggerConfig = JSON.parse(body);
|
|
2650
|
+
const config = loadConfig(projectRoot);
|
|
2651
|
+
config.taggers = taggerConfig;
|
|
2652
|
+
const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
|
|
2653
|
+
import_node_fs13.default.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2654
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2655
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2656
|
+
} catch (err) {
|
|
2657
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2658
|
+
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
2659
|
+
}
|
|
2660
|
+
});
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
if (req.method === "GET" && url2.pathname === "/api/tags") {
|
|
2664
|
+
const store = readTagStore(projectRoot);
|
|
2665
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2666
|
+
res.end(JSON.stringify(store));
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
if (req.method === "POST" && url2.pathname === "/api/tags") {
|
|
2670
|
+
let body = "";
|
|
2671
|
+
req.on("data", (chunk) => {
|
|
2672
|
+
body += chunk.toString();
|
|
2673
|
+
});
|
|
2674
|
+
req.on("end", () => {
|
|
2675
|
+
try {
|
|
2676
|
+
const { nodeId, key, value } = JSON.parse(body);
|
|
2677
|
+
if (!nodeId || !key || !value) {
|
|
2678
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2679
|
+
res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
setTag(projectRoot, nodeId, key, value);
|
|
2683
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2684
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2685
|
+
} catch (err) {
|
|
2686
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2687
|
+
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
2688
|
+
}
|
|
2689
|
+
});
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
if (req.method === "DELETE" && url2.pathname === "/api/tags") {
|
|
2693
|
+
let body = "";
|
|
2694
|
+
req.on("data", (chunk) => {
|
|
2695
|
+
body += chunk.toString();
|
|
2696
|
+
});
|
|
2697
|
+
req.on("end", () => {
|
|
2698
|
+
try {
|
|
2699
|
+
const { nodeId, key } = JSON.parse(body);
|
|
2700
|
+
if (!nodeId || !key) {
|
|
2701
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2702
|
+
res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
removeTag(projectRoot, nodeId, key);
|
|
2706
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2707
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2708
|
+
} catch (err) {
|
|
2709
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2710
|
+
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
2711
|
+
}
|
|
2712
|
+
});
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
1705
2715
|
if (url2.pathname !== "/") {
|
|
1706
|
-
const staticPath =
|
|
2716
|
+
const staticPath = import_node_path15.default.join(clientDir, url2.pathname);
|
|
1707
2717
|
if (serveStatic(res, staticPath)) return;
|
|
1708
2718
|
}
|
|
1709
2719
|
serveIndex(res, clientDir);
|
|
@@ -1712,7 +2722,8 @@ async function startChartServer(opts = {}) {
|
|
|
1712
2722
|
res.end(JSON.stringify({ error: String(err) }));
|
|
1713
2723
|
}
|
|
1714
2724
|
});
|
|
1715
|
-
const
|
|
2725
|
+
const startPort = opts.port ?? randomPort();
|
|
2726
|
+
const port = await bindWithFallback(server, startPort);
|
|
1716
2727
|
const url = `http://localhost:${port}`;
|
|
1717
2728
|
writeLock({
|
|
1718
2729
|
pid: process.pid,
|
|
@@ -1720,9 +2731,9 @@ async function startChartServer(opts = {}) {
|
|
|
1720
2731
|
cwd,
|
|
1721
2732
|
url,
|
|
1722
2733
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1723
|
-
});
|
|
2734
|
+
}, projectRoot);
|
|
1724
2735
|
const cleanup = () => {
|
|
1725
|
-
clearLock();
|
|
2736
|
+
clearLock(projectRoot);
|
|
1726
2737
|
server.close();
|
|
1727
2738
|
};
|
|
1728
2739
|
process.once("SIGINT", () => {
|