@launchsecure/launch-kit 0.0.21 → 0.0.22

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.
@@ -285,6 +285,12 @@ function parseFileTS(absPath) {
285
285
  if (linkTemplate) {
286
286
  navigations.push({ kind: "link-href", target: linkTemplate, isTemplate: true });
287
287
  }
288
+ if (caps["nav.redirect.literal"]) {
289
+ navigations.push({ kind: "router-replace", target: caps["nav.redirect.literal"], isTemplate: false });
290
+ }
291
+ if (caps["nav.redirect.template"]) {
292
+ navigations.push({ kind: "router-replace", target: caps["nav.redirect.template"], isTemplate: true });
293
+ }
288
294
  if (caps["nav.window.literal"]) {
289
295
  navigations.push({ kind: "window-location", target: caps["nav.window.literal"], isTemplate: false });
290
296
  }
@@ -363,15 +369,25 @@ function extractDbCallsTS(absPath) {
363
369
  const seen = /* @__PURE__ */ new Set();
364
370
  for (const m of matches) {
365
371
  const caps = captureMap(m);
372
+ const sbTable = caps["sb.table"];
373
+ const sbMethod = caps["sb.method"];
374
+ if (sbTable && sbMethod) {
375
+ const key2 = `sql:${sbTable}.${sbMethod}`;
376
+ if (seen.has(key2)) continue;
377
+ seen.add(key2);
378
+ const isMutation = SUPABASE_MUTATION_METHODS_BUILTIN.has(sbMethod) || extraMutationMethods.includes(sbMethod);
379
+ calls.push({ model: sbTable, method: sbMethod, isMutation, kind: "sql" });
380
+ continue;
381
+ }
366
382
  const identifier = caps["db.identifier"];
367
383
  const model = caps["db.model"];
368
384
  const method = caps["db.method"];
369
385
  if (!identifier || !model || !method) continue;
370
386
  if (!dbIdentifiers.has(identifier)) continue;
371
- const key = `${model}.${method}`;
387
+ const key = `orm:${model}.${method}`;
372
388
  if (seen.has(key)) continue;
373
389
  seen.add(key);
374
- calls.push({ model, method, isMutation: getMutationMethods().has(method) });
390
+ calls.push({ model, method, isMutation: getMutationMethods().has(method), kind: "orm" });
375
391
  }
376
392
  return calls;
377
393
  }
@@ -385,6 +401,9 @@ function classifyFile(absPath) {
385
401
  const captures = classifyQuery.captures(root);
386
402
  const capNames = new Set(captures.map((c) => c.name));
387
403
  if (capNames.has("http_export") || capNames.has("http_export_fn")) return "endpoint";
404
+ if (capNames.has("use_server_directive") && fileName !== "page.tsx" && fileName !== "layout.tsx") {
405
+ return "server-action";
406
+ }
388
407
  if (fileName === "page.tsx" && capNames.has("has_jsx")) return "page";
389
408
  if (fileName === "layout.tsx" && capNames.has("has_jsx")) return "layout";
390
409
  if (capNames.has("has_create_context") || capNames.has("has_create_context_bare")) return "context";
@@ -405,6 +424,36 @@ function extractAuthWrappersTS(absPath) {
405
424
  wrappers.add(caps["wrapper.fn_name"]);
406
425
  }
407
426
  }
427
+ const inlineHelpers = /* @__PURE__ */ new Set();
428
+ for (const stmt of childrenOfType(root, "import_statement")) {
429
+ const sourceNode = childOfType(stmt, "string");
430
+ const frag = sourceNode ? childOfType(sourceNode, "string_fragment") : void 0;
431
+ if (!frag) continue;
432
+ const provider = INLINE_AUTH_IMPORTS.find((p) => p.module.test(frag.text));
433
+ if (!provider) continue;
434
+ const clause = childOfType(stmt, "import_clause");
435
+ if (!clause) continue;
436
+ const named = childOfType(clause, "named_imports");
437
+ if (!named) continue;
438
+ for (const specNode of childrenOfType(named, "import_specifier")) {
439
+ const ids = childrenOfType(specNode, "identifier");
440
+ const importedName = ids[0]?.text;
441
+ const localName = ids[ids.length - 1]?.text;
442
+ if (importedName && provider.helpers.includes(importedName) && localName) {
443
+ inlineHelpers.add(localName);
444
+ }
445
+ }
446
+ }
447
+ if (inlineHelpers.size > 0) {
448
+ const text = root.text;
449
+ for (const name of inlineHelpers) {
450
+ const re = new RegExp(`\\b${name}\\s*\\(`);
451
+ if (re.test(text)) {
452
+ wrappers.add("inline");
453
+ break;
454
+ }
455
+ }
456
+ }
408
457
  return wrappers;
409
458
  }
