@launchsecure/launch-kit 0.0.21 → 0.0.23

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,9 +1342,17 @@ 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
- if (route) routeToNodeId.set(route, id);
1349
+ if (route) {
1350
+ routeToNodeId.set(route, id);
1351
+ const trimmed = route.replace(/\/\*[^/]+\?$/, "");
1352
+ if (trimmed && trimmed !== route && !routeToNodeId.has(trimmed)) {
1353
+ routeToNodeId.set(trimmed, id);
1354
+ }
1355
+ }
1244
1356
  }
1245
1357
  }
1246
1358
  const uiEdges = [];
@@ -1262,6 +1374,29 @@ function generate(rootDir) {
1262
1374
  uiEdges.push(...edges);
1263
1375
  uiFlagged.push(...flagged);
1264
1376
  }
1377
+ const layoutsById = /* @__PURE__ */ new Set();
1378
+ for (const n of uiNodes) {
1379
+ if (n.type === "layout") layoutsById.add(n.id);
1380
+ }
1381
+ function findClosestLayout(pageId) {
1382
+ let dir = pageId.replace(/\/page\.tsx$/, "");
1383
+ while (dir.length > 0) {
1384
+ const candidate = `${dir}/layout.tsx`;
1385
+ if (layoutsById.has(candidate)) return candidate;
1386
+ const slash = dir.lastIndexOf("/");
1387
+ if (slash < 0) break;
1388
+ dir = dir.slice(0, slash);
1389
+ }
1390
+ if (layoutsById.has("app/layout.tsx")) return "app/layout.tsx";
1391
+ return null;
1392
+ }
1393
+ for (const n of uiNodes) {
1394
+ if (n.type !== "page") continue;
1395
+ const layoutId = findClosestLayout(n.id);
1396
+ if (layoutId && layoutId !== n.id) {
1397
+ uiEdges.push({ source: layoutId, target: n.id, type: "wraps" });
1398
+ }
1399
+ }
1265
1400
  const fetchCallEntries = [];
