@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.
@@ -81,41 +81,160 @@ var import_node_path8 = require("node:path");
81
81
  var import_node_fs3 = require("node:fs");
82
82
  var import_node_path3 = require("node:path");
83
83
 
84
- // src/server/graph/core/ast-helpers.ts
84
+ // src/server/graph/core/ts-extractor.ts
85
85
  var import_node_fs2 = require("node:fs");
86
86
  var import_node_path2 = require("node:path");
87
- var tsModule;
88
- function getTs() {
89
- if (!tsModule) {
90
- tsModule = require("typescript");
87
+ var tsxLanguage;
88
+ var parserInstance;
89
+ var initPromise;
90
+ var initialized = false;
91
+ var queriesDir = (() => {
92
+ const srcPath = (0, import_node_path2.join)((0, import_node_path2.dirname)(__filename), "..", "queries");
93
+ if (require("fs").existsSync(srcPath)) return srcPath;
94
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)(__filename), "graph", "queries");
95
+ })();
96
+ var queryCache = /* @__PURE__ */ new Map();
97
+ async function initTreeSitter() {
98
+ if (initialized) return;
99
+ if (initPromise) return initPromise;
100
+ initPromise = (async () => {
101
+ const TreeSitter = require("web-tree-sitter");
102
+ await TreeSitter.init();
103
+ const wasmPath = require.resolve("tree-sitter-typescript/tree-sitter-tsx.wasm");
104
+ tsxLanguage = await TreeSitter.Language.load(wasmPath);
105
+ parserInstance = new TreeSitter();
106
+ parserInstance.setLanguage(tsxLanguage);
107
+ initialized = true;
108
+ })();
109
+ return initPromise;
110
+ }
111
+ function ensureInit() {
112
+ if (!initialized || !tsxLanguage || !parserInstance) {
113
+ throw new Error("Tree-sitter not initialized. Call initTreeSitter() first.");
114
+ }
115
+ }
116
+ function getQuery(name) {
117
+ ensureInit();
118
+ const cached = queryCache.get(name);
119
+ if (cached) return cached;
120
+ const scmPath = (0, import_node_path2.join)(queriesDir, `${name}.scm`);
121
+ const scm = (0, import_node_fs2.readFileSync)(scmPath, "utf-8");
122
+ const query = tsxLanguage.query(scm);
123
+ queryCache.set(name, query);
124
+ return query;
125
+ }
126
+ function parseSource(absPath) {
127
+ ensureInit();
128
+ const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
129
+ return parserInstance.parse(content);
130
+ }
131
+ var PRISMA_MUTATION_METHODS_BUILTIN = [
132
+ "create",
133
+ "createMany",
134
+ "createManyAndReturn",
135
+ "update",
136
+ "updateMany",
137
+ "updateManyAndReturn",
138
+ "upsert",
139
+ "delete",
140
+ "deleteMany"
141
+ ];
142
+ var DB_IDENTIFIERS_FALLBACK = ["db", "prisma"];
143
+ var extraDbIdentifiers = [];
144
+ var extraMutationMethods = [];
145
+ function setExtractorConfig(config) {
146
+ extraDbIdentifiers = config.dbIdentifiers ?? [];
147
+ extraMutationMethods = config.mutationMethods ?? [];
148
+ }
149
+ function getMutationMethods() {
150
+ return /* @__PURE__ */ new Set([...PRISMA_MUTATION_METHODS_BUILTIN, ...extraMutationMethods]);
151
+ }
152
+ function getFallbackDbIdentifiers() {
153
+ return /* @__PURE__ */ new Set([...DB_IDENTIFIERS_FALLBACK, ...extraDbIdentifiers]);
154
+ }
155
+ function looksLikeUrl(s) {
156
+ return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
157
+ }
158
+ function templateStartsWithSlash(text) {
159
+ const content = text.slice(1);
160
+ return content.startsWith("/");
161
+ }
162
+ function captureMap(match) {
163
+ const map = {};
164
+ for (const c of match.captures) {
165
+ map[c.name] = c.node.text;
91
166
  }
92
- return tsModule;
167
+ return map;
93
168
  }
