@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.
@@ -3960,6 +3960,12 @@ function parseFileTS(absPath) {
3960
3960
  if (linkTemplate) {
3961
3961
  navigations.push({ kind: "link-href", target: linkTemplate, isTemplate: true });
3962
3962
  }
3963
+ if (caps["nav.redirect.literal"]) {
3964
+ navigations.push({ kind: "router-replace", target: caps["nav.redirect.literal"], isTemplate: false });
3965
+ }
3966
+ if (caps["nav.redirect.template"]) {
3967
+ navigations.push({ kind: "router-replace", target: caps["nav.redirect.template"], isTemplate: true });
3968
+ }
3963
3969
  if (caps["nav.window.literal"]) {
3964
3970
  navigations.push({ kind: "window-location", target: caps["nav.window.literal"], isTemplate: false });
3965
3971
  }
@@ -4038,15 +4044,25 @@ function extractDbCallsTS(absPath) {
4038
4044
  const seen = /* @__PURE__ */ new Set();
4039
4045
  for (const m of matches) {
4040
4046
  const caps = captureMap(m);
4047
+ const sbTable = caps["sb.table"];
4048
+ const sbMethod = caps["sb.method"];
4049
+ if (sbTable && sbMethod) {
4050
+ const key2 = `sql:${sbTable}.${sbMethod}`;
4051
+ if (seen.has(key2)) continue;
4052
+ seen.add(key2);
4053
+ const isMutation = SUPABASE_MUTATION_METHODS_BUILTIN.has(sbMethod) || extraMutationMethods.includes(sbMethod);
4054
+ calls.push({ model: sbTable, method: sbMethod, isMutation, kind: "sql" });
4055
+ continue;
4056
+ }
4041
4057
  const identifier = caps["db.identifier"];
4042
4058
  const model = caps["db.model"];
4043
4059
  const method = caps["db.method"];
4044
4060
  if (!identifier || !model || !method) continue;
4045
4061
  if (!dbIdentifiers.has(identifier)) continue;
4046
- const key = `${model}.${method}`;
4062
+ const key = `orm:${model}.${method}`;
4047
4063
  if (seen.has(key)) continue;
4048
4064
  seen.add(key);
4049
- calls.push({ model, method, isMutation: getMutationMethods().has(method) });
4065
+ calls.push({ model, method, isMutation: getMutationMethods().has(method), kind: "orm" });
4050
4066
  }
4051
4067
  return calls;
4052
4068
  }
@@ -4060,6 +4076,9 @@ function classifyFile(absPath) {
4060
4076
  const captures = classifyQuery.captures(root);
4061
4077
  const capNames = new Set(captures.map((c) => c.name));
4062
4078
  if (capNames.has("http_export") || capNames.has("http_export_fn")) return "endpoint";
4079
+ if (capNames.has("use_server_directive") && fileName !== "page.tsx" && fileName !== "layout.tsx") {
4080
+ return "server-action";
4081
+ }
4063
4082
  if (fileName === "page.tsx" && capNames.has("has_jsx")) return "page";
4064
4083
  if (fileName === "layout.tsx" && capNames.has("has_jsx")) return "layout";
4065
4084
  if (capNames.has("has_create_context") || capNames.has("has_create_context_bare")) return "context";
@@ -4080,6 +4099,36 @@ function extractAuthWrappersTS(absPath) {
4080
4099
  wrappers.add(caps["wrapper.fn_name"]);
4081
4100
  }
4082
4101
  }
4102
+ const inlineHelpers = /* @__PURE__ */ new Set();
4103
+ for (const stmt of childrenOfType(root, "import_statement")) {
4104
+ const sourceNode = childOfType(stmt, "string");
4105
+ const frag = sourceNode ? childOfType(sourceNode, "string_fragment") : void 0;
4106
+ if (!frag) continue;
4107
+ const provider = INLINE_AUTH_IMPORTS.find((p) => p.module.test(frag.text));
4108
+ if (!provider) continue;
4109
+ const clause = childOfType(stmt, "import_clause");
4110
+ if (!clause) continue;
4111
+ const named = childOfType(clause, "named_imports");
4112
+ if (!named) continue;
4113
+ for (const specNode of childrenOfType(named, "import_specifier")) {
4114
+ const ids = childrenOfType(specNode, "identifier");
4115
+ const importedName = ids[0]?.text;
4116
+ const localName = ids[ids.length - 1]?.text;
4117
+ if (importedName && provider.helpers.includes(importedName) && localName) {
4118
+ inlineHelpers.add(localName);
4119
+ }
4120
+ }
4121
+ }
4122
+ if (inlineHelpers.size > 0) {
4123
+ const text = root.text;
4124
+ for (const name of inlineHelpers) {
4125
+ const re = new RegExp(`\\b${name}\\s*\\(`);
4126
+ if (re.test(text)) {
4127
+ wrappers.add("inline");
4128
+ break;
4129
+ }
4130
+ }
4131
+ }
4083
4132
  return wrappers;
4084
4133
  }
4085
4134
  function trunc(s, max = 120) {
@@ -4238,7 +4287,7 @@ function extractDeep(absPath) {
4238
4287
  }
4239
4288
  return { elements, stateVars, conditions, variables, responses, params };
4240
4289
  }
