@launchsecure/launch-kit 0.0.8 → 0.0.10

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.
@@ -6363,41 +6363,160 @@ var import_node_path8 = require("node:path");
6363
6363
  var import_node_fs3 = require("node:fs");
6364
6364
  var import_node_path3 = require("node:path");
6365
6365
 
6366
- // src/server/graph/core/ast-helpers.ts
6366
+ // src/server/graph/core/ts-extractor.ts
6367
6367
  var import_node_fs2 = require("node:fs");
6368
6368
  var import_node_path2 = require("node:path");
6369
- var tsModule;
6370
- function getTs() {
6371
- if (!tsModule) {
6372
- tsModule = require("typescript");
6369
+ var tsxLanguage;
6370
+ var parserInstance;
6371
+ var initPromise;
6372
+ var initialized = false;
6373
+ var queriesDir = (() => {
6374
+ const srcPath = (0, import_node_path2.join)((0, import_node_path2.dirname)(__filename), "..", "queries");
6375
+ if (require("fs").existsSync(srcPath)) return srcPath;
6376
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)(__filename), "graph", "queries");
6377
+ })();
6378
+ var queryCache = /* @__PURE__ */ new Map();
6379
+ async function initTreeSitter() {
6380
+ if (initialized) return;
6381
+ if (initPromise) return initPromise;
6382
+ initPromise = (async () => {
6383
+ const TreeSitter = require("web-tree-sitter");
6384
+ await TreeSitter.init();
6385
+ const wasmPath = require.resolve("tree-sitter-typescript/tree-sitter-tsx.wasm");
6386
+ tsxLanguage = await TreeSitter.Language.load(wasmPath);
6387
+ parserInstance = new TreeSitter();
6388
+ parserInstance.setLanguage(tsxLanguage);
6389
+ initialized = true;
6390
+ })();
6391
+ return initPromise;
6392
+ }
6393
+ function ensureInit() {
6394
+ if (!initialized || !tsxLanguage || !parserInstance) {
6395
+ throw new Error("Tree-sitter not initialized. Call initTreeSitter() first.");
6396
+ }
6397
+ }
6398
+ function getQuery(name) {
6399
+ ensureInit();
6400
+ const cached = queryCache.get(name);
6401
+ if (cached) return cached;
6402
+ const scmPath = (0, import_node_path2.join)(queriesDir, `${name}.scm`);
6403
+ const scm = (0, import_node_fs2.readFileSync)(scmPath, "utf-8");
6404
+ const query = tsxLanguage.query(scm);
6405
+ queryCache.set(name, query);
6406
+ return query;
6407
+ }
6408
+ function parseSource(absPath) {
6409
+ ensureInit();
6410
+ const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
6411
+ return parserInstance.parse(content);
6412
+ }
6413
+ var PRISMA_MUTATION_METHODS_BUILTIN = [
6414
+ "create",
6415
+ "createMany",
6416
+ "createManyAndReturn",
6417
+ "update",
6418
+ "updateMany",
6419
+ "updateManyAndReturn",
6420
+ "upsert",
6421
+ "delete",
6422
+ "deleteMany"
6423
+ ];
6424
+ var DB_IDENTIFIERS_FALLBACK = ["db", "prisma"];
6425
+ var extraDbIdentifiers = [];
6426
+ var extraMutationMethods = [];
6427
+ function setExtractorConfig(config) {
6428
+ extraDbIdentifiers = config.dbIdentifiers ?? [];
6429
+ extraMutationMethods = config.mutationMethods ?? [];
6430
+ }
6431
+ function getMutationMethods() {
6432
+ return /* @__PURE__ */ new Set([...PRISMA_MUTATION_METHODS_BUILTIN, ...extraMutationMethods]);
6433
+ }
6434
+ function getFallbackDbIdentifiers() {
6435
+ return /* @__PURE__ */ new Set([...DB_IDENTIFIERS_FALLBACK, ...extraDbIdentifiers]);
6436
+ }
6437
+ function looksLikeUrl(s) {
6438
+ return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
6439
+ }
6440
+ function templateStartsWithSlash(text) {
6441
+ const content = text.slice(1);
6442
+ return content.startsWith("/");
6443
+ }
6444
+ function captureMap(match) {
6445
+ const map = {};
6446
+ for (const c of match.captures) {
6447
+ map[c.name] = c.node.text;
6373
6448
  }
6374
- return tsModule;
6449
+ return map;
6375
6450
  }