94
- var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
95
- function parseFile(absPath) {
96
- const ts = getTs();
97
- const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
98
- const ext = (0, import_node_path2.extname)(absPath);
99
- const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
100
- const sourceFile = ts.createSourceFile(
101
- absPath,
102
- content,
103
- ts.ScriptTarget.Latest,
104
- /* setParentNodes */
105
- true,
106
- scriptKind
107
- );
169
+ function childrenOfType(node, type) {
170
+ return node.children.filter((n) => n.type === type);
171
+ }
172
+ function childOfType(node, type) {
173
+ return node.children.find((n) => n.type === type);
174
+ }
175
+ function parseFileTS(absPath) {
176
+ const tree = parseSource(absPath);
177
+ const root = tree.rootNode;
178
+ const imports = [];
179
+ const importStatements = childrenOfType(root, "import_statement");
180
+ for (const stmt of importStatements) {
181
+ const sourceNode = childOfType(stmt, "string");
182
+ const frag = sourceNode ? childOfType(sourceNode, "string_fragment") : void 0;
183
+ if (!frag) continue;
184
+ const specifier = frag.text;
185
+ const hasTypeKeyword = stmt.children.some(
186
+ (n) => n.type === "type" && n.text === "type"
187
+ );
188
+ const clause = childOfType(stmt, "import_clause");
189
+ if (!clause) {
190
+ imports.push({ names: [], specifier, isTypeOnly: false, typeNames: /* @__PURE__ */ new Set() });
191
+ continue;
192
+ }
193
+ const names = [];
194
+ const typeNames = /* @__PURE__ */ new Set();
195
+ const defaultId = childOfType(clause, "identifier");
196
+ if (defaultId) names.push(defaultId.text);
197
+ const nsImport = childOfType(clause, "namespace_import");
198
+ if (nsImport) {
199
+ const nsId = childOfType(nsImport, "identifier");
200
+ if (nsId) names.push(nsId.text);
201
+ }
202
+ const namedImports = childOfType(clause, "named_imports");
203
+ if (namedImports) {
204
+ for (const spec of childrenOfType(namedImports, "import_specifier")) {
205
+ const identifiers = childrenOfType(spec, "identifier");
206
+ const importedName = identifiers.length > 1 ? identifiers[identifiers.length - 1].text : identifiers[0]?.text;
207
+ if (importedName) {
208
+ names.push(importedName);
209
+ const specIsType = spec.children.some(
210
+ (n) => n.type === "type" && n.text === "type"
211
+ );
212
+ if (specIsType) typeNames.add(importedName);
213
+ }
214
+ }
215
+ }
216
+ if (names.length > 0 || hasTypeKeyword) {
217
+ imports.push({ names, specifier, isTypeOnly: hasTypeKeyword, typeNames });
218
+ }
219
+ }
220
+ const importQuery = getQuery("imports");
221
+ const importMatches = importQuery.matches(root);
222
+ for (const m of importMatches) {
223
+ const caps = captureMap(m);
224
+ if (caps["import.dynamic"]) {
225
+ imports.push({
226
+ names: [],
227
+ specifier: caps["import.dynamic"],
228
+ isTypeOnly: false,
229
+ typeNames: /* @__PURE__ */ new Set()
230
+ });
231
+ }
232
+ }
108
233
  const exportsSet = /* @__PURE__ */ new Set();
109
234
  const exportsOrdered = [];
110
235
  let defaultName = null;
111
236
  let firstValueExport = null;
112
237
  let firstTypeExport = null;
113
- const imports = [];
114
- const reExports = [];
115
- const jsxElements = /* @__PURE__ */ new Set();
116
- const navigations = [];
117
- const fetchCalls = [];
118
- const includeConcat = process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1";
119
238
  function addExport(name2, kind) {
120
239
  if (!exportsSet.has(name2)) {
121
240
  exportsSet.add(name2);
@@ -125,300 +244,339 @@ function parseFile(absPath) {
125
244
  else if (kind === "value" && !firstValueExport) firstValueExport = name2;
126
245
  else if (kind === "type" && !firstTypeExport) firstTypeExport = name2;
127
246
  }
128
- function hasModifier(node, kind) {
129
- const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
130
- return modifiers?.some((m) => m.kind === kind) ?? false;
131
- }
132
- function extractTargetFromExpression(expr) {
133
- if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
134
- return { target: expr.text, isTemplate: false };
135
- }
136
- if (ts.isTemplateExpression(expr)) {
137
- return { target: expr.getText(sourceFile), isTemplate: true };
247
+ const exportQuery = getQuery("exports");
248
+ const exportMatches = exportQuery.matches(root);
249
+ for (const m of exportMatches) {
250
+ const caps = captureMap(m);
251
+ if (caps["export.default.func"]) addExport(caps["export.default.func"], "default");
252
+ else if (caps["export.default.class"]) addExport(caps["export.default.class"], "default");
253
+ else if (caps["export.default.value"]) addExport(caps["export.default.value"], "default");
254
+ else if (caps["export.named.func"]) addExport(caps["export.named.func"], "value");
255
+ else if (caps["export.named.class"]) addExport(caps["export.named.class"], "value");
256
+ else if (caps["export.named.const"]) addExport(caps["export.named.const"], "value");
257
+ else if (caps["export.named.enum"]) addExport(caps["export.named.enum"], "value");
258
+ else if (caps["export.named.type"]) addExport(caps["export.named.type"], "type");
259
+ else if (caps["export.named.interface"]) addExport(caps["export.named.interface"], "type");
260
+ else if (caps["export.bare.name"]) addExport(caps["export.bare.name"], "value");
261
+ if (caps["reexport.name"]) addExport(caps["reexport.name"], "value");
262
+ }
263
+ for (const stmt of childrenOfType(root, "export_statement")) {
264
+ const hasDefault = stmt.children.some((n) => n.text === "default");
265
+ if (hasDefault && !defaultName) {
266
+ defaultName = "default";
138
267
  }
139
- return null;
140
- }
141
- function looksLikeUrl(s) {
142
- return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
143
268
  }
144
- function templateStartsWithSlash(expr) {
145
- const head = expr.head.text;
146
- return head.startsWith("/");
147
- }
148
- function extractUrlFromFetchArg(arg) {
149
- if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
150
- if (!looksLikeUrl(arg.text)) return null;
151
- return { url: arg.text, isTemplate: false };
152
- }
153
- if (ts.isTemplateExpression(arg)) {
154
- if (!templateStartsWithSlash(arg)) return null;
155
- return { url: arg.getText(sourceFile), isTemplate: true };
269
+ const reExports = [];
270
+ for (const m of exportMatches) {
271
+ const caps = captureMap(m);
272
+ if (caps["reexport.name"] && caps["reexport.source"]) {
273
+ reExports.push({ name: caps["reexport.name"], from: caps["reexport.source"] });
156
274
  }
157
- if (includeConcat && ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.PlusToken) {
158
- let leftmost = arg;
159
- while (ts.isBinaryExpression(leftmost) && leftmost.operatorToken.kind === ts.SyntaxKind.PlusToken) {
160
- leftmost = leftmost.left;
161
- }
162
- if ((ts.isStringLiteral(leftmost) || ts.isNoSubstitutionTemplateLiteral(leftmost)) && leftmost.text.startsWith("/")) {
163
- return { url: arg.getText(sourceFile), isTemplate: false, isConcat: true };
164
- }
275
+ if (caps["reexport.wildcard.source"]) {
276
+ reExports.push({ name: "*", from: caps["reexport.wildcard.source"], isWildcard: true });
165
277
  }
166
- return null;
167
278
  }
168
- function visit(node) {
169
- if (ts.isImportDeclaration(node)) {
170
- const moduleSpec = node.moduleSpecifier;
171
- if (ts.isStringLiteral(moduleSpec)) {
172
- const specifier = moduleSpec.text;
173
- const clause = node.importClause;
174
- const isTypeOnly = !!clause?.isTypeOnly;
175
- const names = [];
176
- const typeNames = /* @__PURE__ */ new Set();
177
- if (clause) {
178
- if (clause.name) names.push(clause.name.text);
179
- const nb = clause.namedBindings;
180
- if (nb && ts.isNamedImports(nb)) {
181
- for (const el of nb.elements) {
182
- names.push(el.name.text);
183
- if (el.isTypeOnly) typeNames.add(el.name.text);
184
- }
185
- } else if (nb && ts.isNamespaceImport(nb)) {
186
- names.push(nb.name.text);
187
- }
188
- }
189
- if (names.length > 0 || isTypeOnly) {
190
- imports.push({ names, specifier, isTypeOnly, typeNames });
191
- } else if (!clause) {
192
- imports.push({ names: [], specifier, isTypeOnly: false, typeNames: /* @__PURE__ */ new Set() });
193
- }
194
- }
195
- }
196
- if (ts.isExportDeclaration(node)) {
197
- const fromSpec = node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : null;
198
- if (node.exportClause && ts.isNamedExports(node.exportClause)) {
199
- for (const el of node.exportClause.elements) {
200
- const exportedName = el.name.text;
201
- addExport(exportedName, "value");
202
- if (fromSpec) {
203
- reExports.push({ name: exportedName, from: fromSpec });
204
- }
205
- }
206
- } else if (!node.exportClause && fromSpec) {
207
- reExports.push({ name: "*", from: fromSpec, isWildcard: true });
279
+ const jsxElements = /* @__PURE__ */ new Set();
280
+ const jsxQuery = getQuery("jsx-elements");
281
+ const jsxCaptures = jsxQuery.captures(root);
282
+ for (const c of jsxCaptures) {
283
+ if (c.name === "jsx.tag") {
284
+ if (/^[A-Z]/.test(c.node.text)) {
285
+ jsxElements.add(c.node.text);
208
286
  }
209
- }
210
- if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
211
- const arg = node.arguments[0];
212
- if (arg && ts.isStringLiteral(arg)) {
213
- imports.push({
214
- names: [],
215
- specifier: arg.text,
216
- isTypeOnly: false,
217
- typeNames: /* @__PURE__ */ new Set()
218
- });
287
+ } else if (c.name === "jsx.member_tag") {
288
+ const rootName = c.node.text.split(".")[0];
289
+ if (rootName && /^[A-Z]/.test(rootName)) {
290
+ jsxElements.add(rootName);
219
291
  }
220
292
  }
221
- if (ts.isExportAssignment(node) && !node.isExportEquals) {
222
- if (ts.isIdentifier(node.expression)) {
223
- addExport(node.expression.text, "default");
224
- } else {
225
- if (!defaultName) defaultName = "default";
226
- }
293
+ }
294
+ const navigations = [];
295
+ const navQuery = getQuery("navigations");
296
+ const navMatches = navQuery.matches(root);
297
+ for (const m of navMatches) {
298
+ const caps = captureMap(m);
299
+ if (caps["nav.target.literal"] && caps["nav.method"]) {
300
+ navigations.push({
301
+ kind: caps["nav.method"] === "push" ? "router-push" : "router-replace",
302
+ target: caps["nav.target.literal"],
303
+ isTemplate: false
304
+ });
227
305
  }
228
- if (ts.isFunctionDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
229
- const isDefault = hasModifier(node, ts.SyntaxKind.DefaultKeyword);
230
- if (node.name) addExport(node.name.text, isDefault ? "default" : "value");
306
+ if (caps["nav.target.template"] && caps["nav.method.template"]) {
307
+ navigations.push({
308
+ kind: caps["nav.method.template"] === "push" ? "router-push" : "router-replace",
309
+ target: caps["nav.target.template"],
310
+ isTemplate: true
311
+ });
231
312
  }
232
- if (ts.isVariableStatement(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
233
- for (const decl of node.declarationList.declarations) {
234
- if (ts.isIdentifier(decl.name)) {
235
- addExport(decl.name.text, "value");
236
- }
237
- }
313
+ const linkLiteral = caps["nav.link.literal"] || caps["nav.link.literal.self"];
314
+ if (linkLiteral) {
315
+ navigations.push({ kind: "link-href", target: linkLiteral, isTemplate: false });
238
316
  }
239
- if (ts.isClassDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
240
- const isDefault = hasModifier(node, ts.SyntaxKind.DefaultKeyword);
241
- if (node.name) addExport(node.name.text, isDefault ? "default" : "value");
317
+ const linkTemplate = caps["nav.link.template"] || caps["nav.link.template.self"];
318
+ if (linkTemplate) {
319
+ navigations.push({ kind: "link-href", target: linkTemplate, isTemplate: true });
242
320
  }
243
- if (ts.isTypeAliasDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
244
- addExport(node.name.text, "type");
321
+ if (caps["nav.window.literal"]) {
322
+ navigations.push({ kind: "window-location", target: caps["nav.window.literal"], isTemplate: false });
245
323
  }
246
- if (ts.isInterfaceDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
247
- addExport(node.name.text, "type");
324
+ if (caps["nav.window.assign.target"]) {
325
+ navigations.push({ kind: "window-location", target: caps["nav.window.assign.target"], isTemplate: false });
248
326
  }
249
- if (ts.isEnumDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
250
- addExport(node.name.text, "value");
327
+ }
328
+ const fetchCalls = [];
329
+ const fetchQuery = getQuery("fetch-calls");
330
+ const fetchMatches = fetchQuery.matches(root);
331
+ for (const m of fetchMatches) {
332
+ const caps = captureMap(m);
333
+ if (caps["fetch.url.literal"] && looksLikeUrl(caps["fetch.url.literal"])) {
334
+ fetchCalls.push({ url: caps["fetch.url.literal"], isTemplate: false, kind: "fetch" });
335
+ }
336
+ if (caps["fetch.url.template"] && templateStartsWithSlash(caps["fetch.url.template"])) {
337
+ fetchCalls.push({ url: caps["fetch.url.template"], isTemplate: true, kind: "fetch" });
338
+ }
339
+ const clientUrl = caps["fetch.client.url.literal"] || caps["fetch.await.url.literal"];
340
+ const clientMethod = caps["fetch.method"] || caps["fetch.await.method"];
341
+ if (clientUrl && clientMethod && looksLikeUrl(clientUrl)) {
342
+ fetchCalls.push({
343
+ method: clientMethod.toUpperCase(),
344
+ url: clientUrl,
345
+ isTemplate: false,
346
+ kind: "client-method"
347
+ });
251
348
  }
252
- if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
253
- const tagName = node.tagName;
254
- if (ts.isIdentifier(tagName) && /^[A-Z]/.test(tagName.text)) {
255
- jsxElements.add(tagName.text);
256
- } else if (ts.isPropertyAccessExpression(tagName)) {
257
- let root = tagName;
258
- while (ts.isPropertyAccessExpression(root)) root = root.expression;
259
- if (ts.isIdentifier(root) && /^[A-Z]/.test(root.text)) {
260
- jsxElements.add(root.text);
261
- }
262
- }
349
+ const clientUrlTpl = caps["fetch.client.url.template"] || caps["fetch.await.url.template"];
350
+ const clientMethodTpl = caps["fetch.method.template"] || caps["fetch.await.method.template"];
351
+ if (clientUrlTpl && clientMethodTpl && templateStartsWithSlash(clientUrlTpl)) {
352
+ fetchCalls.push({
353
+ method: clientMethodTpl.toUpperCase(),
354
+ url: clientUrlTpl,
355
+ isTemplate: true,
356
+ kind: "client-method"
357
+ });
263
358
  }
264
- if (ts.isCallExpression(node)) {
265
- const expr = node.expression;
266
- if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && expr.expression.text === "router" && (expr.name.text === "push" || expr.name.text === "replace")) {
267
- const arg = node.arguments[0];
268
- if (arg) {
269
- const parsed = extractTargetFromExpression(arg);
270
- if (parsed) {
271
- navigations.push({
272
- kind: expr.name.text === "push" ? "router-push" : "router-replace",
273
- target: parsed.target,
274
- isTemplate: parsed.isTemplate
275
- });
359
+ }
360
+ const name = defaultName ?? firstValueExport ?? firstTypeExport ?? "";
361
+ return { name, exports: exportsOrdered, imports, reExports, jsxElements, navigations, fetchCalls };
362
+ }
363
+ function extractDbCallsTS(absPath) {
364
+ const tree = parseSource(absPath);
365
+ const root = tree.rootNode;
366
+ const dbQuery = getQuery("db-calls");
367
+ const matches = dbQuery.matches(root);
368
+ const dbIdentifiers = /* @__PURE__ */ new Set();
369
+ for (const stmt of childrenOfType(root, "import_statement")) {
370
+ const sourceNode = childOfType(stmt, "string");
371
+ const frag = sourceNode ? childOfType(sourceNode, "string_fragment") : void 0;
372
+ if (!frag) continue;
373
+ const spec = frag.text;
374
+ if (spec.includes("prisma") || spec.includes("/db") || spec === "@prisma/client") {
375
+ const clause = childOfType(stmt, "import_clause");
376
+ if (clause) {
377
+ const defaultId = childOfType(clause, "identifier");
378
+ if (defaultId) dbIdentifiers.add(defaultId.text);
379
+ const named = childOfType(clause, "named_imports");
380
+ if (named) {
381
+ for (const specNode of childrenOfType(named, "import_specifier")) {
382
+ const ids = childrenOfType(specNode, "identifier");
383
+ const importedName = ids[ids.length - 1];
384
+ if (importedName) dbIdentifiers.add(importedName.text);
276
385
  }
277
386
  }
278
387
  }
279
388
  }
280
- if (ts.isCallExpression(node) && node.arguments.length > 0) {
281
- const expr = node.expression;
282
- const firstArg = node.arguments[0];
283
- if (ts.isIdentifier(expr) && expr.text === "fetch") {
284
- const extracted = extractUrlFromFetchArg(firstArg);
285
- if (extracted) {
286
- fetchCalls.push({
287
- url: extracted.url,
288
- isTemplate: extracted.isTemplate,
289
- ...extracted.isConcat ? { isConcat: true } : {},
290
- kind: "fetch"
291
- });
292
- }
293
- }
294
- if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) {
295
- const methodName = expr.name.text;
296
- if (HTTP_METHODS.has(methodName)) {
297
- const extracted = extractUrlFromFetchArg(firstArg);
298
- if (extracted) {
299
- fetchCalls.push({
300
- method: methodName.toUpperCase(),
301
- url: extracted.url,
302
- isTemplate: extracted.isTemplate,
303
- ...extracted.isConcat ? { isConcat: true } : {},
304
- kind: "client-method"
305
- });
306
- }
307
- }
308
- }
389
+ }
390
+ if (dbIdentifiers.size === 0) {
391
+ for (const id of getFallbackDbIdentifiers()) dbIdentifiers.add(id);
392
+ } else {
393
+ for (const id of extraDbIdentifiers) dbIdentifiers.add(id);
394
+ }
395
+ const calls = [];
396
+ const seen = /* @__PURE__ */ new Set();
397
+ for (const m of matches) {
398
+ const caps = captureMap(m);
399
+ const identifier = caps["db.identifier"];
400
+ const model = caps["db.model"];
401
+ const method = caps["db.method"];
402
+ if (!identifier || !model || !method) continue;
403
+ if (!dbIdentifiers.has(identifier)) continue;
404
+ const key = `${model}.${method}`;
405
+ if (seen.has(key)) continue;
406
+ seen.add(key);
407
+ calls.push({ model, method, isMutation: getMutationMethods().has(method) });
408
+ }
409
+ return calls;
410
+ }
411
+ function extractAuthWrappersTS(absPath) {
412
+ const tree = parseSource(absPath);
413
+ const root = tree.rootNode;
414
+ const wrapperQuery = getQuery("wrappers");
415
+ const matches = wrapperQuery.matches(root);
416
+ const wrappers = /* @__PURE__ */ new Set();
417
+ for (const m of matches) {
418
+ const caps = captureMap(m);
419
+ if (caps["wrapper.fn_name"]) {
420
+ wrappers.add(caps["wrapper.fn_name"]);
309
421
  }
310
- if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
311
- const tagName = node.tagName;
312
- if (ts.isIdentifier(tagName) && tagName.text === "Link") {
313
- for (const attr of node.attributes.properties) {
314
- if (ts.isJsxAttribute(attr) && attr.name.getText(sourceFile) === "href" && attr.initializer) {
315
- const init = attr.initializer;
316
- if (ts.isStringLiteral(init)) {
317
- navigations.push({ kind: "link-href", target: init.text, isTemplate: false });
318
- } else if (ts.isJsxExpression(init) && init.expression) {
319
- const parsed = extractTargetFromExpression(init.expression);
320
- if (parsed) {
321
- navigations.push({ kind: "link-href", target: parsed.target, isTemplate: parsed.isTemplate });
322
- }
323
- }
324
- }
325
- }
422
+ }
423
+ return wrappers;
424
+ }
425
+ function trunc(s, max = 120) {
426
+ return s.length > max ? s.slice(0, max) + "..." : s;
427
+ }
428
+ function extractDeep(absPath) {
429
+ const tree = parseSource(absPath);
430
+ const root = tree.rootNode;
431
+ const elements = [];
432
+ const elQuery = getQuery("deep/jsx-semantic");
433
+ const elMatches = elQuery.matches(root);
434
+ const elementMap = /* @__PURE__ */ new Map();
435
+ for (const m of elMatches) {
436
+ const tag = m.captures.find((c) => c.name === "el.tag")?.node;
437
+ if (!tag || !/^[A-Z]/.test(tag.text)) continue;
438
+ const elNode = m.captures.find((c) => c.name === "el.self" || c.name === "el.open")?.node;
439
+ const key = elNode ? `${elNode.startPosition.row}:${elNode.startPosition.column}` : `${tag.startPosition.row}:${tag.startPosition.column}`;
440
+ if (!elementMap.has(key)) {
441
+ elementMap.set(key, { tag: tag.text, props: {}, nodeKey: key });
442
+ }
443
+ const entry = elementMap.get(key);
444
+ const propName = m.captures.find((c) => c.name === "el.prop.name")?.node.text;
445
+ const propVal = m.captures.find((c) => c.name === "el.prop.value.str")?.node.text;
446
+ const propExpr = m.captures.find((c) => c.name === "el.prop.value.expr")?.node.text;
447
+ if (propName) {
448
+ entry.props[propName] = propVal ?? (propExpr ? trunc(propExpr, 60) : "true");
449
+ }
450
+ }
451
+ for (const m of elMatches) {
452
+ const textTag = m.captures.find((c) => c.name === "el.text.tag")?.node;
453
+ const textContent = m.captures.find((c) => c.name === "el.text.content")?.node;
454
+ if (textTag && textContent && /^[A-Z]/.test(textTag.text)) {
455
+ const key = `${textTag.startPosition.row}:${textTag.startPosition.column}`;
456
+ if (!elementMap.has(key)) {
457
+ elementMap.set(key, { tag: textTag.text, props: {}, nodeKey: key });
326
458
  }
459
+ const entry = elementMap.get(key);
460
+ const text = textContent.text.trim();
461
+ if (text) entry.props["_text"] = text;
462
+ }
463
+ }
464
+ for (const entry of elementMap.values()) {
465
+ const el = { tag: entry.tag, props: entry.props };
466
+ const hasExpr = Object.values(entry.props).some((v) => v.includes("{") || v.includes("("));
467
+ if (hasExpr) el.dynamic = true;
468
+ if (entry.props["_text"]) {
469
+ el.text = entry.props["_text"];
470
+ delete el.props["_text"];
471
+ }
472
+ elements.push(el);
473
+ }
474
+ const stateVars = [];
475
+ const hookQuery = getQuery("deep/state-hooks");
476
+ const hookMatches = hookQuery.matches(root);
477
+ for (const m of hookMatches) {
478
+ const caps = captureMap(m);
479
+ if (caps["hook.var"] && caps["hook.setter"] && caps["hook.fn"]) {
480
+ stateVars.push({
481
+ name: caps["hook.var"],
482
+ setter: caps["hook.setter"],
483
+ hook: caps["hook.fn"],
484
+ init: trunc(caps["hook.init"] ?? "", 80)
485
+ });
327
486
  }
328
- if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
329
- const left = node.left;
330
- 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") {
331
- const parsed = extractTargetFromExpression(node.right);
332
- if (parsed) {
333
- navigations.push({ kind: "window-location", target: parsed.target, isTemplate: parsed.isTemplate });
334
- }
335
- }
487
+ if (caps["reducer.var"] && caps["reducer.dispatch"]) {
488
+ stateVars.push({
489
+ name: caps["reducer.var"],
490
+ setter: caps["reducer.dispatch"],
491
+ hook: "useReducer",
492
+ init: trunc(caps["reducer.init"] ?? "", 80)
493
+ });
336
494
  }
337
- if (ts.isCallExpression(node)) {
338
- const expr = node.expression;
339
- 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")) {
340
- const arg = node.arguments[0];
341
- if (arg) {
342
- const parsed = extractTargetFromExpression(arg);
343
- if (parsed) {
344
- navigations.push({ kind: "window-location", target: parsed.target, isTemplate: parsed.isTemplate });
345
- }
346
- }
347
- }
495
+ }
496
+ const conditions = [];
497
+ const condQuery = getQuery("deep/conditions");
498
+ const condMatches = condQuery.matches(root);
499
+ for (const m of condMatches) {
500
+ const caps = captureMap(m);
501
+ if (caps["cond.test"]) {
502
+ conditions.push({
503
+ kind: "if",
504
+ test: trunc(caps["cond.test"], 100),
505
+ consequence: caps["cond.consequence"] ? trunc(caps["cond.consequence"], 80) : void 0
506
+ });
507
+ }
508
+ if (caps["ternary.test"]) {
509
+ conditions.push({
510
+ kind: "ternary",
511
+ test: trunc(caps["ternary.test"], 100)
512
+ });
348
513
  }
349
- ts.forEachChild(node, visit);
350
514
  }
351
- visit(sourceFile);
352
- const name = defaultName ?? firstValueExport ?? firstTypeExport ?? "";
353
- return {
354
- name,
355
- exports: exportsOrdered,
356
- imports,
357
- reExports,
358
- jsxElements,
359
- navigations,
360
- fetchCalls
361
- };
362
- }
363
- var MUTATION_METHODS = /* @__PURE__ */ new Set([
364
- "create",
365
- "createMany",
366
- "createManyAndReturn",
367
- "update",
368
- "updateMany",
369
- "updateManyAndReturn",
370
- "upsert",
371
- "delete",
372
- "deleteMany"
373
- ]);
374
- function extractDbCalls(absPath) {
375
- const ts = getTs();
376
- const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
377
- const ext = (0, import_node_path2.extname)(absPath);
378
- const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
379
- const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
380
- const calls = [];
381
- const seen = /* @__PURE__ */ new Set();
382
- function visit(node) {
383
- if (ts.isCallExpression(node)) {
384
- const expr = node.expression;
385
- if (ts.isPropertyAccessExpression(expr) && ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.expression) && expr.expression.expression.text === "db") {
386
- const model = expr.expression.name.text;
387
- const method = expr.name.text;
388
- const key = `${model}.${method}`;
389
- if (!seen.has(key)) {
390
- seen.add(key);
391
- calls.push({
392
- model,
393
- method,
394
- isMutation: MUTATION_METHODS.has(method)
395
- });
396
- }
397
- }
515
+ const variables = [];
516
+ const varQuery = getQuery("deep/variables");
517
+ const varMatches = varQuery.matches(root);
518
+ for (const m of varMatches) {
519
+ const caps = captureMap(m);
520
+ const declNode = m.captures.find((c) => c.name === "var.decl" || c.name === "var.destructured" || c.name === "var.array")?.node;
521
+ const kind = declNode?.children.find((n) => n.type === "const" || n.type === "let" || n.type === "var")?.type ?? "const";
522
+ if (caps["var.name"] && caps["var.init"]) {
523
+ variables.push({
524
+ name: caps["var.name"],
525
+ kind,
526
+ init: trunc(caps["var.init"], 100)
527
+ });
528
+ }
529
+ if (caps["var.destructured.obj"]) {
530
+ variables.push({
531
+ name: trunc(caps["var.destructured.obj"], 60),
532
+ kind,
533
+ init: trunc(caps["var.destructured.init"], 100)
534
+ });
535
+ }
536
+ if (caps["var.array.pattern"]) {
537
+ variables.push({
538
+ name: trunc(caps["var.array.pattern"], 60),
539
+ kind,
540
+ init: trunc(caps["var.array.init"], 100)
541
+ });
398
542
  }
399
- ts.forEachChild(node, visit);
400
543
  }
401
- visit(sourceFile);
402
- return calls;
403
- }
404
- function extractAuthWrappers(absPath) {
405
- const ts = getTs();
406
- const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
407
- const ext = (0, import_node_path2.extname)(absPath);
408
- const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
409
- const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
410
- const wrappers = /* @__PURE__ */ new Set();
411
- const AUTH_WRAPPERS = /* @__PURE__ */ new Set(["withAuth", "withPermission", "withRole", "requireAuth"]);
412
- function visit(node) {
413
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
414
- if (AUTH_WRAPPERS.has(node.expression.text)) {
415
- wrappers.add(node.expression.text);
544
+ const responses = [];
545
+ const respQuery = getQuery("deep/responses");
546
+ const respMatches = respQuery.matches(root);
547
+ const explicitBodies = /* @__PURE__ */ new Set();
548
+ for (const m of respMatches) {
549
+ const caps = captureMap(m);
550
+ if (caps["resp.status"] && caps["resp.body"]) {
551
+ explicitBodies.add(trunc(caps["resp.body"], 80));
552
+ responses.push({
553
+ status: caps["resp.status"],
554
+ body: trunc(caps["resp.body"], 80)
555
+ });
556
+ }
557
+ }
558
+ for (const m of respMatches) {
559
+ const caps = captureMap(m);
560
+ if (caps["resp.body.default"] && !caps["resp.status"]) {
561
+ const bodyText = trunc(caps["resp.body.default"], 80);
562
+ if (!explicitBodies.has(bodyText)) {
563
+ responses.push({ status: "200", body: bodyText });
416
564
  }
417
565
  }
418
- ts.forEachChild(node, visit);
419
566
  }
420
- visit(sourceFile);
421
- return wrappers;
567
+ const params = [];
568
+ const paramQuery = getQuery("deep/request-params");
569
+ const paramMatches = paramQuery.matches(root);
570
+ for (const m of paramMatches) {
571
+ const caps = captureMap(m);
572
+ if (caps["param.name"]) {
573
+ params.push({ name: caps["param.name"], source: "body-field" });
574
+ }
575
+ if (caps["param.body"]) {
576
+ params.push({ name: caps["param.body"], source: "body" });
577
+ }
578
+ }
579
+ return { elements, stateVars, conditions, variables, responses, params };
422
580
  }
423
581
 
424
582
  // src/server/graph/parsers/ui/react-nextjs.ts
@@ -744,7 +902,7 @@ function generate(rootDir) {
744
902
  const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
745
903
  const parsedByPath = /* @__PURE__ */ new Map();
746
904
  for (const absPath of allDiscovered) {
747
- parsedByPath.set(absPath, parseFile(absPath));
905
+ parsedByPath.set(absPath, parseFileTS(absPath));
748
906
  }
749
907
  const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
750
908
  const fileSet = allDiscovered.filter((f) => !(0, import_node_path3.basename)(f).startsWith("index."));
@@ -758,7 +916,18 @@ function generate(rootDir) {
758
916
  const parsed = parsedByPath.get(absPath);
759
917
  const name = parsed.name || nameFromFilename(absPath);
760
918
  const route = extractRoute(id);
761
- nodes.push({ id, type, name, route, exports: parsed.exports });
919
+ const deep = extractDeep(absPath);
920
+ nodes.push({
921
+ id,
922
+ type,
923
+ name,
924
+ route,
925
+ exports: parsed.exports,
926
+ elements: deep.elements,
927
+ stateVars: deep.stateVars,
928
+ conditions: deep.conditions,
929
+ variables: deep.variables
930
+ });
762
931
  nodeIdSet.add(id);
763
932
  nodeTypeMap.set(id, type);
764
933
  if (route) routeToNodeId.set(route, id);
@@ -817,7 +986,7 @@ function generate(rootDir) {
817
986
  if (externalScanned.has(normalized)) continue;
818
987
  let parsed;
819
988
  try {
820
- parsed = parseFile(absPath);
989
+ parsed = parseFileTS(absPath);
821
990
  } catch {
822
991
  continue;
823
992
  }
@@ -941,7 +1110,7 @@ var reactNextjsParser = {
941
1110
  // src/server/graph/parsers/api/nextjs-routes.ts
942
1111
  var import_node_fs4 = require("node:fs");
943
1112
  var import_node_path4 = require("node:path");
944
- var HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
1113
+ var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
945
1114
  function walk2(dir) {
946
1115
  const results = [];
947
1116
  if (!(0, import_node_fs4.existsSync)(dir)) return results;
@@ -980,12 +1149,12 @@ function generate2(rootDir) {
980
1149
  let endpointsWithAuth = 0;
981
1150
  let endpointsWithDbAccess = 0;
982
1151
  for (const absPath of routeFiles) {
983
- const parsed = parseFile(absPath);
984
- const dbCalls = extractDbCalls(absPath);
985
- const authWrappers = extractAuthWrappers(absPath);
1152
+ const parsed = parseFileTS(absPath);
1153
+ const dbCalls = extractDbCallsTS(absPath);
1154
+ const authWrappers = extractAuthWrappersTS(absPath);
986
1155
  const methods = [];
987
1156
  for (const exp of parsed.exports) {
988
- if (HTTP_METHODS2.has(exp)) methods.push(exp);
1157
+ if (HTTP_METHODS.has(exp)) methods.push(exp);
989
1158
  }
990
1159
  const routePath = filePathToRoute(apiDir, absPath);
991
1160
  const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
@@ -1004,6 +1173,7 @@ function generate2(rootDir) {
1004
1173
  authUsage[w] = (authUsage[w] ?? 0) + 1;
1005
1174
  }
1006
1175
  if (authStrategy.length > 0) endpointsWithAuth++;
1176
+ const deep = extractDeep(absPath);
1007
1177
  nodes.push({
1008
1178
  id: relPath,
1009
1179
  type: "endpoint",
@@ -1015,7 +1185,12 @@ function generate2(rootDir) {
1015
1185
  mutates,
1016
1186
  auth: authStrategy.length > 0 ? authStrategy : ["public"],
1017
1187
  db_models: [...new Set(dbCalls.map((c) => c.model))],
1018
- db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))]
1188
+ db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
1189
+ // Deep extraction fields
1190
+ conditions: deep.conditions,
1191
+ variables: deep.variables,
1192
+ responses: deep.responses,
1193
+ params: deep.params
1019
1194
  });
1020
1195
  const seenModels = /* @__PURE__ */ new Set();
1021
1196
  for (const call of dbCalls) {
@@ -1055,7 +1230,7 @@ function generate2(rootDir) {
1055
1230
  flagged_edges: [],
1056
1231
  patterns: {
1057
1232
  total_endpoints: nodes.length,
1058
- methods_breakdown: [...HTTP_METHODS2].reduce((acc, m) => {
1233
+ methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
1059
1234
  acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
1060
1235
  return acc;
1061
1236
  }, {}),
@@ -1910,9 +2085,10 @@ function matchParts(pat, pi, id, ii) {
1910
2085
  while (pi < pat.length && pat[pi] === "**") pi++;
1911
2086
  return pi === pat.length && ii === id.length;
1912
2087
  }
1913
- var CONVENTION_DIRS = ["features", "modules", "domains", "areas"];
1914
- function detectConventionDirs(rootDir) {
2088
+ var CONVENTION_DIRS_BUILTIN = ["features", "modules", "domains", "areas"];
2089
+ function detectConventionDirs(rootDir, extraConventionDirs = []) {
1915
2090
  const result = /* @__PURE__ */ new Map();
2091
+ const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
1916
2092
  const searchDirs = [
1917
2093
  rootDir,
1918
2094
  (0, import_node_path10.join)(rootDir, "src"),
@@ -1920,7 +2096,7 @@ function detectConventionDirs(rootDir) {
1920
2096
  (0, import_node_path10.join)(rootDir, "lib")
1921
2097
  ];
1922
2098
  for (const base of searchDirs) {
1923
- for (const convention of CONVENTION_DIRS) {
2099
+ for (const convention of conventionDirs) {
1924
2100
  const dir = (0, import_node_path10.join)(base, convention);
1925
2101
  if (!(0, import_node_fs9.existsSync)(dir)) continue;
1926
2102
  try {
@@ -1946,7 +2122,76 @@ function extractRouteGroups(id) {
1946
2122
  }
1947
2123
  return groups;
1948
2124
  }
1949
- var SKIP_SEGMENTS = /* @__PURE__ */ new Set([
2125
+ var GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
2126
+ // JS/TS
2127
+ "components",
2128
+ "hooks",
2129
+ "pages",
2130
+ "views",
2131
+ "screens",
2132
+ "layouts",
2133
+ "utils",
2134
+ "helpers",
2135
+ "lib",
2136
+ "libs",
2137
+ "services",
2138
+ "api",
2139
+ "apis",
2140
+ "stores",
2141
+ "state",
2142
+ "store",
2143
+ "context",
2144
+ "contexts",
2145
+ "providers",
2146
+ "types",
2147
+ "interfaces",
2148
+ "models",
2149
+ "schemas",
2150
+ "constants",
2151
+ "config",
2152
+ "configs",
2153
+ "assets",
2154
+ "styles",
2155
+ "public",
2156
+ "middleware",
2157
+ "middlewares",
2158
+ "routes",
2159
+ "router",
2160
+ "tests",
2161
+ "test",
2162
+ "__tests__",
2163
+ "spec",
2164
+ "specs",
2165
+ // Go
2166
+ "cmd",
2167
+ "pkg",
2168
+ "internal",
2169
+ // Python
2170
+ "management",
2171
+ "migrations",
2172
+ "templatetags",
2173
+ "templates",
2174
+ // Java
2175
+ "controller",
2176
+ "controllers",
2177
+ "service",
2178
+ "repository",
2179
+ "repositories",
2180
+ "entity",
2181
+ "entities",
2182
+ "dto",
2183
+ "dtos",
2184
+ // General
2185
+ "shared",
2186
+ "common",
2187
+ "core",
2188
+ "base",
2189
+ "app",
2190
+ // Next.js specific
2191
+ "client",
2192
+ "server"
2193
+ ]);
2194
+ var SKIP_SEGMENTS_BUILTIN = /* @__PURE__ */ new Set([
1950
2195
  "src",
1951
2196
  "app",
1952
2197
  "client",
@@ -2004,9 +2249,13 @@ function isTrivialGroup(name, extraTrivial) {
2004
2249
  function normalizeGroupName(name) {
2005
2250
  return name.toLowerCase().replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
2006
2251
  }
2007
- function extractModuleFromPath(id, extraTrivial) {
2252
+ function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
2008
2253
  const segments = id.split("/");
2009
2254
  const routeGroups = extractRouteGroups(id);
2255
+ const skipSegments = new Set(SKIP_SEGMENTS_BUILTIN);
2256
+ if (extraSkipSegments) {
2257
+ for (const s of extraSkipSegments) skipSegments.add(s);
2258
+ }
2010
2259
  const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g, extraTrivial)).map(normalizeGroupName);
2011
2260
  if (moduleGroups.length > 0) {
2012
2261
  return moduleGroups[moduleGroups.length - 1];
@@ -2017,7 +2266,7 @@ function extractModuleFromPath(id, extraTrivial) {
2017
2266
  if (isRouteGroup(seg)) continue;
2018
2267
  if (isDynamicSegment(seg)) continue;
2019
2268
  if (isDomainDir(seg)) continue;
2020
- if (SKIP_SEGMENTS.has(seg)) continue;
2269
+ if (skipSegments.has(seg)) continue;
2021
2270
  meaningful.push(seg);
2022
2271
  }
2023
2272
  if (meaningful.length > 0) {
@@ -2034,12 +2283,10 @@ var moduleTagger = {
2034
2283
  layers: null,
2035
2284
  // applies to all layers
2036
2285
  tag(nodes, layer, rootDir) {
2037
- if (cachedRootDir !== rootDir) {
2038
- cachedConventionDirs = detectConventionDirs(rootDir);
2039
- cachedRootDir = rootDir;
2040
- }
2041
2286
  let configRules = [];
2042
2287
  let extraTrivial;
2288
+ let extraSkipSegments;
2289
+ let extraConventionDirs = [];
2043
2290
  try {
2044
2291
  const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
2045
2292
  const config = loadConfig2(rootDir);
@@ -2048,8 +2295,21 @@ var moduleTagger = {
2048
2295
  if (trivialFromConfig?.length) {
2049
2296
  extraTrivial = new Set(trivialFromConfig);
2050
2297
  }
2298
+ const skipFromConfig = config.taggers?.module?.skipSegments;
2299
+ if (skipFromConfig?.length) {
2300
+ extraSkipSegments = new Set(skipFromConfig);
2301
+ }
2302
+ extraConventionDirs = config.taggers?.module?.conventionDirs ?? [];
2303
+ const roleNamesFromConfig = config.taggers?.module?.genericRoleNames;
2304
+ if (roleNamesFromConfig?.length) {
2305
+ for (const name of roleNamesFromConfig) GENERIC_ROLE_NAMES_BUILTIN.add(name);
2306
+ }
2051
2307
  } catch {
2052
2308
  }
2309
+ if (cachedRootDir !== rootDir) {
2310
+ cachedConventionDirs = detectConventionDirs(rootDir, extraConventionDirs);
2311
+ cachedRootDir = rootDir;
2312
+ }
2053
2313
  const result = /* @__PURE__ */ new Map();
2054
2314
  for (const node of nodes) {
2055
2315
  const id = node.id;
@@ -2075,7 +2335,7 @@ var moduleTagger = {
2075
2335
  }
2076
2336
  }
2077
2337
  if (matched) continue;
2078
- const module2 = extractModuleFromPath(id, extraTrivial);
2338
+ const module2 = extractModuleFromPath(id, extraTrivial, extraSkipSegments);
2079
2339
  result.set(id, module2);
2080
2340
  }
2081
2341
  return result;
@@ -2303,7 +2563,13 @@ function readAllGraphs(rootDir) {
2303
2563
  }
2304
2564
  return result;
2305
2565
  }
2306
- function generateGraph(rootDir, layer) {
2566
+ async function generateGraph(rootDir, layer) {
2567
+ await initTreeSitter();
2568
+ const config = loadConfig(rootDir);
2569
+ setExtractorConfig({
2570
+ dbIdentifiers: config.parsers?.patterns?.dbIdentifiers,
2571
+ mutationMethods: config.parsers?.patterns?.mutationMethods
2572
+ });
2307
2573
  const dir = graphsDir(rootDir);
2308
2574
  (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
2309
2575
  const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
@@ -2443,10 +2709,10 @@ function findProjectRoot(startDir) {
2443
2709
  }
2444
2710
  return startDir;
2445
2711
  }
2446
- function buildMergedGraph(projectRoot) {
2712
+ async function buildMergedGraph(projectRoot) {
2447
2713
  let graphs = readAllGraphs(projectRoot);
2448
2714
  if (!graphs.ui && !graphs.api && !graphs.db) {
2449
- generateGraph(projectRoot);
2715
+ await generateGraph(projectRoot);
2450
2716
  graphs = readAllGraphs(projectRoot);
2451
2717
  }
2452
2718
  const nodes = [];
@@ -2556,13 +2822,18 @@ async function startChartServer(opts = {}) {
2556
2822
  const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
2557
2823
  if (req.method === "GET" && url2.pathname === "/api/project-graph") {
2558
2824
  const regenerate = url2.searchParams.get("regenerate") === "1";
2559
- if (regenerate) generateGraph(projectRoot);
2560
- const merged = buildMergedGraph(projectRoot);
2561
- res.writeHead(200, { "Content-Type": "application/json" });
2562
- res.end(JSON.stringify({
2563
- ...merged,
2564
- debug: { cwd, projectRoot }
2565
- }));
2825
+ (async () => {
2826
+ if (regenerate) await generateGraph(projectRoot);
2827
+ const merged = await buildMergedGraph(projectRoot);
2828
+ res.writeHead(200, { "Content-Type": "application/json" });
2829
+ res.end(JSON.stringify({
2830
+ ...merged,
2831
+ debug: { cwd, projectRoot }
2832
+ }));
2833
+ })().catch((e) => {
2834
+ res.writeHead(500);
2835
+ res.end(String(e));
2836
+ });
2566
2837
  return;
2567
2838
  }
2568
2839
  if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
@@ -2572,8 +2843,8 @@ async function startChartServer(opts = {}) {
2572
2843
  return;
2573
2844
  }
2574
2845
  if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
2575
- try {
2576
- generateGraph(projectRoot);
2846
+ (async () => {
2847
+ await generateGraph(projectRoot);
2577
2848
  const graphs = readAllGraphs(projectRoot);
2578
2849
  res.writeHead(200, { "Content-Type": "application/json" });
2579
2850
  res.end(JSON.stringify({
@@ -2582,10 +2853,10 @@ async function startChartServer(opts = {}) {
2582
2853
  api: graphs.api ?? null,
2583
2854
  db: graphs.db ?? null
2584
2855
  }));
2585
- } catch (err) {
2856
+ })().catch((err) => {
2586
2857
  res.writeHead(500, { "Content-Type": "application/json" });
2587
2858
  res.end(JSON.stringify({ ok: false, error: String(err) }));
2588
- }
2859
+ });
2589
2860
  return;
2590
2861
  }
2591
2862
  if (req.method === "GET" && url2.pathname === "/api/file-content") {
@@ -2614,7 +2885,7 @@ async function startChartServer(opts = {}) {
2614
2885
  return;
2615
2886
  }
2616
2887
  if (req.method === "GET" && url2.pathname === "/api/parser-config") {
2617
- const config = loadConfig(projectRoot);
2888
+ const config2 = loadConfig(projectRoot);
2618
2889
  const detection = [
2619
2890
  { id: "react-nextjs", layer: "ui", label: "React + Next.js", detected: reactNextjsParser.detect(projectRoot) },
2620
2891
  { id: "nextjs-routes", layer: "api", label: "Next.js API Routes", detected: nextjsRoutesParser.detect(projectRoot) },
@@ -2626,7 +2897,7 @@ async function startChartServer(opts = {}) {
2626
2897
  { id: "url-literal-scanner", label: "/api/... URL literals" }
2627
2898
  ];
2628
2899
  res.writeHead(200, { "Content-Type": "application/json" });
2629
- res.end(JSON.stringify({ config, detection, crosslayerParsers }));
2900
+ res.end(JSON.stringify({ config: config2, detection, crosslayerParsers }));
2630
2901
  return;
2631
2902
  }
2632
2903
  if (req.method === "POST" && url2.pathname === "/api/parser-config") {
@@ -2649,14 +2920,14 @@ async function startChartServer(opts = {}) {
2649
2920
  return;
2650
2921
  }
2651
2922
  if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
2652
- const config = loadConfig(projectRoot);
2923
+ const config2 = loadConfig(projectRoot);
2653
2924
  const builtinTaggers = [
2654
- { id: "module", tagKey: "module", trackUntagged: config.taggers?.trackUntagged?.module ?? true },
2655
- { id: "screen", tagKey: "screen", trackUntagged: config.taggers?.trackUntagged?.screen ?? true }
2925
+ { id: "module", tagKey: "module", trackUntagged: config2.taggers?.trackUntagged?.module ?? true },
2926
+ { id: "screen", tagKey: "screen", trackUntagged: config2.taggers?.trackUntagged?.screen ?? true }
2656
2927
  ];
2657
- const disabled = config.taggers?.disabled ?? [];
2658
- const customTaggers = config.taggers?.custom ?? [];
2659
- const moduleRules = config.taggers?.module?.rules ?? [];
2928
+ const disabled = config2.taggers?.disabled ?? [];
2929
+ const customTaggers = config2.taggers?.custom ?? [];
2930
+ const moduleRules = config2.taggers?.module?.rules ?? [];
2660
2931
  res.writeHead(200, { "Content-Type": "application/json" });
2661
2932
  res.end(JSON.stringify({ builtinTaggers, disabled, customTaggers, moduleRules }));
2662
2933
  return;
@@ -2669,10 +2940,10 @@ async function startChartServer(opts = {}) {
2669
2940
  req.on("end", () => {
2670
2941
  try {
2671
2942
  const taggerConfig = JSON.parse(body);
2672
- const config = loadConfig(projectRoot);
2673
- config.taggers = taggerConfig;
2943
+ const config2 = loadConfig(projectRoot);
2944
+ config2.taggers = taggerConfig;
2674
2945
  const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
2675
- import_node_fs13.default.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2946
+ import_node_fs13.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
2676
2947
  res.writeHead(200, { "Content-Type": "application/json" });
2677
2948
  res.end(JSON.stringify({ ok: true }));
2678
2949
  } catch (err) {
@@ -2744,7 +3015,8 @@ async function startChartServer(opts = {}) {
2744
3015
  res.end(JSON.stringify({ error: String(err) }));
2745
3016
  }
2746
3017
  });
2747
- const startPort = opts.port ?? randomPort();
3018
+ const config = loadConfig(projectRoot);
3019
+ const startPort = opts.port ?? config.port ?? randomPort();
2748
3020
  const port = await bindWithFallback(server, startPort);
2749
3021
  const url = `http://localhost:${port}`;
2750
3022
  writeLock({