4241
- var import_node_fs4, import_node_path4, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods;
4290
+ 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;
4242
4291
  var init_ts_extractor = __esm({
4243
4292
  "src/server/graph/core/ts-extractor.ts"() {
4244
4293
  "use strict";
@@ -4262,9 +4311,21 @@ var init_ts_extractor = __esm({
4262
4311
  "delete",
4263
4312
  "deleteMany"
4264
4313
  ];
4314
+ SUPABASE_MUTATION_METHODS_BUILTIN = /* @__PURE__ */ new Set([
4315
+ "insert",
4316
+ "update",
4317
+ "delete",
4318
+ "upsert"
4319
+ ]);
4265
4320
  DB_IDENTIFIERS_FALLBACK = ["db", "prisma"];
4266
4321
  extraDbIdentifiers = [];
4267
4322
  extraMutationMethods = [];
4323
+ INLINE_AUTH_IMPORTS = [
4324
+ { module: /^@clerk\/nextjs(\/server)?$/, helpers: ["auth", "currentUser"] },
4325
+ { module: /^next-auth(\/.+)?$/, helpers: ["auth", "getServerSession"] },
4326
+ { module: /^@auth\//, helpers: ["auth"] },
4327
+ { module: /^@supabase\/auth-helpers/, helpers: ["createServerClient"] }
4328
+ ];
4268
4329
  }
4269
4330
  });
4270
4331
 
@@ -7217,6 +7278,7 @@ init_ts_extractor();
7217
7278
  var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
7218
7279
  var CLASSIFICATION_TO_LAYER = {
7219
7280
  endpoint: "api",
7281
+ "server-action": "api",
7220
7282
  page: "ui",
7221
7283
  layout: "ui",
7222
7284
  component: "ui",
@@ -7315,6 +7377,8 @@ function extractRoute(id) {
7315
7377
  if (!id.endsWith("/page.tsx")) return null;
7316
7378
  let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
7317
7379
  route = route.replace(/\/\([^)]+\)/g, "");
7380
+ route = route.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "*$1?");
7381
+ route = route.replace(/\[\.\.\.([^\]]+)\]/g, "*$1");
7318
7382
  route = route.replace(/\[([^\]]+)\]/g, ":$1");
7319
7383
  route = route.replace(/\/+/g, "/");
7320
7384
  if (!route.startsWith("/")) route = "/" + route;
@@ -7326,6 +7390,7 @@ function nameFromFilename(absPath) {
7326
7390
  function filePathToAppRoute(appDir, absPath) {
7327
7391
  let route = ("/" + (0, import_node_path5.relative)(appDir, absPath).replace(/\\/g, "/")).replace(/\/route\.tsx?$/, "");
7328
7392
  route = route.replace(/\/\([^)]+\)/g, "");
7393
+ route = route.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "*$1?");
7329
7394
  route = route.replace(/\[\.\.\.([^\]]+)\]/g, "*$1");
7330
7395
  route = route.replace(/\[([^\]]+)\]/g, ":$1");
7331
7396
  route = route.replace(/\/+/g, "/");