410
459
  function trunc(s, max = 120) {
@@ -563,7 +612,7 @@ function extractDeep(absPath) {
563
612
  }
564
613
  return { elements, stateVars, conditions, variables, responses, params };
565
614
  }
566
- var import_node_fs4, import_node_path4, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods;
615
+ var import_node_fs4, import_node_path4, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, SUPABASE_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods, INLINE_AUTH_IMPORTS;
567
616
  var init_ts_extractor = __esm({
568
617
  "src/server/graph/core/ts-extractor.ts"() {
569
618
  "use strict";
@@ -587,9 +636,21 @@ var init_ts_extractor = __esm({
587
636
  "delete",
588
637
  "deleteMany"
589
638
  ];
639
+ SUPABASE_MUTATION_METHODS_BUILTIN = /* @__PURE__ */ new Set([
640
+ "insert",
641
+ "update",
642
+ "delete",
643
+ "upsert"
644
+ ]);
590
645
  DB_IDENTIFIERS_FALLBACK = ["db", "prisma"];
591
646
  extraDbIdentifiers = [];
592
647
  extraMutationMethods = [];
648
+ INLINE_AUTH_IMPORTS = [
649
+ { module: /^@clerk\/nextjs(\/server)?$/, helpers: ["auth", "currentUser"] },
650
+ { module: /^next-auth(\/.+)?$/, helpers: ["auth", "getServerSession"] },
651
+ { module: /^@auth\//, helpers: ["auth"] },
652
+ { module: /^@supabase\/auth-helpers/, helpers: ["createServerClient"] }
653
+ ];
593
654
  }
594
655
  });
595
656
 
@@ -860,6 +921,7 @@ init_ts_extractor();
860
921
  var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
861
922
  var CLASSIFICATION_TO_LAYER = {
862
923
  endpoint: "api",
924
+ "server-action": "api",
863
925
  page: "ui",
864
926
  layout: "ui",
865
927
  component: "ui",
@@ -958,6 +1020,8 @@ function extractRoute(id) {
958
1020
  if (!id.endsWith("/page.tsx")) return null;
959
1021
  let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
960
1022
  route = route.replace(/\/\([^)]+\)/g, "");
1023
+ route = route.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "*$1?");
1024
+ route = route.replace(/\[\.\.\.([^\]]+)\]/g, "*$1");
961
1025
  route = route.replace(/\[([^\]]+)\]/g, ":$1");
962
1026
  route = route.replace(/\/+/g, "/");
963
1027
  if (!route.startsWith("/")) route = "/" + route;
@@ -969,6 +1033,7 @@ function nameFromFilename(absPath) {
969
1033
  function filePathToAppRoute(appDir, absPath) {
970
1034
  let route = ("/" + (0, import_node_path5.relative)(appDir, absPath).replace(/\\/g, "/")).replace(/\/route\.tsx?$/, "");
971
1035
  route = route.replace(/\/\([^)]+\)/g, "");
1036
+ route = route.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "*$1?");
972
1037
  route = route.replace(/\[\.\.\.([^\]]+)\]/g, "*$1");
973
1038
  route = route.replace(/\[([^\]]+)\]/g, ":$1");
974
1039
  route = route.replace(/\/+/g, "/");
