@neat.is/core 0.7.3 → 0.7.4
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/{chunk-3SBEY67S.js → chunk-KN6O7KMK.js} +2 -2
- package/dist/{chunk-BY6ZP5WA.js → chunk-UU4XKSTO.js} +3734 -3418
- package/dist/chunk-UU4XKSTO.js.map +1 -0
- package/dist/cli.cjs +1861 -1533
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +18 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1192 -878
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +1192 -878
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +1256 -942
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-BY6ZP5WA.js.map +0 -1
- /package/dist/{chunk-3SBEY67S.js.map → chunk-KN6O7KMK.js.map} +0 -0
package/dist/cli.cjs
CHANGED
|
@@ -825,8 +825,8 @@ init_cjs_shims();
|
|
|
825
825
|
|
|
826
826
|
// src/ingest.ts
|
|
827
827
|
init_cjs_shims();
|
|
828
|
-
var
|
|
829
|
-
var
|
|
828
|
+
var import_node_fs8 = require("fs");
|
|
829
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
830
830
|
var sourceMapJs = __toESM(require("source-map-js"), 1);
|
|
831
831
|
|
|
832
832
|
// src/policy.ts
|
|
@@ -2175,16 +2175,16 @@ var PolicyViolationsLog = class {
|
|
|
2175
2175
|
};
|
|
2176
2176
|
|
|
2177
2177
|
// src/ingest.ts
|
|
2178
|
-
var
|
|
2178
|
+
var import_types8 = require("@neat.is/types");
|
|
2179
2179
|
|
|
2180
2180
|
// src/extract/routes.ts
|
|
2181
2181
|
init_cjs_shims();
|
|
2182
|
-
var
|
|
2183
|
-
var
|
|
2184
|
-
var
|
|
2185
|
-
var
|
|
2186
|
-
var
|
|
2187
|
-
var
|
|
2182
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
2183
|
+
var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
|
|
2184
|
+
var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
|
|
2185
|
+
var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
|
|
2186
|
+
var import_tree_sitter_go2 = __toESM(require("tree-sitter-go"), 1);
|
|
2187
|
+
var import_types6 = require("@neat.is/types");
|
|
2188
2188
|
|
|
2189
2189
|
// src/extract/shared.ts
|
|
2190
2190
|
init_cjs_shims();
|
|
@@ -2567,7 +2567,15 @@ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
|
|
|
2567
2567
|
return { fileNodeId, nodesAdded, edgesAdded };
|
|
2568
2568
|
}
|
|
2569
2569
|
|
|
2570
|
-
// src/extract/
|
|
2570
|
+
// src/extract/imports.ts
|
|
2571
|
+
init_cjs_shims();
|
|
2572
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
2573
|
+
var import_node_fs7 = require("fs");
|
|
2574
|
+
var import_tree_sitter = __toESM(require("tree-sitter"), 1);
|
|
2575
|
+
var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
|
|
2576
|
+
var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
|
|
2577
|
+
var import_tree_sitter_go = __toESM(require("tree-sitter-go"), 1);
|
|
2578
|
+
var import_types5 = require("@neat.is/types");
|
|
2571
2579
|
var PARSE_CHUNK = 16384;
|
|
2572
2580
|
function parseSource(parser, source) {
|
|
2573
2581
|
return parser.parse(
|
|
@@ -2589,533 +2597,910 @@ function makeGoParser() {
|
|
|
2589
2597
|
p.setLanguage(import_tree_sitter_go.default);
|
|
2590
2598
|
return p;
|
|
2591
2599
|
}
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
"
|
|
2600
|
-
"all"
|
|
2601
|
-
]);
|
|
2602
|
-
var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
2603
|
-
var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
|
|
2604
|
-
function ginRoutesFromSource(source, parser) {
|
|
2605
|
-
const tree = parseSource(parser, source);
|
|
2606
|
-
const prefixes = /* @__PURE__ */ new Map();
|
|
2607
|
-
const out = [];
|
|
2608
|
-
walk(tree.rootNode, (node) => {
|
|
2609
|
-
if (node.type === "short_var_declaration" || node.type === "var_spec") {
|
|
2610
|
-
const name = node.childForFieldName("left")?.namedChild(0)?.text ?? node.childForFieldName("name")?.text;
|
|
2611
|
-
const value = node.childForFieldName("right")?.namedChild(0) ?? node.childForFieldName("value");
|
|
2612
|
-
if (name && value?.type === "call_expression") {
|
|
2613
|
-
const fn2 = value.childForFieldName("function");
|
|
2614
|
-
const field = fn2?.childForFieldName("field")?.text;
|
|
2615
|
-
const first2 = value.childForFieldName("arguments")?.namedChild(0);
|
|
2616
|
-
if (field === "Group" && first2?.type === "interpreted_string_literal") {
|
|
2617
|
-
prefixes.set(name, first2.text.slice(1, -1));
|
|
2618
|
-
}
|
|
2619
|
-
}
|
|
2620
|
-
return;
|
|
2621
|
-
}
|
|
2622
|
-
if (node.type !== "call_expression") return;
|
|
2623
|
-
const fn = node.childForFieldName("function");
|
|
2624
|
-
if (fn?.type !== "selector_expression") return;
|
|
2625
|
-
const method = fn.childForFieldName("field")?.text?.toUpperCase();
|
|
2626
|
-
if (!method || !ROUTER_METHODS.has(method.toLowerCase())) return;
|
|
2627
|
-
const receiver = fn.childForFieldName("operand")?.text ?? "";
|
|
2628
|
-
const first = node.childForFieldName("arguments")?.namedChild(0);
|
|
2629
|
-
if (first?.type !== "interpreted_string_literal") return;
|
|
2630
|
-
const leaf = first.text.slice(1, -1);
|
|
2631
|
-
out.push({
|
|
2632
|
-
method: method === "ALL" ? "ALL" : method,
|
|
2633
|
-
pathTemplate: canonicalizeTemplate((prefixes.get(receiver) ?? "") + leaf),
|
|
2634
|
-
line: node.startPosition.row + 1,
|
|
2635
|
-
framework: "gin"
|
|
2636
|
-
});
|
|
2637
|
-
});
|
|
2638
|
-
return out;
|
|
2639
|
-
}
|
|
2640
|
-
var FASTAPI_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
|
|
2641
|
-
var NESTJS_METHODS = /* @__PURE__ */ new Map([
|
|
2642
|
-
["Get", "GET"],
|
|
2643
|
-
["Post", "POST"],
|
|
2644
|
-
["Put", "PUT"],
|
|
2645
|
-
["Patch", "PATCH"],
|
|
2646
|
-
["Delete", "DELETE"],
|
|
2647
|
-
["Options", "OPTIONS"],
|
|
2648
|
-
["Head", "HEAD"],
|
|
2649
|
-
["All", "ALL"]
|
|
2650
|
-
]);
|
|
2651
|
-
function canonicalizeTemplate(raw) {
|
|
2652
|
-
let p = raw.split("?")[0].split("#")[0];
|
|
2653
|
-
if (!p.startsWith("/")) p = "/" + p;
|
|
2654
|
-
if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
|
|
2655
|
-
return p;
|
|
2656
|
-
}
|
|
2657
|
-
function isDynamicSegment(seg) {
|
|
2658
|
-
if (seg.length === 0) return false;
|
|
2659
|
-
if (seg.includes(":")) return true;
|
|
2660
|
-
if (seg.startsWith("{") || seg.startsWith("[")) return true;
|
|
2661
|
-
if (/^\d+$/.test(seg)) return true;
|
|
2662
|
-
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(seg)) return true;
|
|
2663
|
-
if (/^[0-9a-f]{24,}$/i.test(seg)) return true;
|
|
2664
|
-
return false;
|
|
2600
|
+
function stringLiteralText(node) {
|
|
2601
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2602
|
+
const child = node.child(i);
|
|
2603
|
+
if (child?.type === "string_fragment") return child.text;
|
|
2604
|
+
}
|
|
2605
|
+
const raw = node.text;
|
|
2606
|
+
if (raw.length >= 2) return raw.slice(1, -1);
|
|
2607
|
+
return raw.length === 0 ? null : "";
|
|
2665
2608
|
}
|
|
2666
|
-
function
|
|
2667
|
-
const
|
|
2668
|
-
|
|
2669
|
-
const normalised = segments.map((seg) => isDynamicSegment(seg) ? ":param" : seg.toLowerCase());
|
|
2670
|
-
return "/" + normalised.join("/");
|
|
2609
|
+
function clipSnippet(text) {
|
|
2610
|
+
const oneLine = text.split("\n")[0] ?? text;
|
|
2611
|
+
return oneLine.length > 120 ? oneLine.slice(0, 120) : oneLine;
|
|
2671
2612
|
}
|
|
2672
|
-
function
|
|
2673
|
-
|
|
2613
|
+
function collectGoImports(node, out) {
|
|
2614
|
+
if (node.type === "import_spec") {
|
|
2615
|
+
const pathNode = node.childForFieldName("path");
|
|
2616
|
+
if (pathNode) {
|
|
2617
|
+
const specifier = pathNode.text.replace(/^`|`$/g, "").replace(/^"|"$/g, "");
|
|
2618
|
+
if (specifier) out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
2619
|
+
}
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2674
2622
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2675
2623
|
const child = node.namedChild(i);
|
|
2676
|
-
if (child)
|
|
2624
|
+
if (child) collectGoImports(child, out);
|
|
2677
2625
|
}
|
|
2678
2626
|
}
|
|
2679
|
-
function
|
|
2680
|
-
if (node.type === "
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2627
|
+
function collectJsImports(node, out) {
|
|
2628
|
+
if (node.type === "import_statement") {
|
|
2629
|
+
const source = node.childForFieldName("source");
|
|
2630
|
+
if (source) {
|
|
2631
|
+
const specifier = stringLiteralText(source);
|
|
2632
|
+
if (specifier) {
|
|
2633
|
+
out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
2634
|
+
}
|
|
2684
2635
|
}
|
|
2685
|
-
return
|
|
2636
|
+
return;
|
|
2686
2637
|
}
|
|
2687
|
-
if (node.type === "
|
|
2688
|
-
|
|
2689
|
-
|
|
2638
|
+
if (node.type === "call_expression") {
|
|
2639
|
+
const fn = node.childForFieldName("function");
|
|
2640
|
+
if (fn?.type === "identifier" && fn.text === "require") {
|
|
2641
|
+
const args = node.childForFieldName("arguments");
|
|
2642
|
+
const firstArg = args?.namedChild(0);
|
|
2643
|
+
if (firstArg?.type === "string") {
|
|
2644
|
+
const specifier = stringLiteralText(firstArg);
|
|
2645
|
+
if (specifier) {
|
|
2646
|
+
out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2690
2649
|
}
|
|
2691
|
-
const raw = node.text;
|
|
2692
|
-
return raw.length >= 2 ? raw.slice(1, -1) : "";
|
|
2693
2650
|
}
|
|
2694
|
-
|
|
2651
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2652
|
+
const child = node.namedChild(i);
|
|
2653
|
+
if (child) collectJsImports(child, out);
|
|
2654
|
+
}
|
|
2695
2655
|
}
|
|
2696
|
-
function
|
|
2697
|
-
|
|
2698
|
-
const
|
|
2699
|
-
if (
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2656
|
+
function collectImportedNames(node, out) {
|
|
2657
|
+
if (node.type === "aliased_import") {
|
|
2658
|
+
const nameNode = node.childForFieldName("name");
|
|
2659
|
+
if (nameNode) out.push(nameNode.text);
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
if (node.type === "dotted_name") {
|
|
2663
|
+
out.push(node.text);
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2667
|
+
const child = node.namedChild(i);
|
|
2668
|
+
if (child) collectImportedNames(child, out);
|
|
2706
2669
|
}
|
|
2707
|
-
return null;
|
|
2708
2670
|
}
|
|
2709
|
-
function
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
const
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2671
|
+
function collectPyImports(node, out) {
|
|
2672
|
+
if (node.type === "import_from_statement") {
|
|
2673
|
+
let level = 0;
|
|
2674
|
+
let modulePath = "";
|
|
2675
|
+
const names = [];
|
|
2676
|
+
let pastFrom = false;
|
|
2677
|
+
let pastImport = false;
|
|
2678
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2679
|
+
const child = node.child(i);
|
|
2680
|
+
if (!child) continue;
|
|
2681
|
+
if (!pastFrom) {
|
|
2682
|
+
if (child.type === "from") pastFrom = true;
|
|
2683
|
+
continue;
|
|
2684
|
+
}
|
|
2685
|
+
if (!pastImport) {
|
|
2686
|
+
if (child.type === "import") {
|
|
2687
|
+
pastImport = true;
|
|
2688
|
+
continue;
|
|
2689
|
+
}
|
|
2690
|
+
if (child.type === "relative_import") {
|
|
2691
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
2692
|
+
const rc = child.child(j);
|
|
2693
|
+
if (!rc) continue;
|
|
2694
|
+
if (rc.type === "import_prefix") {
|
|
2695
|
+
for (let k = 0; k < rc.childCount; k++) {
|
|
2696
|
+
if (rc.child(k)?.type === ".") level++;
|
|
2697
|
+
}
|
|
2698
|
+
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
2699
|
+
}
|
|
2700
|
+
} else if (child.type === "dotted_name") {
|
|
2701
|
+
modulePath = child.text;
|
|
2729
2702
|
}
|
|
2703
|
+
continue;
|
|
2730
2704
|
}
|
|
2731
|
-
|
|
2705
|
+
collectImportedNames(child, names);
|
|
2706
|
+
}
|
|
2707
|
+
if (level > 0 || modulePath) {
|
|
2708
|
+
out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
2732
2709
|
}
|
|
2733
2710
|
}
|
|
2734
|
-
|
|
2711
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2712
|
+
const child = node.namedChild(i);
|
|
2713
|
+
if (child) collectPyImports(child, out);
|
|
2714
|
+
}
|
|
2735
2715
|
}
|
|
2736
|
-
function
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
if (child.type !== "import_specifier") return;
|
|
2744
|
-
const imported = child.childForFieldName("name")?.text;
|
|
2745
|
-
const local = child.childForFieldName("alias")?.text ?? imported;
|
|
2746
|
-
if (!imported || !local) return;
|
|
2747
|
-
if (imported === "Controller" || NESTJS_METHODS.has(imported)) {
|
|
2748
|
-
imports.set(local, imported);
|
|
2749
|
-
}
|
|
2750
|
-
});
|
|
2751
|
-
});
|
|
2752
|
-
return imports;
|
|
2753
|
-
}
|
|
2754
|
-
function decoratorCall(node) {
|
|
2755
|
-
if (node.type !== "decorator") return null;
|
|
2756
|
-
const expression = node.namedChild(0);
|
|
2757
|
-
return expression?.type === "call_expression" ? expression : null;
|
|
2716
|
+
async function fileExists(p) {
|
|
2717
|
+
try {
|
|
2718
|
+
await import_node_fs7.promises.access(p);
|
|
2719
|
+
return true;
|
|
2720
|
+
} catch {
|
|
2721
|
+
return false;
|
|
2722
|
+
}
|
|
2758
2723
|
}
|
|
2759
|
-
function
|
|
2760
|
-
const
|
|
2761
|
-
|
|
2762
|
-
if (!fn || fn.type !== "identifier") return null;
|
|
2763
|
-
return imports.get(fn.text) ?? null;
|
|
2724
|
+
function isWithinServiceDir(candidate, serviceDir) {
|
|
2725
|
+
const rel = import_node_path7.default.relative(serviceDir, candidate);
|
|
2726
|
+
return rel !== "" && !rel.startsWith("..") && !import_node_path7.default.isAbsolute(rel);
|
|
2764
2727
|
}
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
for (let i = 0; i < first.namedChildCount; i++) {
|
|
2774
|
-
const item = first.namedChild(i);
|
|
2775
|
-
if (!item) continue;
|
|
2776
|
-
const value = staticStringText(item);
|
|
2777
|
-
if (value !== null) paths.push(value);
|
|
2728
|
+
var JS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
2729
|
+
var JS_INDEX_FILES = JS_EXTENSIONS.map((ext) => `index${ext}`);
|
|
2730
|
+
async function firstExistingCandidate(base, serviceDir) {
|
|
2731
|
+
for (const ext of JS_EXTENSIONS) {
|
|
2732
|
+
const candidate = base + ext;
|
|
2733
|
+
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
2734
|
+
return toPosix(import_node_path7.default.relative(serviceDir, candidate));
|
|
2735
|
+
}
|
|
2778
2736
|
}
|
|
2779
|
-
|
|
2737
|
+
for (const indexFile of JS_INDEX_FILES) {
|
|
2738
|
+
const candidate = import_node_path7.default.join(base, indexFile);
|
|
2739
|
+
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
2740
|
+
return toPosix(import_node_path7.default.relative(serviceDir, candidate));
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
return null;
|
|
2780
2744
|
}
|
|
2781
|
-
function
|
|
2782
|
-
const
|
|
2783
|
-
|
|
2745
|
+
async function loadTsPathConfig(serviceDir) {
|
|
2746
|
+
const tsconfigPath = import_node_path7.default.join(serviceDir, "tsconfig.json");
|
|
2747
|
+
let raw;
|
|
2748
|
+
try {
|
|
2749
|
+
raw = await import_node_fs7.promises.readFile(tsconfigPath, "utf8");
|
|
2750
|
+
} catch {
|
|
2751
|
+
return null;
|
|
2752
|
+
}
|
|
2753
|
+
try {
|
|
2754
|
+
const parsed = JSON.parse(raw);
|
|
2755
|
+
const paths = parsed.compilerOptions?.paths;
|
|
2756
|
+
if (!paths || Object.keys(paths).length === 0) return null;
|
|
2757
|
+
const baseUrl = parsed.compilerOptions?.baseUrl;
|
|
2758
|
+
return { paths, baseDir: baseUrl ? import_node_path7.default.resolve(serviceDir, baseUrl) : serviceDir };
|
|
2759
|
+
} catch (err) {
|
|
2760
|
+
recordExtractionError("import alias resolution", tsconfigPath, err);
|
|
2761
|
+
return null;
|
|
2762
|
+
}
|
|
2784
2763
|
}
|
|
2785
|
-
function
|
|
2786
|
-
const
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
const classDecorators = [];
|
|
2794
|
-
for (let i = 0; i < decoratorOwner.namedChildCount; i++) {
|
|
2795
|
-
const child = decoratorOwner.namedChild(i);
|
|
2796
|
-
if (child?.type === "decorator") classDecorators.push(child);
|
|
2764
|
+
async function resolveTsAlias(specifier, config, serviceDir) {
|
|
2765
|
+
for (const [pattern, targets] of Object.entries(config.paths)) {
|
|
2766
|
+
let suffix = null;
|
|
2767
|
+
if (pattern === specifier) {
|
|
2768
|
+
suffix = "";
|
|
2769
|
+
} else if (pattern.endsWith("/*")) {
|
|
2770
|
+
const prefix = pattern.slice(0, -1);
|
|
2771
|
+
if (specifier.startsWith(prefix)) suffix = specifier.slice(prefix.length);
|
|
2797
2772
|
}
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
if (!body) return;
|
|
2807
|
-
for (let i = 0; i < body.namedChildCount; i++) {
|
|
2808
|
-
const methodNode = body.namedChild(i);
|
|
2809
|
-
if (methodNode?.type !== "method_definition") continue;
|
|
2810
|
-
for (let j = 0; j < methodNode.namedChildCount; j++) {
|
|
2811
|
-
const decorator = methodNode.namedChild(j);
|
|
2812
|
-
if (decorator?.type !== "decorator") continue;
|
|
2813
|
-
const canonical = decoratorCanonicalName(decorator, imports);
|
|
2814
|
-
const method = canonical ? NESTJS_METHODS.get(canonical) : void 0;
|
|
2815
|
-
const call = decoratorCall(decorator);
|
|
2816
|
-
if (!method || !call) continue;
|
|
2817
|
-
const leaves = nestStaticPaths(call);
|
|
2818
|
-
for (const prefix of prefixes) {
|
|
2819
|
-
for (const leaf of leaves) {
|
|
2820
|
-
out.push({
|
|
2821
|
-
method,
|
|
2822
|
-
pathTemplate: nestJoinedPath(prefix, leaf),
|
|
2823
|
-
line: decorator.startPosition.row + 1,
|
|
2824
|
-
framework: "nestjs"
|
|
2825
|
-
});
|
|
2826
|
-
}
|
|
2827
|
-
}
|
|
2773
|
+
if (suffix === null) continue;
|
|
2774
|
+
for (const target of targets) {
|
|
2775
|
+
const targetBase = target.endsWith("/*") ? target.slice(0, -2) : target.replace(/\*$/, "");
|
|
2776
|
+
const resolvedBase = import_node_path7.default.resolve(config.baseDir, targetBase, suffix);
|
|
2777
|
+
const hit = await firstExistingCandidate(resolvedBase, serviceDir);
|
|
2778
|
+
if (hit) return hit;
|
|
2779
|
+
if (isWithinServiceDir(resolvedBase, serviceDir) && await fileExists(resolvedBase)) {
|
|
2780
|
+
return toPosix(import_node_path7.default.relative(serviceDir, resolvedBase));
|
|
2828
2781
|
}
|
|
2829
2782
|
}
|
|
2830
|
-
}
|
|
2831
|
-
return
|
|
2783
|
+
}
|
|
2784
|
+
return null;
|
|
2832
2785
|
}
|
|
2833
|
-
function
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
if (
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
const first = args?.namedChild(0);
|
|
2846
|
-
if (!first) return;
|
|
2847
|
-
const line = node.startPosition.row + 1;
|
|
2848
|
-
if (ROUTER_METHODS.has(method)) {
|
|
2849
|
-
const p = staticStringText(first);
|
|
2850
|
-
if (p && p.startsWith("/")) {
|
|
2851
|
-
out.push({
|
|
2852
|
-
method: method === "all" ? "ALL" : method.toUpperCase(),
|
|
2853
|
-
pathTemplate: canonicalizeTemplate(p),
|
|
2854
|
-
line,
|
|
2855
|
-
framework
|
|
2856
|
-
});
|
|
2786
|
+
async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
|
|
2787
|
+
if (!specifier) return null;
|
|
2788
|
+
if (specifier.startsWith("./") || specifier.startsWith("../")) {
|
|
2789
|
+
const base = import_node_path7.default.resolve(importerDir, specifier);
|
|
2790
|
+
const ext = import_node_path7.default.extname(specifier);
|
|
2791
|
+
if (ext) {
|
|
2792
|
+
if (ext === ".js" || ext === ".jsx") {
|
|
2793
|
+
const tsExt = ext === ".jsx" ? ".tsx" : ".ts";
|
|
2794
|
+
const tsSibling = base.slice(0, -ext.length) + tsExt;
|
|
2795
|
+
if (isWithinServiceDir(tsSibling, serviceDir) && await fileExists(tsSibling)) {
|
|
2796
|
+
return toPosix(import_node_path7.default.relative(serviceDir, tsSibling));
|
|
2797
|
+
}
|
|
2857
2798
|
}
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
const methods = fastifyRouteMethods(first);
|
|
2864
|
-
const list = methods.length > 0 ? methods : ["ALL"];
|
|
2865
|
-
for (const m of list) {
|
|
2866
|
-
out.push({
|
|
2867
|
-
method: m === "ALL" ? "ALL" : m.toUpperCase(),
|
|
2868
|
-
pathTemplate: canonicalizeTemplate(url),
|
|
2869
|
-
line,
|
|
2870
|
-
framework: "fastify"
|
|
2871
|
-
});
|
|
2799
|
+
if (isWithinServiceDir(base, serviceDir) && await fileExists(base)) {
|
|
2800
|
+
return toPosix(import_node_path7.default.relative(serviceDir, base));
|
|
2801
|
+
}
|
|
2802
|
+
if (!JS_EXTENSIONS.includes(ext)) {
|
|
2803
|
+
return firstExistingCandidate(base, serviceDir);
|
|
2872
2804
|
}
|
|
2805
|
+
return null;
|
|
2873
2806
|
}
|
|
2874
|
-
|
|
2875
|
-
return out;
|
|
2876
|
-
}
|
|
2877
|
-
function segmentsOf(relFile) {
|
|
2878
|
-
return toPosix(relFile).split("/").filter((s) => s.length > 0);
|
|
2879
|
-
}
|
|
2880
|
-
function isNextAppRouteFile(relFile) {
|
|
2881
|
-
const segs = segmentsOf(relFile);
|
|
2882
|
-
if (!segs.includes("app")) return false;
|
|
2883
|
-
const base = segs[segs.length - 1] ?? "";
|
|
2884
|
-
return /^route\.(?:js|jsx|mjs|cjs|ts|tsx)$/.test(base);
|
|
2885
|
-
}
|
|
2886
|
-
function isNextPagesApiFile(relFile) {
|
|
2887
|
-
const segs = segmentsOf(relFile);
|
|
2888
|
-
const pagesIdx = segs.indexOf("pages");
|
|
2889
|
-
if (pagesIdx === -1 || segs[pagesIdx + 1] !== "api") return false;
|
|
2890
|
-
const base = segs[segs.length - 1] ?? "";
|
|
2891
|
-
if (/^_(app|document|middleware)\./.test(base)) return false;
|
|
2892
|
-
return JS_ROUTE_EXTENSIONS.has(import_node_path7.default.extname(base));
|
|
2893
|
-
}
|
|
2894
|
-
function nextSegment(seg) {
|
|
2895
|
-
if (seg.startsWith("(") && seg.endsWith(")")) return null;
|
|
2896
|
-
const catchAll = seg.match(/^\[\[?\.\.\.(.+?)\]?\]$/);
|
|
2897
|
-
if (catchAll) return ":" + catchAll[1];
|
|
2898
|
-
const dynamic = seg.match(/^\[(.+?)\]$/);
|
|
2899
|
-
if (dynamic) return ":" + dynamic[1];
|
|
2900
|
-
return seg;
|
|
2901
|
-
}
|
|
2902
|
-
function nextAppPathTemplate(relFile) {
|
|
2903
|
-
const segs = segmentsOf(relFile);
|
|
2904
|
-
const appIdx = segs.lastIndexOf("app");
|
|
2905
|
-
const between = segs.slice(appIdx + 1, segs.length - 1);
|
|
2906
|
-
const parts = [];
|
|
2907
|
-
for (const seg of between) {
|
|
2908
|
-
const mapped = nextSegment(seg);
|
|
2909
|
-
if (mapped !== null) parts.push(mapped);
|
|
2807
|
+
return firstExistingCandidate(base, serviceDir);
|
|
2910
2808
|
}
|
|
2911
|
-
return
|
|
2809
|
+
if (tsPaths) return resolveTsAlias(specifier, tsPaths, serviceDir);
|
|
2810
|
+
return null;
|
|
2912
2811
|
}
|
|
2913
|
-
function
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2812
|
+
async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
2813
|
+
let baseDir;
|
|
2814
|
+
if (imp.level > 0) {
|
|
2815
|
+
baseDir = import_node_path7.default.dirname(importerPath);
|
|
2816
|
+
for (let i = 1; i < imp.level; i++) baseDir = import_node_path7.default.dirname(baseDir);
|
|
2817
|
+
} else {
|
|
2818
|
+
baseDir = serviceDir;
|
|
2819
|
+
}
|
|
2820
|
+
const moduleBase = imp.modulePath ? import_node_path7.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
|
|
2821
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
2822
|
+
let needModuleFile = imp.names.length === 0;
|
|
2823
|
+
for (const name of imp.names) {
|
|
2824
|
+
const submoduleFile = import_node_path7.default.join(moduleBase, `${name}.py`);
|
|
2825
|
+
const subpackageInit = import_node_path7.default.join(moduleBase, name, "__init__.py");
|
|
2826
|
+
if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
|
|
2827
|
+
resolved.add(toPosix(import_node_path7.default.relative(serviceDir, submoduleFile)));
|
|
2828
|
+
} else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
|
|
2829
|
+
resolved.add(toPosix(import_node_path7.default.relative(serviceDir, subpackageInit)));
|
|
2830
|
+
} else {
|
|
2831
|
+
needModuleFile = true;
|
|
2923
2832
|
}
|
|
2924
|
-
const mapped = nextSegment(seg);
|
|
2925
|
-
if (mapped !== null) parts.push(mapped);
|
|
2926
2833
|
}
|
|
2927
|
-
|
|
2834
|
+
if (needModuleFile) {
|
|
2835
|
+
const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path7.default.join(moduleBase, "__init__.py")] : [import_node_path7.default.join(moduleBase, "__init__.py")];
|
|
2836
|
+
for (const candidate of moduleFileCandidates) {
|
|
2837
|
+
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
2838
|
+
resolved.add(toPosix(import_node_path7.default.relative(serviceDir, candidate)));
|
|
2839
|
+
break;
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
return [...resolved];
|
|
2928
2844
|
}
|
|
2929
|
-
function
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2845
|
+
async function resolveGoImport(specifier, modulePath, serviceDir) {
|
|
2846
|
+
if (specifier !== modulePath && !specifier.startsWith(`${modulePath}/`)) return null;
|
|
2847
|
+
const suffix = specifier === modulePath ? "" : specifier.slice(modulePath.length + 1);
|
|
2848
|
+
const dir = import_node_path7.default.join(serviceDir, suffix);
|
|
2849
|
+
const entries = await import_node_fs7.promises.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2850
|
+
const candidates = entries.filter((entry2) => entry2.isFile() && entry2.name.endsWith(".go") && !entry2.name.endsWith("_test.go")).map((entry2) => import_node_path7.default.join(dir, entry2.name));
|
|
2851
|
+
if (candidates.length !== 1) return null;
|
|
2852
|
+
return toPosix(import_node_path7.default.relative(serviceDir, candidates[0]));
|
|
2853
|
+
}
|
|
2854
|
+
function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
|
|
2855
|
+
const importeeFileId = (0, import_types5.fileId)(serviceName, importeeRelPath);
|
|
2856
|
+
if (!graph.hasNode(importeeFileId)) return 0;
|
|
2857
|
+
const edgeId = (0, import_types5.extractedEdgeId)(importerFileId, importeeFileId, import_types5.EdgeType.IMPORTS);
|
|
2858
|
+
if (graph.hasEdge(edgeId)) return 0;
|
|
2859
|
+
const edge = {
|
|
2860
|
+
id: edgeId,
|
|
2861
|
+
source: importerFileId,
|
|
2862
|
+
target: importeeFileId,
|
|
2863
|
+
type: import_types5.EdgeType.IMPORTS,
|
|
2864
|
+
provenance: import_types5.Provenance.EXTRACTED,
|
|
2865
|
+
confidence: (0, import_types5.confidenceForExtracted)("structural"),
|
|
2866
|
+
evidence: { file: importerRelPath, line, snippet: snippet2 }
|
|
2867
|
+
};
|
|
2868
|
+
graph.addEdgeWithKey(edgeId, importerFileId, importeeFileId, edge);
|
|
2869
|
+
return 1;
|
|
2870
|
+
}
|
|
2871
|
+
async function addImports(graph, services) {
|
|
2872
|
+
const jsParser = makeJsParser();
|
|
2873
|
+
const pyParser = makePyParser();
|
|
2874
|
+
const goParser = makeGoParser();
|
|
2875
|
+
let edgesAdded = 0;
|
|
2876
|
+
for (const service of services) {
|
|
2877
|
+
const tsPaths = await loadTsPathConfig(service.dir);
|
|
2878
|
+
const files = await loadSourceFiles(service.dir);
|
|
2879
|
+
for (const file of files) {
|
|
2880
|
+
if (isTestPath(file.path)) continue;
|
|
2881
|
+
const relFile = toPosix(import_node_path7.default.relative(service.dir, file.path));
|
|
2882
|
+
const importerFileId = (0, import_types5.fileId)(service.pkg.name, relFile);
|
|
2883
|
+
const isPython = import_node_path7.default.extname(file.path) === ".py";
|
|
2884
|
+
const isGo = import_node_path7.default.extname(file.path) === ".go";
|
|
2885
|
+
if (isGo) {
|
|
2886
|
+
let goImports = [];
|
|
2887
|
+
try {
|
|
2888
|
+
const tree = parseSource(goParser, file.content);
|
|
2889
|
+
collectGoImports(tree.rootNode, goImports);
|
|
2890
|
+
} catch (err) {
|
|
2891
|
+
recordExtractionError("import extraction", file.path, err);
|
|
2892
|
+
continue;
|
|
2893
|
+
}
|
|
2894
|
+
const goMod = await import_node_fs7.promises.readFile(import_node_path7.default.join(service.dir, "go.mod"), "utf8").catch(() => "");
|
|
2895
|
+
const modulePath = goMod.match(/^\s*module\s+(\S+)\s*$/m)?.[1];
|
|
2896
|
+
if (!modulePath) continue;
|
|
2897
|
+
for (const imp of goImports) {
|
|
2898
|
+
const resolved = await resolveGoImport(imp.specifier, modulePath, service.dir);
|
|
2899
|
+
if (!resolved) continue;
|
|
2900
|
+
edgesAdded += emitImportEdge(graph, service.pkg.name, importerFileId, relFile, resolved, imp.line, imp.snippet);
|
|
2901
|
+
}
|
|
2902
|
+
continue;
|
|
2903
|
+
}
|
|
2904
|
+
if (isPython) {
|
|
2905
|
+
let pyImports = [];
|
|
2906
|
+
try {
|
|
2907
|
+
const tree = parseSource(pyParser, file.content);
|
|
2908
|
+
collectPyImports(tree.rootNode, pyImports);
|
|
2909
|
+
} catch (err) {
|
|
2910
|
+
recordExtractionError("import extraction", file.path, err);
|
|
2911
|
+
continue;
|
|
2912
|
+
}
|
|
2913
|
+
for (const imp of pyImports) {
|
|
2914
|
+
const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
|
|
2915
|
+
for (const resolved of resolvedPaths) {
|
|
2916
|
+
edgesAdded += emitImportEdge(
|
|
2917
|
+
graph,
|
|
2918
|
+
service.pkg.name,
|
|
2919
|
+
importerFileId,
|
|
2920
|
+
relFile,
|
|
2921
|
+
resolved,
|
|
2922
|
+
imp.line,
|
|
2923
|
+
imp.snippet
|
|
2924
|
+
);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
continue;
|
|
2928
|
+
}
|
|
2929
|
+
let jsImports = [];
|
|
2930
|
+
try {
|
|
2931
|
+
const tree = parseSource(jsParser, file.content);
|
|
2932
|
+
collectJsImports(tree.rootNode, jsImports);
|
|
2933
|
+
} catch (err) {
|
|
2934
|
+
recordExtractionError("import extraction", file.path, err);
|
|
2935
|
+
continue;
|
|
2936
|
+
}
|
|
2937
|
+
for (const imp of jsImports) {
|
|
2938
|
+
const resolved = await resolveJsImport(imp.specifier, import_node_path7.default.dirname(file.path), service.dir, tsPaths);
|
|
2939
|
+
if (!resolved) continue;
|
|
2940
|
+
edgesAdded += emitImportEdge(
|
|
2941
|
+
graph,
|
|
2942
|
+
service.pkg.name,
|
|
2943
|
+
importerFileId,
|
|
2944
|
+
relFile,
|
|
2945
|
+
resolved,
|
|
2946
|
+
imp.line,
|
|
2947
|
+
imp.snippet
|
|
2948
|
+
);
|
|
2949
|
+
}
|
|
2940
2950
|
}
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2951
|
+
}
|
|
2952
|
+
return { nodesAdded: 0, edgesAdded };
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
// src/extract/routes.ts
|
|
2956
|
+
var PARSE_CHUNK2 = 16384;
|
|
2957
|
+
function parseSource2(parser, source) {
|
|
2958
|
+
return parser.parse(
|
|
2959
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
|
|
2960
|
+
);
|
|
2961
|
+
}
|
|
2962
|
+
function makeJsParser2() {
|
|
2963
|
+
const p = new import_tree_sitter2.default();
|
|
2964
|
+
p.setLanguage(import_tree_sitter_javascript2.default);
|
|
2965
|
+
return p;
|
|
2966
|
+
}
|
|
2967
|
+
function makePyParser2() {
|
|
2968
|
+
const p = new import_tree_sitter2.default();
|
|
2969
|
+
p.setLanguage(import_tree_sitter_python2.default);
|
|
2970
|
+
return p;
|
|
2971
|
+
}
|
|
2972
|
+
function makeGoParser2() {
|
|
2973
|
+
const p = new import_tree_sitter2.default();
|
|
2974
|
+
p.setLanguage(import_tree_sitter_go2.default);
|
|
2975
|
+
return p;
|
|
2976
|
+
}
|
|
2977
|
+
var ROUTER_METHODS = /* @__PURE__ */ new Set([
|
|
2978
|
+
"get",
|
|
2979
|
+
"post",
|
|
2980
|
+
"put",
|
|
2981
|
+
"patch",
|
|
2982
|
+
"delete",
|
|
2983
|
+
"options",
|
|
2984
|
+
"head",
|
|
2985
|
+
"all"
|
|
2986
|
+
]);
|
|
2987
|
+
var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
2988
|
+
var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
|
|
2989
|
+
function ginRoutesFromSource(source, parser) {
|
|
2990
|
+
const tree = parseSource2(parser, source);
|
|
2991
|
+
const prefixes = /* @__PURE__ */ new Map();
|
|
2992
|
+
const out = [];
|
|
2993
|
+
walk(tree.rootNode, (node) => {
|
|
2994
|
+
if (node.type === "short_var_declaration" || node.type === "var_spec") {
|
|
2995
|
+
const name = node.childForFieldName("left")?.namedChild(0)?.text ?? node.childForFieldName("name")?.text;
|
|
2996
|
+
const value = node.childForFieldName("right")?.namedChild(0) ?? node.childForFieldName("value");
|
|
2997
|
+
if (name && value?.type === "call_expression") {
|
|
2998
|
+
const fn2 = value.childForFieldName("function");
|
|
2999
|
+
const field = fn2?.childForFieldName("field")?.text;
|
|
3000
|
+
const first2 = value.childForFieldName("arguments")?.namedChild(0);
|
|
3001
|
+
if (field === "Group" && first2?.type === "interpreted_string_literal") {
|
|
3002
|
+
prefixes.set(name, first2.text.slice(1, -1));
|
|
3003
|
+
}
|
|
2947
3004
|
}
|
|
3005
|
+
return;
|
|
2948
3006
|
}
|
|
3007
|
+
if (node.type !== "call_expression") return;
|
|
3008
|
+
const fn = node.childForFieldName("function");
|
|
3009
|
+
if (fn?.type !== "selector_expression") return;
|
|
3010
|
+
const method = fn.childForFieldName("field")?.text?.toUpperCase();
|
|
3011
|
+
if (!method || !ROUTER_METHODS.has(method.toLowerCase())) return;
|
|
3012
|
+
const receiver = fn.childForFieldName("operand")?.text ?? "";
|
|
3013
|
+
const first = node.childForFieldName("arguments")?.namedChild(0);
|
|
3014
|
+
if (first?.type !== "interpreted_string_literal") return;
|
|
3015
|
+
const leaf = first.text.slice(1, -1);
|
|
3016
|
+
out.push({
|
|
3017
|
+
method: method === "ALL" ? "ALL" : method,
|
|
3018
|
+
pathTemplate: canonicalizeTemplate((prefixes.get(receiver) ?? "") + leaf),
|
|
3019
|
+
line: node.startPosition.row + 1,
|
|
3020
|
+
framework: "gin"
|
|
3021
|
+
});
|
|
2949
3022
|
});
|
|
2950
3023
|
return out;
|
|
2951
3024
|
}
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
line: 1,
|
|
2969
|
-
framework: "next"
|
|
2970
|
-
}
|
|
2971
|
-
];
|
|
2972
|
-
}
|
|
2973
|
-
return [];
|
|
3025
|
+
var FASTAPI_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
|
|
3026
|
+
var NESTJS_METHODS = /* @__PURE__ */ new Map([
|
|
3027
|
+
["Get", "GET"],
|
|
3028
|
+
["Post", "POST"],
|
|
3029
|
+
["Put", "PUT"],
|
|
3030
|
+
["Patch", "PATCH"],
|
|
3031
|
+
["Delete", "DELETE"],
|
|
3032
|
+
["Options", "OPTIONS"],
|
|
3033
|
+
["Head", "HEAD"],
|
|
3034
|
+
["All", "ALL"]
|
|
3035
|
+
]);
|
|
3036
|
+
function canonicalizeTemplate(raw) {
|
|
3037
|
+
let p = raw.split("?")[0].split("#")[0];
|
|
3038
|
+
if (!p.startsWith("/")) p = "/" + p;
|
|
3039
|
+
if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
|
|
3040
|
+
return p;
|
|
2974
3041
|
}
|
|
2975
|
-
function
|
|
2976
|
-
if (
|
|
3042
|
+
function isDynamicSegment(seg) {
|
|
3043
|
+
if (seg.length === 0) return false;
|
|
3044
|
+
if (seg.includes(":")) return true;
|
|
3045
|
+
if (seg.startsWith("{") || seg.startsWith("[")) return true;
|
|
3046
|
+
if (/^\d+$/.test(seg)) return true;
|
|
3047
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(seg)) return true;
|
|
3048
|
+
if (/^[0-9a-f]{24,}$/i.test(seg)) return true;
|
|
3049
|
+
return false;
|
|
3050
|
+
}
|
|
3051
|
+
function normalizePathTemplate(raw) {
|
|
3052
|
+
const canonical = canonicalizeTemplate(raw);
|
|
3053
|
+
const segments = canonical.split("/").filter((s) => s.length > 0);
|
|
3054
|
+
const normalised = segments.map((seg) => isDynamicSegment(seg) ? ":param" : seg.toLowerCase());
|
|
3055
|
+
return "/" + normalised.join("/");
|
|
3056
|
+
}
|
|
3057
|
+
function walk(node, visit) {
|
|
3058
|
+
visit(node);
|
|
2977
3059
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2978
3060
|
const child = node.namedChild(i);
|
|
2979
|
-
if (child
|
|
2980
|
-
if (child?.type === "string_content") return child.text;
|
|
3061
|
+
if (child) walk(child, visit);
|
|
2981
3062
|
}
|
|
2982
|
-
return "";
|
|
2983
3063
|
}
|
|
2984
|
-
function
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
const val = arg.childForFieldName("value");
|
|
2990
|
-
if (!val || val.type !== "list") return [];
|
|
2991
|
-
const out = [];
|
|
2992
|
-
for (let j = 0; j < val.namedChildCount; j++) {
|
|
2993
|
-
const el = val.namedChild(j);
|
|
2994
|
-
if (el?.type === "string") {
|
|
2995
|
-
const s = pyStaticStringText(el);
|
|
2996
|
-
if (s) out.push(s);
|
|
2997
|
-
}
|
|
3064
|
+
function staticStringText(node) {
|
|
3065
|
+
if (node.type === "string") {
|
|
3066
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3067
|
+
const child = node.namedChild(i);
|
|
3068
|
+
if (child?.type === "string_fragment") return child.text;
|
|
2998
3069
|
}
|
|
2999
|
-
return
|
|
3070
|
+
return "";
|
|
3000
3071
|
}
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
const prefixes = /* @__PURE__ */ new Map();
|
|
3005
|
-
walk(root, (node) => {
|
|
3006
|
-
if (node.type !== "assignment") return;
|
|
3007
|
-
const right = node.childForFieldName("right");
|
|
3008
|
-
if (!right || right.type !== "call") return;
|
|
3009
|
-
const fn = right.childForFieldName("function");
|
|
3010
|
-
if (!fn) return;
|
|
3011
|
-
const ctor = fn.type === "attribute" ? fn.childForFieldName("attribute")?.text : fn.text;
|
|
3012
|
-
const prefixKey = ctor === "APIRouter" ? "prefix" : ctor === "Blueprint" ? "url_prefix" : null;
|
|
3013
|
-
if (!prefixKey) return;
|
|
3014
|
-
const left = node.childForFieldName("left");
|
|
3015
|
-
if (!left || left.type !== "identifier") return;
|
|
3016
|
-
const args = right.childForFieldName("arguments");
|
|
3017
|
-
if (!args) return;
|
|
3018
|
-
for (let i = 0; i < args.namedChildCount; i++) {
|
|
3019
|
-
const arg = args.namedChild(i);
|
|
3020
|
-
if (arg?.type !== "keyword_argument") continue;
|
|
3021
|
-
if (arg.childForFieldName("name")?.text !== prefixKey) continue;
|
|
3022
|
-
const val = arg.childForFieldName("value");
|
|
3023
|
-
const p = val ? pyStaticStringText(val) : null;
|
|
3024
|
-
if (p !== null) prefixes.set(left.text, p);
|
|
3072
|
+
if (node.type === "template_string") {
|
|
3073
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3074
|
+
if (node.namedChild(i)?.type === "template_substitution") return null;
|
|
3025
3075
|
}
|
|
3026
|
-
|
|
3027
|
-
|
|
3076
|
+
const raw = node.text;
|
|
3077
|
+
return raw.length >= 2 ? raw.slice(1, -1) : "";
|
|
3078
|
+
}
|
|
3079
|
+
return null;
|
|
3028
3080
|
}
|
|
3029
|
-
function
|
|
3030
|
-
|
|
3081
|
+
function objectStringProp(objNode, key) {
|
|
3082
|
+
for (let i = 0; i < objNode.namedChildCount; i++) {
|
|
3083
|
+
const pair = objNode.namedChild(i);
|
|
3084
|
+
if (!pair || pair.type !== "pair") continue;
|
|
3085
|
+
const k = pair.childForFieldName("key");
|
|
3086
|
+
if (!k) continue;
|
|
3087
|
+
const kText = k.type === "string" ? staticStringText(k) : k.text;
|
|
3088
|
+
if (kText !== key) continue;
|
|
3089
|
+
const v = pair.childForFieldName("value");
|
|
3090
|
+
if (v) return staticStringText(v);
|
|
3091
|
+
}
|
|
3092
|
+
return null;
|
|
3093
|
+
}
|
|
3094
|
+
function fastifyRouteMethods(objNode) {
|
|
3095
|
+
for (let i = 0; i < objNode.namedChildCount; i++) {
|
|
3096
|
+
const pair = objNode.namedChild(i);
|
|
3097
|
+
if (!pair || pair.type !== "pair") continue;
|
|
3098
|
+
const k = pair.childForFieldName("key");
|
|
3099
|
+
const kText = k ? k.type === "string" ? staticStringText(k) : k.text : null;
|
|
3100
|
+
if (kText !== "method") continue;
|
|
3101
|
+
const v = pair.childForFieldName("value");
|
|
3102
|
+
if (!v) return [];
|
|
3103
|
+
if (v.type === "string" || v.type === "template_string") {
|
|
3104
|
+
const s = staticStringText(v);
|
|
3105
|
+
return s ? [s.toUpperCase()] : [];
|
|
3106
|
+
}
|
|
3107
|
+
if (v.type === "array") {
|
|
3108
|
+
const out = [];
|
|
3109
|
+
for (let j = 0; j < v.namedChildCount; j++) {
|
|
3110
|
+
const el = v.namedChild(j);
|
|
3111
|
+
if (el && (el.type === "string" || el.type === "template_string")) {
|
|
3112
|
+
const s = staticStringText(el);
|
|
3113
|
+
if (s) out.push(s.toUpperCase());
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
return out;
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
return [];
|
|
3120
|
+
}
|
|
3121
|
+
function nestDecoratorImports(root) {
|
|
3122
|
+
const imports = /* @__PURE__ */ new Map();
|
|
3031
3123
|
walk(root, (node) => {
|
|
3032
|
-
if (node.type !== "
|
|
3033
|
-
const
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3124
|
+
if (node.type !== "import_statement") return;
|
|
3125
|
+
const source = node.childForFieldName("source");
|
|
3126
|
+
if (!source || staticStringText(source) !== "@nestjs/common") return;
|
|
3127
|
+
walk(node, (child) => {
|
|
3128
|
+
if (child.type !== "import_specifier") return;
|
|
3129
|
+
const imported = child.childForFieldName("name")?.text;
|
|
3130
|
+
const local = child.childForFieldName("alias")?.text ?? imported;
|
|
3131
|
+
if (!imported || !local) return;
|
|
3132
|
+
if (imported === "Controller" || NESTJS_METHODS.has(imported)) {
|
|
3133
|
+
imports.set(local, imported);
|
|
3134
|
+
}
|
|
3135
|
+
});
|
|
3038
3136
|
});
|
|
3039
|
-
return
|
|
3137
|
+
return imports;
|
|
3040
3138
|
}
|
|
3041
|
-
function
|
|
3042
|
-
if (
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3139
|
+
function decoratorCall(node) {
|
|
3140
|
+
if (node.type !== "decorator") return null;
|
|
3141
|
+
const expression = node.namedChild(0);
|
|
3142
|
+
return expression?.type === "call_expression" ? expression : null;
|
|
3143
|
+
}
|
|
3144
|
+
function decoratorCanonicalName(decorator, imports) {
|
|
3145
|
+
const call = decoratorCall(decorator);
|
|
3146
|
+
const fn = call?.childForFieldName("function");
|
|
3147
|
+
if (!fn || fn.type !== "identifier") return null;
|
|
3148
|
+
return imports.get(fn.text) ?? null;
|
|
3149
|
+
}
|
|
3150
|
+
function nestStaticPaths(call) {
|
|
3151
|
+
const args = call.childForFieldName("arguments");
|
|
3152
|
+
const first = args?.namedChild(0);
|
|
3153
|
+
if (!first) return [""];
|
|
3154
|
+
const single = staticStringText(first);
|
|
3155
|
+
if (single !== null) return [single];
|
|
3156
|
+
if (first.type !== "array") return [];
|
|
3157
|
+
const paths = [];
|
|
3158
|
+
for (let i = 0; i < first.namedChildCount; i++) {
|
|
3159
|
+
const item = first.namedChild(i);
|
|
3160
|
+
if (!item) continue;
|
|
3161
|
+
const value = staticStringText(item);
|
|
3162
|
+
if (value !== null) paths.push(value);
|
|
3048
3163
|
}
|
|
3049
|
-
return
|
|
3164
|
+
return paths;
|
|
3050
3165
|
}
|
|
3051
|
-
function
|
|
3052
|
-
const
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
for (let i = 0; i <
|
|
3065
|
-
const
|
|
3066
|
-
if (
|
|
3067
|
-
|
|
3068
|
-
|
|
3166
|
+
function nestJoinedPath(prefix, leaf) {
|
|
3167
|
+
const segments = [prefix, leaf].map((part) => part.replace(/^\/+|\/+$/g, "")).filter((part) => part.length > 0);
|
|
3168
|
+
return canonicalizeTemplate(segments.join("/"));
|
|
3169
|
+
}
|
|
3170
|
+
function nestjsRoutesFromSource(source, parser) {
|
|
3171
|
+
const tree = parseSource2(parser, source);
|
|
3172
|
+
const imports = nestDecoratorImports(tree.rootNode);
|
|
3173
|
+
if (![...imports.values()].includes("Controller")) return [];
|
|
3174
|
+
const out = [];
|
|
3175
|
+
walk(tree.rootNode, (node) => {
|
|
3176
|
+
if (node.type !== "class_declaration") return;
|
|
3177
|
+
const decoratorOwner = node.parent?.type === "export_statement" ? node.parent : node;
|
|
3178
|
+
const classDecorators = [];
|
|
3179
|
+
for (let i = 0; i < decoratorOwner.namedChildCount; i++) {
|
|
3180
|
+
const child = decoratorOwner.namedChild(i);
|
|
3181
|
+
if (child?.type === "decorator") classDecorators.push(child);
|
|
3182
|
+
}
|
|
3183
|
+
const controller = classDecorators.find(
|
|
3184
|
+
(decorator) => decoratorCanonicalName(decorator, imports) === "Controller"
|
|
3185
|
+
);
|
|
3186
|
+
const controllerCall = controller ? decoratorCall(controller) : null;
|
|
3187
|
+
if (!controllerCall) return;
|
|
3188
|
+
const prefixes = nestStaticPaths(controllerCall);
|
|
3189
|
+
if (prefixes.length === 0) return;
|
|
3190
|
+
const body = node.childForFieldName("body");
|
|
3191
|
+
if (!body) return;
|
|
3192
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
3193
|
+
const methodNode = body.namedChild(i);
|
|
3194
|
+
if (methodNode?.type !== "method_definition") continue;
|
|
3195
|
+
for (let j = 0; j < methodNode.namedChildCount; j++) {
|
|
3196
|
+
const decorator = methodNode.namedChild(j);
|
|
3197
|
+
if (decorator?.type !== "decorator") continue;
|
|
3198
|
+
const canonical = decoratorCanonicalName(decorator, imports);
|
|
3199
|
+
const method = canonical ? NESTJS_METHODS.get(canonical) : void 0;
|
|
3200
|
+
const call = decoratorCall(decorator);
|
|
3201
|
+
if (!method || !call) continue;
|
|
3202
|
+
const leaves = nestStaticPaths(call);
|
|
3203
|
+
for (const prefix of prefixes) {
|
|
3204
|
+
for (const leaf of leaves) {
|
|
3205
|
+
out.push({
|
|
3206
|
+
method,
|
|
3207
|
+
pathTemplate: nestJoinedPath(prefix, leaf),
|
|
3208
|
+
line: decorator.startPosition.row + 1,
|
|
3209
|
+
framework: "nestjs"
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3069
3214
|
}
|
|
3070
|
-
if (prefix !== null && prefix.length > 0) mounts.set(first.text, prefix);
|
|
3071
3215
|
});
|
|
3072
|
-
return
|
|
3216
|
+
return out;
|
|
3073
3217
|
}
|
|
3074
|
-
function
|
|
3075
|
-
const tree =
|
|
3076
|
-
const prefixes = collectPythonRouterPrefixes(tree.rootNode);
|
|
3077
|
-
const consts = collectStringConstants(tree.rootNode);
|
|
3078
|
-
const mounts = collectMountPrefixes(tree.rootNode, consts);
|
|
3218
|
+
function serverRoutesFromSource(source, parser, hasExpress, hasFastify, hasHono = false) {
|
|
3219
|
+
const tree = parseSource2(parser, source);
|
|
3079
3220
|
const out = [];
|
|
3221
|
+
const framework = hasExpress ? "express" : hasFastify ? "fastify" : hasHono ? "hono" : "unknown";
|
|
3080
3222
|
walk(tree.rootNode, (node) => {
|
|
3081
|
-
if (node.type !== "
|
|
3082
|
-
const
|
|
3083
|
-
if (!
|
|
3084
|
-
const
|
|
3085
|
-
if (!
|
|
3086
|
-
const method =
|
|
3087
|
-
|
|
3088
|
-
const isVerb = FASTAPI_METHODS.has(method);
|
|
3089
|
-
const isFlaskRoute = method === "route";
|
|
3090
|
-
const isApiRoute = method === "api_route";
|
|
3091
|
-
if (!isVerb && !isFlaskRoute && !isApiRoute) return;
|
|
3092
|
-
const args = call.childForFieldName("arguments");
|
|
3223
|
+
if (node.type !== "call_expression") return;
|
|
3224
|
+
const fn = node.childForFieldName("function");
|
|
3225
|
+
if (!fn || fn.type !== "member_expression") return;
|
|
3226
|
+
const prop = fn.childForFieldName("property");
|
|
3227
|
+
if (!prop) return;
|
|
3228
|
+
const method = prop.text.toLowerCase();
|
|
3229
|
+
const args = node.childForFieldName("arguments");
|
|
3093
3230
|
const first = args?.namedChild(0);
|
|
3094
|
-
if (!first
|
|
3095
|
-
const rawPath = pyStaticStringText(first);
|
|
3096
|
-
if (rawPath === null || !rawPath.startsWith("/")) return;
|
|
3097
|
-
const obj = fn.childForFieldName("object")?.text;
|
|
3098
|
-
const routerPrefix = obj ? prefixes.get(obj) ?? "" : "";
|
|
3099
|
-
const mountPrefix = obj ? mounts.get(obj) ?? "" : "";
|
|
3100
|
-
const pathTemplate = canonicalizeTemplate(mountPrefix + routerPrefix + rawPath);
|
|
3231
|
+
if (!first) return;
|
|
3101
3232
|
const line = node.startPosition.row + 1;
|
|
3102
|
-
if (
|
|
3103
|
-
|
|
3233
|
+
if (ROUTER_METHODS.has(method)) {
|
|
3234
|
+
const p = staticStringText(first);
|
|
3235
|
+
if (p && p.startsWith("/")) {
|
|
3236
|
+
out.push({
|
|
3237
|
+
method: method === "all" ? "ALL" : method.toUpperCase(),
|
|
3238
|
+
pathTemplate: canonicalizeTemplate(p),
|
|
3239
|
+
line,
|
|
3240
|
+
framework
|
|
3241
|
+
});
|
|
3242
|
+
}
|
|
3104
3243
|
return;
|
|
3105
3244
|
}
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3245
|
+
if (method === "route" && hasFastify && first.type === "object") {
|
|
3246
|
+
const url = objectStringProp(first, "url");
|
|
3247
|
+
if (!url || !url.startsWith("/")) return;
|
|
3248
|
+
const methods = fastifyRouteMethods(first);
|
|
3249
|
+
const list = methods.length > 0 ? methods : ["ALL"];
|
|
3250
|
+
for (const m of list) {
|
|
3251
|
+
out.push({
|
|
3252
|
+
method: m === "ALL" ? "ALL" : m.toUpperCase(),
|
|
3253
|
+
pathTemplate: canonicalizeTemplate(url),
|
|
3254
|
+
line,
|
|
3255
|
+
framework: "fastify"
|
|
3256
|
+
});
|
|
3257
|
+
}
|
|
3110
3258
|
}
|
|
3111
3259
|
});
|
|
3112
3260
|
return out;
|
|
3113
3261
|
}
|
|
3114
|
-
function
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3262
|
+
function segmentsOf(relFile) {
|
|
3263
|
+
return toPosix(relFile).split("/").filter((s) => s.length > 0);
|
|
3264
|
+
}
|
|
3265
|
+
function isNextAppRouteFile(relFile) {
|
|
3266
|
+
const segs = segmentsOf(relFile);
|
|
3267
|
+
if (!segs.includes("app")) return false;
|
|
3268
|
+
const base = segs[segs.length - 1] ?? "";
|
|
3269
|
+
return /^route\.(?:js|jsx|mjs|cjs|ts|tsx)$/.test(base);
|
|
3270
|
+
}
|
|
3271
|
+
function isNextPagesApiFile(relFile) {
|
|
3272
|
+
const segs = segmentsOf(relFile);
|
|
3273
|
+
const pagesIdx = segs.indexOf("pages");
|
|
3274
|
+
if (pagesIdx === -1 || segs[pagesIdx + 1] !== "api") return false;
|
|
3275
|
+
const base = segs[segs.length - 1] ?? "";
|
|
3276
|
+
if (/^_(app|document|middleware)\./.test(base)) return false;
|
|
3277
|
+
return JS_ROUTE_EXTENSIONS.has(import_node_path8.default.extname(base));
|
|
3278
|
+
}
|
|
3279
|
+
function nextSegment(seg) {
|
|
3280
|
+
if (seg.startsWith("(") && seg.endsWith(")")) return null;
|
|
3281
|
+
const catchAll = seg.match(/^\[\[?\.\.\.(.+?)\]?\]$/);
|
|
3282
|
+
if (catchAll) return ":" + catchAll[1];
|
|
3283
|
+
const dynamic = seg.match(/^\[(.+?)\]$/);
|
|
3284
|
+
if (dynamic) return ":" + dynamic[1];
|
|
3285
|
+
return seg;
|
|
3286
|
+
}
|
|
3287
|
+
function nextAppPathTemplate(relFile) {
|
|
3288
|
+
const segs = segmentsOf(relFile);
|
|
3289
|
+
const appIdx = segs.lastIndexOf("app");
|
|
3290
|
+
const between = segs.slice(appIdx + 1, segs.length - 1);
|
|
3291
|
+
const parts = [];
|
|
3292
|
+
for (const seg of between) {
|
|
3293
|
+
const mapped = nextSegment(seg);
|
|
3294
|
+
if (mapped !== null) parts.push(mapped);
|
|
3295
|
+
}
|
|
3296
|
+
return "/" + parts.join("/");
|
|
3297
|
+
}
|
|
3298
|
+
function nextPagesApiPathTemplate(relFile) {
|
|
3299
|
+
const segs = segmentsOf(relFile);
|
|
3300
|
+
const pagesIdx = segs.indexOf("pages");
|
|
3301
|
+
const rest = segs.slice(pagesIdx + 1);
|
|
3302
|
+
const parts = [];
|
|
3303
|
+
for (let i = 0; i < rest.length; i++) {
|
|
3304
|
+
let seg = rest[i];
|
|
3305
|
+
if (i === rest.length - 1) {
|
|
3306
|
+
seg = seg.replace(/\.(?:js|jsx|mjs|cjs|ts|tsx)$/, "");
|
|
3307
|
+
if (seg === "index") continue;
|
|
3308
|
+
}
|
|
3309
|
+
const mapped = nextSegment(seg);
|
|
3310
|
+
if (mapped !== null) parts.push(mapped);
|
|
3311
|
+
}
|
|
3312
|
+
return "/" + parts.join("/");
|
|
3313
|
+
}
|
|
3314
|
+
function nextAppMethods(root) {
|
|
3315
|
+
const out = [];
|
|
3316
|
+
walk(root, (node) => {
|
|
3317
|
+
if (node.type !== "export_statement") return;
|
|
3318
|
+
const decl = node.childForFieldName("declaration");
|
|
3319
|
+
if (!decl) return;
|
|
3320
|
+
const line = node.startPosition.row + 1;
|
|
3321
|
+
if (decl.type === "function_declaration") {
|
|
3322
|
+
const name = decl.childForFieldName("name")?.text;
|
|
3323
|
+
if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
if (decl.type === "lexical_declaration" || decl.type === "variable_declaration") {
|
|
3327
|
+
for (let i = 0; i < decl.namedChildCount; i++) {
|
|
3328
|
+
const d = decl.namedChild(i);
|
|
3329
|
+
if (d?.type !== "variable_declarator") continue;
|
|
3330
|
+
const name = d.childForFieldName("name")?.text;
|
|
3331
|
+
if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
});
|
|
3335
|
+
return out;
|
|
3336
|
+
}
|
|
3337
|
+
function nextRoutesFromFile(source, relFile, parser) {
|
|
3338
|
+
if (isNextAppRouteFile(relFile)) {
|
|
3339
|
+
const tree = parseSource2(parser, source);
|
|
3340
|
+
const template = nextAppPathTemplate(relFile);
|
|
3341
|
+
return nextAppMethods(tree.rootNode).map(({ method, line }) => ({
|
|
3342
|
+
method,
|
|
3343
|
+
pathTemplate: canonicalizeTemplate(template),
|
|
3344
|
+
line,
|
|
3345
|
+
framework: "next"
|
|
3346
|
+
}));
|
|
3347
|
+
}
|
|
3348
|
+
if (isNextPagesApiFile(relFile)) {
|
|
3349
|
+
return [
|
|
3350
|
+
{
|
|
3351
|
+
method: "ALL",
|
|
3352
|
+
pathTemplate: canonicalizeTemplate(nextPagesApiPathTemplate(relFile)),
|
|
3353
|
+
line: 1,
|
|
3354
|
+
framework: "next"
|
|
3355
|
+
}
|
|
3356
|
+
];
|
|
3357
|
+
}
|
|
3358
|
+
return [];
|
|
3359
|
+
}
|
|
3360
|
+
function pyStaticStringText(node) {
|
|
3361
|
+
if (node.type !== "string") return null;
|
|
3362
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3363
|
+
const child = node.namedChild(i);
|
|
3364
|
+
if (child?.type === "interpolation") return null;
|
|
3365
|
+
if (child?.type === "string_content") return child.text;
|
|
3366
|
+
}
|
|
3367
|
+
return "";
|
|
3368
|
+
}
|
|
3369
|
+
function keywordArrayStrings(argsNode, key) {
|
|
3370
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
3371
|
+
const arg = argsNode.namedChild(i);
|
|
3372
|
+
if (arg?.type !== "keyword_argument") continue;
|
|
3373
|
+
if (arg.childForFieldName("name")?.text !== key) continue;
|
|
3374
|
+
const val = arg.childForFieldName("value");
|
|
3375
|
+
if (!val || val.type !== "list") return [];
|
|
3376
|
+
const out = [];
|
|
3377
|
+
for (let j = 0; j < val.namedChildCount; j++) {
|
|
3378
|
+
const el = val.namedChild(j);
|
|
3379
|
+
if (el?.type === "string") {
|
|
3380
|
+
const s = pyStaticStringText(el);
|
|
3381
|
+
if (s) out.push(s);
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
return out;
|
|
3385
|
+
}
|
|
3386
|
+
return [];
|
|
3387
|
+
}
|
|
3388
|
+
function collectPythonRouterPrefixes(root) {
|
|
3389
|
+
const prefixes = /* @__PURE__ */ new Map();
|
|
3390
|
+
walk(root, (node) => {
|
|
3391
|
+
if (node.type !== "assignment") return;
|
|
3392
|
+
const right = node.childForFieldName("right");
|
|
3393
|
+
if (!right || right.type !== "call") return;
|
|
3394
|
+
const fn = right.childForFieldName("function");
|
|
3395
|
+
if (!fn) return;
|
|
3396
|
+
const ctor = fn.type === "attribute" ? fn.childForFieldName("attribute")?.text : fn.text;
|
|
3397
|
+
const prefixKey = ctor === "APIRouter" ? "prefix" : ctor === "Blueprint" ? "url_prefix" : null;
|
|
3398
|
+
if (!prefixKey) return;
|
|
3399
|
+
const left = node.childForFieldName("left");
|
|
3400
|
+
if (!left || left.type !== "identifier") return;
|
|
3401
|
+
const args = right.childForFieldName("arguments");
|
|
3402
|
+
if (!args) return;
|
|
3403
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
3404
|
+
const arg = args.namedChild(i);
|
|
3405
|
+
if (arg?.type !== "keyword_argument") continue;
|
|
3406
|
+
if (arg.childForFieldName("name")?.text !== prefixKey) continue;
|
|
3407
|
+
const val = arg.childForFieldName("value");
|
|
3408
|
+
const p = val ? pyStaticStringText(val) : null;
|
|
3409
|
+
if (p !== null) prefixes.set(left.text, p);
|
|
3410
|
+
}
|
|
3411
|
+
});
|
|
3412
|
+
return prefixes;
|
|
3413
|
+
}
|
|
3414
|
+
function collectStringConstants(root) {
|
|
3415
|
+
const consts = /* @__PURE__ */ new Map();
|
|
3416
|
+
walk(root, (node) => {
|
|
3417
|
+
if (node.type !== "assignment") return;
|
|
3418
|
+
const left = node.childForFieldName("left");
|
|
3419
|
+
const right = node.childForFieldName("right");
|
|
3420
|
+
if (left?.type !== "identifier" || right?.type !== "string") return;
|
|
3421
|
+
const v = pyStaticStringText(right);
|
|
3422
|
+
if (v !== null) consts.set(left.text, v);
|
|
3423
|
+
});
|
|
3424
|
+
return consts;
|
|
3425
|
+
}
|
|
3426
|
+
function resolvePrefixArg(node, consts) {
|
|
3427
|
+
if (!node) return null;
|
|
3428
|
+
if (node.type === "string") return pyStaticStringText(node);
|
|
3429
|
+
if (node.type === "identifier") return consts.get(node.text) ?? null;
|
|
3430
|
+
if (node.type === "attribute") {
|
|
3431
|
+
const attr = node.childForFieldName("attribute")?.text;
|
|
3432
|
+
return attr ? consts.get(attr) ?? null : null;
|
|
3433
|
+
}
|
|
3434
|
+
return null;
|
|
3435
|
+
}
|
|
3436
|
+
function collectMountPrefixes(root, consts) {
|
|
3437
|
+
const mounts = /* @__PURE__ */ new Map();
|
|
3438
|
+
walk(root, (node) => {
|
|
3439
|
+
if (node.type !== "call") return;
|
|
3440
|
+
const fn = node.childForFieldName("function");
|
|
3441
|
+
if (fn?.type !== "attribute") return;
|
|
3442
|
+
const method = fn.childForFieldName("attribute")?.text;
|
|
3443
|
+
const prefixKey = method === "include_router" ? "prefix" : method === "register_blueprint" ? "url_prefix" : null;
|
|
3444
|
+
if (!prefixKey) return;
|
|
3445
|
+
const args = node.childForFieldName("arguments");
|
|
3446
|
+
const first = args?.namedChild(0);
|
|
3447
|
+
if (first?.type !== "identifier") return;
|
|
3448
|
+
let prefix = null;
|
|
3449
|
+
for (let i = 0; i < (args?.namedChildCount ?? 0); i++) {
|
|
3450
|
+
const a = args.namedChild(i);
|
|
3451
|
+
if (a?.type !== "keyword_argument") continue;
|
|
3452
|
+
if (a.childForFieldName("name")?.text !== prefixKey) continue;
|
|
3453
|
+
prefix = resolvePrefixArg(a.childForFieldName("value"), consts);
|
|
3454
|
+
}
|
|
3455
|
+
if (prefix !== null && prefix.length > 0) mounts.set(first.text, prefix);
|
|
3456
|
+
});
|
|
3457
|
+
return mounts;
|
|
3458
|
+
}
|
|
3459
|
+
function pythonRoutesFromSource(source, parser, framework) {
|
|
3460
|
+
const tree = parseSource2(parser, source);
|
|
3461
|
+
const prefixes = collectPythonRouterPrefixes(tree.rootNode);
|
|
3462
|
+
const consts = collectStringConstants(tree.rootNode);
|
|
3463
|
+
const mounts = collectMountPrefixes(tree.rootNode, consts);
|
|
3464
|
+
const out = [];
|
|
3465
|
+
walk(tree.rootNode, (node) => {
|
|
3466
|
+
if (node.type !== "decorator") return;
|
|
3467
|
+
const call = node.namedChild(0);
|
|
3468
|
+
if (!call || call.type !== "call") return;
|
|
3469
|
+
const fn = call.childForFieldName("function");
|
|
3470
|
+
if (!fn || fn.type !== "attribute") return;
|
|
3471
|
+
const method = fn.childForFieldName("attribute")?.text?.toLowerCase();
|
|
3472
|
+
if (!method) return;
|
|
3473
|
+
const isVerb = FASTAPI_METHODS.has(method);
|
|
3474
|
+
const isFlaskRoute = method === "route";
|
|
3475
|
+
const isApiRoute = method === "api_route";
|
|
3476
|
+
if (!isVerb && !isFlaskRoute && !isApiRoute) return;
|
|
3477
|
+
const args = call.childForFieldName("arguments");
|
|
3478
|
+
const first = args?.namedChild(0);
|
|
3479
|
+
if (!first || first.type !== "string") return;
|
|
3480
|
+
const rawPath = pyStaticStringText(first);
|
|
3481
|
+
if (rawPath === null || !rawPath.startsWith("/")) return;
|
|
3482
|
+
const obj = fn.childForFieldName("object")?.text;
|
|
3483
|
+
const routerPrefix = obj ? prefixes.get(obj) ?? "" : "";
|
|
3484
|
+
const mountPrefix = obj ? mounts.get(obj) ?? "" : "";
|
|
3485
|
+
const pathTemplate = canonicalizeTemplate(mountPrefix + routerPrefix + rawPath);
|
|
3486
|
+
const line = node.startPosition.row + 1;
|
|
3487
|
+
if (isVerb) {
|
|
3488
|
+
out.push({ method: method.toUpperCase(), pathTemplate, line, framework });
|
|
3489
|
+
return;
|
|
3490
|
+
}
|
|
3491
|
+
const methods = keywordArrayStrings(args, "methods");
|
|
3492
|
+
const list = methods.length > 0 ? methods : isFlaskRoute ? ["GET"] : ["ALL"];
|
|
3493
|
+
for (const m of list) {
|
|
3494
|
+
out.push({ method: m === "ALL" ? "ALL" : m.toUpperCase(), pathTemplate, line, framework });
|
|
3495
|
+
}
|
|
3496
|
+
});
|
|
3497
|
+
return out;
|
|
3498
|
+
}
|
|
3499
|
+
function djangoRoutesFromSource(source, parser) {
|
|
3500
|
+
const tree = parseSource2(parser, source);
|
|
3501
|
+
const out = [];
|
|
3502
|
+
walk(tree.rootNode, (node) => {
|
|
3503
|
+
if (node.type !== "assignment") return;
|
|
3119
3504
|
if (node.childForFieldName("left")?.text !== "urlpatterns") return;
|
|
3120
3505
|
const list = node.childForFieldName("right");
|
|
3121
3506
|
if (!list || list.type !== "list") return;
|
|
@@ -3140,36 +3525,347 @@ function djangoRoutesFromSource(source, parser) {
|
|
|
3140
3525
|
});
|
|
3141
3526
|
return out;
|
|
3142
3527
|
}
|
|
3143
|
-
|
|
3144
|
-
const
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
const
|
|
3159
|
-
const
|
|
3160
|
-
const
|
|
3528
|
+
function namedArgs(argsNode) {
|
|
3529
|
+
const out = [];
|
|
3530
|
+
if (!argsNode) return out;
|
|
3531
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
3532
|
+
const c = argsNode.namedChild(i);
|
|
3533
|
+
if (c && c.type !== "comment") out.push(c);
|
|
3534
|
+
}
|
|
3535
|
+
return out;
|
|
3536
|
+
}
|
|
3537
|
+
function parseUseMount(callNode) {
|
|
3538
|
+
const args = namedArgs(callNode.childForFieldName("arguments"));
|
|
3539
|
+
if (args.length === 0) return null;
|
|
3540
|
+
const first = args[0];
|
|
3541
|
+
const firstStr = first.type === "string" || first.type === "template_string" ? staticStringText(first) : null;
|
|
3542
|
+
if (firstStr !== null && firstStr.startsWith("/")) {
|
|
3543
|
+
const prefix = canonicalizeTemplate(firstStr);
|
|
3544
|
+
const second = args[1];
|
|
3545
|
+
const target = second && second.type === "identifier" ? second.text : null;
|
|
3546
|
+
return { prefix: prefix === "/" ? "" : prefix, target };
|
|
3547
|
+
}
|
|
3548
|
+
if (args.length === 1 && first.type === "identifier") return { prefix: "", target: first.text };
|
|
3549
|
+
return null;
|
|
3550
|
+
}
|
|
3551
|
+
function unwrapRouterExpr(node, expressLocals, routerCtors) {
|
|
3552
|
+
if (node.type === "identifier") return { base: { alias: node.text }, mounts: [] };
|
|
3553
|
+
if (node.type !== "call_expression") return null;
|
|
3554
|
+
const fn = node.childForFieldName("function");
|
|
3555
|
+
if (!fn) return null;
|
|
3556
|
+
if (fn.type === "member_expression") {
|
|
3557
|
+
const prop = fn.childForFieldName("property")?.text;
|
|
3558
|
+
const obj = fn.childForFieldName("object");
|
|
3559
|
+
if (!prop || !obj) return null;
|
|
3560
|
+
if (prop === "use") {
|
|
3561
|
+
const inner = unwrapRouterExpr(obj, expressLocals, routerCtors);
|
|
3562
|
+
if (!inner) return null;
|
|
3563
|
+
const mount = parseUseMount(node);
|
|
3564
|
+
return { base: inner.base, mounts: mount ? [...inner.mounts, mount] : inner.mounts };
|
|
3565
|
+
}
|
|
3566
|
+
if (prop === "Router" && obj.type === "identifier" && expressLocals.has(obj.text)) {
|
|
3567
|
+
return { base: "newRouter", mounts: [] };
|
|
3568
|
+
}
|
|
3569
|
+
return null;
|
|
3570
|
+
}
|
|
3571
|
+
if (fn.type === "identifier") {
|
|
3572
|
+
if (expressLocals.has(fn.text)) return { base: "app", mounts: [] };
|
|
3573
|
+
if (routerCtors.has(fn.text)) return { base: "newRouter", mounts: [] };
|
|
3574
|
+
}
|
|
3575
|
+
return null;
|
|
3576
|
+
}
|
|
3577
|
+
function collectExpressImports(root) {
|
|
3578
|
+
const expressLocals = /* @__PURE__ */ new Set();
|
|
3579
|
+
const routerCtors = /* @__PURE__ */ new Set();
|
|
3580
|
+
const bindings = [];
|
|
3581
|
+
const addFromExpress = (local, sel, exported) => {
|
|
3582
|
+
if (sel === "default" || sel === "namespace") expressLocals.add(local);
|
|
3583
|
+
else if (exported === "Router") routerCtors.add(local);
|
|
3584
|
+
};
|
|
3585
|
+
walk(root, (node) => {
|
|
3586
|
+
if (node.type === "import_statement") {
|
|
3587
|
+
const source = node.childForFieldName("source");
|
|
3588
|
+
const spec = source ? staticStringText(source) : null;
|
|
3589
|
+
if (!spec) return;
|
|
3590
|
+
let clause = null;
|
|
3591
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3592
|
+
const c = node.namedChild(i);
|
|
3593
|
+
if (c?.type === "import_clause") clause = c;
|
|
3594
|
+
}
|
|
3595
|
+
if (!clause) return;
|
|
3596
|
+
for (let i = 0; i < clause.namedChildCount; i++) {
|
|
3597
|
+
const c = clause.namedChild(i);
|
|
3598
|
+
if (!c) continue;
|
|
3599
|
+
if (c.type === "identifier") {
|
|
3600
|
+
if (spec === "express") addFromExpress(c.text, "default", "default");
|
|
3601
|
+
else bindings.push({ local: c.text, specifier: spec, sel: "default" });
|
|
3602
|
+
} else if (c.type === "namespace_import") {
|
|
3603
|
+
const id = c.namedChild(0);
|
|
3604
|
+
if (id?.type === "identifier") {
|
|
3605
|
+
if (spec === "express") addFromExpress(id.text, "namespace", "namespace");
|
|
3606
|
+
else bindings.push({ local: id.text, specifier: spec, sel: "namespace" });
|
|
3607
|
+
}
|
|
3608
|
+
} else if (c.type === "named_imports") {
|
|
3609
|
+
for (let j = 0; j < c.namedChildCount; j++) {
|
|
3610
|
+
const s = c.namedChild(j);
|
|
3611
|
+
if (s?.type !== "import_specifier") continue;
|
|
3612
|
+
const name = s.childForFieldName("name")?.text;
|
|
3613
|
+
if (!name) continue;
|
|
3614
|
+
const local = s.childForFieldName("alias")?.text ?? name;
|
|
3615
|
+
if (spec === "express") addFromExpress(local, name, name);
|
|
3616
|
+
else bindings.push({ local, specifier: spec, sel: name });
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
return;
|
|
3621
|
+
}
|
|
3622
|
+
if (node.type === "variable_declarator") {
|
|
3623
|
+
const value = node.childForFieldName("value");
|
|
3624
|
+
if (value?.type !== "call_expression") return;
|
|
3625
|
+
const fn = value.childForFieldName("function");
|
|
3626
|
+
if (fn?.type !== "identifier" || fn.text !== "require") return;
|
|
3627
|
+
const arg = namedArgs(value.childForFieldName("arguments"))[0];
|
|
3628
|
+
const spec = arg ? staticStringText(arg) : null;
|
|
3629
|
+
if (!spec) return;
|
|
3630
|
+
const name = node.childForFieldName("name");
|
|
3631
|
+
if (name?.type === "identifier") {
|
|
3632
|
+
if (spec === "express") expressLocals.add(name.text);
|
|
3633
|
+
else bindings.push({ local: name.text, specifier: spec, sel: "default" });
|
|
3634
|
+
} else if (name?.type === "object_pattern") {
|
|
3635
|
+
for (let i = 0; i < name.namedChildCount; i++) {
|
|
3636
|
+
const el = name.namedChild(i);
|
|
3637
|
+
if (!el) continue;
|
|
3638
|
+
let local;
|
|
3639
|
+
let exported;
|
|
3640
|
+
if (el.type === "shorthand_property_identifier_pattern") {
|
|
3641
|
+
local = el.text;
|
|
3642
|
+
exported = el.text;
|
|
3643
|
+
} else if (el.type === "pair_pattern") {
|
|
3644
|
+
exported = el.childForFieldName("key")?.text;
|
|
3645
|
+
local = el.childForFieldName("value")?.text ?? exported;
|
|
3646
|
+
}
|
|
3647
|
+
if (!local || !exported) continue;
|
|
3648
|
+
if (spec === "express") addFromExpress(local, exported, exported);
|
|
3649
|
+
else bindings.push({ local, specifier: spec, sel: exported });
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
});
|
|
3654
|
+
return { expressLocals, routerCtors, bindings };
|
|
3655
|
+
}
|
|
3656
|
+
function analyzeExpressFile(root, dir) {
|
|
3657
|
+
const { expressLocals, routerCtors, bindings } = collectExpressImports(root);
|
|
3658
|
+
const routerVars = /* @__PURE__ */ new Map();
|
|
3659
|
+
const appVars = /* @__PURE__ */ new Set();
|
|
3660
|
+
const exportNamed = /* @__PURE__ */ new Map();
|
|
3661
|
+
let exportDefaultName = null;
|
|
3662
|
+
const getVar = (name) => {
|
|
3663
|
+
let rv = routerVars.get(name);
|
|
3664
|
+
if (!rv) {
|
|
3665
|
+
rv = { declares: false, mounts: [] };
|
|
3666
|
+
routerVars.set(name, rv);
|
|
3667
|
+
}
|
|
3668
|
+
return rv;
|
|
3669
|
+
};
|
|
3670
|
+
const refFromExpr = (expr, key) => {
|
|
3671
|
+
if (expr.type === "identifier") return expr.text;
|
|
3672
|
+
const u = unwrapRouterExpr(expr, expressLocals, routerCtors);
|
|
3673
|
+
if (!u) return null;
|
|
3674
|
+
const rv = getVar(key);
|
|
3675
|
+
for (const m of u.mounts) rv.mounts.push(m);
|
|
3676
|
+
if (typeof u.base === "object") rv.aliasOf = u.base.alias;
|
|
3677
|
+
return key;
|
|
3678
|
+
};
|
|
3679
|
+
const isExported = (declarator) => {
|
|
3680
|
+
const decl = declarator.parent;
|
|
3681
|
+
return decl?.parent?.type === "export_statement";
|
|
3682
|
+
};
|
|
3683
|
+
walk(root, (node) => {
|
|
3684
|
+
if (node.type === "variable_declarator") {
|
|
3685
|
+
const value = node.childForFieldName("value");
|
|
3686
|
+
const name = node.childForFieldName("name");
|
|
3687
|
+
if (name?.type !== "identifier" || !value) return;
|
|
3688
|
+
if (value.type === "call_expression") {
|
|
3689
|
+
const fn = value.childForFieldName("function");
|
|
3690
|
+
if (fn?.type === "identifier" && fn.text === "require") return;
|
|
3691
|
+
}
|
|
3692
|
+
const u = unwrapRouterExpr(value, expressLocals, routerCtors);
|
|
3693
|
+
if (!u) return;
|
|
3694
|
+
const rv = getVar(name.text);
|
|
3695
|
+
for (const m of u.mounts) rv.mounts.push(m);
|
|
3696
|
+
if (u.base === "app") appVars.add(name.text);
|
|
3697
|
+
else if (typeof u.base === "object") rv.aliasOf = u.base.alias;
|
|
3698
|
+
if (isExported(node)) exportNamed.set(name.text, name.text);
|
|
3699
|
+
return;
|
|
3700
|
+
}
|
|
3701
|
+
if (node.type === "call_expression") {
|
|
3702
|
+
const fn = node.childForFieldName("function");
|
|
3703
|
+
if (fn?.type !== "member_expression") return;
|
|
3704
|
+
const obj = fn.childForFieldName("object");
|
|
3705
|
+
const prop = fn.childForFieldName("property")?.text;
|
|
3706
|
+
if (obj?.type !== "identifier" || !prop) return;
|
|
3707
|
+
if (prop === "use") {
|
|
3708
|
+
const m = parseUseMount(node);
|
|
3709
|
+
if (m) getVar(obj.text).mounts.push(m);
|
|
3710
|
+
} else if (ROUTER_METHODS.has(prop.toLowerCase())) {
|
|
3711
|
+
const first = namedArgs(node.childForFieldName("arguments"))[0];
|
|
3712
|
+
const p = first ? staticStringText(first) : null;
|
|
3713
|
+
if (p !== null && p.startsWith("/")) getVar(obj.text).declares = true;
|
|
3714
|
+
}
|
|
3715
|
+
return;
|
|
3716
|
+
}
|
|
3717
|
+
if (node.type === "export_statement") {
|
|
3718
|
+
let clause = null;
|
|
3719
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3720
|
+
const c = node.namedChild(i);
|
|
3721
|
+
if (c?.type === "export_clause") clause = c;
|
|
3722
|
+
}
|
|
3723
|
+
if (clause) {
|
|
3724
|
+
for (let i = 0; i < clause.namedChildCount; i++) {
|
|
3725
|
+
const spec = clause.namedChild(i);
|
|
3726
|
+
if (spec?.type !== "export_specifier") continue;
|
|
3727
|
+
const local = spec.childForFieldName("name")?.text;
|
|
3728
|
+
if (!local) continue;
|
|
3729
|
+
const exportedAs = spec.childForFieldName("alias")?.text ?? local;
|
|
3730
|
+
if (exportedAs === "default") exportDefaultName = local;
|
|
3731
|
+
else exportNamed.set(exportedAs, local);
|
|
3732
|
+
}
|
|
3733
|
+
return;
|
|
3734
|
+
}
|
|
3735
|
+
if (node.childForFieldName("declaration")) return;
|
|
3736
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3737
|
+
const c = node.namedChild(i);
|
|
3738
|
+
if (c && c.type !== "export_clause") {
|
|
3739
|
+
exportDefaultName = refFromExpr(c, "#default");
|
|
3740
|
+
break;
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3745
|
+
if (node.type === "assignment_expression") {
|
|
3746
|
+
const left = node.childForFieldName("left");
|
|
3747
|
+
const right = node.childForFieldName("right");
|
|
3748
|
+
if (left?.type !== "member_expression" || !right) return;
|
|
3749
|
+
const lobj = left.childForFieldName("object")?.text;
|
|
3750
|
+
const lprop = left.childForFieldName("property")?.text;
|
|
3751
|
+
if (lobj === "module" && lprop === "exports") exportDefaultName = refFromExpr(right, "#default");
|
|
3752
|
+
else if (lobj === "exports" && lprop) {
|
|
3753
|
+
const key = refFromExpr(right, `#exp:${lprop}`);
|
|
3754
|
+
if (key) exportNamed.set(lprop, key);
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
});
|
|
3758
|
+
return {
|
|
3759
|
+
dir,
|
|
3760
|
+
routerVars,
|
|
3761
|
+
appVars,
|
|
3762
|
+
exportDefaultName,
|
|
3763
|
+
exportNamed,
|
|
3764
|
+
rawBindings: bindings,
|
|
3765
|
+
importedRouters: /* @__PURE__ */ new Map()
|
|
3766
|
+
};
|
|
3767
|
+
}
|
|
3768
|
+
async function expressMountPrefixes(files, serviceDir, tsPaths) {
|
|
3769
|
+
const jsParser = makeJsParser2();
|
|
3770
|
+
const fileInfo = /* @__PURE__ */ new Map();
|
|
3771
|
+
for (const f of files) {
|
|
3772
|
+
if (!JS_ROUTE_EXTENSIONS.has(import_node_path8.default.extname(f.path))) continue;
|
|
3773
|
+
if (isTestPath(f.path)) continue;
|
|
3774
|
+
const rel = toPosix(import_node_path8.default.relative(serviceDir, f.path));
|
|
3775
|
+
try {
|
|
3776
|
+
const tree = parseSource2(jsParser, f.content);
|
|
3777
|
+
fileInfo.set(rel, analyzeExpressFile(tree.rootNode, import_node_path8.default.dirname(f.path)));
|
|
3778
|
+
} catch {
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
if (fileInfo.size === 0) return /* @__PURE__ */ new Map();
|
|
3782
|
+
for (const info of fileInfo.values()) {
|
|
3783
|
+
for (const b of info.rawBindings) {
|
|
3784
|
+
const resolved = await resolveJsImport(b.specifier, info.dir, serviceDir, tsPaths);
|
|
3785
|
+
if (!resolved || !fileInfo.has(resolved)) continue;
|
|
3786
|
+
info.importedRouters.set(b.local, { file: resolved, sel: b.sel === "namespace" ? "default" : b.sel });
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
const resolveTarget = (name, file) => {
|
|
3790
|
+
const info = fileInfo.get(file);
|
|
3791
|
+
if (!info) return null;
|
|
3792
|
+
if (info.routerVars.has(name)) return { file, name };
|
|
3793
|
+
const imp = info.importedRouters.get(name);
|
|
3794
|
+
if (!imp) return null;
|
|
3795
|
+
const target = fileInfo.get(imp.file);
|
|
3796
|
+
if (!target) return null;
|
|
3797
|
+
const key = imp.sel === "default" ? target.exportDefaultName : target.exportNamed.get(imp.sel);
|
|
3798
|
+
if (!key) return null;
|
|
3799
|
+
return { file: imp.file, name: key };
|
|
3800
|
+
};
|
|
3801
|
+
const filePrefix = /* @__PURE__ */ new Map();
|
|
3802
|
+
const conflicted = /* @__PURE__ */ new Set();
|
|
3803
|
+
const apply4 = (file, prefix) => {
|
|
3804
|
+
if (conflicted.has(file)) return;
|
|
3805
|
+
const existing = filePrefix.get(file);
|
|
3806
|
+
if (existing === void 0) filePrefix.set(file, prefix);
|
|
3807
|
+
else if (existing !== prefix) {
|
|
3808
|
+
filePrefix.delete(file);
|
|
3809
|
+
conflicted.add(file);
|
|
3810
|
+
}
|
|
3811
|
+
};
|
|
3812
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3813
|
+
const collect = (file, name, accPrefix) => {
|
|
3814
|
+
const key = `${file}|${name}|${accPrefix}`;
|
|
3815
|
+
if (visited.has(key)) return;
|
|
3816
|
+
visited.add(key);
|
|
3817
|
+
const info = fileInfo.get(file);
|
|
3818
|
+
const rv = info?.routerVars.get(name);
|
|
3819
|
+
if (!info || !rv) return;
|
|
3820
|
+
if (rv.declares && info.appVars.size === 0) apply4(file, accPrefix);
|
|
3821
|
+
for (const m of rv.mounts) {
|
|
3822
|
+
if (!m.target) continue;
|
|
3823
|
+
const t = resolveTarget(m.target, file);
|
|
3824
|
+
if (t) collect(t.file, t.name, accPrefix + m.prefix);
|
|
3825
|
+
}
|
|
3826
|
+
if (rv.aliasOf) {
|
|
3827
|
+
const t = resolveTarget(rv.aliasOf, file);
|
|
3828
|
+
if (t) collect(t.file, t.name, accPrefix);
|
|
3829
|
+
}
|
|
3830
|
+
};
|
|
3831
|
+
for (const [rel, info] of fileInfo) {
|
|
3832
|
+
for (const appVar of info.appVars) collect(rel, appVar, "");
|
|
3833
|
+
}
|
|
3834
|
+
const out = /* @__PURE__ */ new Map();
|
|
3835
|
+
for (const [file, prefix] of filePrefix) if (prefix && prefix !== "/") out.set(file, prefix);
|
|
3836
|
+
return out;
|
|
3837
|
+
}
|
|
3838
|
+
async function addRoutes(graph, services) {
|
|
3839
|
+
const jsParser = makeJsParser2();
|
|
3840
|
+
const pyParser = makePyParser2();
|
|
3841
|
+
const goParser = makeGoParser2();
|
|
3842
|
+
let nodesAdded = 0;
|
|
3843
|
+
let edgesAdded = 0;
|
|
3844
|
+
for (const service of services) {
|
|
3845
|
+
const deps = {
|
|
3846
|
+
...service.pkg.dependencies ?? {},
|
|
3847
|
+
...service.pkg.devDependencies ?? {}
|
|
3848
|
+
};
|
|
3849
|
+
const hasExpress = deps["express"] !== void 0;
|
|
3850
|
+
const hasFastify = deps["fastify"] !== void 0;
|
|
3851
|
+
const hasHono = deps["hono"] !== void 0;
|
|
3852
|
+
const hasNext = deps["next"] !== void 0;
|
|
3853
|
+
const hasNestjs = deps["@nestjs/core"] !== void 0;
|
|
3854
|
+
const hasFastapi = deps["fastapi"] !== void 0;
|
|
3855
|
+
const hasFlask = deps["flask"] !== void 0;
|
|
3161
3856
|
const hasDjango = deps["django"] !== void 0;
|
|
3162
3857
|
const hasGin = deps["github.com/gin-gonic/gin"] !== void 0;
|
|
3163
3858
|
if (!hasExpress && !hasFastify && !hasHono && !hasNext && !hasNestjs && !hasFastapi && !hasFlask && !hasDjango && !hasGin)
|
|
3164
3859
|
continue;
|
|
3165
3860
|
const files = await loadSourceFiles(service.dir);
|
|
3861
|
+
const mountPrefixes = hasExpress ? await expressMountPrefixes(files, service.dir, await loadTsPathConfig(service.dir)) : /* @__PURE__ */ new Map();
|
|
3166
3862
|
for (const file of files) {
|
|
3167
3863
|
if (isTestPath(file.path)) continue;
|
|
3168
|
-
const ext =
|
|
3864
|
+
const ext = import_node_path8.default.extname(file.path);
|
|
3169
3865
|
const isPy = ext === ".py";
|
|
3170
3866
|
const isGo = ext === ".go";
|
|
3171
3867
|
if (!JS_ROUTE_EXTENSIONS.has(ext) && !isPy && !isGo) continue;
|
|
3172
|
-
const relFile = toPosix(
|
|
3868
|
+
const relFile = toPosix(import_node_path8.default.relative(service.dir, file.path));
|
|
3173
3869
|
let routes;
|
|
3174
3870
|
try {
|
|
3175
3871
|
if (isGo) {
|
|
@@ -3191,16 +3887,18 @@ async function addRoutes(graph, services) {
|
|
|
3191
3887
|
continue;
|
|
3192
3888
|
}
|
|
3193
3889
|
if (routes.length === 0) continue;
|
|
3890
|
+
const mountPrefix = mountPrefixes.get(relFile);
|
|
3194
3891
|
for (const route of routes) {
|
|
3195
|
-
const
|
|
3892
|
+
const pathTemplate = mountPrefix ? canonicalizeTemplate(mountPrefix + route.pathTemplate) : route.pathTemplate;
|
|
3893
|
+
const rid = (0, import_types6.routeId)(service.pkg.name, route.method, pathTemplate);
|
|
3196
3894
|
if (!graph.hasNode(rid)) {
|
|
3197
3895
|
const node = {
|
|
3198
3896
|
id: rid,
|
|
3199
|
-
type:
|
|
3200
|
-
name: `${route.method} ${
|
|
3897
|
+
type: import_types6.NodeType.RouteNode,
|
|
3898
|
+
name: `${route.method} ${pathTemplate}`,
|
|
3201
3899
|
service: service.pkg.name,
|
|
3202
3900
|
method: route.method,
|
|
3203
|
-
pathTemplate
|
|
3901
|
+
pathTemplate,
|
|
3204
3902
|
path: relFile,
|
|
3205
3903
|
line: route.line,
|
|
3206
3904
|
framework: route.framework,
|
|
@@ -3209,15 +3907,15 @@ async function addRoutes(graph, services) {
|
|
|
3209
3907
|
graph.addNode(rid, node);
|
|
3210
3908
|
nodesAdded++;
|
|
3211
3909
|
}
|
|
3212
|
-
const containsId = (0,
|
|
3910
|
+
const containsId = (0, import_types6.extractedEdgeId)(service.node.id, rid, import_types6.EdgeType.CONTAINS);
|
|
3213
3911
|
if (!graph.hasEdge(containsId)) {
|
|
3214
3912
|
const edge = {
|
|
3215
3913
|
id: containsId,
|
|
3216
3914
|
source: service.node.id,
|
|
3217
3915
|
target: rid,
|
|
3218
|
-
type:
|
|
3219
|
-
provenance:
|
|
3220
|
-
confidence: (0,
|
|
3916
|
+
type: import_types6.EdgeType.CONTAINS,
|
|
3917
|
+
provenance: import_types6.Provenance.EXTRACTED,
|
|
3918
|
+
confidence: (0, import_types6.confidenceForExtracted)("structural"),
|
|
3221
3919
|
evidence: {
|
|
3222
3920
|
file: relFile,
|
|
3223
3921
|
line: route.line,
|
|
@@ -3235,7 +3933,7 @@ async function addRoutes(graph, services) {
|
|
|
3235
3933
|
|
|
3236
3934
|
// src/columns.ts
|
|
3237
3935
|
init_cjs_shims();
|
|
3238
|
-
var
|
|
3936
|
+
var import_types7 = require("@neat.is/types");
|
|
3239
3937
|
var OBSERVED_COLUMN_CONFIDENCE = 0.9;
|
|
3240
3938
|
function normalizeProvenances(provenances) {
|
|
3241
3939
|
return [...new Set(provenances)].sort();
|
|
@@ -3263,10 +3961,10 @@ function foldColumns(existing, names, provenance, confidence) {
|
|
|
3263
3961
|
return out;
|
|
3264
3962
|
}
|
|
3265
3963
|
function columnIsDeclared(col) {
|
|
3266
|
-
return col.provenances.includes(
|
|
3964
|
+
return col.provenances.includes(import_types7.Provenance.EXTRACTED);
|
|
3267
3965
|
}
|
|
3268
3966
|
function columnIsObserved(col) {
|
|
3269
|
-
return col.provenances.includes(
|
|
3967
|
+
return col.provenances.includes(import_types7.Provenance.OBSERVED);
|
|
3270
3968
|
}
|
|
3271
3969
|
|
|
3272
3970
|
// src/ingest.ts
|
|
@@ -3489,7 +4187,7 @@ function languageForExt(relPath) {
|
|
|
3489
4187
|
function relPathForRuntimeFile(filepath, serviceNode, scanPath) {
|
|
3490
4188
|
let p = toPosix2(filepath).replace(/^file:\/\//, "");
|
|
3491
4189
|
if (scanPath && scanPath.length > 0) {
|
|
3492
|
-
const absRoot = toPosix2(
|
|
4190
|
+
const absRoot = toPosix2(import_node_path9.default.resolve(scanPath, serviceNode?.repoPath ?? ""));
|
|
3493
4191
|
const anchor = absRoot.endsWith("/") ? absRoot : `${absRoot}/`;
|
|
3494
4192
|
if (p.startsWith(anchor)) return p.slice(anchor.length);
|
|
3495
4193
|
}
|
|
@@ -3517,10 +4215,10 @@ function resolveDistToSrc(absFilepath, line) {
|
|
|
3517
4215
|
entry2 = null;
|
|
3518
4216
|
const mapPath = `${absFilepath}.map`;
|
|
3519
4217
|
try {
|
|
3520
|
-
if ((0,
|
|
3521
|
-
const raw = JSON.parse((0,
|
|
4218
|
+
if ((0, import_node_fs8.existsSync)(mapPath)) {
|
|
4219
|
+
const raw = JSON.parse((0, import_node_fs8.readFileSync)(mapPath, "utf8"));
|
|
3522
4220
|
const consumer = new sourceMapJs.SourceMapConsumer(raw);
|
|
3523
|
-
entry2 = { consumer, dir:
|
|
4221
|
+
entry2 = { consumer, dir: import_node_path9.default.dirname(mapPath) };
|
|
3524
4222
|
}
|
|
3525
4223
|
} catch {
|
|
3526
4224
|
entry2 = null;
|
|
@@ -3535,7 +4233,7 @@ function resolveDistToSrc(absFilepath, line) {
|
|
|
3535
4233
|
});
|
|
3536
4234
|
if (!pos || !pos.source) return null;
|
|
3537
4235
|
const root = entry2.consumer.sourceRoot ?? "";
|
|
3538
|
-
const resolved =
|
|
4236
|
+
const resolved = import_node_path9.default.resolve(entry2.dir, root, pos.source);
|
|
3539
4237
|
return { filepath: resolved, ...pos.line ? { line: pos.line } : {} };
|
|
3540
4238
|
} catch {
|
|
3541
4239
|
return null;
|
|
@@ -3568,11 +4266,11 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
|
|
|
3568
4266
|
};
|
|
3569
4267
|
}
|
|
3570
4268
|
function reconcileObservedRelPath(graph, serviceName, relPath) {
|
|
3571
|
-
if (graph.hasNode((0,
|
|
4269
|
+
if (graph.hasNode((0, import_types8.fileId)(serviceName, relPath))) return relPath;
|
|
3572
4270
|
let best = null;
|
|
3573
4271
|
graph.forEachNode((_id, attrs) => {
|
|
3574
4272
|
const a = attrs;
|
|
3575
|
-
if (a.type !==
|
|
4273
|
+
if (a.type !== import_types8.NodeType.FileNode || a.service !== serviceName) return;
|
|
3576
4274
|
if (a.discoveredVia === "otel") return;
|
|
3577
4275
|
const p = a.path;
|
|
3578
4276
|
if (!p) return;
|
|
@@ -3584,14 +4282,14 @@ function reconcileObservedRelPath(graph, serviceName, relPath) {
|
|
|
3584
4282
|
}
|
|
3585
4283
|
function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
3586
4284
|
const svcAttrs = graph.hasNode(serviceNodeId) ? graph.getNodeAttributes(serviceNodeId) : void 0;
|
|
3587
|
-
const canonicalService = svcAttrs && svcAttrs.type ===
|
|
4285
|
+
const canonicalService = svcAttrs && svcAttrs.type === import_types8.NodeType.ServiceNode && typeof svcAttrs.name === "string" ? svcAttrs.name : serviceName;
|
|
3588
4286
|
const relPath = reconcileObservedRelPath(graph, canonicalService, callSite.relPath);
|
|
3589
|
-
const fileNodeId = (0,
|
|
4287
|
+
const fileNodeId = (0, import_types8.fileId)(canonicalService, relPath);
|
|
3590
4288
|
if (!graph.hasNode(fileNodeId)) {
|
|
3591
4289
|
const language = languageForExt(relPath);
|
|
3592
4290
|
const node = {
|
|
3593
4291
|
id: fileNodeId,
|
|
3594
|
-
type:
|
|
4292
|
+
type: import_types8.NodeType.FileNode,
|
|
3595
4293
|
service: canonicalService,
|
|
3596
4294
|
path: relPath,
|
|
3597
4295
|
...language ? { language } : {},
|
|
@@ -3600,14 +4298,14 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
|
3600
4298
|
};
|
|
3601
4299
|
graph.addNode(fileNodeId, node);
|
|
3602
4300
|
}
|
|
3603
|
-
const containsId = makeObservedEdgeId(
|
|
4301
|
+
const containsId = makeObservedEdgeId(import_types8.EdgeType.CONTAINS, serviceNodeId, fileNodeId);
|
|
3604
4302
|
if (!graph.hasEdge(containsId)) {
|
|
3605
4303
|
const edge = {
|
|
3606
4304
|
id: containsId,
|
|
3607
4305
|
source: serviceNodeId,
|
|
3608
4306
|
target: fileNodeId,
|
|
3609
|
-
type:
|
|
3610
|
-
provenance:
|
|
4307
|
+
type: import_types8.EdgeType.CONTAINS,
|
|
4308
|
+
provenance: import_types8.Provenance.OBSERVED
|
|
3611
4309
|
};
|
|
3612
4310
|
graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
|
|
3613
4311
|
}
|
|
@@ -3631,11 +4329,11 @@ function pickContainingSymbol(candidates, fn) {
|
|
|
3631
4329
|
return [...candidates].sort(bySpan)[0].id;
|
|
3632
4330
|
}
|
|
3633
4331
|
function ensureObservedSymbolNode(graph, fileNodeId, service, relPath, fn, line) {
|
|
3634
|
-
const sid = (0,
|
|
4332
|
+
const sid = (0, import_types8.symbolId)(service, relPath, fn);
|
|
3635
4333
|
if (!graph.hasNode(sid)) {
|
|
3636
4334
|
const node = {
|
|
3637
4335
|
id: sid,
|
|
3638
|
-
type:
|
|
4336
|
+
type: import_types8.NodeType.SymbolNode,
|
|
3639
4337
|
kind: "function",
|
|
3640
4338
|
qualname: fn,
|
|
3641
4339
|
span: { startLine: line, endLine: line },
|
|
@@ -3645,14 +4343,14 @@ function ensureObservedSymbolNode(graph, fileNodeId, service, relPath, fn, line)
|
|
|
3645
4343
|
};
|
|
3646
4344
|
graph.addNode(sid, node);
|
|
3647
4345
|
}
|
|
3648
|
-
const containsId = makeObservedEdgeId(
|
|
4346
|
+
const containsId = makeObservedEdgeId(import_types8.EdgeType.CONTAINS, fileNodeId, sid);
|
|
3649
4347
|
if (!graph.hasEdge(containsId)) {
|
|
3650
4348
|
const edge = {
|
|
3651
4349
|
id: containsId,
|
|
3652
4350
|
source: fileNodeId,
|
|
3653
4351
|
target: sid,
|
|
3654
|
-
type:
|
|
3655
|
-
provenance:
|
|
4352
|
+
type: import_types8.EdgeType.CONTAINS,
|
|
4353
|
+
provenance: import_types8.Provenance.OBSERVED
|
|
3656
4354
|
};
|
|
3657
4355
|
graph.addEdgeWithKey(containsId, fileNodeId, sid, edge);
|
|
3658
4356
|
}
|
|
@@ -3664,9 +4362,9 @@ function landObservedSymbol(graph, fileNodeId, service, relPath, callSite) {
|
|
|
3664
4362
|
let sawSymbol = false;
|
|
3665
4363
|
const candidates = [];
|
|
3666
4364
|
graph.forEachOutboundEdge(fileNodeId, (_edge, edgeAttrs, _source, target) => {
|
|
3667
|
-
if (edgeAttrs.type !==
|
|
4365
|
+
if (edgeAttrs.type !== import_types8.EdgeType.CONTAINS) return;
|
|
3668
4366
|
const t = graph.getNodeAttributes(target);
|
|
3669
|
-
if (t.type !==
|
|
4367
|
+
if (t.type !== import_types8.NodeType.SymbolNode) return;
|
|
3670
4368
|
sawSymbol = true;
|
|
3671
4369
|
if (line >= t.span.startLine && line <= t.span.endLine) {
|
|
3672
4370
|
candidates.push({ id: target, symbol: t });
|
|
@@ -3679,17 +4377,17 @@ function landObservedSymbol(graph, fileNodeId, service, relPath, callSite) {
|
|
|
3679
4377
|
return fileNodeId;
|
|
3680
4378
|
}
|
|
3681
4379
|
function makeObservedEdgeId(type, source, target) {
|
|
3682
|
-
return (0,
|
|
4380
|
+
return (0, import_types8.observedEdgeId)(source, target, type);
|
|
3683
4381
|
}
|
|
3684
4382
|
function makeInferredEdgeId(type, source, target) {
|
|
3685
|
-
return (0,
|
|
4383
|
+
return (0, import_types8.inferredEdgeId)(source, target, type);
|
|
3686
4384
|
}
|
|
3687
4385
|
var INFERRED_CONFIDENCE = 0.6;
|
|
3688
4386
|
var STITCH_MAX_DEPTH = 2;
|
|
3689
4387
|
var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
4388
|
+
import_types8.EdgeType.CALLS,
|
|
4389
|
+
import_types8.EdgeType.CONNECTS_TO,
|
|
4390
|
+
import_types8.EdgeType.DEPENDS_ON
|
|
3693
4391
|
]);
|
|
3694
4392
|
var WIRE_SPAN_KIND_CLIENT = 3;
|
|
3695
4393
|
var WIRE_SPAN_KIND_PRODUCER = 4;
|
|
@@ -3705,11 +4403,11 @@ function spanServesGraphqlOperation(kind) {
|
|
|
3705
4403
|
return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
|
|
3706
4404
|
}
|
|
3707
4405
|
function ensureGraphqlOperationNode(graph, serviceName, operationType, operationName) {
|
|
3708
|
-
const id = (0,
|
|
4406
|
+
const id = (0, import_types8.graphqlOperationId)(serviceName, operationType, operationName);
|
|
3709
4407
|
if (graph.hasNode(id)) return id;
|
|
3710
4408
|
const node = {
|
|
3711
4409
|
id,
|
|
3712
|
-
type:
|
|
4410
|
+
type: import_types8.NodeType.GraphQLOperationNode,
|
|
3713
4411
|
name: operationName,
|
|
3714
4412
|
service: serviceName,
|
|
3715
4413
|
operationType: operationType.toLowerCase(),
|
|
@@ -3723,11 +4421,11 @@ function spanServesGrpcMethod(kind) {
|
|
|
3723
4421
|
return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
|
|
3724
4422
|
}
|
|
3725
4423
|
function ensureGrpcMethodNode(graph, rpcService, rpcMethod) {
|
|
3726
|
-
const id = (0,
|
|
4424
|
+
const id = (0, import_types8.grpcMethodId)(rpcService, rpcMethod);
|
|
3727
4425
|
if (graph.hasNode(id)) return id;
|
|
3728
4426
|
const node = {
|
|
3729
4427
|
id,
|
|
3730
|
-
type:
|
|
4428
|
+
type: import_types8.NodeType.GrpcMethodNode,
|
|
3731
4429
|
name: `${rpcService}/${rpcMethod}`,
|
|
3732
4430
|
rpcService,
|
|
3733
4431
|
rpcMethod,
|
|
@@ -3740,11 +4438,11 @@ function spanServesWebsocketChannel(kind) {
|
|
|
3740
4438
|
return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
|
|
3741
4439
|
}
|
|
3742
4440
|
function ensureWebsocketChannelNode(graph, serviceName, channel) {
|
|
3743
|
-
const id = (0,
|
|
4441
|
+
const id = (0, import_types8.websocketChannelId)(serviceName, channel);
|
|
3744
4442
|
if (graph.hasNode(id)) return id;
|
|
3745
4443
|
const node = {
|
|
3746
4444
|
id,
|
|
3747
|
-
type:
|
|
4445
|
+
type: import_types8.NodeType.WebSocketChannelNode,
|
|
3748
4446
|
name: channel,
|
|
3749
4447
|
service: serviceName,
|
|
3750
4448
|
channel,
|
|
@@ -3757,11 +4455,11 @@ function messagingDestinationKind(system) {
|
|
|
3757
4455
|
return `${system}-topic`;
|
|
3758
4456
|
}
|
|
3759
4457
|
function ensureMessagingDestinationNode(graph, system, destination) {
|
|
3760
|
-
const id = (0,
|
|
4458
|
+
const id = (0, import_types8.infraId)(messagingDestinationKind(system), destination);
|
|
3761
4459
|
if (graph.hasNode(id)) return id;
|
|
3762
4460
|
const node = {
|
|
3763
4461
|
id,
|
|
3764
|
-
type:
|
|
4462
|
+
type: import_types8.NodeType.InfraNode,
|
|
3765
4463
|
name: destination,
|
|
3766
4464
|
provider: "self",
|
|
3767
4465
|
kind: messagingDestinationKind(system)
|
|
@@ -3805,9 +4503,9 @@ function lookupParentSpan(traceId, parentSpanId, now) {
|
|
|
3805
4503
|
};
|
|
3806
4504
|
}
|
|
3807
4505
|
function resolveServiceId(graph, host, env) {
|
|
3808
|
-
const envTagged = (0,
|
|
4506
|
+
const envTagged = (0, import_types8.serviceId)(host, env);
|
|
3809
4507
|
if (graph.hasNode(envTagged)) return envTagged;
|
|
3810
|
-
const envLess = (0,
|
|
4508
|
+
const envLess = (0, import_types8.serviceId)(host);
|
|
3811
4509
|
if (envLess !== envTagged && graph.hasNode(envLess)) return envLess;
|
|
3812
4510
|
let sameEnv = null;
|
|
3813
4511
|
let envLessMatch = null;
|
|
@@ -3815,7 +4513,7 @@ function resolveServiceId(graph, host, env) {
|
|
|
3815
4513
|
graph.forEachNode((id, attrs) => {
|
|
3816
4514
|
if (sameEnv) return;
|
|
3817
4515
|
const a = attrs;
|
|
3818
|
-
if (a.type !==
|
|
4516
|
+
if (a.type !== import_types8.NodeType.ServiceNode) return;
|
|
3819
4517
|
const matchesByName = a.name === host;
|
|
3820
4518
|
const matchesByAlias = a.aliases ? a.aliases.includes(host) : false;
|
|
3821
4519
|
if (!matchesByName && !matchesByAlias) return;
|
|
@@ -3830,14 +4528,14 @@ function resolveServiceId(graph, host, env) {
|
|
|
3830
4528
|
return sameEnv ?? envLessMatch ?? anyMatch;
|
|
3831
4529
|
}
|
|
3832
4530
|
function frontierIdFor(host) {
|
|
3833
|
-
return (0,
|
|
4531
|
+
return (0, import_types8.frontierId)(host);
|
|
3834
4532
|
}
|
|
3835
4533
|
function ensureServiceNode(graph, serviceName, env) {
|
|
3836
|
-
const id = (0,
|
|
4534
|
+
const id = (0, import_types8.serviceId)(serviceName, env);
|
|
3837
4535
|
if (graph.hasNode(id)) return id;
|
|
3838
4536
|
const wanted = serviceName.toLowerCase();
|
|
3839
4537
|
const extractedId = graph.findNode((_nid, attrs) => {
|
|
3840
|
-
if (attrs.type !==
|
|
4538
|
+
if (attrs.type !== import_types8.NodeType.ServiceNode) return false;
|
|
3841
4539
|
const svc = attrs;
|
|
3842
4540
|
if (svc.discoveredVia === "otel") return false;
|
|
3843
4541
|
return typeof svc.name === "string" && svc.name.toLowerCase() === wanted;
|
|
@@ -3845,7 +4543,7 @@ function ensureServiceNode(graph, serviceName, env) {
|
|
|
3845
4543
|
if (extractedId) return extractedId;
|
|
3846
4544
|
const node = {
|
|
3847
4545
|
id,
|
|
3848
|
-
type:
|
|
4546
|
+
type: import_types8.NodeType.ServiceNode,
|
|
3849
4547
|
name: serviceName,
|
|
3850
4548
|
language: "unknown",
|
|
3851
4549
|
discoveredVia: "otel",
|
|
@@ -3855,11 +4553,11 @@ function ensureServiceNode(graph, serviceName, env) {
|
|
|
3855
4553
|
return id;
|
|
3856
4554
|
}
|
|
3857
4555
|
function ensureInfraNode(graph, kind, name, provider) {
|
|
3858
|
-
const id = (0,
|
|
4556
|
+
const id = (0, import_types8.infraId)(kind, name);
|
|
3859
4557
|
if (graph.hasNode(id)) return id;
|
|
3860
4558
|
const node = {
|
|
3861
4559
|
id,
|
|
3862
|
-
type:
|
|
4560
|
+
type: import_types8.NodeType.InfraNode,
|
|
3863
4561
|
name,
|
|
3864
4562
|
provider,
|
|
3865
4563
|
kind
|
|
@@ -3871,7 +4569,7 @@ var COLUMN_BEARING_INFRA_KINDS = /* @__PURE__ */ new Set(["sql-table", "supabase
|
|
|
3871
4569
|
function mergeColumnsAt(graph, tableNodeId, columns, provenance, confidence) {
|
|
3872
4570
|
if (!columns || columns.length === 0 || !graph.hasNode(tableNodeId)) return;
|
|
3873
4571
|
const node = graph.getNodeAttributes(tableNodeId);
|
|
3874
|
-
if (node.type !==
|
|
4572
|
+
if (node.type !== import_types8.NodeType.InfraNode || !node.kind || !COLUMN_BEARING_INFRA_KINDS.has(node.kind)) {
|
|
3875
4573
|
return;
|
|
3876
4574
|
}
|
|
3877
4575
|
graph.replaceNodeAttributes(tableNodeId, {
|
|
@@ -3880,14 +4578,14 @@ function mergeColumnsAt(graph, tableNodeId, columns, provenance, confidence) {
|
|
|
3880
4578
|
});
|
|
3881
4579
|
}
|
|
3882
4580
|
function mergeObservedColumns(graph, tableNodeId, columns) {
|
|
3883
|
-
mergeColumnsAt(graph, tableNodeId, columns,
|
|
4581
|
+
mergeColumnsAt(graph, tableNodeId, columns, import_types8.Provenance.OBSERVED, OBSERVED_COLUMN_CONFIDENCE);
|
|
3884
4582
|
}
|
|
3885
4583
|
function ensureDatabaseNode(graph, host, engine) {
|
|
3886
|
-
const id = (0,
|
|
4584
|
+
const id = (0, import_types8.databaseId)(host);
|
|
3887
4585
|
if (graph.hasNode(id)) return id;
|
|
3888
4586
|
const node = {
|
|
3889
4587
|
id,
|
|
3890
|
-
type:
|
|
4588
|
+
type: import_types8.NodeType.DatabaseNode,
|
|
3891
4589
|
name: host,
|
|
3892
4590
|
engine,
|
|
3893
4591
|
engineVersion: "unknown",
|
|
@@ -3899,11 +4597,11 @@ function ensureDatabaseNode(graph, host, engine) {
|
|
|
3899
4597
|
return id;
|
|
3900
4598
|
}
|
|
3901
4599
|
function ensureLocalDatabaseNode(graph, serviceName, name, engine) {
|
|
3902
|
-
const id = (0,
|
|
4600
|
+
const id = (0, import_types8.localDatabaseId)(serviceName, name);
|
|
3903
4601
|
if (graph.hasNode(id)) return id;
|
|
3904
4602
|
const node = {
|
|
3905
4603
|
id,
|
|
3906
|
-
type:
|
|
4604
|
+
type: import_types8.NodeType.DatabaseNode,
|
|
3907
4605
|
name,
|
|
3908
4606
|
engine,
|
|
3909
4607
|
engineVersion: "unknown",
|
|
@@ -3918,17 +4616,17 @@ function findDeclaredDatabaseForService(graph, serviceNodeId, engine) {
|
|
|
3918
4616
|
const sources = [serviceNodeId];
|
|
3919
4617
|
for (const edgeId of graph.outboundEdges(serviceNodeId)) {
|
|
3920
4618
|
const e = graph.getEdgeAttributes(edgeId);
|
|
3921
|
-
if (e.type ===
|
|
4619
|
+
if (e.type === import_types8.EdgeType.CONTAINS) sources.push(e.target);
|
|
3922
4620
|
}
|
|
3923
4621
|
const matches = /* @__PURE__ */ new Set();
|
|
3924
4622
|
for (const src of sources) {
|
|
3925
4623
|
if (!graph.hasNode(src)) continue;
|
|
3926
4624
|
for (const edgeId of graph.outboundEdges(src)) {
|
|
3927
4625
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
3928
|
-
if (edge.type !==
|
|
4626
|
+
if (edge.type !== import_types8.EdgeType.CONNECTS_TO || edge.provenance !== import_types8.Provenance.EXTRACTED) continue;
|
|
3929
4627
|
if (!graph.hasNode(edge.target)) continue;
|
|
3930
4628
|
const target = graph.getNodeAttributes(edge.target);
|
|
3931
|
-
if (target.type !==
|
|
4629
|
+
if (target.type !== import_types8.NodeType.DatabaseNode || target.engine !== engine) continue;
|
|
3932
4630
|
matches.add(edge.target);
|
|
3933
4631
|
}
|
|
3934
4632
|
}
|
|
@@ -3943,7 +4641,7 @@ function ensureFrontierNode(graph, host, ts) {
|
|
|
3943
4641
|
}
|
|
3944
4642
|
const node = {
|
|
3945
4643
|
id,
|
|
3946
|
-
type:
|
|
4644
|
+
type: import_types8.NodeType.FrontierNode,
|
|
3947
4645
|
name: host,
|
|
3948
4646
|
host,
|
|
3949
4647
|
firstObserved: ts,
|
|
@@ -3967,11 +4665,11 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
3967
4665
|
};
|
|
3968
4666
|
const updated = {
|
|
3969
4667
|
...existing,
|
|
3970
|
-
provenance:
|
|
4668
|
+
provenance: import_types8.Provenance.OBSERVED,
|
|
3971
4669
|
lastObserved: ts,
|
|
3972
4670
|
callCount: newSpanCount,
|
|
3973
4671
|
signal: newSignal,
|
|
3974
|
-
confidence: (0,
|
|
4672
|
+
confidence: (0, import_types8.confidenceForObservedSignal)(newSignal),
|
|
3975
4673
|
grain
|
|
3976
4674
|
// backfills legacy edges that predate ADR-142
|
|
3977
4675
|
};
|
|
@@ -3988,8 +4686,8 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
3988
4686
|
source,
|
|
3989
4687
|
target,
|
|
3990
4688
|
type,
|
|
3991
|
-
provenance:
|
|
3992
|
-
confidence: (0,
|
|
4689
|
+
provenance: import_types8.Provenance.OBSERVED,
|
|
4690
|
+
confidence: (0, import_types8.confidenceForObservedSignal)(signal),
|
|
3993
4691
|
lastObserved: ts,
|
|
3994
4692
|
callCount: 1,
|
|
3995
4693
|
signal,
|
|
@@ -4011,9 +4709,9 @@ function stitchTrace(graph, sourceServiceId, ts) {
|
|
|
4011
4709
|
const outbound = graph.outboundEdges(nodeId);
|
|
4012
4710
|
for (const edgeId of outbound) {
|
|
4013
4711
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4014
|
-
if (edge.provenance !==
|
|
4712
|
+
if (edge.provenance !== import_types8.Provenance.EXTRACTED) continue;
|
|
4015
4713
|
if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
|
|
4016
|
-
if (graph.hasEdge((0,
|
|
4714
|
+
if (graph.hasEdge((0, import_types8.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
|
|
4017
4715
|
upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
|
|
4018
4716
|
if (!visited.has(edge.target)) {
|
|
4019
4717
|
visited.add(edge.target);
|
|
@@ -4035,23 +4733,23 @@ function upsertInferredEdge(graph, type, source, target, ts) {
|
|
|
4035
4733
|
source,
|
|
4036
4734
|
target,
|
|
4037
4735
|
type,
|
|
4038
|
-
provenance:
|
|
4736
|
+
provenance: import_types8.Provenance.INFERRED,
|
|
4039
4737
|
confidence: INFERRED_CONFIDENCE,
|
|
4040
4738
|
lastObserved: ts
|
|
4041
4739
|
};
|
|
4042
4740
|
graph.addEdgeWithKey(id, source, target, edge);
|
|
4043
4741
|
}
|
|
4044
4742
|
async function appendErrorEvent(ctx, ev) {
|
|
4045
|
-
await
|
|
4046
|
-
await
|
|
4743
|
+
await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(ctx.errorsPath), { recursive: true });
|
|
4744
|
+
await import_node_fs8.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
4047
4745
|
}
|
|
4048
4746
|
function incidentAffectedNode(span, graph, scanPath) {
|
|
4049
|
-
const sid = (0,
|
|
4747
|
+
const sid = (0, import_types8.serviceId)(span.service, span.env);
|
|
4050
4748
|
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
4051
4749
|
const callSite = callSiteFromSpan(span, serviceNode, scanPath);
|
|
4052
4750
|
if (callSite) {
|
|
4053
4751
|
const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
|
|
4054
|
-
return (0,
|
|
4752
|
+
return (0, import_types8.fileId)(span.service, relPath);
|
|
4055
4753
|
}
|
|
4056
4754
|
return sid;
|
|
4057
4755
|
}
|
|
@@ -4084,8 +4782,8 @@ function makeErrorSpanWriter(errorsPath, graph, scanPath) {
|
|
|
4084
4782
|
return async (span) => {
|
|
4085
4783
|
const ev = buildErrorEventForReceiver(span, graph, scanPath);
|
|
4086
4784
|
if (!ev) return;
|
|
4087
|
-
await
|
|
4088
|
-
await
|
|
4785
|
+
await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(errorsPath), { recursive: true });
|
|
4786
|
+
await import_node_fs8.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
4089
4787
|
};
|
|
4090
4788
|
}
|
|
4091
4789
|
async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp, statusCode, count, firstTimestamp) {
|
|
@@ -4176,7 +4874,7 @@ function findRouteNodeByHttpRoute(graph, serviceName, method, httpRoute) {
|
|
|
4176
4874
|
graph.forEachNode((id, attrs) => {
|
|
4177
4875
|
if (found) return;
|
|
4178
4876
|
const a = attrs;
|
|
4179
|
-
if (a.type !==
|
|
4877
|
+
if (a.type !== import_types8.NodeType.RouteNode || a.service !== serviceName) return;
|
|
4180
4878
|
if (m && a.method !== "ALL" && a.method !== m) return;
|
|
4181
4879
|
if (normalizePathTemplate(a.pathTemplate) === target) found = id;
|
|
4182
4880
|
});
|
|
@@ -4207,7 +4905,7 @@ async function handleSpan(ctx, span) {
|
|
|
4207
4905
|
let targetId;
|
|
4208
4906
|
if (host) {
|
|
4209
4907
|
ensureDatabaseNode(ctx.graph, host, span.dbSystem);
|
|
4210
|
-
targetId = (0,
|
|
4908
|
+
targetId = (0, import_types8.databaseId)(host);
|
|
4211
4909
|
} else {
|
|
4212
4910
|
const declared = findDeclaredDatabaseForService(ctx.graph, sourceId, span.dbSystem);
|
|
4213
4911
|
if (declared) {
|
|
@@ -4224,7 +4922,7 @@ async function handleSpan(ctx, span) {
|
|
|
4224
4922
|
}
|
|
4225
4923
|
const result = upsertObservedEdge(
|
|
4226
4924
|
ctx.graph,
|
|
4227
|
-
|
|
4925
|
+
import_types8.EdgeType.CONNECTS_TO,
|
|
4228
4926
|
observedSource(),
|
|
4229
4927
|
targetId,
|
|
4230
4928
|
ts,
|
|
@@ -4236,7 +4934,7 @@ async function handleSpan(ctx, span) {
|
|
|
4236
4934
|
const collectionId = ensureInfraNode(ctx.graph, "mongodb-collection", span.dbCollection, "self");
|
|
4237
4935
|
upsertObservedEdge(
|
|
4238
4936
|
ctx.graph,
|
|
4239
|
-
|
|
4937
|
+
import_types8.EdgeType.CALLS,
|
|
4240
4938
|
observedSource(),
|
|
4241
4939
|
collectionId,
|
|
4242
4940
|
ts,
|
|
@@ -4248,7 +4946,7 @@ async function handleSpan(ctx, span) {
|
|
|
4248
4946
|
const tableId = ensureInfraNode(ctx.graph, "sql-table", span.dbTable, "self");
|
|
4249
4947
|
upsertObservedEdge(
|
|
4250
4948
|
ctx.graph,
|
|
4251
|
-
|
|
4949
|
+
import_types8.EdgeType.CALLS,
|
|
4252
4950
|
observedSource(),
|
|
4253
4951
|
tableId,
|
|
4254
4952
|
ts,
|
|
@@ -4264,7 +4962,7 @@ async function handleSpan(ctx, span) {
|
|
|
4264
4962
|
span.messagingSystem,
|
|
4265
4963
|
span.messagingDestination
|
|
4266
4964
|
);
|
|
4267
|
-
const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ?
|
|
4965
|
+
const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? import_types8.EdgeType.CONSUMES_FROM : import_types8.EdgeType.PUBLISHES_TO;
|
|
4268
4966
|
const result = upsertObservedEdge(
|
|
4269
4967
|
ctx.graph,
|
|
4270
4968
|
edgeType,
|
|
@@ -4284,7 +4982,7 @@ async function handleSpan(ctx, span) {
|
|
|
4284
4982
|
);
|
|
4285
4983
|
const result = upsertObservedEdge(
|
|
4286
4984
|
ctx.graph,
|
|
4287
|
-
|
|
4985
|
+
import_types8.EdgeType.CONTAINS,
|
|
4288
4986
|
observedSource(),
|
|
4289
4987
|
targetId,
|
|
4290
4988
|
ts,
|
|
@@ -4296,7 +4994,7 @@ async function handleSpan(ctx, span) {
|
|
|
4296
4994
|
const targetId = ensureGrpcMethodNode(ctx.graph, span.rpcService, span.rpcMethod);
|
|
4297
4995
|
const result = upsertObservedEdge(
|
|
4298
4996
|
ctx.graph,
|
|
4299
|
-
|
|
4997
|
+
import_types8.EdgeType.CONTAINS,
|
|
4300
4998
|
observedSource(),
|
|
4301
4999
|
targetId,
|
|
4302
5000
|
ts,
|
|
@@ -4312,7 +5010,7 @@ async function handleSpan(ctx, span) {
|
|
|
4312
5010
|
);
|
|
4313
5011
|
const result = upsertObservedEdge(
|
|
4314
5012
|
ctx.graph,
|
|
4315
|
-
|
|
5013
|
+
import_types8.EdgeType.CONNECTS_TO,
|
|
4316
5014
|
observedSource(),
|
|
4317
5015
|
targetId,
|
|
4318
5016
|
ts,
|
|
@@ -4328,7 +5026,7 @@ async function handleSpan(ctx, span) {
|
|
|
4328
5026
|
if (targetId && targetId !== sourceId) {
|
|
4329
5027
|
upsertObservedEdge(
|
|
4330
5028
|
ctx.graph,
|
|
4331
|
-
|
|
5029
|
+
import_types8.EdgeType.CALLS,
|
|
4332
5030
|
observedSource(),
|
|
4333
5031
|
targetId,
|
|
4334
5032
|
ts,
|
|
@@ -4341,7 +5039,7 @@ async function handleSpan(ctx, span) {
|
|
|
4341
5039
|
const frontierNodeId = ensureFrontierNode(ctx.graph, host, ts);
|
|
4342
5040
|
upsertObservedEdge(
|
|
4343
5041
|
ctx.graph,
|
|
4344
|
-
|
|
5042
|
+
import_types8.EdgeType.CALLS,
|
|
4345
5043
|
observedSource(),
|
|
4346
5044
|
frontierNodeId,
|
|
4347
5045
|
ts,
|
|
@@ -4367,7 +5065,7 @@ async function handleSpan(ctx, span) {
|
|
|
4367
5065
|
} : void 0;
|
|
4368
5066
|
upsertObservedEdge(
|
|
4369
5067
|
ctx.graph,
|
|
4370
|
-
|
|
5068
|
+
import_types8.EdgeType.CALLS,
|
|
4371
5069
|
fallbackSource,
|
|
4372
5070
|
sourceId,
|
|
4373
5071
|
ts,
|
|
@@ -4386,7 +5084,7 @@ async function handleSpan(ctx, span) {
|
|
|
4386
5084
|
);
|
|
4387
5085
|
if (routeNodeId) {
|
|
4388
5086
|
const routeSvc = ctx.graph.getNodeAttributes(routeNodeId).service;
|
|
4389
|
-
upsertObservedEdge(ctx.graph,
|
|
5087
|
+
upsertObservedEdge(ctx.graph, import_types8.EdgeType.CONTAINS, (0, import_types8.serviceId)(routeSvc), routeNodeId, ts, isError);
|
|
4390
5088
|
}
|
|
4391
5089
|
}
|
|
4392
5090
|
if (span.statusCode === 2) {
|
|
@@ -4425,7 +5123,7 @@ function promoteFrontierNodes(graph, opts = {}) {
|
|
|
4425
5123
|
const aliasIndex = /* @__PURE__ */ new Map();
|
|
4426
5124
|
graph.forEachNode((id, attrs) => {
|
|
4427
5125
|
const a = attrs;
|
|
4428
|
-
if (a.type !==
|
|
5126
|
+
if (a.type !== import_types8.NodeType.ServiceNode) return;
|
|
4429
5127
|
aliasIndex.set(a.name, id);
|
|
4430
5128
|
if (a.aliases) {
|
|
4431
5129
|
for (const alias of a.aliases) aliasIndex.set(alias, id);
|
|
@@ -4434,7 +5132,7 @@ function promoteFrontierNodes(graph, opts = {}) {
|
|
|
4434
5132
|
const toPromote = [];
|
|
4435
5133
|
graph.forEachNode((id, attrs) => {
|
|
4436
5134
|
const a = attrs;
|
|
4437
|
-
if (a.type !==
|
|
5135
|
+
if (a.type !== import_types8.NodeType.FrontierNode) return;
|
|
4438
5136
|
const target = aliasIndex.get(a.host);
|
|
4439
5137
|
if (!target) return;
|
|
4440
5138
|
if (target === id) return;
|
|
@@ -4468,7 +5166,7 @@ function rewireFrontierEdges(graph, frontierId2, serviceId7) {
|
|
|
4468
5166
|
}
|
|
4469
5167
|
function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
|
|
4470
5168
|
graph.dropEdge(oldEdgeId);
|
|
4471
|
-
const newId = edge.provenance ===
|
|
5169
|
+
const newId = edge.provenance === import_types8.Provenance.OBSERVED ? (0, import_types8.observedEdgeId)(newSource, newTarget, edge.type) : edge.provenance === import_types8.Provenance.INFERRED ? (0, import_types8.inferredEdgeId)(newSource, newTarget, edge.type) : (0, import_types8.extractedEdgeId)(newSource, newTarget, edge.type);
|
|
4472
5170
|
if (graph.hasEdge(newId)) {
|
|
4473
5171
|
const existing = graph.getEdgeAttributes(newId);
|
|
4474
5172
|
const merged = {
|
|
@@ -4502,12 +5200,12 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
4502
5200
|
const project = options.project ?? DEFAULT_PROJECT;
|
|
4503
5201
|
graph.forEachEdge((id, attrs) => {
|
|
4504
5202
|
const e = attrs;
|
|
4505
|
-
if (e.provenance !==
|
|
5203
|
+
if (e.provenance !== import_types8.Provenance.OBSERVED) return;
|
|
4506
5204
|
if (!e.lastObserved) return;
|
|
4507
5205
|
const threshold = thresholdForEdgeType(e.type, thresholds);
|
|
4508
5206
|
const age = now - new Date(e.lastObserved).getTime();
|
|
4509
5207
|
if (age > threshold) {
|
|
4510
|
-
const updated = { ...e, provenance:
|
|
5208
|
+
const updated = { ...e, provenance: import_types8.Provenance.STALE, confidence: 0.3 };
|
|
4511
5209
|
graph.replaceEdgeAttributes(id, updated);
|
|
4512
5210
|
events.push({
|
|
4513
5211
|
edgeId: id,
|
|
@@ -4524,8 +5222,8 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
4524
5222
|
project,
|
|
4525
5223
|
payload: {
|
|
4526
5224
|
edgeId: id,
|
|
4527
|
-
from:
|
|
4528
|
-
to:
|
|
5225
|
+
from: import_types8.Provenance.OBSERVED,
|
|
5226
|
+
to: import_types8.Provenance.STALE
|
|
4529
5227
|
}
|
|
4530
5228
|
});
|
|
4531
5229
|
}
|
|
@@ -4536,13 +5234,13 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
4536
5234
|
return { count: events.length, events };
|
|
4537
5235
|
}
|
|
4538
5236
|
async function appendStaleEvents(staleEventsPath, events) {
|
|
4539
|
-
await
|
|
5237
|
+
await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(staleEventsPath), { recursive: true });
|
|
4540
5238
|
const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
4541
|
-
await
|
|
5239
|
+
await import_node_fs8.promises.appendFile(staleEventsPath, lines, "utf8");
|
|
4542
5240
|
}
|
|
4543
5241
|
async function readStaleEvents(staleEventsPath) {
|
|
4544
5242
|
try {
|
|
4545
|
-
const raw = await
|
|
5243
|
+
const raw = await import_node_fs8.promises.readFile(staleEventsPath, "utf8");
|
|
4546
5244
|
return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
4547
5245
|
} catch (err) {
|
|
4548
5246
|
if (err.code === "ENOENT") return [];
|
|
@@ -4576,7 +5274,7 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
4576
5274
|
}
|
|
4577
5275
|
async function readErrorEvents(errorsPath) {
|
|
4578
5276
|
try {
|
|
4579
|
-
const raw = await
|
|
5277
|
+
const raw = await import_node_fs8.promises.readFile(errorsPath, "utf8");
|
|
4580
5278
|
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
4581
5279
|
return dedupeIncidents(events);
|
|
4582
5280
|
} catch (err) {
|
|
@@ -4634,7 +5332,7 @@ function mergeSnapshot(graph, snapshot) {
|
|
|
4634
5332
|
const validEdges = [];
|
|
4635
5333
|
for (const node of incomingNodes) {
|
|
4636
5334
|
if (node.attributes === void 0) continue;
|
|
4637
|
-
const parsed =
|
|
5335
|
+
const parsed = import_types8.GraphNodeSchema.safeParse(node.attributes);
|
|
4638
5336
|
if (!parsed.success) {
|
|
4639
5337
|
issues.push(`node "${node.key}": ${describeZodIssues(parsed.error)}`);
|
|
4640
5338
|
continue;
|
|
@@ -4643,7 +5341,7 @@ function mergeSnapshot(graph, snapshot) {
|
|
|
4643
5341
|
}
|
|
4644
5342
|
for (const edge of incomingEdges) {
|
|
4645
5343
|
if (edge.attributes === void 0) continue;
|
|
4646
|
-
const parsed =
|
|
5344
|
+
const parsed = import_types8.GraphEdgeSchema.safeParse(edge.attributes);
|
|
4647
5345
|
if (!parsed.success) {
|
|
4648
5346
|
const label = edge.key ?? `${edge.source}->${edge.target}`;
|
|
4649
5347
|
issues.push(`edge "${label}": ${describeZodIssues(parsed.error)}`);
|
|
@@ -4673,16 +5371,16 @@ function mergeSnapshot(graph, snapshot) {
|
|
|
4673
5371
|
|
|
4674
5372
|
// src/extract/services.ts
|
|
4675
5373
|
init_cjs_shims();
|
|
4676
|
-
var
|
|
4677
|
-
var
|
|
5374
|
+
var import_node_fs12 = require("fs");
|
|
5375
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
4678
5376
|
var import_ignore = __toESM(require("ignore"), 1);
|
|
4679
5377
|
var import_minimatch2 = require("minimatch");
|
|
4680
|
-
var
|
|
5378
|
+
var import_types10 = require("@neat.is/types");
|
|
4681
5379
|
|
|
4682
5380
|
// src/extract/python.ts
|
|
4683
5381
|
init_cjs_shims();
|
|
4684
|
-
var
|
|
4685
|
-
var
|
|
5382
|
+
var import_node_fs9 = require("fs");
|
|
5383
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
4686
5384
|
var import_smol_toml = require("smol-toml");
|
|
4687
5385
|
var REQUIREMENT_LINE = /^\s*([A-Za-z0-9_.-]+)(?:\[[^\]]*\])?\s*(?:(==)\s*([A-Za-z0-9_.+-]+))?/;
|
|
4688
5386
|
function parseRequirementsTxt(content) {
|
|
@@ -4715,25 +5413,25 @@ function depsFromPyProject(pyproject) {
|
|
|
4715
5413
|
return out;
|
|
4716
5414
|
}
|
|
4717
5415
|
async function discoverPythonService(serviceDir) {
|
|
4718
|
-
const pyprojectPath =
|
|
4719
|
-
const requirementsPath =
|
|
4720
|
-
const setupPath =
|
|
5416
|
+
const pyprojectPath = import_node_path10.default.join(serviceDir, "pyproject.toml");
|
|
5417
|
+
const requirementsPath = import_node_path10.default.join(serviceDir, "requirements.txt");
|
|
5418
|
+
const setupPath = import_node_path10.default.join(serviceDir, "setup.py");
|
|
4721
5419
|
const hasPyproject = await exists(pyprojectPath);
|
|
4722
5420
|
const hasRequirements = await exists(requirementsPath);
|
|
4723
5421
|
const hasSetup = await exists(setupPath);
|
|
4724
5422
|
if (!hasPyproject && !hasRequirements && !hasSetup) return null;
|
|
4725
|
-
let name =
|
|
5423
|
+
let name = import_node_path10.default.basename(serviceDir);
|
|
4726
5424
|
let version;
|
|
4727
5425
|
const dependencies = {};
|
|
4728
5426
|
if (hasPyproject) {
|
|
4729
|
-
const raw = await
|
|
5427
|
+
const raw = await import_node_fs9.promises.readFile(pyprojectPath, "utf8");
|
|
4730
5428
|
const pyproject = (0, import_smol_toml.parse)(raw);
|
|
4731
5429
|
name = pyproject.project?.name ?? pyproject.tool?.poetry?.name ?? name;
|
|
4732
5430
|
version = pyproject.project?.version ?? pyproject.tool?.poetry?.version ?? void 0;
|
|
4733
5431
|
Object.assign(dependencies, depsFromPyProject(pyproject));
|
|
4734
5432
|
}
|
|
4735
5433
|
if (hasRequirements) {
|
|
4736
|
-
const raw = await
|
|
5434
|
+
const raw = await import_node_fs9.promises.readFile(requirementsPath, "utf8");
|
|
4737
5435
|
Object.assign(dependencies, parseRequirementsTxt(raw));
|
|
4738
5436
|
}
|
|
4739
5437
|
return { name, version, dependencies };
|
|
@@ -4748,9 +5446,9 @@ function pythonToPackage(service) {
|
|
|
4748
5446
|
|
|
4749
5447
|
// src/extract/go.ts
|
|
4750
5448
|
init_cjs_shims();
|
|
4751
|
-
var
|
|
4752
|
-
var
|
|
4753
|
-
var
|
|
5449
|
+
var import_node_fs10 = require("fs");
|
|
5450
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
5451
|
+
var import_types9 = require("@neat.is/types");
|
|
4754
5452
|
function parseGoMod(source) {
|
|
4755
5453
|
const module2 = source.match(/^\s*module\s+(\S+)\s*$/m)?.[1];
|
|
4756
5454
|
if (!module2) return null;
|
|
@@ -4769,7 +5467,7 @@ function parseGoMod(source) {
|
|
|
4769
5467
|
async function discoverGoService(scanPath, dir) {
|
|
4770
5468
|
let raw;
|
|
4771
5469
|
try {
|
|
4772
|
-
raw = await
|
|
5470
|
+
raw = await import_node_fs10.promises.readFile(import_node_path11.default.join(dir, "go.mod"), "utf8");
|
|
4773
5471
|
} catch {
|
|
4774
5472
|
return null;
|
|
4775
5473
|
}
|
|
@@ -4778,12 +5476,12 @@ async function discoverGoService(scanPath, dir) {
|
|
|
4778
5476
|
const name = mod.module.split("/").filter(Boolean).pop() ?? mod.module;
|
|
4779
5477
|
const pkg = { name, dependencies: mod.dependencies };
|
|
4780
5478
|
const node = {
|
|
4781
|
-
id: (0,
|
|
4782
|
-
type:
|
|
5479
|
+
id: (0, import_types9.serviceId)(name),
|
|
5480
|
+
type: import_types9.NodeType.ServiceNode,
|
|
4783
5481
|
name,
|
|
4784
5482
|
language: "go",
|
|
4785
5483
|
dependencies: mod.dependencies,
|
|
4786
|
-
repoPath:
|
|
5484
|
+
repoPath: import_node_path11.default.relative(scanPath, dir),
|
|
4787
5485
|
...mod.dependencies["github.com/gin-gonic/gin"] ? { framework: "gin" } : {}
|
|
4788
5486
|
};
|
|
4789
5487
|
return { pkg, dir, node };
|
|
@@ -4791,17 +5489,17 @@ async function discoverGoService(scanPath, dir) {
|
|
|
4791
5489
|
|
|
4792
5490
|
// src/extract/owners.ts
|
|
4793
5491
|
init_cjs_shims();
|
|
4794
|
-
var
|
|
4795
|
-
var
|
|
5492
|
+
var import_node_fs11 = require("fs");
|
|
5493
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
4796
5494
|
var import_minimatch = require("minimatch");
|
|
4797
5495
|
async function loadCodeowners(scanPath) {
|
|
4798
5496
|
const candidates = [
|
|
4799
|
-
|
|
4800
|
-
|
|
5497
|
+
import_node_path12.default.join(scanPath, "CODEOWNERS"),
|
|
5498
|
+
import_node_path12.default.join(scanPath, ".github", "CODEOWNERS")
|
|
4801
5499
|
];
|
|
4802
5500
|
for (const file of candidates) {
|
|
4803
5501
|
if (await exists(file)) {
|
|
4804
|
-
const raw = await
|
|
5502
|
+
const raw = await import_node_fs11.promises.readFile(file, "utf8");
|
|
4805
5503
|
return parseCodeowners(raw);
|
|
4806
5504
|
}
|
|
4807
5505
|
}
|
|
@@ -4819,7 +5517,7 @@ function parseCodeowners(raw) {
|
|
|
4819
5517
|
return { rules };
|
|
4820
5518
|
}
|
|
4821
5519
|
function matchOwner(file, repoPath) {
|
|
4822
|
-
const normalized = repoPath.split(
|
|
5520
|
+
const normalized = repoPath.split(import_node_path12.default.sep).join("/");
|
|
4823
5521
|
for (const rule of file.rules) {
|
|
4824
5522
|
if (matchesPattern(rule.pattern, normalized)) return rule.owners;
|
|
4825
5523
|
}
|
|
@@ -4835,7 +5533,7 @@ function matchesPattern(rawPattern, repoPath) {
|
|
|
4835
5533
|
return false;
|
|
4836
5534
|
}
|
|
4837
5535
|
async function readPackageJsonAuthor(serviceDir) {
|
|
4838
|
-
const pkgPath =
|
|
5536
|
+
const pkgPath = import_node_path12.default.join(serviceDir, "package.json");
|
|
4839
5537
|
if (!await exists(pkgPath)) return null;
|
|
4840
5538
|
try {
|
|
4841
5539
|
const pkg = await readJson(pkgPath);
|
|
@@ -4847,985 +5545,601 @@ async function readPackageJsonAuthor(serviceDir) {
|
|
|
4847
5545
|
return null;
|
|
4848
5546
|
}
|
|
4849
5547
|
}
|
|
4850
|
-
async function computeServiceOwner(codeowners, repoPath, serviceDir) {
|
|
4851
|
-
if (codeowners && repoPath !== void 0) {
|
|
4852
|
-
const owner = matchOwner(codeowners, repoPath);
|
|
4853
|
-
if (owner) return owner;
|
|
4854
|
-
}
|
|
4855
|
-
const author = await readPackageJsonAuthor(serviceDir);
|
|
4856
|
-
return author ?? void 0;
|
|
4857
|
-
}
|
|
4858
|
-
|
|
4859
|
-
// src/extract/services.ts
|
|
4860
|
-
var DEFAULT_SCAN_DEPTH = 5;
|
|
4861
|
-
function parseScanDepth() {
|
|
4862
|
-
const raw = process.env.NEAT_SCAN_DEPTH;
|
|
4863
|
-
if (!raw) return DEFAULT_SCAN_DEPTH;
|
|
4864
|
-
const n = Number.parseInt(raw, 10);
|
|
4865
|
-
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_SCAN_DEPTH;
|
|
4866
|
-
}
|
|
4867
|
-
function workspaceGlobs(pkg) {
|
|
4868
|
-
const ws = pkg.workspaces;
|
|
4869
|
-
if (!ws) return null;
|
|
4870
|
-
if (Array.isArray(ws)) return ws.length > 0 ? ws : null;
|
|
4871
|
-
if (Array.isArray(ws.packages)) return ws.packages.length > 0 ? ws.packages : null;
|
|
4872
|
-
return null;
|
|
4873
|
-
}
|
|
4874
|
-
async function hasPythonManifest(dir) {
|
|
4875
|
-
return await exists(import_node_path12.default.join(dir, "pyproject.toml")) || await exists(import_node_path12.default.join(dir, "requirements.txt")) || await exists(import_node_path12.default.join(dir, "setup.py"));
|
|
4876
|
-
}
|
|
4877
|
-
async function hasGoManifest(dir) {
|
|
4878
|
-
return exists(import_node_path12.default.join(dir, "go.mod"));
|
|
4879
|
-
}
|
|
4880
|
-
async function loadGitignore(scanPath) {
|
|
4881
|
-
const gitignorePath = import_node_path12.default.join(scanPath, ".gitignore");
|
|
4882
|
-
if (!await exists(gitignorePath)) return null;
|
|
4883
|
-
const raw = await import_node_fs11.promises.readFile(gitignorePath, "utf8");
|
|
4884
|
-
return (0, import_ignore.default)().add(raw);
|
|
4885
|
-
}
|
|
4886
|
-
async function walkDirs(start, scanPath, options, visit) {
|
|
4887
|
-
async function recurse(current, depth) {
|
|
4888
|
-
if (depth > options.maxDepth) return;
|
|
4889
|
-
const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
4890
|
-
for (const entry2 of entries) {
|
|
4891
|
-
if (!entry2.isDirectory()) continue;
|
|
4892
|
-
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
4893
|
-
const child = import_node_path12.default.join(current, entry2.name);
|
|
4894
|
-
if (options.ig) {
|
|
4895
|
-
const rel = import_node_path12.default.relative(scanPath, child).split(import_node_path12.default.sep).join("/");
|
|
4896
|
-
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
4897
|
-
}
|
|
4898
|
-
if (await isPythonVenvDir(child)) continue;
|
|
4899
|
-
await visit(child);
|
|
4900
|
-
await recurse(child, depth + 1);
|
|
4901
|
-
}
|
|
4902
|
-
}
|
|
4903
|
-
await recurse(start, 0);
|
|
4904
|
-
}
|
|
4905
|
-
async function expandWorkspaceGlobs(scanPath, globs) {
|
|
4906
|
-
const found = /* @__PURE__ */ new Set();
|
|
4907
|
-
const scanDepth = parseScanDepth();
|
|
4908
|
-
for (const raw of globs) {
|
|
4909
|
-
const pattern = raw.replace(/^\.\//, "");
|
|
4910
|
-
if (!pattern.includes("*")) {
|
|
4911
|
-
const candidate = import_node_path12.default.join(scanPath, pattern);
|
|
4912
|
-
if (await exists(import_node_path12.default.join(candidate, "package.json"))) found.add(candidate);
|
|
4913
|
-
continue;
|
|
4914
|
-
}
|
|
4915
|
-
const segments = pattern.split("/");
|
|
4916
|
-
const staticSegments = [];
|
|
4917
|
-
for (const seg of segments) {
|
|
4918
|
-
if (seg.includes("*")) break;
|
|
4919
|
-
staticSegments.push(seg);
|
|
4920
|
-
}
|
|
4921
|
-
const start = import_node_path12.default.join(scanPath, ...staticSegments);
|
|
4922
|
-
if (!await exists(start)) continue;
|
|
4923
|
-
const hasDoubleStar = pattern.includes("**");
|
|
4924
|
-
const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
|
|
4925
|
-
await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
|
|
4926
|
-
const rel = import_node_path12.default.relative(scanPath, dir).split(import_node_path12.default.sep).join("/");
|
|
4927
|
-
if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path12.default.join(dir, "package.json"))) {
|
|
4928
|
-
found.add(dir);
|
|
4929
|
-
}
|
|
4930
|
-
});
|
|
4931
|
-
}
|
|
4932
|
-
return [...found];
|
|
4933
|
-
}
|
|
4934
|
-
function detectJsFramework(pkg) {
|
|
4935
|
-
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
4936
|
-
if (deps["next"] !== void 0) return "next";
|
|
4937
|
-
if (deps["remix"] !== void 0) return "remix";
|
|
4938
|
-
for (const k of Object.keys(deps)) {
|
|
4939
|
-
if (k.startsWith("@remix-run/")) return "remix";
|
|
4940
|
-
}
|
|
4941
|
-
if (deps["@sveltejs/kit"] !== void 0) return "sveltekit";
|
|
4942
|
-
if (deps["nuxt"] !== void 0) return "nuxt";
|
|
4943
|
-
if (deps["astro"] !== void 0) return "astro";
|
|
4944
|
-
if (deps["@nestjs/core"] !== void 0) return "nestjs";
|
|
4945
|
-
return void 0;
|
|
4946
|
-
}
|
|
4947
|
-
async function detectJsServiceLanguage(dir, pkg) {
|
|
4948
|
-
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
4949
|
-
if (deps["typescript"] !== void 0) return "typescript";
|
|
4950
|
-
const entries = await import_node_fs11.promises.readdir(dir).catch(() => []);
|
|
4951
|
-
if (entries.some((name) => /^tsconfig(\..+)?\.json$/.test(name))) return "typescript";
|
|
4952
|
-
return "javascript";
|
|
4953
|
-
}
|
|
4954
|
-
async function discoverNodeService(scanPath, dir) {
|
|
4955
|
-
const pkgPath = import_node_path12.default.join(dir, "package.json");
|
|
4956
|
-
if (!await exists(pkgPath)) return null;
|
|
4957
|
-
let pkg;
|
|
4958
|
-
try {
|
|
4959
|
-
pkg = await readJson(pkgPath);
|
|
4960
|
-
} catch (err) {
|
|
4961
|
-
recordExtractionError("services", import_node_path12.default.relative(scanPath, pkgPath), err);
|
|
4962
|
-
return null;
|
|
4963
|
-
}
|
|
4964
|
-
if (!pkg.name) return null;
|
|
4965
|
-
const framework = detectJsFramework(pkg);
|
|
4966
|
-
const language = await detectJsServiceLanguage(dir, pkg);
|
|
4967
|
-
const node = {
|
|
4968
|
-
id: (0, import_types9.serviceId)(pkg.name),
|
|
4969
|
-
type: import_types9.NodeType.ServiceNode,
|
|
4970
|
-
name: pkg.name,
|
|
4971
|
-
language,
|
|
4972
|
-
version: pkg.version,
|
|
4973
|
-
dependencies: pkg.dependencies ?? {},
|
|
4974
|
-
repoPath: import_node_path12.default.relative(scanPath, dir),
|
|
4975
|
-
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
|
|
4976
|
-
...framework ? { framework } : {}
|
|
4977
|
-
};
|
|
4978
|
-
return { pkg, dir, node };
|
|
4979
|
-
}
|
|
4980
|
-
async function discoverPyService(scanPath, dir) {
|
|
4981
|
-
const py = await discoverPythonService(dir);
|
|
4982
|
-
if (!py) return null;
|
|
4983
|
-
const pkg = pythonToPackage(py);
|
|
4984
|
-
const node = {
|
|
4985
|
-
id: (0, import_types9.serviceId)(py.name),
|
|
4986
|
-
type: import_types9.NodeType.ServiceNode,
|
|
4987
|
-
name: py.name,
|
|
4988
|
-
language: "python",
|
|
4989
|
-
version: py.version,
|
|
4990
|
-
dependencies: py.dependencies,
|
|
4991
|
-
repoPath: import_node_path12.default.relative(scanPath, dir)
|
|
4992
|
-
};
|
|
4993
|
-
return { pkg, dir, node };
|
|
4994
|
-
}
|
|
4995
|
-
async function discoverServices(scanPath) {
|
|
4996
|
-
const rootPkgPath = import_node_path12.default.join(scanPath, "package.json");
|
|
4997
|
-
let rootPkg = null;
|
|
4998
|
-
if (await exists(rootPkgPath)) {
|
|
4999
|
-
try {
|
|
5000
|
-
rootPkg = await readJson(rootPkgPath);
|
|
5001
|
-
} catch (err) {
|
|
5002
|
-
recordExtractionError(
|
|
5003
|
-
"services workspaces",
|
|
5004
|
-
import_node_path12.default.relative(scanPath, rootPkgPath),
|
|
5005
|
-
err
|
|
5006
|
-
);
|
|
5007
|
-
}
|
|
5008
|
-
}
|
|
5009
|
-
const wsGlobs = rootPkg ? workspaceGlobs(rootPkg) : null;
|
|
5010
|
-
const candidateDirs = [];
|
|
5011
|
-
if (wsGlobs) {
|
|
5012
|
-
candidateDirs.push(...await expandWorkspaceGlobs(scanPath, wsGlobs));
|
|
5013
|
-
} else {
|
|
5014
|
-
if (rootPkg && rootPkg.name) {
|
|
5015
|
-
candidateDirs.push(scanPath);
|
|
5016
|
-
} else if (await hasPythonManifest(scanPath) || await hasGoManifest(scanPath)) {
|
|
5017
|
-
candidateDirs.push(scanPath);
|
|
5018
|
-
}
|
|
5019
|
-
const ig = await loadGitignore(scanPath);
|
|
5020
|
-
await walkDirs(
|
|
5021
|
-
scanPath,
|
|
5022
|
-
scanPath,
|
|
5023
|
-
{ maxDepth: parseScanDepth(), ig },
|
|
5024
|
-
async (dir) => {
|
|
5025
|
-
if (await exists(import_node_path12.default.join(dir, "package.json"))) {
|
|
5026
|
-
candidateDirs.push(dir);
|
|
5027
|
-
} else if (await hasPythonManifest(dir) || await hasGoManifest(dir)) {
|
|
5028
|
-
candidateDirs.push(dir);
|
|
5029
|
-
}
|
|
5030
|
-
}
|
|
5031
|
-
);
|
|
5032
|
-
}
|
|
5033
|
-
candidateDirs.sort();
|
|
5034
|
-
const seen = /* @__PURE__ */ new Map();
|
|
5035
|
-
const out = [];
|
|
5036
|
-
for (const dir of candidateDirs) {
|
|
5037
|
-
const service = await discoverNodeService(scanPath, dir) ?? await discoverPyService(scanPath, dir) ?? await discoverGoService(scanPath, dir);
|
|
5038
|
-
if (!service) continue;
|
|
5039
|
-
const existingDir = seen.get(service.node.name);
|
|
5040
|
-
if (existingDir !== void 0) {
|
|
5041
|
-
const a = import_node_path12.default.relative(scanPath, existingDir) || ".";
|
|
5042
|
-
const b = import_node_path12.default.relative(scanPath, dir) || ".";
|
|
5043
|
-
console.warn(
|
|
5044
|
-
`[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
|
|
5045
|
-
);
|
|
5046
|
-
continue;
|
|
5047
|
-
}
|
|
5048
|
-
seen.set(service.node.name, dir);
|
|
5049
|
-
out.push(service);
|
|
5050
|
-
}
|
|
5051
|
-
const codeowners = await loadCodeowners(scanPath);
|
|
5052
|
-
for (const service of out) {
|
|
5053
|
-
const owner = await computeServiceOwner(codeowners, service.node.repoPath, service.dir);
|
|
5054
|
-
if (owner !== void 0) service.node.owner = owner;
|
|
5055
|
-
}
|
|
5056
|
-
return out;
|
|
5057
|
-
}
|
|
5058
|
-
function addServiceNodes(graph, services) {
|
|
5059
|
-
let nodesAdded = 0;
|
|
5060
|
-
for (const service of services) {
|
|
5061
|
-
if (!graph.hasNode(service.node.id)) {
|
|
5062
|
-
graph.addNode(service.node.id, { ...service.node, discoveredVia: "static" });
|
|
5063
|
-
nodesAdded++;
|
|
5064
|
-
continue;
|
|
5065
|
-
}
|
|
5066
|
-
const existing = graph.getNodeAttributes(service.node.id);
|
|
5067
|
-
const mergedDiscoveredVia = existing.discoveredVia === "otel" ? "merged" : "static";
|
|
5068
|
-
graph.replaceNodeAttributes(service.node.id, {
|
|
5069
|
-
...existing,
|
|
5070
|
-
...service.node,
|
|
5071
|
-
discoveredVia: mergedDiscoveredVia
|
|
5072
|
-
});
|
|
5073
|
-
}
|
|
5074
|
-
return nodesAdded;
|
|
5075
|
-
}
|
|
5076
|
-
|
|
5077
|
-
// src/extract/aliases.ts
|
|
5078
|
-
init_cjs_shims();
|
|
5079
|
-
var import_node_path13 = __toESM(require("path"), 1);
|
|
5080
|
-
var import_node_fs12 = require("fs");
|
|
5081
|
-
var import_yaml2 = require("yaml");
|
|
5082
|
-
var import_types10 = require("@neat.is/types");
|
|
5083
|
-
var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
5084
|
-
"Service",
|
|
5085
|
-
"Deployment",
|
|
5086
|
-
"StatefulSet",
|
|
5087
|
-
"DaemonSet"
|
|
5088
|
-
]);
|
|
5089
|
-
function addAliases(graph, serviceId7, candidates) {
|
|
5090
|
-
if (!graph.hasNode(serviceId7)) return;
|
|
5091
|
-
const node = graph.getNodeAttributes(serviceId7);
|
|
5092
|
-
if (node.type !== import_types10.NodeType.ServiceNode) return;
|
|
5093
|
-
const set = new Set(node.aliases ?? []);
|
|
5094
|
-
for (const c of candidates) {
|
|
5095
|
-
if (!c) continue;
|
|
5096
|
-
if (c === node.name) continue;
|
|
5097
|
-
set.add(c);
|
|
5098
|
-
}
|
|
5099
|
-
if (set.size === 0) return;
|
|
5100
|
-
const updated = { ...node, aliases: [...set].sort() };
|
|
5101
|
-
graph.replaceNodeAttributes(serviceId7, updated);
|
|
5102
|
-
}
|
|
5103
|
-
function indexServicesByName(services) {
|
|
5104
|
-
const map = /* @__PURE__ */ new Map();
|
|
5105
|
-
for (const s of services) {
|
|
5106
|
-
map.set(s.node.name, s.node.id);
|
|
5107
|
-
map.set(import_node_path13.default.basename(s.dir), s.node.id);
|
|
5108
|
-
}
|
|
5109
|
-
return map;
|
|
5110
|
-
}
|
|
5111
|
-
async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
5112
|
-
let composePath = null;
|
|
5113
|
-
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
5114
|
-
const abs = import_node_path13.default.join(scanPath, name);
|
|
5115
|
-
if (await exists(abs)) {
|
|
5116
|
-
composePath = abs;
|
|
5117
|
-
break;
|
|
5118
|
-
}
|
|
5119
|
-
}
|
|
5120
|
-
if (!composePath) return;
|
|
5121
|
-
let compose;
|
|
5122
|
-
try {
|
|
5123
|
-
compose = await readYaml(composePath);
|
|
5124
|
-
} catch (err) {
|
|
5125
|
-
recordExtractionError(
|
|
5126
|
-
"aliases compose",
|
|
5127
|
-
import_node_path13.default.relative(scanPath, composePath),
|
|
5128
|
-
err
|
|
5129
|
-
);
|
|
5130
|
-
return;
|
|
5131
|
-
}
|
|
5132
|
-
if (!compose?.services) return;
|
|
5133
|
-
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
5134
|
-
const serviceId7 = serviceIndex.get(composeName);
|
|
5135
|
-
if (!serviceId7) continue;
|
|
5136
|
-
const aliases = /* @__PURE__ */ new Set([composeName]);
|
|
5137
|
-
if (svc.container_name) aliases.add(svc.container_name);
|
|
5138
|
-
if (svc.hostname) aliases.add(svc.hostname);
|
|
5139
|
-
addAliases(graph, serviceId7, aliases);
|
|
5140
|
-
}
|
|
5141
|
-
}
|
|
5142
|
-
var LABEL_KEYS = /* @__PURE__ */ new Set([
|
|
5143
|
-
"service",
|
|
5144
|
-
"service.name",
|
|
5145
|
-
"app",
|
|
5146
|
-
"app.name",
|
|
5147
|
-
"com.docker.compose.service",
|
|
5148
|
-
"org.opencontainers.image.title"
|
|
5149
|
-
]);
|
|
5150
|
-
function parseDockerfileLabels(content) {
|
|
5151
|
-
const out = [];
|
|
5152
|
-
const lineRegex = /^\s*label\s+(.+)$/i;
|
|
5153
|
-
for (const raw of content.split("\n")) {
|
|
5154
|
-
const m = lineRegex.exec(raw);
|
|
5155
|
-
if (!m) continue;
|
|
5156
|
-
const rest = m[1];
|
|
5157
|
-
const pairRegex = /([\w.-]+)\s*=\s*("([^"]*)"|'([^']*)'|([^\s]+))/g;
|
|
5158
|
-
let pair;
|
|
5159
|
-
while ((pair = pairRegex.exec(rest)) !== null) {
|
|
5160
|
-
const key = pair[1].toLowerCase();
|
|
5161
|
-
if (!LABEL_KEYS.has(key)) continue;
|
|
5162
|
-
const value = pair[3] ?? pair[4] ?? pair[5] ?? "";
|
|
5163
|
-
if (value) out.push(value);
|
|
5164
|
-
}
|
|
5165
|
-
}
|
|
5166
|
-
return out;
|
|
5167
|
-
}
|
|
5168
|
-
async function collectDockerfileAliases(graph, services) {
|
|
5169
|
-
for (const service of services) {
|
|
5170
|
-
const dockerfilePath = import_node_path13.default.join(service.dir, "Dockerfile");
|
|
5171
|
-
if (!await exists(dockerfilePath)) continue;
|
|
5172
|
-
let content;
|
|
5173
|
-
try {
|
|
5174
|
-
content = await import_node_fs12.promises.readFile(dockerfilePath, "utf8");
|
|
5175
|
-
} catch (err) {
|
|
5176
|
-
recordExtractionError("aliases dockerfile", dockerfilePath, err);
|
|
5177
|
-
continue;
|
|
5178
|
-
}
|
|
5179
|
-
const aliases = parseDockerfileLabels(content);
|
|
5180
|
-
if (aliases.length > 0) addAliases(graph, service.node.id, aliases);
|
|
5181
|
-
}
|
|
5182
|
-
}
|
|
5183
|
-
async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
5184
|
-
if (depth > max) return [];
|
|
5185
|
-
const out = [];
|
|
5186
|
-
const entries = await import_node_fs12.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
5187
|
-
for (const entry2 of entries) {
|
|
5188
|
-
if (entry2.isDirectory()) {
|
|
5189
|
-
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
5190
|
-
const child = import_node_path13.default.join(start, entry2.name);
|
|
5191
|
-
if (await isPythonVenvDir(child)) continue;
|
|
5192
|
-
out.push(...await walkYamlFiles(child, depth + 1, max));
|
|
5193
|
-
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path13.default.extname(entry2.name))) {
|
|
5194
|
-
out.push(import_node_path13.default.join(start, entry2.name));
|
|
5195
|
-
}
|
|
5196
|
-
}
|
|
5197
|
-
return out;
|
|
5198
|
-
}
|
|
5199
|
-
function k8sHostnames(name, namespace) {
|
|
5200
|
-
const ns = namespace ?? "default";
|
|
5201
|
-
return [
|
|
5202
|
-
name,
|
|
5203
|
-
`${name}.${ns}`,
|
|
5204
|
-
`${name}.${ns}.svc`,
|
|
5205
|
-
`${name}.${ns}.svc.cluster.local`
|
|
5206
|
-
];
|
|
5207
|
-
}
|
|
5208
|
-
function k8sServiceTarget(doc, byName) {
|
|
5209
|
-
const selector = doc.spec?.selector;
|
|
5210
|
-
const selectorApp = selector?.app ?? selector?.matchLabels?.app;
|
|
5211
|
-
if (selectorApp && byName.has(selectorApp)) return byName.get(selectorApp);
|
|
5212
|
-
const labelApp = doc.metadata?.labels?.app;
|
|
5213
|
-
if (labelApp && byName.has(labelApp)) return byName.get(labelApp);
|
|
5214
|
-
const metaName = doc.metadata?.name;
|
|
5215
|
-
if (metaName && byName.has(metaName)) return byName.get(metaName);
|
|
5216
|
-
return null;
|
|
5217
|
-
}
|
|
5218
|
-
async function collectK8sAliases(graph, scanPath, serviceIndex) {
|
|
5219
|
-
const files = await walkYamlFiles(scanPath);
|
|
5220
|
-
for (const file of files) {
|
|
5221
|
-
const content = await import_node_fs12.promises.readFile(file, "utf8");
|
|
5222
|
-
let docs;
|
|
5223
|
-
try {
|
|
5224
|
-
docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
5225
|
-
} catch {
|
|
5226
|
-
continue;
|
|
5227
|
-
}
|
|
5228
|
-
for (const doc of docs) {
|
|
5229
|
-
if (!doc?.kind || !doc.metadata?.name) continue;
|
|
5230
|
-
if (!K8S_KINDS_WITH_HOSTNAMES.has(doc.kind)) continue;
|
|
5231
|
-
const target = k8sServiceTarget(doc, serviceIndex);
|
|
5232
|
-
if (!target) continue;
|
|
5233
|
-
addAliases(graph, target, k8sHostnames(doc.metadata.name, doc.metadata.namespace));
|
|
5234
|
-
}
|
|
5235
|
-
}
|
|
5236
|
-
}
|
|
5237
|
-
async function addServiceAliases(graph, scanPath, services) {
|
|
5238
|
-
const byName = indexServicesByName(services);
|
|
5239
|
-
await collectComposeAliases(graph, scanPath, byName);
|
|
5240
|
-
await collectDockerfileAliases(graph, services);
|
|
5241
|
-
await collectK8sAliases(graph, scanPath, byName);
|
|
5242
|
-
}
|
|
5243
|
-
|
|
5244
|
-
// src/extract/files.ts
|
|
5245
|
-
init_cjs_shims();
|
|
5246
|
-
var import_node_path14 = __toESM(require("path"), 1);
|
|
5247
|
-
async function addFiles(graph, services) {
|
|
5248
|
-
let nodesAdded = 0;
|
|
5249
|
-
let edgesAdded = 0;
|
|
5250
|
-
for (const service of services) {
|
|
5251
|
-
const filePaths = await walkSourceFiles(service.dir);
|
|
5252
|
-
for (const filePath of filePaths) {
|
|
5253
|
-
const relPath = toPosix(import_node_path14.default.relative(service.dir, filePath));
|
|
5254
|
-
const { nodesAdded: n, edgesAdded: e } = ensureFileNode(
|
|
5255
|
-
graph,
|
|
5256
|
-
service.pkg.name,
|
|
5257
|
-
service.node.id,
|
|
5258
|
-
relPath
|
|
5259
|
-
);
|
|
5260
|
-
nodesAdded += n;
|
|
5261
|
-
edgesAdded += e;
|
|
5262
|
-
}
|
|
5263
|
-
}
|
|
5264
|
-
return { nodesAdded, edgesAdded };
|
|
5265
|
-
}
|
|
5266
|
-
|
|
5267
|
-
// src/extract/symbols.ts
|
|
5268
|
-
init_cjs_shims();
|
|
5269
|
-
var import_node_path15 = __toESM(require("path"), 1);
|
|
5270
|
-
var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
|
|
5271
|
-
var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
|
|
5272
|
-
var import_tree_sitter_typescript = __toESM(require("tree-sitter-typescript"), 1);
|
|
5273
|
-
var import_types11 = require("@neat.is/types");
|
|
5274
|
-
var PARSE_CHUNK2 = 16384;
|
|
5275
|
-
var GRAMMAR_BY_EXT = {
|
|
5276
|
-
".ts": import_tree_sitter_typescript.default.typescript,
|
|
5277
|
-
".tsx": import_tree_sitter_typescript.default.tsx,
|
|
5278
|
-
".js": import_tree_sitter_javascript2.default,
|
|
5279
|
-
".jsx": import_tree_sitter_javascript2.default,
|
|
5280
|
-
".mjs": import_tree_sitter_javascript2.default,
|
|
5281
|
-
".cjs": import_tree_sitter_javascript2.default
|
|
5282
|
-
};
|
|
5283
|
-
function parseSource2(parser, source) {
|
|
5284
|
-
return parser.parse(
|
|
5285
|
-
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
|
|
5286
|
-
);
|
|
5287
|
-
}
|
|
5288
|
-
function methodName(node) {
|
|
5289
|
-
const name = node.childForFieldName("name");
|
|
5290
|
-
return name ? name.text : null;
|
|
5291
|
-
}
|
|
5292
|
-
function collectSymbolDefs(root) {
|
|
5293
|
-
const out = [];
|
|
5294
|
-
const push = (kind, qualname, node) => {
|
|
5295
|
-
out.push({
|
|
5296
|
-
kind,
|
|
5297
|
-
qualname,
|
|
5298
|
-
startLine: node.startPosition.row + 1,
|
|
5299
|
-
endLine: node.endPosition.row + 1
|
|
5300
|
-
});
|
|
5301
|
-
};
|
|
5302
|
-
const visit = (node, classCtx) => {
|
|
5303
|
-
switch (node.type) {
|
|
5304
|
-
case "function_declaration":
|
|
5305
|
-
case "generator_function_declaration": {
|
|
5306
|
-
const name = node.childForFieldName("name")?.text;
|
|
5307
|
-
if (name) push("function", name, node);
|
|
5308
|
-
break;
|
|
5309
|
-
}
|
|
5310
|
-
case "class_declaration":
|
|
5311
|
-
case "abstract_class_declaration":
|
|
5312
|
-
case "class": {
|
|
5313
|
-
const name = node.childForFieldName("name")?.text;
|
|
5314
|
-
if (name) push("class", name, node);
|
|
5315
|
-
const body = node.childForFieldName("body");
|
|
5316
|
-
if (body) {
|
|
5317
|
-
for (let i = 0; i < body.namedChildCount; i++) {
|
|
5318
|
-
const child = body.namedChild(i);
|
|
5319
|
-
if (child) visit(child, name ?? classCtx);
|
|
5320
|
-
}
|
|
5321
|
-
}
|
|
5322
|
-
return;
|
|
5323
|
-
}
|
|
5324
|
-
case "method_definition": {
|
|
5325
|
-
const name = methodName(node);
|
|
5326
|
-
if (name) {
|
|
5327
|
-
const kind = name === "constructor" ? "constructor" : "method";
|
|
5328
|
-
push(kind, classCtx ? `${classCtx}.${name}` : name, node);
|
|
5329
|
-
}
|
|
5330
|
-
break;
|
|
5331
|
-
}
|
|
5332
|
-
case "variable_declarator": {
|
|
5333
|
-
const value = node.childForFieldName("value");
|
|
5334
|
-
if (value && (value.type === "arrow_function" || value.type === "function" || value.type === "function_expression" || value.type === "generator_function")) {
|
|
5335
|
-
const nameNode = node.childForFieldName("name");
|
|
5336
|
-
if (nameNode && nameNode.type === "identifier") {
|
|
5337
|
-
push("function", nameNode.text, node);
|
|
5338
|
-
}
|
|
5339
|
-
}
|
|
5340
|
-
break;
|
|
5341
|
-
}
|
|
5342
|
-
}
|
|
5343
|
-
for (let i = 0; i < node.namedChildCount; i++) {
|
|
5344
|
-
const child = node.namedChild(i);
|
|
5345
|
-
if (child) visit(child, classCtx);
|
|
5346
|
-
}
|
|
5347
|
-
};
|
|
5348
|
-
visit(root, void 0);
|
|
5349
|
-
return out;
|
|
5350
|
-
}
|
|
5351
|
-
function disambiguate(defs) {
|
|
5352
|
-
const counts = /* @__PURE__ */ new Map();
|
|
5353
|
-
for (const def of defs) counts.set(def.qualname, (counts.get(def.qualname) ?? 0) + 1);
|
|
5354
|
-
const seen = /* @__PURE__ */ new Map();
|
|
5355
|
-
return defs.map((def) => {
|
|
5356
|
-
if ((counts.get(def.qualname) ?? 0) <= 1) return { def };
|
|
5357
|
-
const ordinal = seen.get(def.qualname) ?? 0;
|
|
5358
|
-
seen.set(def.qualname, ordinal + 1);
|
|
5359
|
-
return { def, disambiguator: ordinal };
|
|
5360
|
-
});
|
|
5361
|
-
}
|
|
5362
|
-
async function addSymbols(graph, services) {
|
|
5363
|
-
const parsers = /* @__PURE__ */ new Map();
|
|
5364
|
-
const parserForExt2 = (ext) => {
|
|
5365
|
-
const grammar = GRAMMAR_BY_EXT[ext];
|
|
5366
|
-
if (!grammar) return null;
|
|
5367
|
-
let parser = parsers.get(ext);
|
|
5368
|
-
if (!parser) {
|
|
5369
|
-
parser = new import_tree_sitter2.default();
|
|
5370
|
-
parser.setLanguage(grammar);
|
|
5371
|
-
parsers.set(ext, parser);
|
|
5372
|
-
}
|
|
5373
|
-
return parser;
|
|
5374
|
-
};
|
|
5375
|
-
let nodesAdded = 0;
|
|
5376
|
-
let edgesAdded = 0;
|
|
5377
|
-
for (const service of services) {
|
|
5378
|
-
const files = await loadSourceFiles(service.dir);
|
|
5379
|
-
for (const file of files) {
|
|
5380
|
-
const parser = parserForExt2(import_node_path15.default.extname(file.path));
|
|
5381
|
-
if (!parser) continue;
|
|
5382
|
-
const relPath = toPosix(import_node_path15.default.relative(service.dir, file.path));
|
|
5383
|
-
let defs;
|
|
5384
|
-
try {
|
|
5385
|
-
const tree = parseSource2(parser, file.content);
|
|
5386
|
-
defs = collectSymbolDefs(tree.rootNode);
|
|
5387
|
-
} catch (err) {
|
|
5388
|
-
recordExtractionError("symbol extraction", file.path, err);
|
|
5389
|
-
continue;
|
|
5390
|
-
}
|
|
5391
|
-
if (defs.length === 0) continue;
|
|
5392
|
-
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
5393
|
-
graph,
|
|
5394
|
-
service.pkg.name,
|
|
5395
|
-
service.node.id,
|
|
5396
|
-
relPath
|
|
5397
|
-
);
|
|
5398
|
-
nodesAdded += fn;
|
|
5399
|
-
edgesAdded += fe;
|
|
5400
|
-
for (const { def, disambiguator } of disambiguate(defs)) {
|
|
5401
|
-
const sid = (0, import_types11.symbolId)(service.pkg.name, relPath, def.qualname, disambiguator);
|
|
5402
|
-
if (!graph.hasNode(sid)) {
|
|
5403
|
-
const node = {
|
|
5404
|
-
id: sid,
|
|
5405
|
-
type: import_types11.NodeType.SymbolNode,
|
|
5406
|
-
kind: def.kind,
|
|
5407
|
-
qualname: def.qualname,
|
|
5408
|
-
span: { startLine: def.startLine, endLine: def.endLine },
|
|
5409
|
-
service: service.pkg.name,
|
|
5410
|
-
relPath,
|
|
5411
|
-
discoveredVia: "static"
|
|
5412
|
-
};
|
|
5413
|
-
graph.addNode(sid, node);
|
|
5414
|
-
nodesAdded++;
|
|
5415
|
-
}
|
|
5416
|
-
const containsId = (0, import_types11.extractedEdgeId)(fileNodeId, sid, import_types11.EdgeType.CONTAINS);
|
|
5417
|
-
if (!graph.hasEdge(containsId)) {
|
|
5418
|
-
const edge = {
|
|
5419
|
-
id: containsId,
|
|
5420
|
-
source: fileNodeId,
|
|
5421
|
-
target: sid,
|
|
5422
|
-
type: import_types11.EdgeType.CONTAINS,
|
|
5423
|
-
provenance: import_types11.Provenance.EXTRACTED,
|
|
5424
|
-
confidence: (0, import_types11.confidenceForExtracted)("structural"),
|
|
5425
|
-
evidence: {
|
|
5426
|
-
file: relPath,
|
|
5427
|
-
line: def.startLine,
|
|
5428
|
-
snippet: snippet(file.content, def.startLine)
|
|
5429
|
-
}
|
|
5430
|
-
};
|
|
5431
|
-
graph.addEdgeWithKey(containsId, fileNodeId, sid, edge);
|
|
5432
|
-
edgesAdded++;
|
|
5433
|
-
}
|
|
5434
|
-
}
|
|
5435
|
-
}
|
|
5548
|
+
async function computeServiceOwner(codeowners, repoPath, serviceDir) {
|
|
5549
|
+
if (codeowners && repoPath !== void 0) {
|
|
5550
|
+
const owner = matchOwner(codeowners, repoPath);
|
|
5551
|
+
if (owner) return owner;
|
|
5436
5552
|
}
|
|
5437
|
-
|
|
5553
|
+
const author = await readPackageJsonAuthor(serviceDir);
|
|
5554
|
+
return author ?? void 0;
|
|
5438
5555
|
}
|
|
5439
5556
|
|
|
5440
|
-
// src/extract/
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
init_cjs_shims();
|
|
5448
|
-
var import_node_path16 = __toESM(require("path"), 1);
|
|
5449
|
-
var import_node_fs13 = require("fs");
|
|
5450
|
-
var import_tree_sitter3 = __toESM(require("tree-sitter"), 1);
|
|
5451
|
-
var import_tree_sitter_javascript3 = __toESM(require("tree-sitter-javascript"), 1);
|
|
5452
|
-
var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
|
|
5453
|
-
var import_tree_sitter_go2 = __toESM(require("tree-sitter-go"), 1);
|
|
5454
|
-
var import_types12 = require("@neat.is/types");
|
|
5455
|
-
var PARSE_CHUNK3 = 16384;
|
|
5456
|
-
function parseSource3(parser, source) {
|
|
5457
|
-
return parser.parse(
|
|
5458
|
-
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK3)
|
|
5459
|
-
);
|
|
5557
|
+
// src/extract/services.ts
|
|
5558
|
+
var DEFAULT_SCAN_DEPTH = 5;
|
|
5559
|
+
function parseScanDepth() {
|
|
5560
|
+
const raw = process.env.NEAT_SCAN_DEPTH;
|
|
5561
|
+
if (!raw) return DEFAULT_SCAN_DEPTH;
|
|
5562
|
+
const n = Number.parseInt(raw, 10);
|
|
5563
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_SCAN_DEPTH;
|
|
5460
5564
|
}
|
|
5461
|
-
function
|
|
5462
|
-
const
|
|
5463
|
-
|
|
5464
|
-
return
|
|
5565
|
+
function workspaceGlobs(pkg) {
|
|
5566
|
+
const ws = pkg.workspaces;
|
|
5567
|
+
if (!ws) return null;
|
|
5568
|
+
if (Array.isArray(ws)) return ws.length > 0 ? ws : null;
|
|
5569
|
+
if (Array.isArray(ws.packages)) return ws.packages.length > 0 ? ws.packages : null;
|
|
5570
|
+
return null;
|
|
5465
5571
|
}
|
|
5466
|
-
function
|
|
5467
|
-
|
|
5468
|
-
p.setLanguage(import_tree_sitter_python2.default);
|
|
5469
|
-
return p;
|
|
5572
|
+
async function hasPythonManifest(dir) {
|
|
5573
|
+
return await exists(import_node_path13.default.join(dir, "pyproject.toml")) || await exists(import_node_path13.default.join(dir, "requirements.txt")) || await exists(import_node_path13.default.join(dir, "setup.py"));
|
|
5470
5574
|
}
|
|
5471
|
-
function
|
|
5472
|
-
|
|
5473
|
-
p.setLanguage(import_tree_sitter_go2.default);
|
|
5474
|
-
return p;
|
|
5575
|
+
async function hasGoManifest(dir) {
|
|
5576
|
+
return exists(import_node_path13.default.join(dir, "go.mod"));
|
|
5475
5577
|
}
|
|
5476
|
-
function
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
const raw = node.text;
|
|
5482
|
-
if (raw.length >= 2) return raw.slice(1, -1);
|
|
5483
|
-
return raw.length === 0 ? null : "";
|
|
5578
|
+
async function loadGitignore(scanPath) {
|
|
5579
|
+
const gitignorePath = import_node_path13.default.join(scanPath, ".gitignore");
|
|
5580
|
+
if (!await exists(gitignorePath)) return null;
|
|
5581
|
+
const raw = await import_node_fs12.promises.readFile(gitignorePath, "utf8");
|
|
5582
|
+
return (0, import_ignore.default)().add(raw);
|
|
5484
5583
|
}
|
|
5485
|
-
function
|
|
5486
|
-
|
|
5487
|
-
|
|
5584
|
+
async function walkDirs(start, scanPath, options, visit) {
|
|
5585
|
+
async function recurse(current, depth) {
|
|
5586
|
+
if (depth > options.maxDepth) return;
|
|
5587
|
+
const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
5588
|
+
for (const entry2 of entries) {
|
|
5589
|
+
if (!entry2.isDirectory()) continue;
|
|
5590
|
+
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
5591
|
+
const child = import_node_path13.default.join(current, entry2.name);
|
|
5592
|
+
if (options.ig) {
|
|
5593
|
+
const rel = import_node_path13.default.relative(scanPath, child).split(import_node_path13.default.sep).join("/");
|
|
5594
|
+
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
5595
|
+
}
|
|
5596
|
+
if (await isPythonVenvDir(child)) continue;
|
|
5597
|
+
await visit(child);
|
|
5598
|
+
await recurse(child, depth + 1);
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
await recurse(start, 0);
|
|
5488
5602
|
}
|
|
5489
|
-
function
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5603
|
+
async function expandWorkspaceGlobs(scanPath, globs) {
|
|
5604
|
+
const found = /* @__PURE__ */ new Set();
|
|
5605
|
+
const scanDepth = parseScanDepth();
|
|
5606
|
+
for (const raw of globs) {
|
|
5607
|
+
const pattern = raw.replace(/^\.\//, "");
|
|
5608
|
+
if (!pattern.includes("*")) {
|
|
5609
|
+
const candidate = import_node_path13.default.join(scanPath, pattern);
|
|
5610
|
+
if (await exists(import_node_path13.default.join(candidate, "package.json"))) found.add(candidate);
|
|
5611
|
+
continue;
|
|
5495
5612
|
}
|
|
5496
|
-
|
|
5613
|
+
const segments = pattern.split("/");
|
|
5614
|
+
const staticSegments = [];
|
|
5615
|
+
for (const seg of segments) {
|
|
5616
|
+
if (seg.includes("*")) break;
|
|
5617
|
+
staticSegments.push(seg);
|
|
5618
|
+
}
|
|
5619
|
+
const start = import_node_path13.default.join(scanPath, ...staticSegments);
|
|
5620
|
+
if (!await exists(start)) continue;
|
|
5621
|
+
const hasDoubleStar = pattern.includes("**");
|
|
5622
|
+
const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
|
|
5623
|
+
await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
|
|
5624
|
+
const rel = import_node_path13.default.relative(scanPath, dir).split(import_node_path13.default.sep).join("/");
|
|
5625
|
+
if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path13.default.join(dir, "package.json"))) {
|
|
5626
|
+
found.add(dir);
|
|
5627
|
+
}
|
|
5628
|
+
});
|
|
5497
5629
|
}
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5630
|
+
return [...found];
|
|
5631
|
+
}
|
|
5632
|
+
function detectJsFramework(pkg) {
|
|
5633
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
5634
|
+
if (deps["next"] !== void 0) return "next";
|
|
5635
|
+
if (deps["remix"] !== void 0) return "remix";
|
|
5636
|
+
for (const k of Object.keys(deps)) {
|
|
5637
|
+
if (k.startsWith("@remix-run/")) return "remix";
|
|
5501
5638
|
}
|
|
5639
|
+
if (deps["@sveltejs/kit"] !== void 0) return "sveltekit";
|
|
5640
|
+
if (deps["nuxt"] !== void 0) return "nuxt";
|
|
5641
|
+
if (deps["astro"] !== void 0) return "astro";
|
|
5642
|
+
if (deps["@nestjs/core"] !== void 0) return "nestjs";
|
|
5643
|
+
return void 0;
|
|
5502
5644
|
}
|
|
5503
|
-
function
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5645
|
+
async function detectJsServiceLanguage(dir, pkg) {
|
|
5646
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
5647
|
+
if (deps["typescript"] !== void 0) return "typescript";
|
|
5648
|
+
const entries = await import_node_fs12.promises.readdir(dir).catch(() => []);
|
|
5649
|
+
if (entries.some((name) => /^tsconfig(\..+)?\.json$/.test(name))) return "typescript";
|
|
5650
|
+
return "javascript";
|
|
5651
|
+
}
|
|
5652
|
+
async function discoverNodeService(scanPath, dir) {
|
|
5653
|
+
const pkgPath = import_node_path13.default.join(dir, "package.json");
|
|
5654
|
+
if (!await exists(pkgPath)) return null;
|
|
5655
|
+
let pkg;
|
|
5656
|
+
try {
|
|
5657
|
+
pkg = await readJson(pkgPath);
|
|
5658
|
+
} catch (err) {
|
|
5659
|
+
recordExtractionError("services", import_node_path13.default.relative(scanPath, pkgPath), err);
|
|
5660
|
+
return null;
|
|
5661
|
+
}
|
|
5662
|
+
if (!pkg.name) return null;
|
|
5663
|
+
const framework = detectJsFramework(pkg);
|
|
5664
|
+
const language = await detectJsServiceLanguage(dir, pkg);
|
|
5665
|
+
const node = {
|
|
5666
|
+
id: (0, import_types10.serviceId)(pkg.name),
|
|
5667
|
+
type: import_types10.NodeType.ServiceNode,
|
|
5668
|
+
name: pkg.name,
|
|
5669
|
+
language,
|
|
5670
|
+
version: pkg.version,
|
|
5671
|
+
dependencies: pkg.dependencies ?? {},
|
|
5672
|
+
repoPath: import_node_path13.default.relative(scanPath, dir),
|
|
5673
|
+
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
|
|
5674
|
+
...framework ? { framework } : {}
|
|
5675
|
+
};
|
|
5676
|
+
return { pkg, dir, node };
|
|
5677
|
+
}
|
|
5678
|
+
async function discoverPyService(scanPath, dir) {
|
|
5679
|
+
const py = await discoverPythonService(dir);
|
|
5680
|
+
if (!py) return null;
|
|
5681
|
+
const pkg = pythonToPackage(py);
|
|
5682
|
+
const node = {
|
|
5683
|
+
id: (0, import_types10.serviceId)(py.name),
|
|
5684
|
+
type: import_types10.NodeType.ServiceNode,
|
|
5685
|
+
name: py.name,
|
|
5686
|
+
language: "python",
|
|
5687
|
+
version: py.version,
|
|
5688
|
+
dependencies: py.dependencies,
|
|
5689
|
+
repoPath: import_node_path13.default.relative(scanPath, dir)
|
|
5690
|
+
};
|
|
5691
|
+
return { pkg, dir, node };
|
|
5692
|
+
}
|
|
5693
|
+
async function discoverServices(scanPath) {
|
|
5694
|
+
const rootPkgPath = import_node_path13.default.join(scanPath, "package.json");
|
|
5695
|
+
let rootPkg = null;
|
|
5696
|
+
if (await exists(rootPkgPath)) {
|
|
5697
|
+
try {
|
|
5698
|
+
rootPkg = await readJson(rootPkgPath);
|
|
5699
|
+
} catch (err) {
|
|
5700
|
+
recordExtractionError(
|
|
5701
|
+
"services workspaces",
|
|
5702
|
+
import_node_path13.default.relative(scanPath, rootPkgPath),
|
|
5703
|
+
err
|
|
5704
|
+
);
|
|
5511
5705
|
}
|
|
5512
|
-
return;
|
|
5513
5706
|
}
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5707
|
+
const wsGlobs = rootPkg ? workspaceGlobs(rootPkg) : null;
|
|
5708
|
+
const candidateDirs = [];
|
|
5709
|
+
if (wsGlobs) {
|
|
5710
|
+
candidateDirs.push(...await expandWorkspaceGlobs(scanPath, wsGlobs));
|
|
5711
|
+
} else {
|
|
5712
|
+
if (rootPkg && rootPkg.name) {
|
|
5713
|
+
candidateDirs.push(scanPath);
|
|
5714
|
+
} else if (await hasPythonManifest(scanPath) || await hasGoManifest(scanPath)) {
|
|
5715
|
+
candidateDirs.push(scanPath);
|
|
5716
|
+
}
|
|
5717
|
+
const ig = await loadGitignore(scanPath);
|
|
5718
|
+
await walkDirs(
|
|
5719
|
+
scanPath,
|
|
5720
|
+
scanPath,
|
|
5721
|
+
{ maxDepth: parseScanDepth(), ig },
|
|
5722
|
+
async (dir) => {
|
|
5723
|
+
if (await exists(import_node_path13.default.join(dir, "package.json"))) {
|
|
5724
|
+
candidateDirs.push(dir);
|
|
5725
|
+
} else if (await hasPythonManifest(dir) || await hasGoManifest(dir)) {
|
|
5726
|
+
candidateDirs.push(dir);
|
|
5523
5727
|
}
|
|
5524
5728
|
}
|
|
5729
|
+
);
|
|
5730
|
+
}
|
|
5731
|
+
candidateDirs.sort();
|
|
5732
|
+
const seen = /* @__PURE__ */ new Map();
|
|
5733
|
+
const out = [];
|
|
5734
|
+
for (const dir of candidateDirs) {
|
|
5735
|
+
const service = await discoverNodeService(scanPath, dir) ?? await discoverPyService(scanPath, dir) ?? await discoverGoService(scanPath, dir);
|
|
5736
|
+
if (!service) continue;
|
|
5737
|
+
const existingDir = seen.get(service.node.name);
|
|
5738
|
+
if (existingDir !== void 0) {
|
|
5739
|
+
const a = import_node_path13.default.relative(scanPath, existingDir) || ".";
|
|
5740
|
+
const b = import_node_path13.default.relative(scanPath, dir) || ".";
|
|
5741
|
+
console.warn(
|
|
5742
|
+
`[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
|
|
5743
|
+
);
|
|
5744
|
+
continue;
|
|
5745
|
+
}
|
|
5746
|
+
seen.set(service.node.name, dir);
|
|
5747
|
+
out.push(service);
|
|
5748
|
+
}
|
|
5749
|
+
const codeowners = await loadCodeowners(scanPath);
|
|
5750
|
+
for (const service of out) {
|
|
5751
|
+
const owner = await computeServiceOwner(codeowners, service.node.repoPath, service.dir);
|
|
5752
|
+
if (owner !== void 0) service.node.owner = owner;
|
|
5753
|
+
}
|
|
5754
|
+
return out;
|
|
5755
|
+
}
|
|
5756
|
+
function addServiceNodes(graph, services) {
|
|
5757
|
+
let nodesAdded = 0;
|
|
5758
|
+
for (const service of services) {
|
|
5759
|
+
if (!graph.hasNode(service.node.id)) {
|
|
5760
|
+
graph.addNode(service.node.id, { ...service.node, discoveredVia: "static" });
|
|
5761
|
+
nodesAdded++;
|
|
5762
|
+
continue;
|
|
5525
5763
|
}
|
|
5764
|
+
const existing = graph.getNodeAttributes(service.node.id);
|
|
5765
|
+
const mergedDiscoveredVia = existing.discoveredVia === "otel" ? "merged" : "static";
|
|
5766
|
+
graph.replaceNodeAttributes(service.node.id, {
|
|
5767
|
+
...existing,
|
|
5768
|
+
...service.node,
|
|
5769
|
+
discoveredVia: mergedDiscoveredVia
|
|
5770
|
+
});
|
|
5771
|
+
}
|
|
5772
|
+
return nodesAdded;
|
|
5773
|
+
}
|
|
5774
|
+
|
|
5775
|
+
// src/extract/aliases.ts
|
|
5776
|
+
init_cjs_shims();
|
|
5777
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
5778
|
+
var import_node_fs13 = require("fs");
|
|
5779
|
+
var import_yaml2 = require("yaml");
|
|
5780
|
+
var import_types11 = require("@neat.is/types");
|
|
5781
|
+
var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
5782
|
+
"Service",
|
|
5783
|
+
"Deployment",
|
|
5784
|
+
"StatefulSet",
|
|
5785
|
+
"DaemonSet"
|
|
5786
|
+
]);
|
|
5787
|
+
function addAliases(graph, serviceId7, candidates) {
|
|
5788
|
+
if (!graph.hasNode(serviceId7)) return;
|
|
5789
|
+
const node = graph.getNodeAttributes(serviceId7);
|
|
5790
|
+
if (node.type !== import_types11.NodeType.ServiceNode) return;
|
|
5791
|
+
const set = new Set(node.aliases ?? []);
|
|
5792
|
+
for (const c of candidates) {
|
|
5793
|
+
if (!c) continue;
|
|
5794
|
+
if (c === node.name) continue;
|
|
5795
|
+
set.add(c);
|
|
5526
5796
|
}
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5797
|
+
if (set.size === 0) return;
|
|
5798
|
+
const updated = { ...node, aliases: [...set].sort() };
|
|
5799
|
+
graph.replaceNodeAttributes(serviceId7, updated);
|
|
5800
|
+
}
|
|
5801
|
+
function indexServicesByName(services) {
|
|
5802
|
+
const map = /* @__PURE__ */ new Map();
|
|
5803
|
+
for (const s of services) {
|
|
5804
|
+
map.set(s.node.name, s.node.id);
|
|
5805
|
+
map.set(import_node_path14.default.basename(s.dir), s.node.id);
|
|
5530
5806
|
}
|
|
5807
|
+
return map;
|
|
5531
5808
|
}
|
|
5532
|
-
function
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5809
|
+
async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
5810
|
+
let composePath = null;
|
|
5811
|
+
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
5812
|
+
const abs = import_node_path14.default.join(scanPath, name);
|
|
5813
|
+
if (await exists(abs)) {
|
|
5814
|
+
composePath = abs;
|
|
5815
|
+
break;
|
|
5816
|
+
}
|
|
5537
5817
|
}
|
|
5538
|
-
if (
|
|
5539
|
-
|
|
5818
|
+
if (!composePath) return;
|
|
5819
|
+
let compose;
|
|
5820
|
+
try {
|
|
5821
|
+
compose = await readYaml(composePath);
|
|
5822
|
+
} catch (err) {
|
|
5823
|
+
recordExtractionError(
|
|
5824
|
+
"aliases compose",
|
|
5825
|
+
import_node_path14.default.relative(scanPath, composePath),
|
|
5826
|
+
err
|
|
5827
|
+
);
|
|
5540
5828
|
return;
|
|
5541
5829
|
}
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5830
|
+
if (!compose?.services) return;
|
|
5831
|
+
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
5832
|
+
const serviceId7 = serviceIndex.get(composeName);
|
|
5833
|
+
if (!serviceId7) continue;
|
|
5834
|
+
const aliases = /* @__PURE__ */ new Set([composeName]);
|
|
5835
|
+
if (svc.container_name) aliases.add(svc.container_name);
|
|
5836
|
+
if (svc.hostname) aliases.add(svc.hostname);
|
|
5837
|
+
addAliases(graph, serviceId7, aliases);
|
|
5545
5838
|
}
|
|
5546
5839
|
}
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
if (!rc) continue;
|
|
5570
|
-
if (rc.type === "import_prefix") {
|
|
5571
|
-
for (let k = 0; k < rc.childCount; k++) {
|
|
5572
|
-
if (rc.child(k)?.type === ".") level++;
|
|
5573
|
-
}
|
|
5574
|
-
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
5575
|
-
}
|
|
5576
|
-
} else if (child.type === "dotted_name") {
|
|
5577
|
-
modulePath = child.text;
|
|
5578
|
-
}
|
|
5579
|
-
continue;
|
|
5580
|
-
}
|
|
5581
|
-
collectImportedNames(child, names);
|
|
5582
|
-
}
|
|
5583
|
-
if (level > 0 || modulePath) {
|
|
5584
|
-
out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
5840
|
+
var LABEL_KEYS = /* @__PURE__ */ new Set([
|
|
5841
|
+
"service",
|
|
5842
|
+
"service.name",
|
|
5843
|
+
"app",
|
|
5844
|
+
"app.name",
|
|
5845
|
+
"com.docker.compose.service",
|
|
5846
|
+
"org.opencontainers.image.title"
|
|
5847
|
+
]);
|
|
5848
|
+
function parseDockerfileLabels(content) {
|
|
5849
|
+
const out = [];
|
|
5850
|
+
const lineRegex = /^\s*label\s+(.+)$/i;
|
|
5851
|
+
for (const raw of content.split("\n")) {
|
|
5852
|
+
const m = lineRegex.exec(raw);
|
|
5853
|
+
if (!m) continue;
|
|
5854
|
+
const rest = m[1];
|
|
5855
|
+
const pairRegex = /([\w.-]+)\s*=\s*("([^"]*)"|'([^']*)'|([^\s]+))/g;
|
|
5856
|
+
let pair;
|
|
5857
|
+
while ((pair = pairRegex.exec(rest)) !== null) {
|
|
5858
|
+
const key = pair[1].toLowerCase();
|
|
5859
|
+
if (!LABEL_KEYS.has(key)) continue;
|
|
5860
|
+
const value = pair[3] ?? pair[4] ?? pair[5] ?? "";
|
|
5861
|
+
if (value) out.push(value);
|
|
5585
5862
|
}
|
|
5586
5863
|
}
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5864
|
+
return out;
|
|
5865
|
+
}
|
|
5866
|
+
async function collectDockerfileAliases(graph, services) {
|
|
5867
|
+
for (const service of services) {
|
|
5868
|
+
const dockerfilePath = import_node_path14.default.join(service.dir, "Dockerfile");
|
|
5869
|
+
if (!await exists(dockerfilePath)) continue;
|
|
5870
|
+
let content;
|
|
5871
|
+
try {
|
|
5872
|
+
content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
|
|
5873
|
+
} catch (err) {
|
|
5874
|
+
recordExtractionError("aliases dockerfile", dockerfilePath, err);
|
|
5875
|
+
continue;
|
|
5876
|
+
}
|
|
5877
|
+
const aliases = parseDockerfileLabels(content);
|
|
5878
|
+
if (aliases.length > 0) addAliases(graph, service.node.id, aliases);
|
|
5590
5879
|
}
|
|
5591
5880
|
}
|
|
5592
|
-
async function
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5881
|
+
async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
5882
|
+
if (depth > max) return [];
|
|
5883
|
+
const out = [];
|
|
5884
|
+
const entries = await import_node_fs13.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
5885
|
+
for (const entry2 of entries) {
|
|
5886
|
+
if (entry2.isDirectory()) {
|
|
5887
|
+
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
5888
|
+
const child = import_node_path14.default.join(start, entry2.name);
|
|
5889
|
+
if (await isPythonVenvDir(child)) continue;
|
|
5890
|
+
out.push(...await walkYamlFiles(child, depth + 1, max));
|
|
5891
|
+
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path14.default.extname(entry2.name))) {
|
|
5892
|
+
out.push(import_node_path14.default.join(start, entry2.name));
|
|
5893
|
+
}
|
|
5598
5894
|
}
|
|
5895
|
+
return out;
|
|
5599
5896
|
}
|
|
5600
|
-
function
|
|
5601
|
-
const
|
|
5602
|
-
return
|
|
5897
|
+
function k8sHostnames(name, namespace) {
|
|
5898
|
+
const ns = namespace ?? "default";
|
|
5899
|
+
return [
|
|
5900
|
+
name,
|
|
5901
|
+
`${name}.${ns}`,
|
|
5902
|
+
`${name}.${ns}.svc`,
|
|
5903
|
+
`${name}.${ns}.svc.cluster.local`
|
|
5904
|
+
];
|
|
5603
5905
|
}
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5906
|
+
function k8sServiceTarget(doc, byName) {
|
|
5907
|
+
const selector = doc.spec?.selector;
|
|
5908
|
+
const selectorApp = selector?.app ?? selector?.matchLabels?.app;
|
|
5909
|
+
if (selectorApp && byName.has(selectorApp)) return byName.get(selectorApp);
|
|
5910
|
+
const labelApp = doc.metadata?.labels?.app;
|
|
5911
|
+
if (labelApp && byName.has(labelApp)) return byName.get(labelApp);
|
|
5912
|
+
const metaName = doc.metadata?.name;
|
|
5913
|
+
if (metaName && byName.has(metaName)) return byName.get(metaName);
|
|
5914
|
+
return null;
|
|
5915
|
+
}
|
|
5916
|
+
async function collectK8sAliases(graph, scanPath, serviceIndex) {
|
|
5917
|
+
const files = await walkYamlFiles(scanPath);
|
|
5918
|
+
for (const file of files) {
|
|
5919
|
+
const content = await import_node_fs13.promises.readFile(file, "utf8");
|
|
5920
|
+
let docs;
|
|
5921
|
+
try {
|
|
5922
|
+
docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
5923
|
+
} catch {
|
|
5924
|
+
continue;
|
|
5611
5925
|
}
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5926
|
+
for (const doc of docs) {
|
|
5927
|
+
if (!doc?.kind || !doc.metadata?.name) continue;
|
|
5928
|
+
if (!K8S_KINDS_WITH_HOSTNAMES.has(doc.kind)) continue;
|
|
5929
|
+
const target = k8sServiceTarget(doc, serviceIndex);
|
|
5930
|
+
if (!target) continue;
|
|
5931
|
+
addAliases(graph, target, k8sHostnames(doc.metadata.name, doc.metadata.namespace));
|
|
5617
5932
|
}
|
|
5618
5933
|
}
|
|
5619
|
-
return null;
|
|
5620
5934
|
}
|
|
5621
|
-
async function
|
|
5622
|
-
const
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
} catch {
|
|
5627
|
-
return null;
|
|
5628
|
-
}
|
|
5629
|
-
try {
|
|
5630
|
-
const parsed = JSON.parse(raw);
|
|
5631
|
-
const paths = parsed.compilerOptions?.paths;
|
|
5632
|
-
if (!paths || Object.keys(paths).length === 0) return null;
|
|
5633
|
-
const baseUrl = parsed.compilerOptions?.baseUrl;
|
|
5634
|
-
return { paths, baseDir: baseUrl ? import_node_path16.default.resolve(serviceDir, baseUrl) : serviceDir };
|
|
5635
|
-
} catch (err) {
|
|
5636
|
-
recordExtractionError("import alias resolution", tsconfigPath, err);
|
|
5637
|
-
return null;
|
|
5638
|
-
}
|
|
5935
|
+
async function addServiceAliases(graph, scanPath, services) {
|
|
5936
|
+
const byName = indexServicesByName(services);
|
|
5937
|
+
await collectComposeAliases(graph, scanPath, byName);
|
|
5938
|
+
await collectDockerfileAliases(graph, services);
|
|
5939
|
+
await collectK8sAliases(graph, scanPath, byName);
|
|
5639
5940
|
}
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
const
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5941
|
+
|
|
5942
|
+
// src/extract/files.ts
|
|
5943
|
+
init_cjs_shims();
|
|
5944
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
5945
|
+
async function addFiles(graph, services) {
|
|
5946
|
+
let nodesAdded = 0;
|
|
5947
|
+
let edgesAdded = 0;
|
|
5948
|
+
for (const service of services) {
|
|
5949
|
+
const filePaths = await walkSourceFiles(service.dir);
|
|
5950
|
+
for (const filePath of filePaths) {
|
|
5951
|
+
const relPath = toPosix(import_node_path15.default.relative(service.dir, filePath));
|
|
5952
|
+
const { nodesAdded: n, edgesAdded: e } = ensureFileNode(
|
|
5953
|
+
graph,
|
|
5954
|
+
service.pkg.name,
|
|
5955
|
+
service.node.id,
|
|
5956
|
+
relPath
|
|
5957
|
+
);
|
|
5958
|
+
nodesAdded += n;
|
|
5959
|
+
edgesAdded += e;
|
|
5658
5960
|
}
|
|
5659
5961
|
}
|
|
5660
|
-
return
|
|
5962
|
+
return { nodesAdded, edgesAdded };
|
|
5963
|
+
}
|
|
5964
|
+
|
|
5965
|
+
// src/extract/symbols.ts
|
|
5966
|
+
init_cjs_shims();
|
|
5967
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
5968
|
+
var import_tree_sitter3 = __toESM(require("tree-sitter"), 1);
|
|
5969
|
+
var import_tree_sitter_javascript3 = __toESM(require("tree-sitter-javascript"), 1);
|
|
5970
|
+
var import_tree_sitter_typescript = __toESM(require("tree-sitter-typescript"), 1);
|
|
5971
|
+
var import_types12 = require("@neat.is/types");
|
|
5972
|
+
var PARSE_CHUNK3 = 16384;
|
|
5973
|
+
var GRAMMAR_BY_EXT = {
|
|
5974
|
+
".ts": import_tree_sitter_typescript.default.typescript,
|
|
5975
|
+
".tsx": import_tree_sitter_typescript.default.tsx,
|
|
5976
|
+
".js": import_tree_sitter_javascript3.default,
|
|
5977
|
+
".jsx": import_tree_sitter_javascript3.default,
|
|
5978
|
+
".mjs": import_tree_sitter_javascript3.default,
|
|
5979
|
+
".cjs": import_tree_sitter_javascript3.default
|
|
5980
|
+
};
|
|
5981
|
+
function parseSource3(parser, source) {
|
|
5982
|
+
return parser.parse(
|
|
5983
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK3)
|
|
5984
|
+
);
|
|
5661
5985
|
}
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5986
|
+
function methodName(node) {
|
|
5987
|
+
const name = node.childForFieldName("name");
|
|
5988
|
+
return name ? name.text : null;
|
|
5989
|
+
}
|
|
5990
|
+
function collectSymbolDefs(root) {
|
|
5991
|
+
const out = [];
|
|
5992
|
+
const push = (kind, qualname, node) => {
|
|
5993
|
+
out.push({
|
|
5994
|
+
kind,
|
|
5995
|
+
qualname,
|
|
5996
|
+
startLine: node.startPosition.row + 1,
|
|
5997
|
+
endLine: node.endPosition.row + 1
|
|
5998
|
+
});
|
|
5999
|
+
};
|
|
6000
|
+
const visit = (node, classCtx) => {
|
|
6001
|
+
switch (node.type) {
|
|
6002
|
+
case "function_declaration":
|
|
6003
|
+
case "generator_function_declaration": {
|
|
6004
|
+
const name = node.childForFieldName("name")?.text;
|
|
6005
|
+
if (name) push("function", name, node);
|
|
6006
|
+
break;
|
|
6007
|
+
}
|
|
6008
|
+
case "class_declaration":
|
|
6009
|
+
case "abstract_class_declaration":
|
|
6010
|
+
case "class": {
|
|
6011
|
+
const name = node.childForFieldName("name")?.text;
|
|
6012
|
+
if (name) push("class", name, node);
|
|
6013
|
+
const body = node.childForFieldName("body");
|
|
6014
|
+
if (body) {
|
|
6015
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
6016
|
+
const child = body.namedChild(i);
|
|
6017
|
+
if (child) visit(child, name ?? classCtx);
|
|
6018
|
+
}
|
|
5673
6019
|
}
|
|
6020
|
+
return;
|
|
5674
6021
|
}
|
|
5675
|
-
|
|
5676
|
-
|
|
6022
|
+
case "method_definition": {
|
|
6023
|
+
const name = methodName(node);
|
|
6024
|
+
if (name) {
|
|
6025
|
+
const kind = name === "constructor" ? "constructor" : "method";
|
|
6026
|
+
push(kind, classCtx ? `${classCtx}.${name}` : name, node);
|
|
6027
|
+
}
|
|
6028
|
+
break;
|
|
5677
6029
|
}
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
}
|
|
5685
|
-
|
|
5686
|
-
let baseDir;
|
|
5687
|
-
if (imp.level > 0) {
|
|
5688
|
-
baseDir = import_node_path16.default.dirname(importerPath);
|
|
5689
|
-
for (let i = 1; i < imp.level; i++) baseDir = import_node_path16.default.dirname(baseDir);
|
|
5690
|
-
} else {
|
|
5691
|
-
baseDir = serviceDir;
|
|
5692
|
-
}
|
|
5693
|
-
const moduleBase = imp.modulePath ? import_node_path16.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
|
|
5694
|
-
const resolved = /* @__PURE__ */ new Set();
|
|
5695
|
-
let needModuleFile = imp.names.length === 0;
|
|
5696
|
-
for (const name of imp.names) {
|
|
5697
|
-
const submoduleFile = import_node_path16.default.join(moduleBase, `${name}.py`);
|
|
5698
|
-
const subpackageInit = import_node_path16.default.join(moduleBase, name, "__init__.py");
|
|
5699
|
-
if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
|
|
5700
|
-
resolved.add(toPosix(import_node_path16.default.relative(serviceDir, submoduleFile)));
|
|
5701
|
-
} else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
|
|
5702
|
-
resolved.add(toPosix(import_node_path16.default.relative(serviceDir, subpackageInit)));
|
|
5703
|
-
} else {
|
|
5704
|
-
needModuleFile = true;
|
|
5705
|
-
}
|
|
5706
|
-
}
|
|
5707
|
-
if (needModuleFile) {
|
|
5708
|
-
const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path16.default.join(moduleBase, "__init__.py")] : [import_node_path16.default.join(moduleBase, "__init__.py")];
|
|
5709
|
-
for (const candidate of moduleFileCandidates) {
|
|
5710
|
-
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
5711
|
-
resolved.add(toPosix(import_node_path16.default.relative(serviceDir, candidate)));
|
|
6030
|
+
case "variable_declarator": {
|
|
6031
|
+
const value = node.childForFieldName("value");
|
|
6032
|
+
if (value && (value.type === "arrow_function" || value.type === "function" || value.type === "function_expression" || value.type === "generator_function")) {
|
|
6033
|
+
const nameNode = node.childForFieldName("name");
|
|
6034
|
+
if (nameNode && nameNode.type === "identifier") {
|
|
6035
|
+
push("function", nameNode.text, node);
|
|
6036
|
+
}
|
|
6037
|
+
}
|
|
5712
6038
|
break;
|
|
5713
6039
|
}
|
|
5714
6040
|
}
|
|
5715
|
-
|
|
5716
|
-
|
|
6041
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
6042
|
+
const child = node.namedChild(i);
|
|
6043
|
+
if (child) visit(child, classCtx);
|
|
6044
|
+
}
|
|
6045
|
+
};
|
|
6046
|
+
visit(root, void 0);
|
|
6047
|
+
return out;
|
|
5717
6048
|
}
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
const
|
|
5721
|
-
const
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
6049
|
+
function disambiguate(defs) {
|
|
6050
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6051
|
+
for (const def of defs) counts.set(def.qualname, (counts.get(def.qualname) ?? 0) + 1);
|
|
6052
|
+
const seen = /* @__PURE__ */ new Map();
|
|
6053
|
+
return defs.map((def) => {
|
|
6054
|
+
if ((counts.get(def.qualname) ?? 0) <= 1) return { def };
|
|
6055
|
+
const ordinal = seen.get(def.qualname) ?? 0;
|
|
6056
|
+
seen.set(def.qualname, ordinal + 1);
|
|
6057
|
+
return { def, disambiguator: ordinal };
|
|
6058
|
+
});
|
|
5726
6059
|
}
|
|
5727
|
-
function
|
|
5728
|
-
const
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
evidence: { file: importerRelPath, line, snippet: snippet2 }
|
|
6060
|
+
async function addSymbols(graph, services) {
|
|
6061
|
+
const parsers = /* @__PURE__ */ new Map();
|
|
6062
|
+
const parserForExt2 = (ext) => {
|
|
6063
|
+
const grammar = GRAMMAR_BY_EXT[ext];
|
|
6064
|
+
if (!grammar) return null;
|
|
6065
|
+
let parser = parsers.get(ext);
|
|
6066
|
+
if (!parser) {
|
|
6067
|
+
parser = new import_tree_sitter3.default();
|
|
6068
|
+
parser.setLanguage(grammar);
|
|
6069
|
+
parsers.set(ext, parser);
|
|
6070
|
+
}
|
|
6071
|
+
return parser;
|
|
5740
6072
|
};
|
|
5741
|
-
|
|
5742
|
-
return 1;
|
|
5743
|
-
}
|
|
5744
|
-
async function addImports(graph, services) {
|
|
5745
|
-
const jsParser = makeJsParser2();
|
|
5746
|
-
const pyParser = makePyParser2();
|
|
5747
|
-
const goParser = makeGoParser2();
|
|
6073
|
+
let nodesAdded = 0;
|
|
5748
6074
|
let edgesAdded = 0;
|
|
5749
6075
|
for (const service of services) {
|
|
5750
|
-
const tsPaths = await loadTsPathConfig(service.dir);
|
|
5751
6076
|
const files = await loadSourceFiles(service.dir);
|
|
5752
6077
|
for (const file of files) {
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
const
|
|
5756
|
-
|
|
5757
|
-
const isGo = import_node_path16.default.extname(file.path) === ".go";
|
|
5758
|
-
if (isGo) {
|
|
5759
|
-
let goImports = [];
|
|
5760
|
-
try {
|
|
5761
|
-
const tree = parseSource3(goParser, file.content);
|
|
5762
|
-
collectGoImports(tree.rootNode, goImports);
|
|
5763
|
-
} catch (err) {
|
|
5764
|
-
recordExtractionError("import extraction", file.path, err);
|
|
5765
|
-
continue;
|
|
5766
|
-
}
|
|
5767
|
-
const goMod = await import_node_fs13.promises.readFile(import_node_path16.default.join(service.dir, "go.mod"), "utf8").catch(() => "");
|
|
5768
|
-
const modulePath = goMod.match(/^\s*module\s+(\S+)\s*$/m)?.[1];
|
|
5769
|
-
if (!modulePath) continue;
|
|
5770
|
-
for (const imp of goImports) {
|
|
5771
|
-
const resolved = await resolveGoImport(imp.specifier, modulePath, service.dir);
|
|
5772
|
-
if (!resolved) continue;
|
|
5773
|
-
edgesAdded += emitImportEdge(graph, service.pkg.name, importerFileId, relFile, resolved, imp.line, imp.snippet);
|
|
5774
|
-
}
|
|
5775
|
-
continue;
|
|
5776
|
-
}
|
|
5777
|
-
if (isPython) {
|
|
5778
|
-
let pyImports = [];
|
|
5779
|
-
try {
|
|
5780
|
-
const tree = parseSource3(pyParser, file.content);
|
|
5781
|
-
collectPyImports(tree.rootNode, pyImports);
|
|
5782
|
-
} catch (err) {
|
|
5783
|
-
recordExtractionError("import extraction", file.path, err);
|
|
5784
|
-
continue;
|
|
5785
|
-
}
|
|
5786
|
-
for (const imp of pyImports) {
|
|
5787
|
-
const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
|
|
5788
|
-
for (const resolved of resolvedPaths) {
|
|
5789
|
-
edgesAdded += emitImportEdge(
|
|
5790
|
-
graph,
|
|
5791
|
-
service.pkg.name,
|
|
5792
|
-
importerFileId,
|
|
5793
|
-
relFile,
|
|
5794
|
-
resolved,
|
|
5795
|
-
imp.line,
|
|
5796
|
-
imp.snippet
|
|
5797
|
-
);
|
|
5798
|
-
}
|
|
5799
|
-
}
|
|
5800
|
-
continue;
|
|
5801
|
-
}
|
|
5802
|
-
let jsImports = [];
|
|
6078
|
+
const parser = parserForExt2(import_node_path16.default.extname(file.path));
|
|
6079
|
+
if (!parser) continue;
|
|
6080
|
+
const relPath = toPosix(import_node_path16.default.relative(service.dir, file.path));
|
|
6081
|
+
let defs;
|
|
5803
6082
|
try {
|
|
5804
|
-
const tree = parseSource3(
|
|
5805
|
-
|
|
6083
|
+
const tree = parseSource3(parser, file.content);
|
|
6084
|
+
defs = collectSymbolDefs(tree.rootNode);
|
|
5806
6085
|
} catch (err) {
|
|
5807
|
-
recordExtractionError("
|
|
6086
|
+
recordExtractionError("symbol extraction", file.path, err);
|
|
5808
6087
|
continue;
|
|
5809
6088
|
}
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
)
|
|
6089
|
+
if (defs.length === 0) continue;
|
|
6090
|
+
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
6091
|
+
graph,
|
|
6092
|
+
service.pkg.name,
|
|
6093
|
+
service.node.id,
|
|
6094
|
+
relPath
|
|
6095
|
+
);
|
|
6096
|
+
nodesAdded += fn;
|
|
6097
|
+
edgesAdded += fe;
|
|
6098
|
+
for (const { def, disambiguator } of disambiguate(defs)) {
|
|
6099
|
+
const sid = (0, import_types12.symbolId)(service.pkg.name, relPath, def.qualname, disambiguator);
|
|
6100
|
+
if (!graph.hasNode(sid)) {
|
|
6101
|
+
const node = {
|
|
6102
|
+
id: sid,
|
|
6103
|
+
type: import_types12.NodeType.SymbolNode,
|
|
6104
|
+
kind: def.kind,
|
|
6105
|
+
qualname: def.qualname,
|
|
6106
|
+
span: { startLine: def.startLine, endLine: def.endLine },
|
|
6107
|
+
service: service.pkg.name,
|
|
6108
|
+
relPath,
|
|
6109
|
+
discoveredVia: "static"
|
|
6110
|
+
};
|
|
6111
|
+
graph.addNode(sid, node);
|
|
6112
|
+
nodesAdded++;
|
|
6113
|
+
}
|
|
6114
|
+
const containsId = (0, import_types12.extractedEdgeId)(fileNodeId, sid, import_types12.EdgeType.CONTAINS);
|
|
6115
|
+
if (!graph.hasEdge(containsId)) {
|
|
6116
|
+
const edge = {
|
|
6117
|
+
id: containsId,
|
|
6118
|
+
source: fileNodeId,
|
|
6119
|
+
target: sid,
|
|
6120
|
+
type: import_types12.EdgeType.CONTAINS,
|
|
6121
|
+
provenance: import_types12.Provenance.EXTRACTED,
|
|
6122
|
+
confidence: (0, import_types12.confidenceForExtracted)("structural"),
|
|
6123
|
+
evidence: {
|
|
6124
|
+
file: relPath,
|
|
6125
|
+
line: def.startLine,
|
|
6126
|
+
snippet: snippet(file.content, def.startLine)
|
|
6127
|
+
}
|
|
6128
|
+
};
|
|
6129
|
+
graph.addEdgeWithKey(containsId, fileNodeId, sid, edge);
|
|
6130
|
+
edgesAdded++;
|
|
6131
|
+
}
|
|
5822
6132
|
}
|
|
5823
6133
|
}
|
|
5824
6134
|
}
|
|
5825
|
-
return { nodesAdded
|
|
6135
|
+
return { nodesAdded, edgesAdded };
|
|
5826
6136
|
}
|
|
5827
6137
|
|
|
5828
6138
|
// src/extract/symbol-edges.ts
|
|
6139
|
+
init_cjs_shims();
|
|
6140
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
6141
|
+
var import_tree_sitter4 = __toESM(require("tree-sitter"), 1);
|
|
6142
|
+
var import_types13 = require("@neat.is/types");
|
|
5829
6143
|
function extendsInfo(classHeritage) {
|
|
5830
6144
|
for (let i = 0; i < classHeritage.namedChildCount; i++) {
|
|
5831
6145
|
const child = classHeritage.namedChild(i);
|
|
@@ -5936,7 +6250,7 @@ async function addSymbolEdges(graph, services) {
|
|
|
5936
6250
|
const fileDir = import_node_path17.default.dirname(file.path);
|
|
5937
6251
|
let root;
|
|
5938
6252
|
try {
|
|
5939
|
-
root =
|
|
6253
|
+
root = parseSource3(parser, file.content).rootNode;
|
|
5940
6254
|
} catch (err) {
|
|
5941
6255
|
recordExtractionError("symbol edge extraction", file.path, err);
|
|
5942
6256
|
continue;
|
|
@@ -8388,7 +8702,7 @@ function columnsFromObject(obj) {
|
|
|
8388
8702
|
}
|
|
8389
8703
|
function drizzleEndpointsFromFile(file, serviceDir) {
|
|
8390
8704
|
if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
|
|
8391
|
-
const tree =
|
|
8705
|
+
const tree = parseSource3(parserForExt(import_node_path38.default.extname(file.path)), file.content);
|
|
8392
8706
|
const out = [];
|
|
8393
8707
|
const seen = /* @__PURE__ */ new Set();
|
|
8394
8708
|
const walk6 = (node) => {
|
|
@@ -15004,9 +15318,11 @@ var ALL_PHASES = [
|
|
|
15004
15318
|
"services",
|
|
15005
15319
|
"aliases",
|
|
15006
15320
|
"files",
|
|
15321
|
+
"symbols",
|
|
15007
15322
|
"imports",
|
|
15008
15323
|
"databases",
|
|
15009
15324
|
"configs",
|
|
15325
|
+
"routes",
|
|
15010
15326
|
"calls",
|
|
15011
15327
|
"infra"
|
|
15012
15328
|
];
|
|
@@ -15029,7 +15345,9 @@ function classifyChange(relPath) {
|
|
|
15029
15345
|
}
|
|
15030
15346
|
if (/\.(?:js|jsx|mjs|cjs|ts|tsx|py)$/.test(base)) {
|
|
15031
15347
|
phases.add("files");
|
|
15348
|
+
phases.add("symbols");
|
|
15032
15349
|
phases.add("imports");
|
|
15350
|
+
phases.add("routes");
|
|
15033
15351
|
phases.add("calls");
|
|
15034
15352
|
}
|
|
15035
15353
|
if (/\.ya?ml$/.test(base) && !/^docker-compose.*\.ya?ml$/.test(base)) {
|
|
@@ -15056,6 +15374,11 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
|
|
|
15056
15374
|
nodesAdded += r.nodesAdded;
|
|
15057
15375
|
edgesAdded += r.edgesAdded;
|
|
15058
15376
|
}
|
|
15377
|
+
if (phases.has("symbols")) {
|
|
15378
|
+
const r = await addSymbols(graph, services);
|
|
15379
|
+
nodesAdded += r.nodesAdded;
|
|
15380
|
+
edgesAdded += r.edgesAdded;
|
|
15381
|
+
}
|
|
15059
15382
|
if (phases.has("imports")) {
|
|
15060
15383
|
const r = await addImports(graph, services);
|
|
15061
15384
|
nodesAdded += r.nodesAdded;
|
|
@@ -15071,6 +15394,11 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
|
|
|
15071
15394
|
nodesAdded += r.nodesAdded;
|
|
15072
15395
|
edgesAdded += r.edgesAdded;
|
|
15073
15396
|
}
|
|
15397
|
+
if (phases.has("routes")) {
|
|
15398
|
+
const r = await addRoutes(graph, services);
|
|
15399
|
+
nodesAdded += r.nodesAdded;
|
|
15400
|
+
edgesAdded += r.edgesAdded;
|
|
15401
|
+
}
|
|
15074
15402
|
if (phases.has("calls")) {
|
|
15075
15403
|
const r = await addCallEdges(graph, services);
|
|
15076
15404
|
nodesAdded += r.nodesAdded;
|