6376
- var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
6377
- function parseFile(absPath) {
6378
- const ts = getTs();
6379
- const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
6380
- const ext = (0, import_node_path2.extname)(absPath);
6381
- const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
6382
- const sourceFile = ts.createSourceFile(
6383
- absPath,
6384
- content,
6385
- ts.ScriptTarget.Latest,
6386
- /* setParentNodes */
6387
- true,
6388
- scriptKind
6389
- );
6451
+ function childrenOfType(node, type) {
6452
+ return node.children.filter((n) => n.type === type);
6453
+ }
6454
+ function childOfType(node, type) {
6455
+ return node.children.find((n) => n.type === type);
6456
+ }
6457
+ function parseFileTS(absPath) {
6458
+ const tree = parseSource(absPath);
6459
+ const root = tree.rootNode;
6460
+ const imports = [];
6461
+ const importStatements = childrenOfType(root, "import_statement");
6462
+ for (const stmt of importStatements) {
6463
+ const sourceNode = childOfType(stmt, "string");
6464
+ const frag = sourceNode ? childOfType(sourceNode, "string_fragment") : void 0;
6465
+ if (!frag) continue;
6466
+ const specifier = frag.text;
6467
+ const hasTypeKeyword = stmt.children.some(
6468
+ (n) => n.type === "type" && n.text === "type"
6469
+ );
6470
+ const clause = childOfType(stmt, "import_clause");
6471
+ if (!clause) {
6472
+ imports.push({ names: [], specifier, isTypeOnly: false, typeNames: /* @__PURE__ */ new Set() });
6473
+ continue;
6474
+ }
6475
+ const names = [];
6476
+ const typeNames = /* @__PURE__ */ new Set();
6477
+ const defaultId = childOfType(clause, "identifier");
6478
+ if (defaultId) names.push(defaultId.text);
6479
+ const nsImport = childOfType(clause, "namespace_import");
6480
+ if (nsImport) {
6481
+ const nsId = childOfType(nsImport, "identifier");
6482
+ if (nsId) names.push(nsId.text);
6483
+ }
6484
+ const namedImports = childOfType(clause, "named_imports");
6485
+ if (namedImports) {
6486
+ for (const spec of childrenOfType(namedImports, "import_specifier")) {
6487
+ const identifiers = childrenOfType(spec, "identifier");
6488
+ const importedName = identifiers.length > 1 ? identifiers[identifiers.length - 1].text : identifiers[0]?.text;
6489
+ if (importedName) {
6490
+ names.push(importedName);
6491
+ const specIsType = spec.children.some(
6492
+ (n) => n.type === "type" && n.text === "type"
6493
+ );
6494
+ if (specIsType) typeNames.add(importedName);
6495
+ }
6496
+ }
6497
+ }
6498
+ if (names.length > 0 || hasTypeKeyword) {
6499
+ imports.push({ names, specifier, isTypeOnly: hasTypeKeyword, typeNames });
6500
+ }
6501
+ }
6502
+ const importQuery = getQuery("imports");
6503
+ const importMatches = importQuery.matches(root);
6504
+ for (const m of importMatches) {
6505
+ const caps = captureMap(m);
6506
+ if (caps["import.dynamic"]) {
6507
+ imports.push({
6508
+ names: [],
6509
+ specifier: caps["import.dynamic"],
6510
+ isTypeOnly: false,
6511
+ typeNames: /* @__PURE__ */ new Set()
6512
+ });
6513
+ }
6514
+ }
6390
6515
  const exportsSet = /* @__PURE__ */ new Set();
6391
6516
  const exportsOrdered = [];
6392
6517
  let defaultName = null;
6393
6518
  let firstValueExport = null;
6394
6519
  let firstTypeExport = null;
6395
- const imports = [];
6396
- const reExports = [];
6397
- const jsxElements = /* @__PURE__ */ new Set();
6398
- const navigations = [];
6399
- const fetchCalls = [];
6400
- const includeConcat = process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1";
6401
6520
  function addExport(name2, kind) {
6402
6521
  if (!exportsSet.has(name2)) {
6403
6522
  exportsSet.add(name2);
@@ -6407,300 +6526,339 @@ function parseFile(absPath) {
6407
6526
  else if (kind === "value" && !firstValueExport) firstValueExport = name2;
6408
6527
  else if (kind === "type" && !firstTypeExport) firstTypeExport = name2;
6409
6528
  }
6410
- function hasModifier(node, kind) {
6411
- const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
6412
- return modifiers?.some((m) => m.kind === kind) ?? false;
6413
- }
6414
- function extractTargetFromExpression(expr) {
6415
- if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
6416
- return { target: expr.text, isTemplate: false };
6417
- }
6418
- if (ts.isTemplateExpression(expr)) {
6419
- return { target: expr.getText(sourceFile), isTemplate: true };
6529
+ const exportQuery = getQuery("exports");
6530
+ const exportMatches = exportQuery.matches(root);
6531
+ for (const m of exportMatches) {
6532
+ const caps = captureMap(m);
6533
+ if (caps["export.default.func"]) addExport(caps["export.default.func"], "default");
6534
+ else if (caps["export.default.class"]) addExport(caps["export.default.class"], "default");
6535
+ else if (caps["export.default.value"]) addExport(caps["export.default.value"], "default");
6536
+ else if (caps["export.named.func"]) addExport(caps["export.named.func"], "value");
6537
+ else if (caps["export.named.class"]) addExport(caps["export.named.class"], "value");
6538
+ else if (caps["export.named.const"]) addExport(caps["export.named.const"], "value");
6539
+ else if (caps["export.named.enum"]) addExport(caps["export.named.enum"], "value");
6540
+ else if (caps["export.named.type"]) addExport(caps["export.named.type"], "type");
6541
+ else if (caps["export.named.interface"]) addExport(caps["export.named.interface"], "type");
6542
+ else if (caps["export.bare.name"]) addExport(caps["export.bare.name"], "value");
6543
+ if (caps["reexport.name"]) addExport(caps["reexport.name"], "value");
6544
+ }
6545
+ for (const stmt of childrenOfType(root, "export_statement")) {
6546
+ const hasDefault = stmt.children.some((n) => n.text === "default");
6547
+ if (hasDefault && !defaultName) {
6548
+ defaultName = "default";
6420
6549
  }
6421
- return null;
6422
- }
6423
- function looksLikeUrl(s) {
6424
- return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
6425
- }
6426
- function templateStartsWithSlash(expr) {
6427
- const head = expr.head.text;
6428
- return head.startsWith("/");
6429
6550
  }
6430
- function extractUrlFromFetchArg(arg) {
6431
- if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
6432
- if (!looksLikeUrl(arg.text)) return null;
6433
- return { url: arg.text, isTemplate: false };
6551
+ const reExports = [];
6552
+ for (const m of exportMatches) {
6553
+ const caps = captureMap(m);
6554
+ if (caps["reexport.name"] && caps["reexport.source"]) {
6555
+ reExports.push({ name: caps["reexport.name"], from: caps["reexport.source"] });
6434
6556
  }
6435
- if (ts.isTemplateExpression(arg)) {
6436
- if (!templateStartsWithSlash(arg)) return null;
6437
- return { url: arg.getText(sourceFile), isTemplate: true };
6557
+ if (caps["reexport.wildcard.source"]) {
6558
+ reExports.push({ name: "*", from: caps["reexport.wildcard.source"], isWildcard: true });
6438
6559
  }
6439
- if (includeConcat && ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.PlusToken) {
6440
- let leftmost = arg;
6441
- while (ts.isBinaryExpression(leftmost) && leftmost.operatorToken.kind === ts.SyntaxKind.PlusToken) {
6442
- leftmost = leftmost.left;
6560
+ }
6561
+ const jsxElements = /* @__PURE__ */ new Set();
6562
+ const jsxQuery = getQuery("jsx-elements");
6563
+ const jsxCaptures = jsxQuery.captures(root);
6564
+ for (const c of jsxCaptures) {
6565
+ if (c.name === "jsx.tag") {
6566
+ if (/^[A-Z]/.test(c.node.text)) {
6567
+ jsxElements.add(c.node.text);
6443
6568
  }
6444
- if ((ts.isStringLiteral(leftmost) || ts.isNoSubstitutionTemplateLiteral(leftmost)) && leftmost.text.startsWith("/")) {
6445
- return { url: arg.getText(sourceFile), isTemplate: false, isConcat: true };
6569
+ } else if (c.name === "jsx.member_tag") {
6570
+ const rootName = c.node.text.split(".")[0];
6571
+ if (rootName && /^[A-Z]/.test(rootName)) {
6572
+ jsxElements.add(rootName);
6446
6573
  }
6447
6574
  }
6448
- return null;
6449
6575
  }
6450
- function visit(node) {
6451
- if (ts.isImportDeclaration(node)) {
6452
- const moduleSpec = node.moduleSpecifier;
6453
- if (ts.isStringLiteral(moduleSpec)) {
6454
- const specifier = moduleSpec.text;
6455
- const clause = node.importClause;
6456
- const isTypeOnly = !!clause?.isTypeOnly;
6457
- const names = [];
6458
- const typeNames = /* @__PURE__ */ new Set();
6459
- if (clause) {
6460
- if (clause.name) names.push(clause.name.text);
6461
- const nb = clause.namedBindings;
6462
- if (nb && ts.isNamedImports(nb)) {
6463
- for (const el of nb.elements) {
6464
- names.push(el.name.text);
6465
- if (el.isTypeOnly) typeNames.add(el.name.text);
6466
- }
6467
- } else if (nb && ts.isNamespaceImport(nb)) {
6468
- names.push(nb.name.text);
6469
- }
6470
- }
6471
- if (names.length > 0 || isTypeOnly) {
6472
- imports.push({ names, specifier, isTypeOnly, typeNames });
6473
- } else if (!clause) {
6474
- imports.push({ names: [], specifier, isTypeOnly: false, typeNames: /* @__PURE__ */ new Set() });
6475
- }
6476
- }
6576
+ const navigations = [];
6577
+ const navQuery = getQuery("navigations");
6578
+ const navMatches = navQuery.matches(root);
6579
+ for (const m of navMatches) {
6580
+ const caps = captureMap(m);
6581
+ if (caps["nav.target.literal"] && caps["nav.method"]) {
6582
+ navigations.push({
6583
+ kind: caps["nav.method"] === "push" ? "router-push" : "router-replace",
6584
+ target: caps["nav.target.literal"],
6585
+ isTemplate: false
6586
+ });
6477
6587
  }
6478
- if (ts.isExportDeclaration(node)) {
6479
- const fromSpec = node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : null;
6480
- if (node.exportClause && ts.isNamedExports(node.exportClause)) {
6481
- for (const el of node.exportClause.elements) {
6482
- const exportedName = el.name.text;
6483
- addExport(exportedName, "value");
6484
- if (fromSpec) {
6485
- reExports.push({ name: exportedName, from: fromSpec });
6486
- }
6487
- }
6488
- } else if (!node.exportClause && fromSpec) {
6489
- reExports.push({ name: "*", from: fromSpec, isWildcard: true });
6490
- }
6588
+ if (caps["nav.target.template"] && caps["nav.method.template"]) {
6589
+ navigations.push({
6590
+ kind: caps["nav.method.template"] === "push" ? "router-push" : "router-replace",
6591
+ target: caps["nav.target.template"],
6592
+ isTemplate: true
6593
+ });
6491
6594
  }
6492
- if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
6493
- const arg = node.arguments[0];
6494
- if (arg && ts.isStringLiteral(arg)) {
6495
- imports.push({
6496
- names: [],
6497
- specifier: arg.text,
6498
- isTypeOnly: false,
6499
- typeNames: /* @__PURE__ */ new Set()
6500
- });
6501
- }
6595
+ const linkLiteral = caps["nav.link.literal"] || caps["nav.link.literal.self"];
6596
+ if (linkLiteral) {
6597
+ navigations.push({ kind: "link-href", target: linkLiteral, isTemplate: false });
6502
6598
  }
6503
- if (ts.isExportAssignment(node) && !node.isExportEquals) {
6504
- if (ts.isIdentifier(node.expression)) {
6505
- addExport(node.expression.text, "default");
6506
- } else {
6507
- if (!defaultName) defaultName = "default";
6508
- }
6599
+ const linkTemplate = caps["nav.link.template"] || caps["nav.link.template.self"];
6600
+ if (linkTemplate) {
6601
+ navigations.push({ kind: "link-href", target: linkTemplate, isTemplate: true });
6602
+ }
6603
+ if (caps["nav.window.literal"]) {
6604
+ navigations.push({ kind: "window-location", target: caps["nav.window.literal"], isTemplate: false });
6509
6605
  }
6510
- if (ts.isFunctionDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
6511
- const isDefault = hasModifier(node, ts.SyntaxKind.DefaultKeyword);
6512
- if (node.name) addExport(node.name.text, isDefault ? "default" : "value");
6606
+ if (caps["nav.window.assign.target"]) {
6607
+ navigations.push({ kind: "window-location", target: caps["nav.window.assign.target"], isTemplate: false });
6513
6608
  }
6514
- if (ts.isVariableStatement(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
6515
- for (const decl of node.declarationList.declarations) {
6516
- if (ts.isIdentifier(decl.name)) {
6517
- addExport(decl.name.text, "value");
6609
+ }
6610
+ const fetchCalls = [];
6611
+ const fetchQuery = getQuery("fetch-calls");
6612
+ const fetchMatches = fetchQuery.matches(root);
6613
+ for (const m of fetchMatches) {
6614
+ const caps = captureMap(m);
6615
+ if (caps["fetch.url.literal"] && looksLikeUrl(caps["fetch.url.literal"])) {
6616
+ fetchCalls.push({ url: caps["fetch.url.literal"], isTemplate: false, kind: "fetch" });
6617
+ }
6618
+ if (caps["fetch.url.template"] && templateStartsWithSlash(caps["fetch.url.template"])) {
6619
+ fetchCalls.push({ url: caps["fetch.url.template"], isTemplate: true, kind: "fetch" });
6620
+ }
6621
+ const clientUrl = caps["fetch.client.url.literal"] || caps["fetch.await.url.literal"];
6622
+ const clientMethod = caps["fetch.method"] || caps["fetch.await.method"];
6623
+ if (clientUrl && clientMethod && looksLikeUrl(clientUrl)) {
6624
+ fetchCalls.push({
6625
+ method: clientMethod.toUpperCase(),
6626
+ url: clientUrl,
6627
+ isTemplate: false,
6628
+ kind: "client-method"
6629
+ });
6630
+ }
6631
+ const clientUrlTpl = caps["fetch.client.url.template"] || caps["fetch.await.url.template"];
6632
+ const clientMethodTpl = caps["fetch.method.template"] || caps["fetch.await.method.template"];
6633
+ if (clientUrlTpl && clientMethodTpl && templateStartsWithSlash(clientUrlTpl)) {
6634
+ fetchCalls.push({
6635
+ method: clientMethodTpl.toUpperCase(),
6636
+ url: clientUrlTpl,
6637
+ isTemplate: true,
6638
+ kind: "client-method"
6639
+ });
6640
+ }
6641
+ }
6642
+ const name = defaultName ?? firstValueExport ?? firstTypeExport ?? "";
6643
+ return { name, exports: exportsOrdered, imports, reExports, jsxElements, navigations, fetchCalls };
6644
+ }
6645
+ function extractDbCallsTS(absPath) {
6646
+ const tree = parseSource(absPath);
6647
+ const root = tree.rootNode;
6648
+ const dbQuery = getQuery("db-calls");
6649
+ const matches = dbQuery.matches(root);
6650
+ const dbIdentifiers = /* @__PURE__ */ new Set();
6651
+ for (const stmt of childrenOfType(root, "import_statement")) {
6652
+ const sourceNode = childOfType(stmt, "string");
6653
+ const frag = sourceNode ? childOfType(sourceNode, "string_fragment") : void 0;
6654
+ if (!frag) continue;
6655
+ const spec = frag.text;
6656
+ if (spec.includes("prisma") || spec.includes("/db") || spec === "@prisma/client") {
6657
+ const clause = childOfType(stmt, "import_clause");
6658
+ if (clause) {
6659
+ const defaultId = childOfType(clause, "identifier");
6660
+ if (defaultId) dbIdentifiers.add(defaultId.text);
6661
+ const named = childOfType(clause, "named_imports");
6662
+ if (named) {
6663
+ for (const specNode of childrenOfType(named, "import_specifier")) {
6664
+ const ids = childrenOfType(specNode, "identifier");
6665
+ const importedName = ids[ids.length - 1];
6666
+ if (importedName) dbIdentifiers.add(importedName.text);
6667
+ }
6518
6668
  }
6519
6669
  }
6520
6670
  }
6521
- if (ts.isClassDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
6522
- const isDefault = hasModifier(node, ts.SyntaxKind.DefaultKeyword);
6523
- if (node.name) addExport(node.name.text, isDefault ? "default" : "value");
6524
- }
6525
- if (ts.isTypeAliasDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
6526
- addExport(node.name.text, "type");
6671
+ }
6672
+ if (dbIdentifiers.size === 0) {
6673
+ for (const id of getFallbackDbIdentifiers()) dbIdentifiers.add(id);
6674
+ } else {
6675
+ for (const id of extraDbIdentifiers) dbIdentifiers.add(id);
6676
+ }
6677
+ const calls = [];
6678
+ const seen = /* @__PURE__ */ new Set();
6679
+ for (const m of matches) {
6680
+ const caps = captureMap(m);
6681
+ const identifier = caps["db.identifier"];
6682
+ const model = caps["db.model"];
6683
+ const method = caps["db.method"];
6684
+ if (!identifier || !model || !method) continue;
6685
+ if (!dbIdentifiers.has(identifier)) continue;
6686
+ const key = `${model}.${method}`;
6687
+ if (seen.has(key)) continue;
6688
+ seen.add(key);
6689
+ calls.push({ model, method, isMutation: getMutationMethods().has(method) });
6690
+ }
6691
+ return calls;
6692
+ }
6693
+ function extractAuthWrappersTS(absPath) {
6694
+ const tree = parseSource(absPath);
6695
+ const root = tree.rootNode;
6696
+ const wrapperQuery = getQuery("wrappers");
6697
+ const matches = wrapperQuery.matches(root);
6698
+ const wrappers = /* @__PURE__ */ new Set();
6699
+ for (const m of matches) {
6700
+ const caps = captureMap(m);
6701
+ if (caps["wrapper.fn_name"]) {
6702
+ wrappers.add(caps["wrapper.fn_name"]);
6527
6703
  }
6528
- if (ts.isInterfaceDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
6529
- addExport(node.name.text, "type");
6704
+ }
6705
+ return wrappers;
6706
+ }
6707
+ function trunc(s, max = 120) {
6708
+ return s.length > max ? s.slice(0, max) + "..." : s;
6709
+ }
6710
+ function extractDeep(absPath) {
6711
+ const tree = parseSource(absPath);
6712
+ const root = tree.rootNode;
6713
+ const elements = [];
6714
+ const elQuery = getQuery("deep/jsx-semantic");
6715
+ const elMatches = elQuery.matches(root);
6716
+ const elementMap = /* @__PURE__ */ new Map();
6717
+ for (const m of elMatches) {
6718
+ const tag = m.captures.find((c) => c.name === "el.tag")?.node;
6719
+ if (!tag || !/^[A-Z]/.test(tag.text)) continue;
6720
+ const elNode = m.captures.find((c) => c.name === "el.self" || c.name === "el.open")?.node;
6721
+ const key = elNode ? `${elNode.startPosition.row}:${elNode.startPosition.column}` : `${tag.startPosition.row}:${tag.startPosition.column}`;
6722
+ if (!elementMap.has(key)) {
6723
+ elementMap.set(key, { tag: tag.text, props: {}, nodeKey: key });
6724
+ }
6725
+ const entry = elementMap.get(key);
6726
+ const propName = m.captures.find((c) => c.name === "el.prop.name")?.node.text;
6727
+ const propVal = m.captures.find((c) => c.name === "el.prop.value.str")?.node.text;
6728
+ const propExpr = m.captures.find((c) => c.name === "el.prop.value.expr")?.node.text;
6729
+ if (propName) {
6730
+ entry.props[propName] = propVal ?? (propExpr ? trunc(propExpr, 60) : "true");
6731
+ }
6732
+ }
6733
+ for (const m of elMatches) {
6734
+ const textTag = m.captures.find((c) => c.name === "el.text.tag")?.node;
6735
+ const textContent = m.captures.find((c) => c.name === "el.text.content")?.node;
6736
+ if (textTag && textContent && /^[A-Z]/.test(textTag.text)) {
6737
+ const key = `${textTag.startPosition.row}:${textTag.startPosition.column}`;
6738
+ if (!elementMap.has(key)) {
6739
+ elementMap.set(key, { tag: textTag.text, props: {}, nodeKey: key });
6740
+ }
6741
+ const entry = elementMap.get(key);
6742
+ const text = textContent.text.trim();
6743
+ if (text) entry.props["_text"] = text;
6744
+ }
6745
+ }
6746
+ for (const entry of elementMap.values()) {
6747
+ const el = { tag: entry.tag, props: entry.props };
6748
+ const hasExpr = Object.values(entry.props).some((v) => v.includes("{") || v.includes("("));
6749
+ if (hasExpr) el.dynamic = true;
6750
+ if (entry.props["_text"]) {
6751
+ el.text = entry.props["_text"];
6752
+ delete el.props["_text"];
6753
+ }
6754
+ elements.push(el);
6755
+ }
6756
+ const stateVars = [];
6757
+ const hookQuery = getQuery("deep/state-hooks");
6758
+ const hookMatches = hookQuery.matches(root);
6759
+ for (const m of hookMatches) {
6760
+ const caps = captureMap(m);
6761
+ if (caps["hook.var"] && caps["hook.setter"] && caps["hook.fn"]) {
6762
+ stateVars.push({
6763
+ name: caps["hook.var"],
6764
+ setter: caps["hook.setter"],
6765
+ hook: caps["hook.fn"],
6766
+ init: trunc(caps["hook.init"] ?? "", 80)
6767
+ });
6530
6768
  }
6531
- if (ts.isEnumDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
6532
- addExport(node.name.text, "value");
6769
+ if (caps["reducer.var"] && caps["reducer.dispatch"]) {
6770
+ stateVars.push({
6771
+ name: caps["reducer.var"],
6772
+ setter: caps["reducer.dispatch"],
6773
+ hook: "useReducer",
6774
+ init: trunc(caps["reducer.init"] ?? "", 80)
6775
+ });
6533
6776
  }
6534
- if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
6535
- const tagName = node.tagName;
6536
- if (ts.isIdentifier(tagName) && /^[A-Z]/.test(tagName.text)) {
6537
- jsxElements.add(tagName.text);
6538
- } else if (ts.isPropertyAccessExpression(tagName)) {
6539
- let root = tagName;
6540
- while (ts.isPropertyAccessExpression(root)) root = root.expression;
6541
- if (ts.isIdentifier(root) && /^[A-Z]/.test(root.text)) {
6542
- jsxElements.add(root.text);
6543
- }
6544
- }
6777
+ }
6778
+ const conditions = [];
6779
+ const condQuery = getQuery("deep/conditions");
6780
+ const condMatches = condQuery.matches(root);
6781
+ for (const m of condMatches) {
6782
+ const caps = captureMap(m);
6783
+ if (caps["cond.test"]) {
6784
+ conditions.push({
6785
+ kind: "if",
6786
+ test: trunc(caps["cond.test"], 100),
6787
+ consequence: caps["cond.consequence"] ? trunc(caps["cond.consequence"], 80) : void 0
6788
+ });
6545
6789
  }
6546
- if (ts.isCallExpression(node)) {
6547
- const expr = node.expression;
6548
- if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && expr.expression.text === "router" && (expr.name.text === "push" || expr.name.text === "replace")) {
6549
- const arg = node.arguments[0];
6550
- if (arg) {
6551
- const parsed = extractTargetFromExpression(arg);
6552
- if (parsed) {
6553
- navigations.push({
6554
- kind: expr.name.text === "push" ? "router-push" : "router-replace",
6555
- target: parsed.target,
6556
- isTemplate: parsed.isTemplate
6557
- });
6558
- }
6559
- }
6560
- }
6790
+ if (caps["ternary.test"]) {
6791
+ conditions.push({
6792
+ kind: "ternary",
6793
+ test: trunc(caps["ternary.test"], 100)
6794
+ });
6561
6795
  }
6562
- if (ts.isCallExpression(node) && node.arguments.length > 0) {
6563
- const expr = node.expression;
6564
- const firstArg = node.arguments[0];
6565
- if (ts.isIdentifier(expr) && expr.text === "fetch") {
6566
- const extracted = extractUrlFromFetchArg(firstArg);
6567
- if (extracted) {
6568
- fetchCalls.push({
6569
- url: extracted.url,
6570
- isTemplate: extracted.isTemplate,
6571
- ...extracted.isConcat ? { isConcat: true } : {},
6572
- kind: "fetch"
6573
- });
6574
- }
6575
- }
6576
- if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) {
6577
- const methodName = expr.name.text;
6578
- if (HTTP_METHODS.has(methodName)) {
6579
- const extracted = extractUrlFromFetchArg(firstArg);
6580
- if (extracted) {
6581
- fetchCalls.push({
6582
- method: methodName.toUpperCase(),
6583
- url: extracted.url,
6584
- isTemplate: extracted.isTemplate,
6585
- ...extracted.isConcat ? { isConcat: true } : {},
6586
- kind: "client-method"
6587
- });
6588
- }
6589
- }
6590
- }
6796
+ }
6797
+ const variables = [];
6798
+ const varQuery = getQuery("deep/variables");
6799
+ const varMatches = varQuery.matches(root);
6800
+ for (const m of varMatches) {
6801
+ const caps = captureMap(m);
6802
+ const declNode = m.captures.find((c) => c.name === "var.decl" || c.name === "var.destructured" || c.name === "var.array")?.node;
6803
+ const kind = declNode?.children.find((n) => n.type === "const" || n.type === "let" || n.type === "var")?.type ?? "const";
6804
+ if (caps["var.name"] && caps["var.init"]) {
6805
+ variables.push({
6806
+ name: caps["var.name"],
6807
+ kind,
6808
+ init: trunc(caps["var.init"], 100)
6809
+ });
6591
6810
  }
6592
- if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
6593
- const tagName = node.tagName;
6594
- if (ts.isIdentifier(tagName) && tagName.text === "Link") {
6595
- for (const attr of node.attributes.properties) {
6596
- if (ts.isJsxAttribute(attr) && attr.name.getText(sourceFile) === "href" && attr.initializer) {
6597
- const init = attr.initializer;
6598
- if (ts.isStringLiteral(init)) {
6599
- navigations.push({ kind: "link-href", target: init.text, isTemplate: false });
6600
- } else if (ts.isJsxExpression(init) && init.expression) {
6601
- const parsed = extractTargetFromExpression(init.expression);
6602
- if (parsed) {
6603
- navigations.push({ kind: "link-href", target: parsed.target, isTemplate: parsed.isTemplate });
6604
- }
6605
- }
6606
- }
6607
- }
6608
- }
6811
+ if (caps["var.destructured.obj"]) {
6812
+ variables.push({
6813
+ name: trunc(caps["var.destructured.obj"], 60),
6814
+ kind,
6815
+ init: trunc(caps["var.destructured.init"], 100)
6816
+ });
6609
6817
  }
6610
- if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
6611
- const left = node.left;
6612
- if (ts.isPropertyAccessExpression(left) && ts.isPropertyAccessExpression(left.expression) && ts.isIdentifier(left.expression.expression) && left.expression.expression.text === "window" && left.expression.name.text === "location" && left.name.text === "href") {
6613
- const parsed = extractTargetFromExpression(node.right);
6614
- if (parsed) {
6615
- navigations.push({ kind: "window-location", target: parsed.target, isTemplate: parsed.isTemplate });
6616
- }
6617
- }
6818
+ if (caps["var.array.pattern"]) {
6819
+ variables.push({
6820
+ name: trunc(caps["var.array.pattern"], 60),
6821
+ kind,
6822
+ init: trunc(caps["var.array.init"], 100)
6823
+ });
6618
6824
  }
6619
- if (ts.isCallExpression(node)) {
6620
- const expr = node.expression;
6621
- if (ts.isPropertyAccessExpression(expr) && ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.expression) && expr.expression.expression.text === "window" && expr.expression.name.text === "location" && (expr.name.text === "assign" || expr.name.text === "replace")) {
6622
- const arg = node.arguments[0];
6623
- if (arg) {
6624
- const parsed = extractTargetFromExpression(arg);
6625
- if (parsed) {
6626
- navigations.push({ kind: "window-location", target: parsed.target, isTemplate: parsed.isTemplate });
6627
- }
6628
- }
6629
- }
6825
+ }
6826
+ const responses = [];
6827
+ const respQuery = getQuery("deep/responses");
6828
+ const respMatches = respQuery.matches(root);
6829
+ const explicitBodies = /* @__PURE__ */ new Set();
6830
+ for (const m of respMatches) {
6831
+ const caps = captureMap(m);
6832
+ if (caps["resp.status"] && caps["resp.body"]) {
6833
+ explicitBodies.add(trunc(caps["resp.body"], 80));
6834
+ responses.push({
6835
+ status: caps["resp.status"],
6836
+ body: trunc(caps["resp.body"], 80)
6837
+ });
6630
6838
  }
6631
- ts.forEachChild(node, visit);
6632
6839
  }
6633
- visit(sourceFile);
6634
- const name = defaultName ?? firstValueExport ?? firstTypeExport ?? "";
6635
- return {
6636
- name,
6637
- exports: exportsOrdered,
6638
- imports,
6639
- reExports,
6640
- jsxElements,
6641
- navigations,
6642
- fetchCalls
6643
- };
6644
- }
6645
- var MUTATION_METHODS = /* @__PURE__ */ new Set([
6646
- "create",
6647
- "createMany",
6648
- "createManyAndReturn",
6649
- "update",
6650
- "updateMany",
6651
- "updateManyAndReturn",
6652
- "upsert",
6653
- "delete",
6654
- "deleteMany"
6655
- ]);
6656
- function extractDbCalls(absPath) {
6657
- const ts = getTs();
6658
- const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
6659
- const ext = (0, import_node_path2.extname)(absPath);
6660
- const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
6661
- const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
6662
- const calls = [];
6663
- const seen = /* @__PURE__ */ new Set();
6664
- function visit(node) {
6665
- if (ts.isCallExpression(node)) {
6666
- const expr = node.expression;
6667
- if (ts.isPropertyAccessExpression(expr) && ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.expression) && expr.expression.expression.text === "db") {
6668
- const model = expr.expression.name.text;
6669
- const method = expr.name.text;
6670
- const key = `${model}.${method}`;
6671
- if (!seen.has(key)) {
6672
- seen.add(key);
6673
- calls.push({
6674
- model,
6675
- method,
6676
- isMutation: MUTATION_METHODS.has(method)
6677
- });
6678
- }
6840
+ for (const m of respMatches) {
6841
+ const caps = captureMap(m);
6842
+ if (caps["resp.body.default"] && !caps["resp.status"]) {
6843
+ const bodyText = trunc(caps["resp.body.default"], 80);
6844
+ if (!explicitBodies.has(bodyText)) {
6845
+ responses.push({ status: "200", body: bodyText });
6679
6846
  }
6680
6847
  }
6681
- ts.forEachChild(node, visit);
6682
6848
  }
6683
- visit(sourceFile);
6684
- return calls;
6685
- }
6686
- function extractAuthWrappers(absPath) {
6687
- const ts = getTs();
6688
- const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
6689
- const ext = (0, import_node_path2.extname)(absPath);
6690
- const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
6691
- const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
6692
- const wrappers = /* @__PURE__ */ new Set();
6693
- const AUTH_WRAPPERS = /* @__PURE__ */ new Set(["withAuth", "withPermission", "withRole", "requireAuth"]);
6694
- function visit(node) {
6695
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
6696
- if (AUTH_WRAPPERS.has(node.expression.text)) {
6697
- wrappers.add(node.expression.text);
6698
- }
6849
+ const params = [];
6850
+ const paramQuery = getQuery("deep/request-params");
6851
+ const paramMatches = paramQuery.matches(root);
6852
+ for (const m of paramMatches) {
6853
+ const caps = captureMap(m);
6854
+ if (caps["param.name"]) {
6855
+ params.push({ name: caps["param.name"], source: "body-field" });
6856
+ }
6857
+ if (caps["param.body"]) {
6858
+ params.push({ name: caps["param.body"], source: "body" });
6699
6859
  }
6700
- ts.forEachChild(node, visit);
6701
6860
  }
6702
- visit(sourceFile);
6703
- return wrappers;
6861
+ return { elements, stateVars, conditions, variables, responses, params };
6704
6862
  }
6705
6863
 
6706
6864
  // src/server/graph/parsers/ui/react-nextjs.ts
@@ -7026,7 +7184,7 @@ function generate(rootDir) {
7026
7184
  const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
7027
7185
  const parsedByPath = /* @__PURE__ */ new Map();
7028
7186
  for (const absPath of allDiscovered) {
7029
- parsedByPath.set(absPath, parseFile(absPath));
7187
+ parsedByPath.set(absPath, parseFileTS(absPath));
7030
7188
  }
7031
7189
  const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
7032
7190
  const fileSet = allDiscovered.filter((f) => !(0, import_node_path3.basename)(f).startsWith("index."));
@@ -7040,7 +7198,18 @@ function generate(rootDir) {
7040
7198
  const parsed = parsedByPath.get(absPath);
7041
7199
  const name = parsed.name || nameFromFilename(absPath);
7042
7200
  const route = extractRoute(id);
7043
- nodes.push({ id, type, name, route, exports: parsed.exports });
7201
+ const deep = extractDeep(absPath);
7202
+ nodes.push({
7203
+ id,
7204
+ type,
7205
+ name,
7206
+ route,
7207
+ exports: parsed.exports,
7208
+ elements: deep.elements,
7209
+ stateVars: deep.stateVars,
7210
+ conditions: deep.conditions,
7211
+ variables: deep.variables
7212
+ });
7044
7213
  nodeIdSet.add(id);
7045
7214
  nodeTypeMap.set(id, type);
7046
7215
  if (route) routeToNodeId.set(route, id);
@@ -7099,7 +7268,7 @@ function generate(rootDir) {
7099
7268
  if (externalScanned.has(normalized)) continue;
7100
7269
  let parsed;
7101
7270
  try {
7102
- parsed = parseFile(absPath);
7271
+ parsed = parseFileTS(absPath);
7103
7272
  } catch {
7104
7273
  continue;
7105
7274
  }
@@ -7223,7 +7392,7 @@ var reactNextjsParser = {
7223
7392
  // src/server/graph/parsers/api/nextjs-routes.ts
7224
7393
  var import_node_fs4 = require("node:fs");
7225
7394
  var import_node_path4 = require("node:path");
7226
- var HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
7395
+ var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
7227
7396
  function walk2(dir) {
7228
7397
  const results = [];
7229
7398
  if (!(0, import_node_fs4.existsSync)(dir)) return results;
@@ -7262,12 +7431,12 @@ function generate2(rootDir) {
7262
7431
  let endpointsWithAuth = 0;
7263
7432
  let endpointsWithDbAccess = 0;
7264
7433
  for (const absPath of routeFiles) {
7265
- const parsed = parseFile(absPath);
7266
- const dbCalls = extractDbCalls(absPath);
7267
- const authWrappers = extractAuthWrappers(absPath);
7434
+ const parsed = parseFileTS(absPath);
7435
+ const dbCalls = extractDbCallsTS(absPath);
7436
+ const authWrappers = extractAuthWrappersTS(absPath);
7268
7437
  const methods = [];
7269
7438
  for (const exp of parsed.exports) {
7270
- if (HTTP_METHODS2.has(exp)) methods.push(exp);
7439
+ if (HTTP_METHODS.has(exp)) methods.push(exp);
7271
7440
  }
7272
7441
  const routePath = filePathToRoute(apiDir, absPath);
7273
7442
  const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
@@ -7286,6 +7455,7 @@ function generate2(rootDir) {
7286
7455
  authUsage[w] = (authUsage[w] ?? 0) + 1;
7287
7456
  }
7288
7457
  if (authStrategy.length > 0) endpointsWithAuth++;
7458
+ const deep = extractDeep(absPath);
7289
7459
  nodes.push({
7290
7460
  id: relPath,
7291
7461
  type: "endpoint",
@@ -7297,7 +7467,12 @@ function generate2(rootDir) {
7297
7467
  mutates,
7298
7468
  auth: authStrategy.length > 0 ? authStrategy : ["public"],
7299
7469
  db_models: [...new Set(dbCalls.map((c) => c.model))],
7300
- db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))]
7470
+ db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
7471
+ // Deep extraction fields
7472
+ conditions: deep.conditions,
7473
+ variables: deep.variables,
7474
+ responses: deep.responses,
7475
+ params: deep.params
7301
7476
  });
7302
7477
  const seenModels = /* @__PURE__ */ new Set();
7303
7478
  for (const call of dbCalls) {
@@ -7337,7 +7512,7 @@ function generate2(rootDir) {
7337
7512
  flagged_edges: [],
7338
7513
  patterns: {
7339
7514
  total_endpoints: nodes.length,
7340
- methods_breakdown: [...HTTP_METHODS2].reduce((acc, m) => {
7515
+ methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
7341
7516
  acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
7342
7517
  return acc;
7343
7518
  }, {}),
@@ -8192,9 +8367,10 @@ function matchParts(pat, pi, id, ii) {
8192
8367
  while (pi < pat.length && pat[pi] === "**") pi++;
8193
8368
  return pi === pat.length && ii === id.length;
8194
8369
  }
8195
- var CONVENTION_DIRS = ["features", "modules", "domains", "areas"];
8196
- function detectConventionDirs(rootDir) {
8370
+ var CONVENTION_DIRS_BUILTIN = ["features", "modules", "domains", "areas"];
8371
+ function detectConventionDirs(rootDir, extraConventionDirs = []) {
8197
8372
  const result = /* @__PURE__ */ new Map();
8373
+ const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
8198
8374
  const searchDirs = [
8199
8375
  rootDir,
8200
8376
  (0, import_node_path10.join)(rootDir, "src"),
@@ -8202,7 +8378,7 @@ function detectConventionDirs(rootDir) {
8202
8378
  (0, import_node_path10.join)(rootDir, "lib")
8203
8379
  ];
8204
8380
  for (const base of searchDirs) {
8205
- for (const convention of CONVENTION_DIRS) {
8381
+ for (const convention of conventionDirs) {
8206
8382
  const dir = (0, import_node_path10.join)(base, convention);
8207
8383
  if (!(0, import_node_fs9.existsSync)(dir)) continue;
8208
8384
  try {
@@ -8228,7 +8404,76 @@ function extractRouteGroups(id) {
8228
8404
  }
8229
8405
  return groups;
8230
8406
  }
8231
- var SKIP_SEGMENTS = /* @__PURE__ */ new Set([
8407
+ var GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
8408
+ // JS/TS
8409
+ "components",
8410
+ "hooks",
8411
+ "pages",
8412
+ "views",
8413
+ "screens",
8414
+ "layouts",
8415
+ "utils",
8416
+ "helpers",
8417
+ "lib",
8418
+ "libs",
8419
+ "services",
8420
+ "api",
8421
+ "apis",
8422
+ "stores",
8423
+ "state",
8424
+ "store",
8425
+ "context",
8426
+ "contexts",
8427
+ "providers",
8428
+ "types",
8429
+ "interfaces",
8430
+ "models",
8431
+ "schemas",
8432
+ "constants",
8433
+ "config",
8434
+ "configs",
8435
+ "assets",
8436
+ "styles",
8437
+ "public",
8438
+ "middleware",
8439
+ "middlewares",
8440
+ "routes",
8441
+ "router",
8442
+ "tests",
8443
+ "test",
8444
+ "__tests__",
8445
+ "spec",
8446
+ "specs",
8447
+ // Go
8448
+ "cmd",
8449
+ "pkg",
8450
+ "internal",
8451
+ // Python
8452
+ "management",
8453
+ "migrations",
8454
+ "templatetags",
8455
+ "templates",
8456
+ // Java
8457
+ "controller",
8458
+ "controllers",
8459
+ "service",
8460
+ "repository",
8461
+ "repositories",
8462
+ "entity",
8463
+ "entities",
8464
+ "dto",
8465
+ "dtos",
8466
+ // General
8467
+ "shared",
8468
+ "common",
8469
+ "core",
8470
+ "base",
8471
+ "app",
8472
+ // Next.js specific
8473
+ "client",
8474
+ "server"
8475
+ ]);
8476
+ var SKIP_SEGMENTS_BUILTIN = /* @__PURE__ */ new Set([
8232
8477
  "src",
8233
8478
  "app",
8234
8479
  "client",
@@ -8286,9 +8531,13 @@ function isTrivialGroup(name, extraTrivial) {
8286
8531
  function normalizeGroupName(name) {
8287
8532
  return name.toLowerCase().replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
8288
8533
  }
8289
- function extractModuleFromPath(id, extraTrivial) {
8534
+ function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
8290
8535
  const segments = id.split("/");
8291
8536
  const routeGroups = extractRouteGroups(id);
8537
+ const skipSegments = new Set(SKIP_SEGMENTS_BUILTIN);
8538
+ if (extraSkipSegments) {
8539
+ for (const s of extraSkipSegments) skipSegments.add(s);
8540
+ }
8292
8541
  const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g, extraTrivial)).map(normalizeGroupName);
8293
8542
  if (moduleGroups.length > 0) {
8294
8543
  return moduleGroups[moduleGroups.length - 1];
@@ -8299,7 +8548,7 @@ function extractModuleFromPath(id, extraTrivial) {
8299
8548
  if (isRouteGroup(seg)) continue;
8300
8549
  if (isDynamicSegment(seg)) continue;
8301
8550
  if (isDomainDir(seg)) continue;
8302
- if (SKIP_SEGMENTS.has(seg)) continue;
8551
+ if (skipSegments.has(seg)) continue;
8303
8552
  meaningful.push(seg);
8304
8553
  }
8305
8554
  if (meaningful.length > 0) {
@@ -8316,12 +8565,10 @@ var moduleTagger = {
8316
8565
  layers: null,
8317
8566
  // applies to all layers
8318
8567
  tag(nodes, layer, rootDir) {
8319
- if (cachedRootDir !== rootDir) {
8320
- cachedConventionDirs = detectConventionDirs(rootDir);
8321
- cachedRootDir = rootDir;
8322
- }
8323
8568
  let configRules = [];
8324
8569
  let extraTrivial;
8570
+ let extraSkipSegments;
8571
+ let extraConventionDirs = [];
8325
8572
  try {
8326
8573
  const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
8327
8574
  const config = loadConfig2(rootDir);
@@ -8330,8 +8577,21 @@ var moduleTagger = {
8330
8577
  if (trivialFromConfig?.length) {
8331
8578
  extraTrivial = new Set(trivialFromConfig);
8332
8579
  }
8580
+ const skipFromConfig = config.taggers?.module?.skipSegments;
8581
+ if (skipFromConfig?.length) {
8582
+ extraSkipSegments = new Set(skipFromConfig);
8583
+ }
8584
+ extraConventionDirs = config.taggers?.module?.conventionDirs ?? [];
8585
+ const roleNamesFromConfig = config.taggers?.module?.genericRoleNames;
8586
+ if (roleNamesFromConfig?.length) {
8587
+ for (const name of roleNamesFromConfig) GENERIC_ROLE_NAMES_BUILTIN.add(name);
8588
+ }
8333
8589
  } catch {
8334
8590
  }
8591
+ if (cachedRootDir !== rootDir) {
8592
+ cachedConventionDirs = detectConventionDirs(rootDir, extraConventionDirs);
8593
+ cachedRootDir = rootDir;
8594
+ }
8335
8595
  const result = /* @__PURE__ */ new Map();
8336
8596
  for (const node of nodes) {
8337
8597
  const id = node.id;
@@ -8357,7 +8617,7 @@ var moduleTagger = {
8357
8617
  }
8358
8618
  }
8359
8619
  if (matched) continue;
8360
- const module2 = extractModuleFromPath(id, extraTrivial);
8620
+ const module2 = extractModuleFromPath(id, extraTrivial, extraSkipSegments);
8361
8621
  result.set(id, module2);
8362
8622
  }
8363
8623
  return result;
@@ -8585,7 +8845,13 @@ function readAllGraphs(rootDir) {
8585
8845
  }
8586
8846
  return result;
8587
8847
  }
8588
- function generateGraph(rootDir, layer) {
8848
+ async function generateGraph(rootDir, layer) {
8849
+ await initTreeSitter();
8850
+ const config = loadConfig(rootDir);
8851
+ setExtractorConfig({
8852
+ dbIdentifiers: config.parsers?.patterns?.dbIdentifiers,
8853
+ mutationMethods: config.parsers?.patterns?.mutationMethods
8854
+ });
8589
8855
  const dir = graphsDir(rootDir);
8590
8856
  (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
8591
8857
  const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
@@ -8610,11 +8876,11 @@ function parseLayerFlag(args) {
8610
8876
  }
8611
8877
  return value;
8612
8878
  }
8613
- function handleGraphCommand(subcommand, args) {
8879
+ async function handleGraphCommand(subcommand, args) {
8614
8880
  const rootDir = process.cwd();
8615
8881
  if (subcommand === "graph:generate") {
8616
8882
  const layer = parseLayerFlag(args);
8617
- const results = generateGraph(rootDir, layer);
8883
+ const results = await generateGraph(rootDir, layer);
8618
8884
  if (results.length === 0) {
8619
8885
  console.error(
8620
8886
  layer ? `No parser detected for the "${layer}" layer in this project.` : "No parsers detected for this project. Check that the project has the expected structure."
@@ -8768,7 +9034,7 @@ var TOOLS = [
8768
9034
  },
8769
9035
  {
8770
9036
  name: "read_graph",
8771
- description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module tag (computed from directory structure, e.g. "auth", "admin", "settings")\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
9037
+ description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module tag (computed from directory structure, e.g. "auth", "admin", "settings")\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nPAGINATION (filter queries):\n- Use `offset` and `limit` to paginate through large result sets.\n- Response includes: `total` (matched), `returned` (in this page), `has_more`, `next_offset`.\n- If `has_more: true`, call again with `offset: next_offset` to get the next page.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
8772
9038
  inputSchema: {
8773
9039
  type: "object",
8774
9040
  properties: {
@@ -8813,6 +9079,14 @@ var TOOLS = [
8813
9079
  type: "boolean",
8814
9080
  description: "Include the edge list in the response. Default TRUE for neighborhood queries (node_id), FALSE for filter queries. Filter responses always include edge_count. Only set true on filter queries when you actually need edge data."
8815
9081
  },
9082
+ offset: {
9083
+ type: "number",
9084
+ description: "Skip first N matched nodes (pagination). Default 0. Use next_offset from a previous response to get the next page."
9085
+ },
9086
+ limit: {
9087
+ type: "number",
9088
+ description: "Max nodes to return. Default: all matched nodes. Use with offset for pagination."
9089
+ },
8816
9090
  queries: {
8817
9091
  type: "array",
8818
9092
  description: "Batch mode \u2014 array of query objects to run in a single call. Each uses the same param schema. When set, top-level params are ignored. Subject to an aggregate size budget \u2014 later queries may return a skipped stub.",
@@ -8881,6 +9155,40 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
8881
9155
  required: ["layer", "pattern"]
8882
9156
  }
8883
9157
  },
9158
+ {
9159
+ name: "inspect_node",
9160
+ description: `Get deep AST data for specific graph nodes \u2014 what's INSIDE a component or endpoint. Returns elements (JSX), state hooks, conditions, variables, responses, and request params.
9161
+
9162
+ USE THIS FOR: "what elements does LoginPage have?", "what conditions does the login endpoint check?", "what state does SettingsPage manage?", "what responses can this endpoint return?"
9163
+
9164
+ DO NOT USE FOR: structural queries (use read_graph), content search (use grep_nodes).
9165
+
9166
+ Returns deep fields only \u2014 not structural metadata (use read_graph for that).`,
9167
+ inputSchema: {
9168
+ type: "object",
9169
+ properties: {
9170
+ layer: {
9171
+ type: "string",
9172
+ enum: ["ui", "api", "db"],
9173
+ description: "Graph layer (required)."
9174
+ },
9175
+ node_id: {
9176
+ type: "string",
9177
+ description: "Node ID to inspect. Use read_graph to find node IDs first."
9178
+ },
9179
+ search: {
9180
+ type: "string",
9181
+ description: "Substring match on node id/name/route. Returns deep data for all matching nodes (max 5)."
9182
+ },
9183
+ fields: {
9184
+ type: "array",
9185
+ items: { type: "string" },
9186
+ description: "Specific deep fields to return. Options: elements, stateVars, conditions, variables, responses, params. Omit for all."
9187
+ }
9188
+ },
9189
+ required: ["layer"]
9190
+ }
9191
+ },
8884
9192
  {
8885
9193
  name: "chart_server_status",
8886
9194
  description: `Check whether the launch-chart UI server is running. Returns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }.
@@ -9009,6 +9317,14 @@ var COMPACT_NODE_KNOWN_KEYS = /* @__PURE__ */ new Set([
9009
9317
  "columns",
9010
9318
  "tags"
9011
9319
  ]);
9320
+ var DEEP_FIELDS = /* @__PURE__ */ new Set([
9321
+ "elements",
9322
+ "stateVars",
9323
+ "conditions",
9324
+ "variables",
9325
+ "responses",
9326
+ "params"
9327
+ ]);
9012
9328
  var EST_CHARS_PER_NODE_FULL = {
9013
9329
  ui: 300,
9014
9330
  api: 300,
@@ -9036,7 +9352,7 @@ function toCompactNode(n) {
9036
9352
  if (n.columns != null) out.c = n.columns;
9037
9353
  if (tags != null) out.tg = tags;
9038
9354
  for (const k of Object.keys(n)) {
9039
- if (!COMPACT_NODE_KNOWN_KEYS.has(k) && n[k] != null) out[k] = n[k];
9355
+ if (!COMPACT_NODE_KNOWN_KEYS.has(k) && !DEEP_FIELDS.has(k) && n[k] != null) out[k] = n[k];
9040
9356
  }
9041
9357
  return out;
9042
9358
  }
@@ -9136,13 +9452,13 @@ function okJson(data) {
9136
9452
  function err(text) {
9137
9453
  return { content: [{ type: "text", text }], isError: true };
9138
9454
  }
9139
- function handleGenerateGraph(args) {
9455
+ async function handleGenerateGraph(args) {
9140
9456
  const rootDir = process.cwd();
9141
9457
  const layer = args.layer;
9142
9458
  if (layer && !["ui", "api", "db"].includes(layer)) {
9143
9459
  return err(`Invalid layer "${layer}". Must be one of: ui, api, db`);
9144
9460
  }
9145
- const results = generateGraph(rootDir, layer);
9461
+ const results = await generateGraph(rootDir, layer);
9146
9462
  if (results.length === 0) {
9147
9463
  return err(
9148
9464
  layer ? `No parser detected for the "${layer}" layer in this project.` : "No parsers detected for this project. Check that the project has the expected structure."
@@ -9175,6 +9491,8 @@ function runReadGraphQueryRaw(rootDir, args) {
9175
9491
  const layerIsDb = args.layer === "db";
9176
9492
  const minimal = args.minimal ?? layerIsDb;
9177
9493
  const includeEdges = args.include_edges;
9494
+ const offset = args.offset ?? 0;
9495
+ const limit = args.limit;
9178
9496
  const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
9179
9497
  if (layer && !["ui", "api", "db"].includes(layer)) {
9180
9498
  return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
@@ -9246,18 +9564,29 @@ function runReadGraphQueryRaw(rootDir, args) {
9246
9564
  hint: "No nodes matched. Check spelling, or call read_graph without filter to see the summary and available types/modules."
9247
9565
  };
9248
9566
  }
9567
+ const totalMatched = matched.length;
9568
+ const paginatedNodes = limit != null ? matched.slice(offset, offset + limit) : matched.slice(offset);
9569
+ const hasMore = offset + paginatedNodes.length < totalMatched;
9249
9570
  const wantEdges = includeEdges ?? false;
9571
+ const returnedIds = new Set(paginatedNodes.map((n) => n.id));
9572
+ const returnedEdges = graph.edges.filter((e) => returnedIds.has(e.source) && returnedIds.has(e.target));
9250
9573
  const result = {
9251
9574
  layer,
9252
9575
  filter: { search, type, module: module_ },
9253
- matched: matched.length,
9254
- edge_count: matchedEdges.length,
9255
- nodes: minimal ? toMinimal(matched) : matched
9576
+ total: totalMatched,
9577
+ returned: paginatedNodes.length,
9578
+ offset,
9579
+ has_more: hasMore,
9580
+ edge_count: returnedEdges.length,
9581
+ nodes: minimal ? toMinimal(paginatedNodes) : paginatedNodes
9256
9582
  };
9583
+ if (hasMore) {
9584
+ result.next_offset = offset + paginatedNodes.length;
9585
+ }
9257
9586
  if (wantEdges) {
9258
- result.edges = matchedEdges;
9259
- } else {
9260
- result.edges_hint = `${matchedEdges.length} edges between matched nodes omitted. Pass include_edges:true to retrieve them (only do this when you actually need edge data).`;
9587
+ result.edges = returnedEdges;
9588
+ } else if (returnedEdges.length > 0) {
9589
+ result.edges_hint = `${returnedEdges.length} edges between matched nodes omitted. Pass include_edges:true to retrieve them (only do this when you actually need edge data).`;
9261
9590
  }
9262
9591
  return result;
9263
9592
  }
@@ -9325,6 +9654,48 @@ function nodeToFilePath(rootDir, layer, nodeId) {
9325
9654
  if (layer === "db") return (0, import_node_path15.join)(rootDir, "prisma", "schema.prisma");
9326
9655
  return null;
9327
9656
  }
9657
+ function handleInspectNode(args) {
9658
+ const rootDir = process.cwd();
9659
+ const layer = args.layer;
9660
+ const nodeId = args.node_id;
9661
+ const search = args.search;
9662
+ const fields = args.fields;
9663
+ if (!layer) return err("layer is required.");
9664
+ if (!nodeId && !search) return err("Either node_id or search is required.");
9665
+ const graph = readGraph(rootDir, layer);
9666
+ if (!graph) return err(`No graph found for layer "${layer}". Run generate_graph first.`);
9667
+ let matched;
9668
+ if (nodeId) {
9669
+ const node = graph.nodes.find((n) => n.id === nodeId);
9670
+ if (!node) return err(`Node "${nodeId}" not found in ${layer} layer.`);
9671
+ matched = [node];
9672
+ } else {
9673
+ const searchLower = search.toLowerCase();
9674
+ matched = graph.nodes.filter(
9675
+ (n) => n.id.toLowerCase().includes(searchLower) || n.name.toLowerCase().includes(searchLower) || n.route?.toLowerCase().includes(searchLower)
9676
+ );
9677
+ }
9678
+ if (matched.length === 0) return err(`No nodes matching "${search}" in ${layer} layer.`);
9679
+ if (matched.length > 5) {
9680
+ return err(`${matched.length} nodes match "${search}". Narrow your search (max 5 for inspect_node).`);
9681
+ }
9682
+ const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params"];
9683
+ const requestedFields = fields ?? allDeepFields;
9684
+ const results = matched.map((node) => {
9685
+ const deep = { id: node.id, name: node.name, type: node.type };
9686
+ for (const field of requestedFields) {
9687
+ if (allDeepFields.includes(field) && node[field] != null) {
9688
+ deep[field] = node[field];
9689
+ }
9690
+ }
9691
+ return deep;
9692
+ });
9693
+ return okJson({
9694
+ layer,
9695
+ matched: results.length,
9696
+ nodes: results
9697
+ });
9698
+ }
9328
9699
  function handleGrepNodes(args) {
9329
9700
  const rootDir = process.cwd();
9330
9701
  const pattern = args.pattern;
@@ -9587,7 +9958,7 @@ function respond(id, result) {
9587
9958
  function respondError(id, code, message) {
9588
9959
  send({ jsonrpc: "2.0", id, error: { code, message } });
9589
9960
  }
9590
- function handleMessage(msg) {
9961
+ async function handleMessage(msg) {
9591
9962
  const method = msg.method;
9592
9963
  const id = msg.id;
9593
9964
  if (method === "initialize") {
@@ -9610,7 +9981,7 @@ function handleMessage(msg) {
9610
9981
  const toolName = params.name;
9611
9982
  const args = params.arguments ?? {};
9612
9983
  if (toolName === "generate_graph") {
9613
- respond(id ?? null, handleGenerateGraph(args));
9984
+ respond(id ?? null, await handleGenerateGraph(args));
9614
9985
  return;
9615
9986
  }
9616
9987
  if (toolName === "read_graph") {
@@ -9621,6 +9992,10 @@ function handleMessage(msg) {
9621
9992
  respond(id ?? null, handleGrepNodes(args));
9622
9993
  return;
9623
9994
  }
9995
+ if (toolName === "inspect_node") {
9996
+ respond(id ?? null, handleInspectNode(args));
9997
+ return;
9998
+ }
9624
9999
  if (toolName === "chart_server_status") {
9625
10000
  respond(id ?? null, handleChartServerStatus());
9626
10001
  return;