@@ -7374,25 +7439,52 @@ function resolveTemplateLiteralRoute(template, routeToNodeId) {
7374
7439
  function routeMatchScore(candidate, known) {
7375
7440
  const segsA = candidate.split("/");
7376
7441
  const segsB = known.split("/");
7377
- if (segsA.length !== segsB.length) return -1;
7378
7442
  let score = 0;
7379
- for (let i = 0; i < segsA.length; i++) {
7380
- const a = segsA[i], b = segsB[i];
7443
+ let i = 0, j = 0;
7444
+ while (i < segsA.length && j < segsB.length) {
7445
+ const a = segsA[i], b = segsB[j];
7446
+ if (b.startsWith("*") && b.endsWith("?")) {
7447
+ score += 1;
7448
+ return score;
7449
+ }
7450
+ if (b.startsWith("*")) {
7451
+ const remaining = segsA.length - i;
7452
+ if (remaining < 1) return -1;
7453
+ score += 1 + remaining;
7454
+ return score;
7455
+ }
7381
7456
  if (a === b) {
7382
7457
  score += 3;
7458
+ i++;
7459
+ j++;
7383
7460
  continue;
7384
7461
  }
7385
7462
  if (a.startsWith(":") && b.startsWith(":")) {
7386
7463
  score += 2;
7464
+ i++;
7465
+ j++;
7387
7466
  continue;
7388
7467
  }
7389
7468
  if (a.startsWith(":") || b.startsWith(":")) {
7390
- score += 0;
7469
+ i++;
7470
+ j++;
7391
7471
  continue;
7392
7472
  }
7393
7473
  return -1;
7394
7474
  }
7395
- return score;
7475
+ if (i === segsA.length) {
7476
+ while (j < segsB.length) {
7477
+ const b = segsB[j];
7478
+ if (b.startsWith("*") && b.endsWith("?")) {
7479
+ score += 1;
7480
+ j++;
7481
+ continue;
7482
+ }
7483
+ return -1;
7484
+ }
7485
+ return score;
7486
+ }
7487
+ return -1;
7396
7488
  }
7397
7489
  function templateToRoute(template) {
7398
7490
  return template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
@@ -7427,7 +7519,7 @@ function extractEdges(srcDir, rootDir, absPath, sourceId, parsed, nodeIdSet, bar
7427
7519
  edges.push(edge);
7428
7520
  }
7429
7521
  function edgeTypeFor(isTypeOnlyImport, importedNames) {
7430
- if (isTypeOnlyImport) return "imports";
7522
+ if (isTypeOnlyImport) return "imports_type";
7431
7523
  const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
7432
7524
  if (anyRendered) return "renders";
7433
7525
  return "imports";
@@ -7458,7 +7550,8 @@ function extractEdges(srcDir, rootDir, absPath, sourceId, parsed, nodeIdSet, bar
7458
7550
  if (resolved) {
7459
7551
  const targetId = toNodeId(srcDir, rootDir, resolved);
7460
7552
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
7461
- addEdge(targetId, edgeTypeFor(isTypeOnly, names));
7553
+ const allType = isTypeOnly || names.length > 0 && names.every((n) => typeNames.has(n));
7554
+ addEdge(targetId, edgeTypeFor(allType, names));
7462
7555
  }
7463
7556
  }
7464
7557
  }
@@ -7467,7 +7560,8 @@ function extractEdges(srcDir, rootDir, absPath, sourceId, parsed, nodeIdSet, bar
7467
7560
  if (resolved) {
7468
7561
  const targetId = toNodeId(srcDir, rootDir, resolved);
7469
7562
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
7470
- addEdge(targetId, edgeTypeFor(isTypeOnly, names));
7563
+ const allType = isTypeOnly || names.length > 0 && names.every((n) => typeNames.has(n));
7564
+ addEdge(targetId, edgeTypeFor(allType, names));
7471
7565
  }
7472
7566
  }
7473
7567
  }
@@ -7552,26 +7646,34 @@ function generate(rootDir) {
7552
7646
  const layer = CLASSIFICATION_TO_LAYER[type] ?? "ui";
7553
7647
  nodeIdSet.add(id);
7554
7648
  if (layer === "api") {
7555
- const methods = [];
7556
- for (const exp of parsed.exports) {
7557
- if (HTTP_METHODS.has(exp)) methods.push(exp);
7558
- }
7559
7649
  const dbCalls = extractDbCallsTS(absPath);
7560
7650
  const authWrappers = extractAuthWrappersTS(absPath);
7561
7651
  const deep = extractDeep(absPath);
7562
- const routePath = filePathToAppRoute(paths.appDir, absPath);
7563
7652
  const mutations = dbCalls.filter((c) => c.isMutation);
7564
7653
  const mutates = mutations.length > 0;
7565
7654
  const authStrategy = [...authWrappers];
7655
+ const isServerAction = type === "server-action";
7656
+ const methods = [];
7657
+ if (!isServerAction) {
7658
+ for (const exp of parsed.exports) {
7659
+ if (HTTP_METHODS.has(exp)) methods.push(exp);
7660
+ }
7661
+ }
7662
+ const routePath = isServerAction ? null : filePathToAppRoute(paths.appDir, absPath);
7663
+ const actions = isServerAction ? parsed.exports.filter((e) => !HTTP_METHODS.has(e)) : [];
7566
7664
  apiNodes.push({
7567
7665
  id,
7568
- type: "endpoint",
7569
- name: routePath,
7666
+ type: isServerAction ? "server-action" : "endpoint",
7667
+ // For HTTP routes: name = URL path. For Server Actions: name = file id +
7668
+ // exported action names — the callable surface, not a URL.
7669
+ name: isServerAction ? actions.length > 0 ? `${id} (${actions.join(", ")})` : id : routePath,
7570
7670
  layer: "api",
7571
7671
  path: routePath,
7672
+ // null for Server Actions
7572
7673
  methods,
7573
7674
  handler: id,
7574
7675
  mutates,
7676
+ ...isServerAction ? { actions } : {},
7575
7677
  auth: authStrategy.length > 0 ? authStrategy : ["public"],
7576
7678
  db_models: [...new Set(dbCalls.map((c) => c.model))],
7577
7679
  db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
@@ -7585,6 +7687,8 @@ function generate(rootDir) {
7585
7687
  } else {
7586
7688
  const route = extractRoute(id);
7587
7689
  const deep = extractDeep(absPath);
7690
+ const dbCalls = extractDbCallsTS(absPath);
7691
+ const authWrappers = type === "page" || type === "layout" ? [...extractAuthWrappersTS(absPath)] : [];
7588
7692
  uiNodes.push({
7589
7693
  id,
7590
7694
  type,
@@ -7595,9 +7699,17 @@ function generate(rootDir) {
7595
7699
  elements: deep.elements,
7596
7700
  stateVars: deep.stateVars,
7597
7701
  conditions: deep.conditions,
7598
- variables: deep.variables
7702
+ variables: deep.variables,
7703
+ ...authWrappers.length > 0 ? { auth: authWrappers } : {},
7704
+ ...dbCalls.length > 0 ? { _dbCalls: dbCalls } : {}
7599
7705
  });
7600
- if (route) routeToNodeId.set(route, id);
7706
+ if (route) {
7707
+ routeToNodeId.set(route, id);
7708
+ const trimmed = route.replace(/\/\*[^/]+\?$/, "");
7709
+ if (trimmed && trimmed !== route && !routeToNodeId.has(trimmed)) {
7710
+ routeToNodeId.set(trimmed, id);
7711
+ }
7712
+ }
7601
7713
  }
7602
7714
  }
7603
7715
  const uiEdges = [];
@@ -7619,6 +7731,29 @@ function generate(rootDir) {
7619
7731
  uiEdges.push(...edges);
7620
7732
  uiFlagged.push(...flagged);
7621
7733
  }
7734
+ const layoutsById = /* @__PURE__ */ new Set();
7735
+ for (const n of uiNodes) {
7736
+ if (n.type === "layout") layoutsById.add(n.id);
7737
+ }
7738
+ function findClosestLayout(pageId) {
7739
+ let dir = pageId.replace(/\/page\.tsx$/, "");
7740
+ while (dir.length > 0) {
7741
+ const candidate = `${dir}/layout.tsx`;
7742
+ if (layoutsById.has(candidate)) return candidate;
7743
+ const slash = dir.lastIndexOf("/");
7744
+ if (slash < 0) break;
7745
+ dir = dir.slice(0, slash);
7746
+ }
7747
+ if (layoutsById.has("app/layout.tsx")) return "app/layout.tsx";
7748
+ return null;
7749
+ }
7750
+ for (const n of uiNodes) {
7751
+ if (n.type !== "page") continue;
7752
+ const layoutId = findClosestLayout(n.id);
7753
+ if (layoutId && layoutId !== n.id) {
7754
+ uiEdges.push({ source: layoutId, target: n.id, type: "wraps" });
7755
+ }
7756
+ }
7622
7757
  const fetchCallEntries = [];
7623
7758
  for (const absPath of fileSet) {
7624
7759
  const sourceId = toNodeId(srcDir, rootDir, absPath);
@@ -7700,17 +7835,37 @@ function generate(rootDir) {
7700
7835
  if (!dbCalls) continue;
7701
7836
  const seenModels = /* @__PURE__ */ new Set();
7702
7837
  for (const call of dbCalls) {
7703
- if (seenModels.has(call.model)) continue;
7704
- seenModels.add(call.model);
7838
+ const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
7839
+ if (seenModels.has(target)) continue;
7840
+ seenModels.add(target);
7705
7841
  apiCrossRefs.push({
7706
7842
  source: node.id,
7707
- target: camelToPascal(call.model),
7843
+ target,
7844
+ type: call.isMutation ? "mutates" : "reads",
7845
+ layer: "db"
7846
+ });
7847
+ }
7848
+ delete node._dbCalls;
7849
+ }
7850
+ const uiCrossRefs = [];
7851
+ for (const node of uiNodes) {
7852
+ const dbCalls = node._dbCalls;
7853
+ if (!dbCalls) continue;
7854
+ const seenModels = /* @__PURE__ */ new Set();
7855
+ for (const call of dbCalls) {
7856
+ const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
7857
+ if (seenModels.has(target)) continue;
7858
+ seenModels.add(target);
7859
+ uiCrossRefs.push({
7860
+ source: node.id,
7861
+ target,
7708
7862
  type: call.isMutation ? "mutates" : "reads",
7709
7863
  layer: "db"
7710
7864
  });
7711
7865
  }
7712
7866
  delete node._dbCalls;
7713
7867
  }
7868
+ uiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
7714
7869
  const apiNodeIds = new Set(apiNodes.map((n) => n.id));
7715
7870
  const apiEdges = [];
7716
7871
  const uiOnlyEdges = [];
@@ -7772,7 +7927,7 @@ function generate(rootDir) {
7772
7927
  },
7773
7928
  nodes: stripLayer(uiNodes),
7774
7929
  edges: uiOnlyEdges,
7775
- cross_refs: [],
7930
+ cross_refs: uiCrossRefs,
7776
7931
  contradictions: [],
7777
7932
  warnings: [],
7778
7933
  flagged_edges: dedupedFlagged,
@@ -8596,6 +8751,10 @@ function buildApiPathMap(routes) {
8596
8751
  const map = /* @__PURE__ */ new Map();
8597
8752
  for (const r of routes) {
8598
8753
  if (!map.has(r.path)) map.set(r.path, r.nodeId);
8754
+ const trimmed = r.path.replace(/\/\*[^/]+\?$/, "");
8755
+ if (trimmed && trimmed !== r.path && !map.has(trimmed)) {
8756
+ map.set(trimmed, r.nodeId);
8757
+ }
8599
8758
  }
8600
8759
  return map;
8601
8760
  }
@@ -8618,26 +8777,54 @@ function normalizeFetchUrl(raw) {
8618
8777
  return { path: s || "/", hadInterpolation };
8619
8778
  }
8620
8779
  function scoreApiRouteMatch(candidate, known) {
8621
- if (candidate.length !== known.length) return -1;
8622
8780
  let score = 0;
8623
- for (let i = 0; i < candidate.length; i++) {
8781
+ let i = 0, j = 0;
8782
+ while (i < candidate.length && j < known.length) {
8624
8783
  const a = candidate[i];
8625
- const b = known[i];
8784
+ const b = known[j];
8785
+ if (b.startsWith("*") && b.endsWith("?")) {
8786
+ score += 1;
8787
+ return score;
8788
+ }
8789
+ if (b.startsWith("*")) {
8790
+ const remaining = candidate.length - i;
8791
+ if (remaining < 1) return -1;
8792
+ score += 1 + remaining;
8793
+ return score;
8794
+ }
8626
8795
  if (a === b) {
8627
8796
  score += 3;
8797
+ i++;
8798
+ j++;
8628
8799
  continue;
8629
8800
  }
8630
8801
  if (a.startsWith(":") && b.startsWith(":")) {
8631
8802
  score += 2;
8803
+ i++;
8804
+ j++;
8632
8805
  continue;
8633
8806
  }
8634
8807
  if (a.startsWith(":") || b.startsWith(":")) {
8635
8808
  score += 1;
8809
+ i++;
8810
+ j++;
8636
8811
  continue;
8637
8812
  }
8638
8813
  return -1;
8639
8814
  }
8640
- return score;
8815
+ if (i === candidate.length) {
8816
+ while (j < known.length) {
8817
+ const b = known[j];
8818
+ if (b.startsWith("*") && b.endsWith("?")) {
8819
+ score += 1;
8820
+ j++;
8821
+ continue;
8822
+ }
8823
+ return -1;
8824
+ }
8825
+ return score;
8826
+ }
8827
+ return -1;
8641
8828
  }
8642
8829
  function resolveFetchCall(call, apiPathMap, apiRoutes) {
8643
8830
  const raw = call.url;
@@ -8794,48 +8981,58 @@ var fetchResolverParser = {
8794
8981
  // src/server/graph/parsers/crosslayer/api-annotations.ts
8795
8982
  var import_node_fs8 = require("node:fs");
8796
8983
  var import_node_path7 = require("node:path");
8984
+ init_config();
8797
8985
  var API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
8798
- function walk2(dir, exts) {
8799
- if (!(0, import_node_fs8.existsSync)(dir)) return [];
8800
- const results = [];
8801
- for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
8802
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
8803
- const full = (0, import_node_path7.join)(dir, entry.name);
8804
- if (entry.isDirectory()) {
8805
- results.push(...walk2(full, exts));
8806
- } else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
8807
- results.push(full);
8808
- }
8986
+ function toNodeId2(srcDir, rootDir, absPath) {
8987
+ const relFromSrc = (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
8988
+ if (relFromSrc.startsWith("..")) {
8989
+ return (0, import_node_path7.relative)(rootDir, absPath).replace(/\\/g, "/");
8809
8990
  }
8810
- return results;
8811
- }
8812
- function toNodeId2(srcDir, absPath) {
8813
- return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
8991
+ return relFromSrc;
8814
8992
  }
8815
8993
  var apiAnnotationsParser = {
8816
8994
  id: "api-annotations",
8817
8995
  layer: "crosslayer",
8818
8996
  concern: "api-binding",
8819
8997
  detect(rootDir) {
8820
- return (0, import_node_fs8.existsSync)((0, import_node_path7.join)(rootDir, "src"));
8998
+ return resolveProjectPaths(rootDir, loadConfig(rootDir)) !== null;
8821
8999
  },
8822
9000
  generate(rootDir, layerOutputs) {
8823
9001
  const apiOutput = layerOutputs.get("api");
8824
9002
  if (!apiOutput) {
8825
9003
  return { cross_refs: [], flagged_edges: [], warnings: [] };
8826
9004
  }
9005
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
9006
+ if (!paths) {
9007
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
9008
+ }
8827
9009
  const uiOutput = layerOutputs.get("ui");
8828
9010
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
8829
9011
  const apiRoutes = loadApiRoutesFromOutput(apiOutput);
8830
9012
  const apiPathMap = buildApiPathMap(apiRoutes);
8831
- const srcDir = (0, import_node_path7.join)(rootDir, "src");
8832
- const files = walk2(srcDir, [".ts", ".tsx"]);
9013
+ const srcDir = paths.srcDir;
9014
+ const seen = /* @__PURE__ */ new Set();
9015
+ const files = [];
9016
+ for (const root of paths.srcRoots) {
9017
+ for (const f of walk(root, [".ts", ".tsx"])) {
9018
+ if (!seen.has(f)) {
9019
+ seen.add(f);
9020
+ files.push(f);
9021
+ }
9022
+ }
9023
+ }
9024
+ for (const conv of paths.conventionFiles) {
9025
+ if (!seen.has(conv)) {
9026
+ seen.add(conv);
9027
+ files.push(conv);
9028
+ }
9029
+ }
8833
9030
  const crossRefs = [];
8834
9031
  const flaggedEdges = [];
8835
- const seen = /* @__PURE__ */ new Set();
9032
+ const seenEdge = /* @__PURE__ */ new Set();
8836
9033
  for (const absPath of files) {
8837
9034
  const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
8838
- const sourceId = toNodeId2(srcDir, absPath);
9035
+ const sourceId = toNodeId2(srcDir, rootDir, absPath);
8839
9036
  if (!uiNodeIds.has(sourceId)) continue;
8840
9037
  let match;
8841
9038
  API_ANNOTATION_RE.lastIndex = 0;
@@ -8845,8 +9042,8 @@ var apiAnnotationsParser = {
8845
9042
  const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
8846
9043
  if (result.kind === "resolved" && result.nodeId) {
8847
9044
  const key = `${sourceId}|${result.nodeId}|calls_api`;
8848
- if (seen.has(key)) continue;
8849
- seen.add(key);
9045
+ if (seenEdge.has(key)) continue;
9046
+ seenEdge.add(key);
8850
9047
  crossRefs.push({
8851
9048
  source: sourceId,
8852
9049
  target: result.nodeId,
@@ -8881,23 +9078,13 @@ var apiAnnotationsParser = {
8881
9078
  var import_node_fs9 = require("node:fs");
8882
9079
  var import_node_path8 = require("node:path");
8883
9080
  init_config();
8884
- var URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
8885
- function walk3(dir, exts) {
8886
- if (!(0, import_node_fs9.existsSync)(dir)) return [];
8887
- const results = [];
8888
- for (const entry of (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true })) {
8889
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
8890
- const full = (0, import_node_path8.join)(dir, entry.name);
8891
- if (entry.isDirectory()) {
8892
- results.push(...walk3(full, exts));
8893
- } else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
8894
- results.push(full);
8895
- }
9081
+ var URL_LITERAL_RE = /['"`](\/[a-zA-Z][^'"`\s]*?)['"`]/g;
9082
+ function toNodeId3(srcDir, rootDir, absPath) {
9083
+ const relFromSrc = (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
9084
+ if (relFromSrc.startsWith("..")) {
9085
+ return (0, import_node_path8.relative)(rootDir, absPath).replace(/\\/g, "/");
8896
9086
  }
8897
- return results;
8898
- }
8899
- function toNodeId3(srcDir, absPath) {
8900
- return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
9087
+ return relFromSrc;
8901
9088
  }
8902
9089
  var urlLiteralScannerParser = {
8903
9090
  id: "url-literal-scanner",
@@ -8918,15 +9105,26 @@ var urlLiteralScannerParser = {
8918
9105
  const apiPathMap = buildApiPathMap(apiRoutes);
8919
9106
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
8920
9107
  const srcDir = paths.srcDir;
8921
- const clientDir = (0, import_node_path8.join)(srcDir, "client");
8922
- const files = [
8923
- ...walk3(clientDir, [".ts", ".tsx"]),
8924
- ...walk3(paths.appDir, [".ts", ".tsx"])
8925
- ];
9108
+ const seenFile = /* @__PURE__ */ new Set();
9109
+ const files = [];
9110
+ for (const root of paths.srcRoots) {
9111
+ for (const f of walk(root, [".ts", ".tsx"])) {
9112
+ if (!seenFile.has(f)) {
9113
+ seenFile.add(f);
9114
+ files.push(f);
9115
+ }
9116
+ }
9117
+ }
9118
+ for (const conv of paths.conventionFiles) {
9119
+ if (!seenFile.has(conv)) {
9120
+ seenFile.add(conv);
9121
+ files.push(conv);
9122
+ }
9123
+ }
8926
9124
  const crossRefs = [];
8927
9125
  const seen = /* @__PURE__ */ new Set();
8928
9126
  for (const absPath of files) {
8929
- const sourceId = toNodeId3(srcDir, absPath);
9127
+ const sourceId = toNodeId3(srcDir, rootDir, absPath);
8930
9128
  if (!uiNodeIds.has(sourceId)) continue;
8931
9129
  const content = (0, import_node_fs9.readFileSync)(absPath, "utf-8");
8932
9130
  let match;
@@ -8961,6 +9159,7 @@ var urlLiteralScannerParser = {
8961
9159
  // src/server/graph/parsers/static/static-values.ts
8962
9160
  var import_node_fs10 = require("node:fs");
8963
9161
  var import_node_path9 = require("node:path");
9162
+ init_config();
8964
9163
  var parseCode = null;
8965
9164
  function tryLoadTreeSitter() {
8966
9165
  if (parseCode) return true;
@@ -8992,10 +9191,15 @@ function classifyScope(source, model) {
8992
9191
  function extractEnumValues(rootDir) {
8993
9192
  const nodes = [];
8994
9193
  const edges = [];
8995
- const schemaPaths = [
8996
- (0, import_node_path9.join)(rootDir, "prisma", "schema.prisma"),
8997
- (0, import_node_path9.join)(rootDir, "prisma", "schema")
8998
- ];
9194
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
9195
+ const schemaPaths = [];
9196
+ if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath) {
9197
+ schemaPaths.push(paths.dbConfig.schemaPath);
9198
+ schemaPaths.push((0, import_node_path9.join)((0, import_node_path9.dirname)(paths.dbConfig.schemaPath), "schema"));
9199
+ } else {
9200
+ schemaPaths.push((0, import_node_path9.join)(rootDir, "prisma", "schema.prisma"));
9201
+ schemaPaths.push((0, import_node_path9.join)(rootDir, "prisma", "schema"));
9202
+ }
8999
9203
  let content = "";
9000
9204
  for (const p of schemaPaths) {
9001
9205
  if ((0, import_node_fs10.existsSync)(p)) {
@@ -9079,7 +9283,7 @@ function extractStringArrayFromNode(node) {
9079
9283
  return values;
9080
9284
  }
9081
9285
  function findArrayDecl(root, varName) {
9082
- function walk4(node) {
9286
+ function walk2(node) {
9083
9287
  if (node.type === "variable_declarator") {
9084
9288
  const nameNode = node.childForFieldName("name");
9085
9289
  const valueNode = node.childForFieldName("value");
@@ -9092,12 +9296,12 @@ function findArrayDecl(root, varName) {
9092
9296
  }
9093
9297
  }
9094
9298
  for (const child of node.namedChildren) {
9095
- const found = walk4(child);
9299
+ const found = walk2(child);
9096
9300
  if (found) return found;
9097
9301
  }
9098
9302
  return null;
9099
9303
  }
9100
- return walk4(root);
9304
+ return walk2(root);
9101
9305
  }
9102
9306
  function extractObjectPropsRegex(objStr) {
9103
9307
  const props = {};
@@ -9160,11 +9364,26 @@ function modelToNodeType(model) {
9160
9364
  function extractSeedData(rootDir) {
9161
9365
  const nodes = [];
9162
9366
  const edges = [];
9163
- const seedFiles = [
9164
- (0, import_node_path9.join)(rootDir, "prisma", "seed.ts"),
9165
- (0, import_node_path9.join)(rootDir, "prisma", "seed.js"),
9166
- (0, import_node_path9.join)(rootDir, "src", "server", "lib", "system-tags.ts")
9167
- ].filter(import_node_fs10.existsSync);
9367
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
9368
+ const candidates = [];
9369
+ if (paths?.dbDir) {
9370
+ candidates.push((0, import_node_path9.join)(paths.dbDir, "seed.ts"));
9371
+ candidates.push((0, import_node_path9.join)(paths.dbDir, "seed.js"));
9372
+ } else {
9373
+ candidates.push((0, import_node_path9.join)(rootDir, "prisma", "seed.ts"));
9374
+ candidates.push((0, import_node_path9.join)(rootDir, "prisma", "seed.js"));
9375
+ }
9376
+ const baseRoots = paths?.srcRoots ?? [(0, import_node_path9.join)(rootDir, "src")];
9377
+ for (const root of baseRoots) {
9378
+ candidates.push((0, import_node_path9.join)(root, "server", "lib", "system-tags.ts"));
9379
+ }
9380
+ const seedFiles = candidates.filter((p) => {
9381
+ try {
9382
+ return (0, import_node_fs10.existsSync)(p) && (0, import_node_fs10.statSync)(p).isFile();
9383
+ } catch {
9384
+ return false;
9385
+ }
9386
+ });
9168
9387
  const useTreeSitter = tryLoadTreeSitter();
9169
9388
  for (const filePath of seedFiles) {
9170
9389
  const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
@@ -9274,9 +9493,20 @@ function walkDir(dir, exts) {
9274
9493
  }
9275
9494
  function extractConstants(rootDir) {
9276
9495
  const nodes = [];
9277
- const srcDir = (0, import_node_path9.join)(rootDir, "src");
9278
- if (!(0, import_node_fs10.existsSync)(srcDir)) return { nodes };
9279
- for (const filePath of walkDir(srcDir, [".ts", ".tsx"])) {
9496
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
9497
+ const roots = paths?.srcRoots ?? [(0, import_node_path9.join)(rootDir, "src")];
9498
+ const seenFile = /* @__PURE__ */ new Set();
9499
+ const allFiles = [];
9500
+ for (const root of roots) {
9501
+ for (const f of walkDir(root, [".ts", ".tsx"])) {
9502
+ if (!seenFile.has(f)) {
9503
+ seenFile.add(f);
9504
+ allFiles.push(f);
9505
+ }
9506
+ }
9507
+ }
9508
+ if (allFiles.length === 0) return { nodes };
9509
+ for (const filePath of allFiles) {
9280
9510
  const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
9281
9511
  const relPath = (0, import_node_path9.relative)(rootDir, filePath);
9282
9512
  const constArrayRe = /export\s+const\s+([A-Z][A-Z_0-9]+)\s*(?::[^=]+)?\s*=\s*\[/g;
@@ -9311,6 +9541,13 @@ function extractConstants(rootDir) {
9311
9541
  return { nodes };
9312
9542
  }
9313
9543
  function detect4(rootDir) {
9544
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
9545
+ if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath && (0, import_node_fs10.existsSync)(paths.dbConfig.schemaPath)) {
9546
+ return true;
9547
+ }
9548
+ if (paths?.dbDir) {
9549
+ 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;
9550
+ }
9314
9551
  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"));
9315
9552
  }
9316
9553
  function generate4(rootDir) {
@@ -9530,13 +9767,22 @@ var staticRefScannerParser = {
9530
9767
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
9531
9768
  if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
9532
9769
  const srcDir = paths.srcDir;
9533
- const files = [
9534
- ...walkWithIgnore((0, import_node_path10.join)(srcDir, "client"), [".ts", ".tsx"]),
9535
- ...walkWithIgnore(paths.appDir, [".ts", ".tsx"]),
9536
- ...walkWithIgnore((0, import_node_path10.join)(srcDir, "server"), [".ts", ".tsx"]),
9537
- ...walkWithIgnore((0, import_node_path10.join)(srcDir, "lib"), [".ts", ".tsx"]),
9538
- ...walkWithIgnore((0, import_node_path10.join)(srcDir, "config"), [".ts", ".tsx"])
9539
- ];
9770
+ const seenFile = /* @__PURE__ */ new Set();
9771
+ const files = [];
9772
+ for (const root of paths.srcRoots) {
9773
+ for (const f of walkWithIgnore(root, [".ts", ".tsx"])) {
9774
+ if (!seenFile.has(f)) {
9775
+ seenFile.add(f);
9776
+ files.push(f);
9777
+ }
9778
+ }
9779
+ }
9780
+ for (const conv of paths.conventionFiles) {
9781
+ if (!seenFile.has(conv)) {
9782
+ seenFile.add(conv);
9783
+ files.push(conv);
9784
+ }
9785
+ }
9540
9786
  const uiOutput = layerOutputs.get("ui");
9541
9787
  const apiOutput = layerOutputs.get("api");
9542
9788
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
@@ -9553,7 +9799,8 @@ var staticRefScannerParser = {
9553
9799
  const seen = /* @__PURE__ */ new Set();
9554
9800
  let filesScanned = 0;
9555
9801
  for (const absPath of files) {
9556
- const sourceId = (0, import_node_path10.relative)(srcDir, absPath).replace(/\\/g, "/");
9802
+ const relFromSrc = (0, import_node_path10.relative)(srcDir, absPath).replace(/\\/g, "/");
9803
+ const sourceId = relFromSrc.startsWith("..") ? (0, import_node_path10.relative)(rootDir, absPath).replace(/\\/g, "/") : relFromSrc;
9557
9804
  const sourceLayer = uiNodeIds.has(sourceId) ? "ui" : apiNodeIds.has(sourceId) ? "api" : null;
9558
9805
  if (!sourceLayer) continue;
9559
9806
  const content = (0, import_node_fs11.readFileSync)(absPath, "utf-8");
@@ -10014,6 +10261,9 @@ var GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
10014
10261
  "__tests__",
10015
10262
  "spec",
10016
10263
  "specs",
10264
+ "scripts",
10265
+ "bin",
10266
+ "tools",
10017
10267
  // Go
10018
10268
  "cmd",
10019
10269
  "pkg",
@@ -10101,13 +10351,17 @@ function isTrivialGroup(name, extraTrivial) {
10101
10351
  function normalizeGroupName(name) {
10102
10352
  return name.toLowerCase().replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
10103
10353
  }
10104
- function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
10354
+ function extractModuleFromPath(id, extraTrivial, extraSkipSegments, extraGenericRoles) {
10105
10355
  const segments = id.split("/");
10106
10356
  const routeGroups = extractRouteGroups(id);
10107
10357
  const skipSegments = new Set(SKIP_SEGMENTS_BUILTIN);
10358
+ for (const r of GENERIC_ROLE_NAMES_BUILTIN) skipSegments.add(r);
10108
10359
  if (extraSkipSegments) {
10109
10360
  for (const s of extraSkipSegments) skipSegments.add(s);
10110
10361
  }
10362
+ if (extraGenericRoles) {
10363
+ for (const r of extraGenericRoles) skipSegments.add(r);
10364
+ }
10111
10365
  const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g, extraTrivial)).map(normalizeGroupName);
10112
10366
  if (moduleGroups.length > 0) {
10113
10367
  return moduleGroups[moduleGroups.length - 1];
@@ -10858,7 +11112,7 @@ function checkDeadScreens(rootDir) {
10858
11112
  const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
10859
11113
  const navTargets = /* @__PURE__ */ new Set();
10860
11114
  for (const e of ui.edges) {
10861
- if (e.type === "navigates" || e.type === "renders" || e.type === "imports") {
11115
+ if (e.type === "navigates" || e.type === "renders" || e.type === "imports" || e.type === "wraps") {
10862
11116
  navTargets.add(e.target);
10863
11117
  }
10864
11118
  }
@@ -10868,13 +11122,15 @@ function checkDeadScreens(rootDir) {
10868
11122
  for (const page of pages) {
10869
11123
  if (page.id.endsWith("layout.tsx") && page.id.split("/").length <= 2) continue;
10870
11124
  if (["error.tsx", "loading.tsx", "not-found.tsx", "template.tsx"].some((s) => page.id.endsWith(s))) continue;
11125
+ const route = page.route;
11126
+ if (route === "/" || route === "") continue;
10871
11127
  if (!navTargets.has(page.id)) {
10872
11128
  findings.push({
10873
11129
  id: `dead:${page.id}`,
10874
11130
  severity: "info",
10875
11131
  category: "dead_screens",
10876
11132
  title: page.name,
10877
- detail: `Page "${page.id}" has no incoming navigation, render, or import edges.`,
11133
+ detail: `Page "${page.id}" has no incoming navigation, render, import, or layout-wrap edges.`,
10878
11134
  file: page.id
10879
11135
  });
10880
11136
  }
@@ -12201,7 +12457,7 @@ function handleDetectProjectStack() {
12201
12457
  const descriptions = {
12202
12458
  "fetch-resolver": "Detects direct fetch()/api.get() calls with inline URLs",
12203
12459
  "api-annotations": "Scans for @api METHOD /path annotations in JSDoc/comments",
12204
- "url-literal-scanner": "Finds /api/... string literals as fallback detection",
12460
+ "url-literal-scanner": "Finds path-like string literals (any /\u2026) and resolves them to API routes",
12205
12461
  "static-ref-scanner": "Finds references to static values (enums, permissions, roles)"
12206
12462
  };
12207
12463
  const grouped = {};