@@ -1017,25 +1082,52 @@ function resolveTemplateLiteralRoute(template, routeToNodeId) {
1017
1082
  function routeMatchScore(candidate, known) {
1018
1083
  const segsA = candidate.split("/");
1019
1084
  const segsB = known.split("/");
1020
- if (segsA.length !== segsB.length) return -1;
1021
1085
  let score = 0;
1022
- for (let i = 0; i < segsA.length; i++) {
1023
- const a = segsA[i], b = segsB[i];
1086
+ let i = 0, j = 0;
1087
+ while (i < segsA.length && j < segsB.length) {
1088
+ const a = segsA[i], b = segsB[j];
1089
+ if (b.startsWith("*") && b.endsWith("?")) {
1090
+ score += 1;
1091
+ return score;
1092
+ }
1093
+ if (b.startsWith("*")) {
1094
+ const remaining = segsA.length - i;
1095
+ if (remaining < 1) return -1;
1096
+ score += 1 + remaining;
1097
+ return score;
1098
+ }
1024
1099
  if (a === b) {
1025
1100
  score += 3;
1101
+ i++;
1102
+ j++;
1026
1103
  continue;
1027
1104
  }
1028
1105
  if (a.startsWith(":") && b.startsWith(":")) {
1029
1106
  score += 2;
1107
+ i++;
1108
+ j++;
1030
1109
  continue;
1031
1110
  }
1032
1111
  if (a.startsWith(":") || b.startsWith(":")) {
1033
- score += 0;
1112
+ i++;
1113
+ j++;
1034
1114
  continue;
1035
1115
  }
1036
1116
  return -1;
1037
1117
  }
1038
- return score;
1118
+ if (i === segsA.length) {
1119
+ while (j < segsB.length) {
1120
+ const b = segsB[j];
1121
+ if (b.startsWith("*") && b.endsWith("?")) {
1122
+ score += 1;
1123
+ j++;
1124
+ continue;
1125
+ }
1126
+ return -1;
1127
+ }
1128
+ return score;
1129
+ }
1130
+ return -1;
1039
1131
  }
1040
1132
  function templateToRoute(template) {
1041
1133
  return template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
@@ -1070,7 +1162,7 @@ function extractEdges(srcDir, rootDir, absPath, sourceId, parsed, nodeIdSet, bar
1070
1162
  edges.push(edge);
1071
1163
  }
1072
1164
  function edgeTypeFor(isTypeOnlyImport, importedNames) {
1073
- if (isTypeOnlyImport) return "imports";
1165
+ if (isTypeOnlyImport) return "imports_type";
1074
1166
  const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
1075
1167
  if (anyRendered) return "renders";
1076
1168
  return "imports";
@@ -1101,7 +1193,8 @@ function extractEdges(srcDir, rootDir, absPath, sourceId, parsed, nodeIdSet, bar
1101
1193
  if (resolved) {
1102
1194
  const targetId = toNodeId(srcDir, rootDir, resolved);
1103
1195
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
1104
- addEdge(targetId, edgeTypeFor(isTypeOnly, names));
1196
+ const allType = isTypeOnly || names.length > 0 && names.every((n) => typeNames.has(n));
1197
+ addEdge(targetId, edgeTypeFor(allType, names));
1105
1198
  }
1106
1199
  }
1107
1200
  }
@@ -1110,7 +1203,8 @@ function extractEdges(srcDir, rootDir, absPath, sourceId, parsed, nodeIdSet, bar
1110
1203
  if (resolved) {
1111
1204
  const targetId = toNodeId(srcDir, rootDir, resolved);
1112
1205
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
1113
- addEdge(targetId, edgeTypeFor(isTypeOnly, names));
1206
+ const allType = isTypeOnly || names.length > 0 && names.every((n) => typeNames.has(n));
1207
+ addEdge(targetId, edgeTypeFor(allType, names));
1114
1208
  }
1115
1209
  }
1116
1210
  }
@@ -1195,26 +1289,34 @@ function generate(rootDir) {
1195
1289
  const layer = CLASSIFICATION_TO_LAYER[type] ?? "ui";
1196
1290
  nodeIdSet.add(id);
1197
1291
  if (layer === "api") {
1198
- const methods = [];
1199
- for (const exp of parsed.exports) {
1200
- if (HTTP_METHODS.has(exp)) methods.push(exp);
1201
- }
1202
1292
  const dbCalls = extractDbCallsTS(absPath);
1203
1293
  const authWrappers = extractAuthWrappersTS(absPath);
1204
1294
  const deep = extractDeep(absPath);
1205
- const routePath = filePathToAppRoute(paths.appDir, absPath);
1206
1295
  const mutations = dbCalls.filter((c) => c.isMutation);
1207
1296
  const mutates = mutations.length > 0;
1208
1297
  const authStrategy = [...authWrappers];
1298
+ const isServerAction = type === "server-action";
1299
+ const methods = [];
1300
+ if (!isServerAction) {
1301
+ for (const exp of parsed.exports) {
1302
+ if (HTTP_METHODS.has(exp)) methods.push(exp);
1303
+ }
1304
+ }
1305
+ const routePath = isServerAction ? null : filePathToAppRoute(paths.appDir, absPath);
1306
+ const actions = isServerAction ? parsed.exports.filter((e) => !HTTP_METHODS.has(e)) : [];
1209
1307
  apiNodes.push({
1210
1308
  id,
1211
- type: "endpoint",
1212
- name: routePath,
1309
+ type: isServerAction ? "server-action" : "endpoint",
1310
+ // For HTTP routes: name = URL path. For Server Actions: name = file id +
1311
+ // exported action names — the callable surface, not a URL.
1312
+ name: isServerAction ? actions.length > 0 ? `${id} (${actions.join(", ")})` : id : routePath,
1213
1313
  layer: "api",
1214
1314
  path: routePath,
1315
+ // null for Server Actions
1215
1316
  methods,
1216
1317
  handler: id,
1217
1318
  mutates,
1319
+ ...isServerAction ? { actions } : {},
1218
1320
  auth: authStrategy.length > 0 ? authStrategy : ["public"],
1219
1321
  db_models: [...new Set(dbCalls.map((c) => c.model))],
1220
1322
  db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
@@ -1228,6 +1330,8 @@ function generate(rootDir) {
1228
1330
  } else {
1229
1331
  const route = extractRoute(id);
1230
1332
  const deep = extractDeep(absPath);
1333
+ const dbCalls = extractDbCallsTS(absPath);
1334
+ const authWrappers = type === "page" || type === "layout" ? [...extractAuthWrappersTS(absPath)] : [];
1231
1335
  uiNodes.push({
1232
1336
  id,
1233
1337
  type,
@@ -1238,7 +1342,9 @@ function generate(rootDir) {
1238
1342
  elements: deep.elements,
1239
1343
  stateVars: deep.stateVars,
1240
1344
  conditions: deep.conditions,
1241
- variables: deep.variables
1345
+ variables: deep.variables,
1346
+ ...authWrappers.length > 0 ? { auth: authWrappers } : {},
1347
+ ...dbCalls.length > 0 ? { _dbCalls: dbCalls } : {}
1242
1348
  });
1243
1349
  if (route) routeToNodeId.set(route, id);
1244
1350
  }
@@ -1262,6 +1368,29 @@ function generate(rootDir) {
1262
1368
  uiEdges.push(...edges);
1263
1369
  uiFlagged.push(...flagged);
1264
1370
  }
1371
+ const layoutsById = /* @__PURE__ */ new Set();
1372
+ for (const n of uiNodes) {
1373
+ if (n.type === "layout") layoutsById.add(n.id);
1374
+ }
1375
+ function findClosestLayout(pageId) {
1376
+ let dir = pageId.replace(/\/page\.tsx$/, "");
1377
+ while (dir.length > 0) {
1378
+ const candidate = `${dir}/layout.tsx`;
1379
+ if (layoutsById.has(candidate)) return candidate;
1380
+ const slash = dir.lastIndexOf("/");
1381
+ if (slash < 0) break;
1382
+ dir = dir.slice(0, slash);
1383
+ }
1384
+ if (layoutsById.has("app/layout.tsx")) return "app/layout.tsx";
1385
+ return null;
1386
+ }
1387
+ for (const n of uiNodes) {
1388
+ if (n.type !== "page") continue;
1389
+ const layoutId = findClosestLayout(n.id);
1390
+ if (layoutId && layoutId !== n.id) {
1391
+ uiEdges.push({ source: layoutId, target: n.id, type: "wraps" });
1392
+ }
1393
+ }
1265
1394
  const fetchCallEntries = [];
1266
1395
  for (const absPath of fileSet) {
1267
1396
  const sourceId = toNodeId(srcDir, rootDir, absPath);
@@ -1343,17 +1472,37 @@ function generate(rootDir) {
1343
1472
  if (!dbCalls) continue;
1344
1473
  const seenModels = /* @__PURE__ */ new Set();
1345
1474
  for (const call of dbCalls) {
1346
- if (seenModels.has(call.model)) continue;
1347
- seenModels.add(call.model);
1475
+ const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
1476
+ if (seenModels.has(target)) continue;
1477
+ seenModels.add(target);
1348
1478
  apiCrossRefs.push({
1349
1479
  source: node.id,
1350
- target: camelToPascal(call.model),
1480
+ target,
1351
1481
  type: call.isMutation ? "mutates" : "reads",
1352
1482
  layer: "db"
1353
1483
  });
1354
1484
  }
1355
1485
  delete node._dbCalls;
1356
1486
  }
1487
+ const uiCrossRefs = [];
1488
+ for (const node of uiNodes) {
1489
+ const dbCalls = node._dbCalls;
1490
+ if (!dbCalls) continue;
1491
+ const seenModels = /* @__PURE__ */ new Set();
1492
+ for (const call of dbCalls) {
1493
+ const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
1494
+ if (seenModels.has(target)) continue;
1495
+ seenModels.add(target);
1496
+ uiCrossRefs.push({
1497
+ source: node.id,
1498
+ target,
1499
+ type: call.isMutation ? "mutates" : "reads",
1500
+ layer: "db"
1501
+ });
1502
+ }
1503
+ delete node._dbCalls;
1504
+ }
1505
+ uiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1357
1506
  const apiNodeIds = new Set(apiNodes.map((n) => n.id));
1358
1507
  const apiEdges = [];
1359
1508
  const uiOnlyEdges = [];
@@ -1415,7 +1564,7 @@ function generate(rootDir) {
1415
1564
  },
1416
1565
  nodes: stripLayer(uiNodes),
1417
1566
  edges: uiOnlyEdges,
1418
- cross_refs: [],
1567
+ cross_refs: uiCrossRefs,
1419
1568
  contradictions: [],
1420
1569
  warnings: [],
1421
1570
  flagged_edges: dedupedFlagged,
@@ -2261,26 +2410,54 @@ function normalizeFetchUrl(raw) {
2261
2410
  return { path: s || "/", hadInterpolation };
2262
2411
  }
2263
2412
  function scoreApiRouteMatch(candidate, known) {
2264
- if (candidate.length !== known.length) return -1;
2265
2413
  let score = 0;
2266
- for (let i = 0; i < candidate.length; i++) {
2414
+ let i = 0, j = 0;
2415
+ while (i < candidate.length && j < known.length) {
2267
2416
  const a = candidate[i];
2268
- const b = known[i];
2417
+ const b = known[j];
2418
+ if (b.startsWith("*") && b.endsWith("?")) {
2419
+ score += 1;
2420
+ return score;
2421
+ }
2422
+ if (b.startsWith("*")) {
2423
+ const remaining = candidate.length - i;
2424
+ if (remaining < 1) return -1;
2425
+ score += 1 + remaining;
2426
+ return score;
2427
+ }
2269
2428
  if (a === b) {
2270
2429
  score += 3;
2430
+ i++;
2431
+ j++;
2271
2432
  continue;
2272
2433
  }
2273
2434
  if (a.startsWith(":") && b.startsWith(":")) {
2274
2435
  score += 2;
2436
+ i++;
2437
+ j++;
2275
2438
  continue;
2276
2439
  }
2277
2440
  if (a.startsWith(":") || b.startsWith(":")) {
2278
2441
  score += 1;
2442
+ i++;
2443
+ j++;
2279
2444
  continue;
2280
2445
  }
2281
2446
  return -1;
2282
2447
  }
2283
- return score;
2448
+ if (i === candidate.length) {
2449
+ while (j < known.length) {
2450
+ const b = known[j];
2451
+ if (b.startsWith("*") && b.endsWith("?")) {
2452
+ score += 1;
2453
+ j++;
2454
+ continue;
2455
+ }
2456
+ return -1;
2457
+ }
2458
+ return score;
2459
+ }
2460
+ return -1;
2284
2461
  }
2285
2462
  function resolveFetchCall(call, apiPathMap, apiRoutes) {
2286
2463
  const raw = call.url;
@@ -2437,48 +2614,58 @@ var fetchResolverParser = {
2437
2614
  // src/server/graph/parsers/crosslayer/api-annotations.ts
2438
2615
  var import_node_fs8 = require("node:fs");
2439
2616
  var import_node_path7 = require("node:path");
2617
+ init_config();
2440
2618
  var API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
2441
- function walk2(dir, exts) {
2442
- if (!(0, import_node_fs8.existsSync)(dir)) return [];
2443
- const results = [];
2444
- for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
2445
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2446
- const full = (0, import_node_path7.join)(dir, entry.name);
2447
- if (entry.isDirectory()) {
2448
- results.push(...walk2(full, exts));
2449
- } else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
2450
- results.push(full);
2451
- }
2619
+ function toNodeId2(srcDir, rootDir, absPath) {
2620
+ const relFromSrc = (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
2621
+ if (relFromSrc.startsWith("..")) {
2622
+ return (0, import_node_path7.relative)(rootDir, absPath).replace(/\\/g, "/");
2452
2623
  }
2453
- return results;
2454
- }
2455
- function toNodeId2(srcDir, absPath) {
2456
- return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
2624
+ return relFromSrc;
2457
2625
  }
2458
2626
  var apiAnnotationsParser = {
2459
2627
  id: "api-annotations",
2460
2628
  layer: "crosslayer",
2461
2629
  concern: "api-binding",
2462
2630
  detect(rootDir) {
2463
- return (0, import_node_fs8.existsSync)((0, import_node_path7.join)(rootDir, "src"));
2631
+ return resolveProjectPaths(rootDir, loadConfig(rootDir)) !== null;
2464
2632
  },
2465
2633
  generate(rootDir, layerOutputs) {
2466
2634
  const apiOutput = layerOutputs.get("api");
2467
2635
  if (!apiOutput) {
2468
2636
  return { cross_refs: [], flagged_edges: [], warnings: [] };
2469
2637
  }
2638
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2639
+ if (!paths) {
2640
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
2641
+ }
2470
2642
  const uiOutput = layerOutputs.get("ui");
2471
2643
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
2472
2644
  const apiRoutes = loadApiRoutesFromOutput(apiOutput);
2473
2645
  const apiPathMap = buildApiPathMap(apiRoutes);
2474
- const srcDir = (0, import_node_path7.join)(rootDir, "src");
2475
- const files = walk2(srcDir, [".ts", ".tsx"]);
2646
+ const srcDir = paths.srcDir;
2647
+ const seen = /* @__PURE__ */ new Set();
2648
+ const files = [];
2649
+ for (const root of paths.srcRoots) {
2650
+ for (const f of walk(root, [".ts", ".tsx"])) {
2651
+ if (!seen.has(f)) {
2652
+ seen.add(f);
2653
+ files.push(f);
2654
+ }
2655
+ }
2656
+ }
2657
+ for (const conv of paths.conventionFiles) {
2658
+ if (!seen.has(conv)) {
2659
+ seen.add(conv);
2660
+ files.push(conv);
2661
+ }
2662
+ }
2476
2663
  const crossRefs = [];
2477
2664
  const flaggedEdges = [];
2478
- const seen = /* @__PURE__ */ new Set();
2665
+ const seenEdge = /* @__PURE__ */ new Set();
2479
2666
  for (const absPath of files) {
2480
2667
  const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
2481
- const sourceId = toNodeId2(srcDir, absPath);
2668
+ const sourceId = toNodeId2(srcDir, rootDir, absPath);
2482
2669
  if (!uiNodeIds.has(sourceId)) continue;
2483
2670
  let match;
2484
2671
  API_ANNOTATION_RE.lastIndex = 0;
@@ -2488,8 +2675,8 @@ var apiAnnotationsParser = {
2488
2675
  const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
2489
2676
  if (result.kind === "resolved" && result.nodeId) {
2490
2677
  const key = `${sourceId}|${result.nodeId}|calls_api`;
2491
- if (seen.has(key)) continue;
2492
- seen.add(key);
2678
+ if (seenEdge.has(key)) continue;
2679
+ seenEdge.add(key);
2493
2680
  crossRefs.push({
2494
2681
  source: sourceId,
2495
2682
  target: result.nodeId,
@@ -2524,23 +2711,13 @@ var apiAnnotationsParser = {
2524
2711
  var import_node_fs9 = require("node:fs");
2525
2712
  var import_node_path8 = require("node:path");
2526
2713
  init_config();
2527
- var URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
2528
- function walk3(dir, exts) {
2529
- if (!(0, import_node_fs9.existsSync)(dir)) return [];
2530
- const results = [];
2531
- for (const entry of (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true })) {
2532
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2533
- const full = (0, import_node_path8.join)(dir, entry.name);
2534
- if (entry.isDirectory()) {
2535
- results.push(...walk3(full, exts));
2536
- } else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
2537
- results.push(full);
2538
- }
2714
+ var URL_LITERAL_RE = /['"`](\/[a-zA-Z][^'"`\s]*?)['"`]/g;
2715
+ function toNodeId3(srcDir, rootDir, absPath) {
2716
+ const relFromSrc = (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
2717
+ if (relFromSrc.startsWith("..")) {
2718
+ return (0, import_node_path8.relative)(rootDir, absPath).replace(/\\/g, "/");
2539
2719
  }
2540
- return results;
2541
- }
2542
- function toNodeId3(srcDir, absPath) {
2543
- return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
2720
+ return relFromSrc;
2544
2721
  }
2545
2722
  var urlLiteralScannerParser = {
2546
2723
  id: "url-literal-scanner",
@@ -2561,15 +2738,26 @@ var urlLiteralScannerParser = {
2561
2738
  const apiPathMap = buildApiPathMap(apiRoutes);
2562
2739
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2563
2740
  const srcDir = paths.srcDir;
2564
- const clientDir = (0, import_node_path8.join)(srcDir, "client");
2565
- const files = [
2566
- ...walk3(clientDir, [".ts", ".tsx"]),
2567
- ...walk3(paths.appDir, [".ts", ".tsx"])
2568
- ];
2741
+ const seenFile = /* @__PURE__ */ new Set();
2742
+ const files = [];
2743
+ for (const root of paths.srcRoots) {
2744
+ for (const f of walk(root, [".ts", ".tsx"])) {
2745
+ if (!seenFile.has(f)) {
2746
+ seenFile.add(f);
2747
+ files.push(f);
2748
+ }
2749
+ }
2750
+ }
2751
+ for (const conv of paths.conventionFiles) {
2752
+ if (!seenFile.has(conv)) {
2753
+ seenFile.add(conv);
2754
+ files.push(conv);
2755
+ }
2756
+ }
2569
2757
  const crossRefs = [];
2570
2758
  const seen = /* @__PURE__ */ new Set();
2571
2759
  for (const absPath of files) {
2572
- const sourceId = toNodeId3(srcDir, absPath);
2760
+ const sourceId = toNodeId3(srcDir, rootDir, absPath);
2573
2761
  if (!uiNodeIds.has(sourceId)) continue;
2574
2762
  const content = (0, import_node_fs9.readFileSync)(absPath, "utf-8");
2575
2763
  let match;
@@ -2604,6 +2792,7 @@ var urlLiteralScannerParser = {
2604
2792
  // src/server/graph/parsers/static/static-values.ts
2605
2793
  var import_node_fs10 = require("node:fs");
2606
2794
  var import_node_path9 = require("node:path");
2795
+ init_config();
2607
2796
  var parseCode = null;
2608
2797
  function tryLoadTreeSitter() {
2609
2798
  if (parseCode) return true;
@@ -2635,10 +2824,15 @@ function classifyScope(source, model) {
2635
2824
  function extractEnumValues(rootDir) {
2636
2825
  const nodes = [];
2637
2826
  const edges = [];
2638
- const schemaPaths = [
2639
- (0, import_node_path9.join)(rootDir, "prisma", "schema.prisma"),
2640
- (0, import_node_path9.join)(rootDir, "prisma", "schema")
2641
- ];
2827
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2828
+ const schemaPaths = [];
2829
+ if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath) {
2830
+ schemaPaths.push(paths.dbConfig.schemaPath);
2831
+ schemaPaths.push((0, import_node_path9.join)((0, import_node_path9.dirname)(paths.dbConfig.schemaPath), "schema"));
2832
+ } else {
2833
+ schemaPaths.push((0, import_node_path9.join)(rootDir, "prisma", "schema.prisma"));
2834
+ schemaPaths.push((0, import_node_path9.join)(rootDir, "prisma", "schema"));
2835
+ }
2642
2836
  let content = "";
2643
2837
  for (const p of schemaPaths) {
2644
2838
  if ((0, import_node_fs10.existsSync)(p)) {
@@ -2722,7 +2916,7 @@ function extractStringArrayFromNode(node) {
2722
2916
  return values;
2723
2917
  }
2724
2918
  function findArrayDecl(root, varName) {
2725
- function walk4(node) {
2919
+ function walk2(node) {
2726
2920
  if (node.type === "variable_declarator") {
2727
2921
  const nameNode = node.childForFieldName("name");
2728
2922
  const valueNode = node.childForFieldName("value");
@@ -2735,12 +2929,12 @@ function findArrayDecl(root, varName) {
2735
2929
  }
2736
2930
  }
2737
2931
  for (const child of node.namedChildren) {
2738
- const found = walk4(child);
2932
+ const found = walk2(child);
2739
2933
  if (found) return found;
2740
2934
  }
2741
2935
  return null;
2742
2936
  }
2743
- return walk4(root);
2937
+ return walk2(root);
2744
2938
  }
2745
2939
  function extractObjectPropsRegex(objStr) {
2746
2940
  const props = {};
@@ -2803,11 +2997,26 @@ function modelToNodeType(model) {
2803
2997
  function extractSeedData(rootDir) {
2804
2998
  const nodes = [];
2805
2999
  const edges = [];
2806
- const seedFiles = [
2807
- (0, import_node_path9.join)(rootDir, "prisma", "seed.ts"),
2808
- (0, import_node_path9.join)(rootDir, "prisma", "seed.js"),
2809
- (0, import_node_path9.join)(rootDir, "src", "server", "lib", "system-tags.ts")
2810
- ].filter(import_node_fs10.existsSync);
3000
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3001
+ const candidates = [];
3002
+ if (paths?.dbDir) {
3003
+ candidates.push((0, import_node_path9.join)(paths.dbDir, "seed.ts"));
3004
+ candidates.push((0, import_node_path9.join)(paths.dbDir, "seed.js"));
3005
+ } else {
3006
+ candidates.push((0, import_node_path9.join)(rootDir, "prisma", "seed.ts"));
3007
+ candidates.push((0, import_node_path9.join)(rootDir, "prisma", "seed.js"));
3008
+ }
3009
+ const baseRoots = paths?.srcRoots ?? [(0, import_node_path9.join)(rootDir, "src")];
3010
+ for (const root of baseRoots) {
3011
+ candidates.push((0, import_node_path9.join)(root, "server", "lib", "system-tags.ts"));
3012
+ }
3013
+ const seedFiles = candidates.filter((p) => {
3014
+ try {
3015
+ return (0, import_node_fs10.existsSync)(p) && (0, import_node_fs10.statSync)(p).isFile();
3016
+ } catch {
3017
+ return false;
3018
+ }
3019
+ });
2811
3020
  const useTreeSitter = tryLoadTreeSitter();
2812
3021
  for (const filePath of seedFiles) {
2813
3022
  const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
@@ -2917,9 +3126,20 @@ function walkDir(dir, exts) {
2917
3126
  }
2918
3127
  function extractConstants(rootDir) {
2919
3128
  const nodes = [];
2920
- const srcDir = (0, import_node_path9.join)(rootDir, "src");
2921
- if (!(0, import_node_fs10.existsSync)(srcDir)) return { nodes };
2922
- for (const filePath of walkDir(srcDir, [".ts", ".tsx"])) {
3129
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3130
+ const roots = paths?.srcRoots ?? [(0, import_node_path9.join)(rootDir, "src")];
3131
+ const seenFile = /* @__PURE__ */ new Set();
3132
+ const allFiles = [];
3133
+ for (const root of roots) {
3134
+ for (const f of walkDir(root, [".ts", ".tsx"])) {
3135
+ if (!seenFile.has(f)) {
3136
+ seenFile.add(f);
3137
+ allFiles.push(f);
3138
+ }
3139
+ }
3140
+ }
3141
+ if (allFiles.length === 0) return { nodes };
3142
+ for (const filePath of allFiles) {
2923
3143
  const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
2924
3144
  const relPath = (0, import_node_path9.relative)(rootDir, filePath);
2925
3145
  const constArrayRe = /export\s+const\s+([A-Z][A-Z_0-9]+)\s*(?::[^=]+)?\s*=\s*\[/g;
@@ -2954,6 +3174,13 @@ function extractConstants(rootDir) {
2954
3174
  return { nodes };
2955
3175
  }
2956
3176
  function detect4(rootDir) {
3177
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3178
+ if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath && (0, import_node_fs10.existsSync)(paths.dbConfig.schemaPath)) {
3179
+ return true;
3180
+ }
3181
+ if (paths?.dbDir) {
3182
+ if ((0, import_node_fs10.existsSync)((0, import_node_path9.join)(paths.dbDir, "seed.ts")) || (0, import_node_fs10.existsSync)((0, import_node_path9.join)(paths.dbDir, "seed.js"))) return true;
3183
+ }
2957
3184
  return (0, import_node_fs10.existsSync)((0, import_node_path9.join)(rootDir, "prisma", "schema.prisma")) || (0, import_node_fs10.existsSync)((0, import_node_path9.join)(rootDir, "prisma", "seed.ts"));
2958
3185
  }
2959
3186
  function generate4(rootDir) {
@@ -3173,13 +3400,22 @@ var staticRefScannerParser = {
3173
3400
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3174
3401
  if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
3175
3402
  const srcDir = paths.srcDir;
3176
- const files = [
3177
- ...walkWithIgnore((0, import_node_path10.join)(srcDir, "client"), [".ts", ".tsx"]),
3178
- ...walkWithIgnore(paths.appDir, [".ts", ".tsx"]),
3179
- ...walkWithIgnore((0, import_node_path10.join)(srcDir, "server"), [".ts", ".tsx"]),
3180
- ...walkWithIgnore((0, import_node_path10.join)(srcDir, "lib"), [".ts", ".tsx"]),
3181
- ...walkWithIgnore((0, import_node_path10.join)(srcDir, "config"), [".ts", ".tsx"])
3182
- ];
3403
+ const seenFile = /* @__PURE__ */ new Set();
3404
+ const files = [];
3405
+ for (const root of paths.srcRoots) {
3406
+ for (const f of walkWithIgnore(root, [".ts", ".tsx"])) {
3407
+ if (!seenFile.has(f)) {
3408
+ seenFile.add(f);
3409
+ files.push(f);
3410
+ }
3411
+ }
3412
+ }
3413
+ for (const conv of paths.conventionFiles) {
3414
+ if (!seenFile.has(conv)) {
3415
+ seenFile.add(conv);
3416
+ files.push(conv);
3417
+ }
3418
+ }
3183
3419
  const uiOutput = layerOutputs.get("ui");
3184
3420
  const apiOutput = layerOutputs.get("api");
3185
3421
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
@@ -3196,7 +3432,8 @@ var staticRefScannerParser = {
3196
3432
  const seen = /* @__PURE__ */ new Set();
3197
3433
  let filesScanned = 0;
3198
3434
  for (const absPath of files) {
3199
- const sourceId = (0, import_node_path10.relative)(srcDir, absPath).replace(/\\/g, "/");
3435
+ const relFromSrc = (0, import_node_path10.relative)(srcDir, absPath).replace(/\\/g, "/");
3436
+ const sourceId = relFromSrc.startsWith("..") ? (0, import_node_path10.relative)(rootDir, absPath).replace(/\\/g, "/") : relFromSrc;
3200
3437
  const sourceLayer = uiNodeIds.has(sourceId) ? "ui" : apiNodeIds.has(sourceId) ? "api" : null;
3201
3438
  if (!sourceLayer) continue;
3202
3439
  const content = (0, import_node_fs11.readFileSync)(absPath, "utf-8");
@@ -3744,13 +3981,17 @@ function isTrivialGroup(name, extraTrivial) {
3744
3981
  function normalizeGroupName(name) {
3745
3982
  return name.toLowerCase().replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
3746
3983
  }
3747
- function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
3984
+ function extractModuleFromPath(id, extraTrivial, extraSkipSegments, extraGenericRoles) {
3748
3985
  const segments = id.split("/");
3749
3986
  const routeGroups = extractRouteGroups(id);
3750
3987
  const skipSegments = new Set(SKIP_SEGMENTS_BUILTIN);
3988
+ for (const r of GENERIC_ROLE_NAMES_BUILTIN) skipSegments.add(r);
3751
3989
  if (extraSkipSegments) {
3752
3990
  for (const s of extraSkipSegments) skipSegments.add(s);
3753
3991
  }
3992
+ if (extraGenericRoles) {
3993
+ for (const r of extraGenericRoles) skipSegments.add(r);
3994
+ }
3754
3995
  const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g, extraTrivial)).map(normalizeGroupName);
3755
3996
  if (moduleGroups.length > 0) {
3756
3997
  return moduleGroups[moduleGroups.length - 1];