1266
1401
  for (const absPath of fileSet) {
1267
1402
  const sourceId = toNodeId(srcDir, rootDir, absPath);
@@ -1343,17 +1478,37 @@ function generate(rootDir) {
1343
1478
  if (!dbCalls) continue;
1344
1479
  const seenModels = /* @__PURE__ */ new Set();
1345
1480
  for (const call of dbCalls) {
1346
- if (seenModels.has(call.model)) continue;
1347
- seenModels.add(call.model);
1481
+ const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
1482
+ if (seenModels.has(target)) continue;
1483
+ seenModels.add(target);
1348
1484
  apiCrossRefs.push({
1349
1485
  source: node.id,
1350
- target: camelToPascal(call.model),
1486
+ target,
1487
+ type: call.isMutation ? "mutates" : "reads",
1488
+ layer: "db"
1489
+ });
1490
+ }
1491
+ delete node._dbCalls;
1492
+ }
1493
+ const uiCrossRefs = [];
1494
+ for (const node of uiNodes) {
1495
+ const dbCalls = node._dbCalls;
1496
+ if (!dbCalls) continue;
1497
+ const seenModels = /* @__PURE__ */ new Set();
1498
+ for (const call of dbCalls) {
1499
+ const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
1500
+ if (seenModels.has(target)) continue;
1501
+ seenModels.add(target);
1502
+ uiCrossRefs.push({
1503
+ source: node.id,
1504
+ target,
1351
1505
  type: call.isMutation ? "mutates" : "reads",
1352
1506
  layer: "db"
1353
1507
  });
1354
1508
  }
1355
1509
  delete node._dbCalls;
1356
1510
  }
1511
+ uiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1357
1512
  const apiNodeIds = new Set(apiNodes.map((n) => n.id));
1358
1513
  const apiEdges = [];
1359
1514
  const uiOnlyEdges = [];
@@ -1415,7 +1570,7 @@ function generate(rootDir) {
1415
1570
  },
1416
1571
  nodes: stripLayer(uiNodes),
1417
1572
  edges: uiOnlyEdges,
1418
- cross_refs: [],
1573
+ cross_refs: uiCrossRefs,
1419
1574
  contradictions: [],
1420
1575
  warnings: [],
1421
1576
  flagged_edges: dedupedFlagged,
@@ -2239,6 +2394,10 @@ function buildApiPathMap(routes) {
2239
2394
  const map = /* @__PURE__ */ new Map();
2240
2395
  for (const r of routes) {
2241
2396
  if (!map.has(r.path)) map.set(r.path, r.nodeId);
2397
+ const trimmed = r.path.replace(/\/\*[^/]+\?$/, "");
2398
+ if (trimmed && trimmed !== r.path && !map.has(trimmed)) {
2399
+ map.set(trimmed, r.nodeId);
2400
+ }
2242
2401
  }
2243
2402
  return map;
2244
2403
  }
@@ -2261,26 +2420,54 @@ function normalizeFetchUrl(raw) {
2261
2420
  return { path: s || "/", hadInterpolation };
2262
2421
  }
2263
2422
  function scoreApiRouteMatch(candidate, known) {
2264
- if (candidate.length !== known.length) return -1;
2265
2423
  let score = 0;
2266
- for (let i = 0; i < candidate.length; i++) {
2424
+ let i = 0, j = 0;
2425
+ while (i < candidate.length && j < known.length) {
2267
2426
  const a = candidate[i];
2268
- const b = known[i];
2427
+ const b = known[j];
2428
+ if (b.startsWith("*") && b.endsWith("?")) {
2429
+ score += 1;
2430
+ return score;
2431
+ }
2432
+ if (b.startsWith("*")) {
2433
+ const remaining = candidate.length - i;
2434
+ if (remaining < 1) return -1;
2435
+ score += 1 + remaining;
2436
+ return score;
2437
+ }
2269
2438
  if (a === b) {
2270
2439
  score += 3;
2440
+ i++;
2441
+ j++;
2271
2442
  continue;
2272
2443
  }
2273
2444
  if (a.startsWith(":") && b.startsWith(":")) {
2274
2445
  score += 2;
2446
+ i++;
2447
+ j++;
2275
2448
  continue;
2276
2449
  }
2277
2450
  if (a.startsWith(":") || b.startsWith(":")) {
2278
2451
  score += 1;
2452
+ i++;
2453
+ j++;
2279
2454
  continue;
2280
2455
  }
2281
2456
  return -1;
2282
2457
  }
2283
- return score;
2458
+ if (i === candidate.length) {
2459
+ while (j < known.length) {
2460
+ const b = known[j];
2461
+ if (b.startsWith("*") && b.endsWith("?")) {
2462
+ score += 1;
2463
+ j++;
2464
+ continue;
2465
+ }
2466
+ return -1;
2467
+ }
2468
+ return score;
2469
+ }
2470
+ return -1;
2284
2471
  }
2285
2472
  function resolveFetchCall(call, apiPathMap, apiRoutes) {
2286
2473
  const raw = call.url;
@@ -2437,48 +2624,58 @@ var fetchResolverParser = {
2437
2624
  // src/server/graph/parsers/crosslayer/api-annotations.ts
2438
2625
  var import_node_fs8 = require("node:fs");
2439
2626
  var import_node_path7 = require("node:path");
2627
+ init_config();
2440
2628
  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
- }
2629
+ function toNodeId2(srcDir, rootDir, absPath) {
2630
+ const relFromSrc = (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
2631
+ if (relFromSrc.startsWith("..")) {
2632
+ return (0, import_node_path7.relative)(rootDir, absPath).replace(/\\/g, "/");
2452
2633
  }
2453
- return results;
2454
- }
2455
- function toNodeId2(srcDir, absPath) {
2456
- return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
2634
+ return relFromSrc;
2457
2635
  }
2458
2636
  var apiAnnotationsParser = {
2459
2637
  id: "api-annotations",
2460
2638
  layer: "crosslayer",
2461
2639
  concern: "api-binding",
2462
2640
  detect(rootDir) {
2463
- return (0, import_node_fs8.existsSync)((0, import_node_path7.join)(rootDir, "src"));
2641
+ return resolveProjectPaths(rootDir, loadConfig(rootDir)) !== null;
2464
2642
  },
2465
2643
  generate(rootDir, layerOutputs) {
2466
2644
  const apiOutput = layerOutputs.get("api");
2467
2645
  if (!apiOutput) {
2468
2646
  return { cross_refs: [], flagged_edges: [], warnings: [] };
2469
2647
  }
2648
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2649
+ if (!paths) {
2650
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
2651
+ }
2470
2652
  const uiOutput = layerOutputs.get("ui");
2471
2653
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
2472
2654
  const apiRoutes = loadApiRoutesFromOutput(apiOutput);
2473
2655
  const apiPathMap = buildApiPathMap(apiRoutes);
2474
- const srcDir = (0, import_node_path7.join)(rootDir, "src");
2475
- const files = walk2(srcDir, [".ts", ".tsx"]);
2656
+ const srcDir = paths.srcDir;
2657
+ const seen = /* @__PURE__ */ new Set();
2658
+ const files = [];
2659
+ for (const root of paths.srcRoots) {
2660
+ for (const f of walk(root, [".ts", ".tsx"])) {
2661
+ if (!seen.has(f)) {
2662
+ seen.add(f);
2663
+ files.push(f);
2664
+ }
2665
+ }
2666
+ }
2667
+ for (const conv of paths.conventionFiles) {
2668
+ if (!seen.has(conv)) {
2669
+ seen.add(conv);
2670
+ files.push(conv);
2671
+ }
2672
+ }
2476
2673
  const crossRefs = [];
2477
2674
  const flaggedEdges = [];
2478
- const seen = /* @__PURE__ */ new Set();
2675
+ const seenEdge = /* @__PURE__ */ new Set();
2479
2676
  for (const absPath of files) {
2480
2677
  const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
2481
- const sourceId = toNodeId2(srcDir, absPath);
2678
+ const sourceId = toNodeId2(srcDir, rootDir, absPath);
2482
2679
  if (!uiNodeIds.has(sourceId)) continue;
2483
2680
  let match;
2484
2681
  API_ANNOTATION_RE.lastIndex = 0;
@@ -2488,8 +2685,8 @@ var apiAnnotationsParser = {
2488
2685
  const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
2489
2686
  if (result.kind === "resolved" && result.nodeId) {
2490
2687
  const key = `${sourceId}|${result.nodeId}|calls_api`;
2491
- if (seen.has(key)) continue;
2492
- seen.add(key);
2688
+ if (seenEdge.has(key)) continue;
2689
+ seenEdge.add(key);
2493
2690
  crossRefs.push({
2494
2691
  source: sourceId,
2495
2692
  target: result.nodeId,
@@ -2524,23 +2721,13 @@ var apiAnnotationsParser = {
2524
2721
  var import_node_fs9 = require("node:fs");
2525
2722
  var import_node_path8 = require("node:path");
2526
2723
  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
- }
2724
+ var URL_LITERAL_RE = /['"`](\/[a-zA-Z][^'"`\s]*?)['"`]/g;
2725
+ function toNodeId3(srcDir, rootDir, absPath) {
2726
+ const relFromSrc = (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
2727
+ if (relFromSrc.startsWith("..")) {
2728
+ return (0, import_node_path8.relative)(rootDir, absPath).replace(/\\/g, "/");
2539
2729
  }
2540
- return results;
2541
- }
2542
- function toNodeId3(srcDir, absPath) {
2543
- return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
2730
+ return relFromSrc;
2544
2731
  }
2545
2732
  var urlLiteralScannerParser = {
2546
2733
  id: "url-literal-scanner",
@@ -2561,15 +2748,26 @@ var urlLiteralScannerParser = {
2561
2748
  const apiPathMap = buildApiPathMap(apiRoutes);
2562
2749
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2563
2750
  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
- ];
2751
+ const seenFile = /* @__PURE__ */ new Set();
2752
+ const files = [];
2753
+ for (const root of paths.srcRoots) {
2754
+ for (const f of walk(root, [".ts", ".tsx"])) {
2755
+ if (!seenFile.has(f)) {
2756
+ seenFile.add(f);
2757
+ files.push(f);
2758
+ }
2759
+ }
2760
+ }
2761
+ for (const conv of paths.conventionFiles) {
2762
+ if (!seenFile.has(conv)) {
2763
+ seenFile.add(conv);
2764
+ files.push(conv);
2765
+ }
2766
+ }
2569
2767
  const crossRefs = [];
2570
2768
  const seen = /* @__PURE__ */ new Set();
2571
2769
  for (const absPath of files) {
2572
- const sourceId = toNodeId3(srcDir, absPath);
2770
+ const sourceId = toNodeId3(srcDir, rootDir, absPath);
2573
2771
  if (!uiNodeIds.has(sourceId)) continue;
2574
2772
  const content = (0, import_node_fs9.readFileSync)(absPath, "utf-8");
2575
2773
  let match;
@@ -2604,6 +2802,7 @@ var urlLiteralScannerParser = {
2604
2802
  // src/server/graph/parsers/static/static-values.ts
2605
2803
  var import_node_fs10 = require("node:fs");
2606
2804
  var import_node_path9 = require("node:path");
2805
+ init_config();
2607
2806
  var parseCode = null;
2608
2807
  function tryLoadTreeSitter() {
2609
2808
  if (parseCode) return true;
@@ -2635,10 +2834,15 @@ function classifyScope(source, model) {
2635
2834
  function extractEnumValues(rootDir) {
2636
2835
  const nodes = [];
2637
2836
  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
- ];
2837
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2838
+ const schemaPaths = [];
2839
+ if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath) {
2840
+ schemaPaths.push(paths.dbConfig.schemaPath);
2841
+ schemaPaths.push((0, import_node_path9.join)((0, import_node_path9.dirname)(paths.dbConfig.schemaPath), "schema"));
2842
+ } else {
2843
+ schemaPaths.push((0, import_node_path9.join)(rootDir, "prisma", "schema.prisma"));
2844
+ schemaPaths.push((0, import_node_path9.join)(rootDir, "prisma", "schema"));
2845
+ }
2642
2846
  let content = "";
2643
2847
  for (const p of schemaPaths) {
2644
2848
  if ((0, import_node_fs10.existsSync)(p)) {
@@ -2722,7 +2926,7 @@ function extractStringArrayFromNode(node) {
2722
2926
  return values;
2723
2927
  }
2724
2928
  function findArrayDecl(root, varName) {
2725
- function walk4(node) {
2929
+ function walk2(node) {
2726
2930
  if (node.type === "variable_declarator") {
2727
2931
  const nameNode = node.childForFieldName("name");
2728
2932
  const valueNode = node.childForFieldName("value");
@@ -2735,12 +2939,12 @@ function findArrayDecl(root, varName) {
2735
2939
  }
2736
2940
  }
2737
2941
  for (const child of node.namedChildren) {
2738
- const found = walk4(child);
2942
+ const found = walk2(child);
2739
2943
  if (found) return found;
2740
2944
  }
2741
2945
  return null;
2742
2946
  }
2743
- return walk4(root);
2947
+ return walk2(root);
2744
2948
  }
2745
2949
  function extractObjectPropsRegex(objStr) {
2746
2950
  const props = {};
@@ -2803,11 +3007,26 @@ function modelToNodeType(model) {
2803
3007
  function extractSeedData(rootDir) {
2804
3008
  const nodes = [];
2805
3009
  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);
3010
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3011
+ const candidates = [];
3012
+ if (paths?.dbDir) {
3013
+ candidates.push((0, import_node_path9.join)(paths.dbDir, "seed.ts"));
3014
+ candidates.push((0, import_node_path9.join)(paths.dbDir, "seed.js"));
3015
+ } else {
3016
+ candidates.push((0, import_node_path9.join)(rootDir, "prisma", "seed.ts"));
3017
+ candidates.push((0, import_node_path9.join)(rootDir, "prisma", "seed.js"));
3018
+ }
3019
+ const baseRoots = paths?.srcRoots ?? [(0, import_node_path9.join)(rootDir, "src")];
3020
+ for (const root of baseRoots) {
3021
+ candidates.push((0, import_node_path9.join)(root, "server", "lib", "system-tags.ts"));
3022
+ }
3023
+ const seedFiles = candidates.filter((p) => {
3024
+ try {
3025
+ return (0, import_node_fs10.existsSync)(p) && (0, import_node_fs10.statSync)(p).isFile();
3026
+ } catch {
3027
+ return false;
3028
+ }
3029
+ });
2811
3030
  const useTreeSitter = tryLoadTreeSitter();
2812
3031
  for (const filePath of seedFiles) {
2813
3032
  const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
@@ -2917,9 +3136,20 @@ function walkDir(dir, exts) {
2917
3136
  }
2918
3137
  function extractConstants(rootDir) {
2919
3138
  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"])) {
3139
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3140
+ const roots = paths?.srcRoots ?? [(0, import_node_path9.join)(rootDir, "src")];
3141
+ const seenFile = /* @__PURE__ */ new Set();
3142
+ const allFiles = [];
3143
+ for (const root of roots) {
3144
+ for (const f of walkDir(root, [".ts", ".tsx"])) {
3145
+ if (!seenFile.has(f)) {
3146
+ seenFile.add(f);
3147
+ allFiles.push(f);
3148
+ }
3149
+ }
3150
+ }
3151
+ if (allFiles.length === 0) return { nodes };
3152
+ for (const filePath of allFiles) {
2923
3153
  const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
2924
3154
  const relPath = (0, import_node_path9.relative)(rootDir, filePath);
2925
3155
  const constArrayRe = /export\s+const\s+([A-Z][A-Z_0-9]+)\s*(?::[^=]+)?\s*=\s*\[/g;
@@ -2954,6 +3184,13 @@ function extractConstants(rootDir) {
2954
3184
  return { nodes };
2955
3185
  }
2956
3186
  function detect4(rootDir) {
3187
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3188
+ if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath && (0, import_node_fs10.existsSync)(paths.dbConfig.schemaPath)) {
3189
+ return true;
3190
+ }
3191
+ if (paths?.dbDir) {
3192
+ 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;
3193
+ }
2957
3194
  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
3195
  }
2959
3196
  function generate4(rootDir) {
@@ -3173,13 +3410,22 @@ var staticRefScannerParser = {
3173
3410
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3174
3411
  if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
3175
3412
  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
- ];
3413
+ const seenFile = /* @__PURE__ */ new Set();
3414
+ const files = [];
3415
+ for (const root of paths.srcRoots) {
3416
+ for (const f of walkWithIgnore(root, [".ts", ".tsx"])) {
3417
+ if (!seenFile.has(f)) {
3418
+ seenFile.add(f);
3419
+ files.push(f);
3420
+ }
3421
+ }
3422
+ }
3423
+ for (const conv of paths.conventionFiles) {
3424
+ if (!seenFile.has(conv)) {
3425
+ seenFile.add(conv);
3426
+ files.push(conv);
3427
+ }
3428
+ }
3183
3429
  const uiOutput = layerOutputs.get("ui");
3184
3430
  const apiOutput = layerOutputs.get("api");
3185
3431
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
@@ -3196,7 +3442,8 @@ var staticRefScannerParser = {
3196
3442
  const seen = /* @__PURE__ */ new Set();
3197
3443
  let filesScanned = 0;
3198
3444
  for (const absPath of files) {
3199
- const sourceId = (0, import_node_path10.relative)(srcDir, absPath).replace(/\\/g, "/");
3445
+ const relFromSrc = (0, import_node_path10.relative)(srcDir, absPath).replace(/\\/g, "/");
3446
+ const sourceId = relFromSrc.startsWith("..") ? (0, import_node_path10.relative)(rootDir, absPath).replace(/\\/g, "/") : relFromSrc;
3200
3447
  const sourceLayer = uiNodeIds.has(sourceId) ? "ui" : apiNodeIds.has(sourceId) ? "api" : null;
3201
3448
  if (!sourceLayer) continue;
3202
3449
  const content = (0, import_node_fs11.readFileSync)(absPath, "utf-8");
@@ -3657,6 +3904,9 @@ var GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
3657
3904
  "__tests__",
3658
3905
  "spec",
3659
3906
  "specs",
3907
+ "scripts",
3908
+ "bin",
3909
+ "tools",
3660
3910
  // Go
3661
3911
  "cmd",
3662
3912
  "pkg",
@@ -3744,13 +3994,17 @@ function isTrivialGroup(name, extraTrivial) {
3744
3994
  function normalizeGroupName(name) {
3745
3995
  return name.toLowerCase().replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
3746
3996
  }
3747
- function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
3997
+ function extractModuleFromPath(id, extraTrivial, extraSkipSegments, extraGenericRoles) {
3748
3998
  const segments = id.split("/");
3749
3999
  const routeGroups = extractRouteGroups(id);
3750
4000
  const skipSegments = new Set(SKIP_SEGMENTS_BUILTIN);
4001
+ for (const r of GENERIC_ROLE_NAMES_BUILTIN) skipSegments.add(r);
3751
4002
  if (extraSkipSegments) {
3752
4003
  for (const s of extraSkipSegments) skipSegments.add(s);
3753
4004
  }
4005
+ if (extraGenericRoles) {
4006
+ for (const r of extraGenericRoles) skipSegments.add(r);
4007
+ }
3754
4008
  const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g, extraTrivial)).map(normalizeGroupName);
3755
4009
  if (moduleGroups.length > 0) {
3756
4010
  return moduleGroups[moduleGroups.length - 1];
@@ -4280,7 +4534,7 @@ function checkDeadScreens(rootDir) {
4280
4534
  const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
4281
4535
  const navTargets = /* @__PURE__ */ new Set();
4282
4536
  for (const e of ui.edges) {
4283
- if (e.type === "navigates" || e.type === "renders" || e.type === "imports") {
4537
+ if (e.type === "navigates" || e.type === "renders" || e.type === "imports" || e.type === "wraps") {
4284
4538
  navTargets.add(e.target);
4285
4539
  }
4286
4540
  }
@@ -4290,13 +4544,15 @@ function checkDeadScreens(rootDir) {
4290
4544
  for (const page of pages) {
4291
4545
  if (page.id.endsWith("layout.tsx") && page.id.split("/").length <= 2) continue;
4292
4546
  if (["error.tsx", "loading.tsx", "not-found.tsx", "template.tsx"].some((s) => page.id.endsWith(s))) continue;
4547
+ const route = page.route;
4548
+ if (route === "/" || route === "") continue;
4293
4549
  if (!navTargets.has(page.id)) {
4294
4550
  findings.push({
4295
4551
  id: `dead:${page.id}`,
4296
4552
  severity: "info",
4297
4553
  category: "dead_screens",
4298
4554
  title: page.name,
4299
- detail: `Page "${page.id}" has no incoming navigation, render, or import edges.`,
4555
+ detail: `Page "${page.id}" has no incoming navigation, render, import, or layout-wrap edges.`,
4300
4556
  file: page.id
4301
4557
  });
4302
4558
  }