@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.
@@ -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,7 +7699,9 @@ 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
7706
  if (route) routeToNodeId.set(route, id);
7601
7707
  }
@@ -7619,6 +7725,29 @@ function generate(rootDir) {
7619
7725
  uiEdges.push(...edges);
7620
7726
  uiFlagged.push(...flagged);
7621
7727
  }
7728
+ const layoutsById = /* @__PURE__ */ new Set();
7729
+ for (const n of uiNodes) {
7730
+ if (n.type === "layout") layoutsById.add(n.id);
7731
+ }
7732
+ function findClosestLayout(pageId) {
7733
+ let dir = pageId.replace(/\/page\.tsx$/, "");
7734
+ while (dir.length > 0) {
7735
+ const candidate = `${dir}/layout.tsx`;
7736
+ if (layoutsById.has(candidate)) return candidate;
7737
+ const slash = dir.lastIndexOf("/");
7738
+ if (slash < 0) break;
7739
+ dir = dir.slice(0, slash);
7740
+ }
7741
+ if (layoutsById.has("app/layout.tsx")) return "app/layout.tsx";
7742
+ return null;
7743
+ }
7744
+ for (const n of uiNodes) {
7745
+ if (n.type !== "page") continue;
7746
+ const layoutId = findClosestLayout(n.id);
7747
+ if (layoutId && layoutId !== n.id) {
7748
+ uiEdges.push({ source: layoutId, target: n.id, type: "wraps" });
7749
+ }
7750
+ }
7622
7751
  const fetchCallEntries = [];
7623
7752
  for (const absPath of fileSet) {
7624
7753
  const sourceId = toNodeId(srcDir, rootDir, absPath);
@@ -7700,17 +7829,37 @@ function generate(rootDir) {
7700
7829
  if (!dbCalls) continue;
7701
7830
  const seenModels = /* @__PURE__ */ new Set();
7702
7831
  for (const call of dbCalls) {
7703
- if (seenModels.has(call.model)) continue;
7704
- seenModels.add(call.model);
7832
+ const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
7833
+ if (seenModels.has(target)) continue;
7834
+ seenModels.add(target);
7705
7835
  apiCrossRefs.push({
7706
7836
  source: node.id,
7707
- target: camelToPascal(call.model),
7837
+ target,
7708
7838
  type: call.isMutation ? "mutates" : "reads",
7709
7839
  layer: "db"
7710
7840
  });
7711
7841
  }
7712
7842
  delete node._dbCalls;
7713
7843
  }
7844
+ const uiCrossRefs = [];
7845
+ for (const node of uiNodes) {
7846
+ const dbCalls = node._dbCalls;
7847
+ if (!dbCalls) continue;
7848
+ const seenModels = /* @__PURE__ */ new Set();
7849
+ for (const call of dbCalls) {
7850
+ const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
7851
+ if (seenModels.has(target)) continue;
7852
+ seenModels.add(target);
7853
+ uiCrossRefs.push({
7854
+ source: node.id,
7855
+ target,
7856
+ type: call.isMutation ? "mutates" : "reads",
7857
+ layer: "db"
7858
+ });
7859
+ }
7860
+ delete node._dbCalls;
7861
+ }
7862
+ uiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
7714
7863
  const apiNodeIds = new Set(apiNodes.map((n) => n.id));
7715
7864
  const apiEdges = [];
7716
7865
  const uiOnlyEdges = [];
@@ -7772,7 +7921,7 @@ function generate(rootDir) {
7772
7921
  },
7773
7922
  nodes: stripLayer(uiNodes),
7774
7923
  edges: uiOnlyEdges,
7775
- cross_refs: [],
7924
+ cross_refs: uiCrossRefs,
7776
7925
  contradictions: [],
7777
7926
  warnings: [],
7778
7927
  flagged_edges: dedupedFlagged,
@@ -8618,26 +8767,54 @@ function normalizeFetchUrl(raw) {
8618
8767
  return { path: s || "/", hadInterpolation };
8619
8768
  }
8620
8769
  function scoreApiRouteMatch(candidate, known) {
8621
- if (candidate.length !== known.length) return -1;
8622
8770
  let score = 0;
8623
- for (let i = 0; i < candidate.length; i++) {
8771
+ let i = 0, j = 0;
8772
+ while (i < candidate.length && j < known.length) {
8624
8773
  const a = candidate[i];
8625
- const b = known[i];
8774
+ const b = known[j];
8775
+ if (b.startsWith("*") && b.endsWith("?")) {
8776
+ score += 1;
8777
+ return score;
8778
+ }
8779
+ if (b.startsWith("*")) {
8780
+ const remaining = candidate.length - i;
8781
+ if (remaining < 1) return -1;
8782
+ score += 1 + remaining;
8783
+ return score;
8784
+ }
8626
8785
  if (a === b) {
8627
8786
  score += 3;
8787
+ i++;
8788
+ j++;
8628
8789
  continue;
8629
8790
  }
8630
8791
  if (a.startsWith(":") && b.startsWith(":")) {
8631
8792
  score += 2;
8793
+ i++;
8794
+ j++;
8632
8795
  continue;
8633
8796
  }
8634
8797
  if (a.startsWith(":") || b.startsWith(":")) {
8635
8798
  score += 1;
8799
+ i++;
8800
+ j++;
8636
8801
  continue;
8637
8802
  }
8638
8803
  return -1;
8639
8804
  }
8640
- return score;
8805
+ if (i === candidate.length) {
8806
+ while (j < known.length) {
8807
+ const b = known[j];
8808
+ if (b.startsWith("*") && b.endsWith("?")) {
8809
+ score += 1;
8810
+ j++;
8811
+ continue;
8812
+ }
8813
+ return -1;
8814
+ }
8815
+ return score;
8816
+ }
8817
+ return -1;
8641
8818
  }
8642
8819
  function resolveFetchCall(call, apiPathMap, apiRoutes) {
8643
8820
  const raw = call.url;
@@ -8794,48 +8971,58 @@ var fetchResolverParser = {
8794
8971
  // src/server/graph/parsers/crosslayer/api-annotations.ts
8795
8972
  var import_node_fs8 = require("node:fs");
8796
8973
  var import_node_path7 = require("node:path");
8974
+ init_config();
8797
8975
  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
- }
8976
+ function toNodeId2(srcDir, rootDir, absPath) {
8977
+ const relFromSrc = (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
8978
+ if (relFromSrc.startsWith("..")) {
8979
+ return (0, import_node_path7.relative)(rootDir, absPath).replace(/\\/g, "/");
8809
8980
  }
8810
- return results;
8811
- }
8812
- function toNodeId2(srcDir, absPath) {
8813
- return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
8981
+ return relFromSrc;
8814
8982
  }
8815
8983
  var apiAnnotationsParser = {
8816
8984
  id: "api-annotations",
8817
8985
  layer: "crosslayer",
8818
8986
  concern: "api-binding",
8819
8987
  detect(rootDir) {
8820
- return (0, import_node_fs8.existsSync)((0, import_node_path7.join)(rootDir, "src"));
8988
+ return resolveProjectPaths(rootDir, loadConfig(rootDir)) !== null;
8821
8989
  },
8822
8990
  generate(rootDir, layerOutputs) {
8823
8991
  const apiOutput = layerOutputs.get("api");
8824
8992
  if (!apiOutput) {
8825
8993
  return { cross_refs: [], flagged_edges: [], warnings: [] };
8826
8994
  }
8995
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
8996
+ if (!paths) {
8997
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
8998
+ }
8827
8999
  const uiOutput = layerOutputs.get("ui");
8828
9000
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
8829
9001
  const apiRoutes = loadApiRoutesFromOutput(apiOutput);
8830
9002
  const apiPathMap = buildApiPathMap(apiRoutes);
8831
- const srcDir = (0, import_node_path7.join)(rootDir, "src");
8832
- const files = walk2(srcDir, [".ts", ".tsx"]);
9003
+ const srcDir = paths.srcDir;
9004
+ const seen = /* @__PURE__ */ new Set();
9005
+ const files = [];
9006
+ for (const root of paths.srcRoots) {
9007
+ for (const f of walk(root, [".ts", ".tsx"])) {
9008
+ if (!seen.has(f)) {
9009
+ seen.add(f);
9010
+ files.push(f);
9011
+ }
9012
+ }
9013
+ }
9014
+ for (const conv of paths.conventionFiles) {
9015
+ if (!seen.has(conv)) {
9016
+ seen.add(conv);
9017
+ files.push(conv);
9018
+ }
9019
+ }
8833
9020
  const crossRefs = [];
8834
9021
  const flaggedEdges = [];
8835
- const seen = /* @__PURE__ */ new Set();
9022
+ const seenEdge = /* @__PURE__ */ new Set();
8836
9023
  for (const absPath of files) {
8837
9024
  const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
8838
- const sourceId = toNodeId2(srcDir, absPath);
9025
+ const sourceId = toNodeId2(srcDir, rootDir, absPath);
8839
9026
  if (!uiNodeIds.has(sourceId)) continue;
8840
9027
  let match;
8841
9028
  API_ANNOTATION_RE.lastIndex = 0;
@@ -8845,8 +9032,8 @@ var apiAnnotationsParser = {
8845
9032
  const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
8846
9033
  if (result.kind === "resolved" && result.nodeId) {
8847
9034
  const key = `${sourceId}|${result.nodeId}|calls_api`;
8848
- if (seen.has(key)) continue;
8849
- seen.add(key);
9035
+ if (seenEdge.has(key)) continue;
9036
+ seenEdge.add(key);
8850
9037
  crossRefs.push({
8851
9038
  source: sourceId,
8852
9039
  target: result.nodeId,
@@ -8881,23 +9068,13 @@ var apiAnnotationsParser = {
8881
9068
  var import_node_fs9 = require("node:fs");
8882
9069
  var import_node_path8 = require("node:path");
8883
9070
  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
- }
9071
+ var URL_LITERAL_RE = /['"`](\/[a-zA-Z][^'"`\s]*?)['"`]/g;
9072
+ function toNodeId3(srcDir, rootDir, absPath) {
9073
+ const relFromSrc = (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
9074
+ if (relFromSrc.startsWith("..")) {
9075
+ return (0, import_node_path8.relative)(rootDir, absPath).replace(/\\/g, "/");
8896
9076
  }
8897
- return results;
8898
- }
8899
- function toNodeId3(srcDir, absPath) {
8900
- return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
9077
+ return relFromSrc;
8901
9078
  }
8902
9079
  var urlLiteralScannerParser = {
8903
9080
  id: "url-literal-scanner",
@@ -8918,15 +9095,26 @@ var urlLiteralScannerParser = {
8918
9095
  const apiPathMap = buildApiPathMap(apiRoutes);
8919
9096
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
8920
9097
  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
- ];
9098
+ const seenFile = /* @__PURE__ */ new Set();
9099
+ const files = [];
9100
+ for (const root of paths.srcRoots) {
9101
+ for (const f of walk(root, [".ts", ".tsx"])) {
9102
+ if (!seenFile.has(f)) {
9103
+ seenFile.add(f);
9104
+ files.push(f);
9105
+ }
9106
+ }
9107
+ }
9108
+ for (const conv of paths.conventionFiles) {
9109
+ if (!seenFile.has(conv)) {
9110
+ seenFile.add(conv);
9111
+ files.push(conv);
9112
+ }
9113
+ }
8926
9114
  const crossRefs = [];
8927
9115
  const seen = /* @__PURE__ */ new Set();
8928
9116
  for (const absPath of files) {
8929
- const sourceId = toNodeId3(srcDir, absPath);
9117
+ const sourceId = toNodeId3(srcDir, rootDir, absPath);
8930
9118
  if (!uiNodeIds.has(sourceId)) continue;
8931
9119
  const content = (0, import_node_fs9.readFileSync)(absPath, "utf-8");
8932
9120
  let match;
@@ -8961,6 +9149,7 @@ var urlLiteralScannerParser = {
8961
9149
  // src/server/graph/parsers/static/static-values.ts
8962
9150
  var import_node_fs10 = require("node:fs");
8963
9151
  var import_node_path9 = require("node:path");
9152
+ init_config();
8964
9153
  var parseCode = null;
8965
9154
  function tryLoadTreeSitter() {
8966
9155
  if (parseCode) return true;
@@ -8992,10 +9181,15 @@ function classifyScope(source, model) {
8992
9181
  function extractEnumValues(rootDir) {
8993
9182
  const nodes = [];
8994
9183
  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
- ];
9184
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
9185
+ const schemaPaths = [];
9186
+ if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath) {
9187
+ schemaPaths.push(paths.dbConfig.schemaPath);
9188
+ schemaPaths.push((0, import_node_path9.join)((0, import_node_path9.dirname)(paths.dbConfig.schemaPath), "schema"));
9189
+ } else {
9190
+ schemaPaths.push((0, import_node_path9.join)(rootDir, "prisma", "schema.prisma"));
9191
+ schemaPaths.push((0, import_node_path9.join)(rootDir, "prisma", "schema"));
9192
+ }
8999
9193
  let content = "";
9000
9194
  for (const p of schemaPaths) {
9001
9195
  if ((0, import_node_fs10.existsSync)(p)) {
@@ -9079,7 +9273,7 @@ function extractStringArrayFromNode(node) {
9079
9273
  return values;
9080
9274
  }
9081
9275
  function findArrayDecl(root, varName) {
9082
- function walk4(node) {
9276
+ function walk2(node) {
9083
9277
  if (node.type === "variable_declarator") {
9084
9278
  const nameNode = node.childForFieldName("name");
9085
9279
  const valueNode = node.childForFieldName("value");
@@ -9092,12 +9286,12 @@ function findArrayDecl(root, varName) {
9092
9286
  }
9093
9287
  }
9094
9288
  for (const child of node.namedChildren) {
9095
- const found = walk4(child);
9289
+ const found = walk2(child);
9096
9290
  if (found) return found;
9097
9291
  }
9098
9292
  return null;
9099
9293
  }
9100
- return walk4(root);
9294
+ return walk2(root);
9101
9295
  }
9102
9296
  function extractObjectPropsRegex(objStr) {
9103
9297
  const props = {};
@@ -9160,11 +9354,26 @@ function modelToNodeType(model) {
9160
9354
  function extractSeedData(rootDir) {
9161
9355
  const nodes = [];
9162
9356
  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);
9357
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
9358
+ const candidates = [];
9359
+ if (paths?.dbDir) {
9360
+ candidates.push((0, import_node_path9.join)(paths.dbDir, "seed.ts"));
9361
+ candidates.push((0, import_node_path9.join)(paths.dbDir, "seed.js"));
9362
+ } else {
9363
+ candidates.push((0, import_node_path9.join)(rootDir, "prisma", "seed.ts"));
9364
+ candidates.push((0, import_node_path9.join)(rootDir, "prisma", "seed.js"));
9365
+ }
9366
+ const baseRoots = paths?.srcRoots ?? [(0, import_node_path9.join)(rootDir, "src")];
9367
+ for (const root of baseRoots) {
9368
+ candidates.push((0, import_node_path9.join)(root, "server", "lib", "system-tags.ts"));
9369
+ }
9370
+ const seedFiles = candidates.filter((p) => {
9371
+ try {
9372
+ return (0, import_node_fs10.existsSync)(p) && (0, import_node_fs10.statSync)(p).isFile();
9373
+ } catch {
9374
+ return false;
9375
+ }
9376
+ });
9168
9377
  const useTreeSitter = tryLoadTreeSitter();
9169
9378
  for (const filePath of seedFiles) {
9170
9379
  const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
@@ -9274,9 +9483,20 @@ function walkDir(dir, exts) {
9274
9483
  }
9275
9484
  function extractConstants(rootDir) {
9276
9485
  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"])) {
9486
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
9487
+ const roots = paths?.srcRoots ?? [(0, import_node_path9.join)(rootDir, "src")];
9488
+ const seenFile = /* @__PURE__ */ new Set();
9489
+ const allFiles = [];
9490
+ for (const root of roots) {
9491
+ for (const f of walkDir(root, [".ts", ".tsx"])) {
9492
+ if (!seenFile.has(f)) {
9493
+ seenFile.add(f);
9494
+ allFiles.push(f);
9495
+ }
9496
+ }
9497
+ }
9498
+ if (allFiles.length === 0) return { nodes };
9499
+ for (const filePath of allFiles) {
9280
9500
  const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
9281
9501
  const relPath = (0, import_node_path9.relative)(rootDir, filePath);
9282
9502
  const constArrayRe = /export\s+const\s+([A-Z][A-Z_0-9]+)\s*(?::[^=]+)?\s*=\s*\[/g;
@@ -9311,6 +9531,13 @@ function extractConstants(rootDir) {
9311
9531
  return { nodes };
9312
9532
  }
9313
9533
  function detect4(rootDir) {
9534
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
9535
+ if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath && (0, import_node_fs10.existsSync)(paths.dbConfig.schemaPath)) {
9536
+ return true;
9537
+ }
9538
+ if (paths?.dbDir) {
9539
+ 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;
9540
+ }
9314
9541
  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
9542
  }
9316
9543
  function generate4(rootDir) {
@@ -9530,13 +9757,22 @@ var staticRefScannerParser = {
9530
9757
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
9531
9758
  if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
9532
9759
  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
- ];
9760
+ const seenFile = /* @__PURE__ */ new Set();
9761
+ const files = [];
9762
+ for (const root of paths.srcRoots) {
9763
+ for (const f of walkWithIgnore(root, [".ts", ".tsx"])) {
9764
+ if (!seenFile.has(f)) {
9765
+ seenFile.add(f);
9766
+ files.push(f);
9767
+ }
9768
+ }
9769
+ }
9770
+ for (const conv of paths.conventionFiles) {
9771
+ if (!seenFile.has(conv)) {
9772
+ seenFile.add(conv);
9773
+ files.push(conv);
9774
+ }
9775
+ }
9540
9776
  const uiOutput = layerOutputs.get("ui");
9541
9777
  const apiOutput = layerOutputs.get("api");
9542
9778
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
@@ -9553,7 +9789,8 @@ var staticRefScannerParser = {
9553
9789
  const seen = /* @__PURE__ */ new Set();
9554
9790
  let filesScanned = 0;
9555
9791
  for (const absPath of files) {
9556
- const sourceId = (0, import_node_path10.relative)(srcDir, absPath).replace(/\\/g, "/");
9792
+ const relFromSrc = (0, import_node_path10.relative)(srcDir, absPath).replace(/\\/g, "/");
9793
+ const sourceId = relFromSrc.startsWith("..") ? (0, import_node_path10.relative)(rootDir, absPath).replace(/\\/g, "/") : relFromSrc;
9557
9794
  const sourceLayer = uiNodeIds.has(sourceId) ? "ui" : apiNodeIds.has(sourceId) ? "api" : null;
9558
9795
  if (!sourceLayer) continue;
9559
9796
  const content = (0, import_node_fs11.readFileSync)(absPath, "utf-8");
@@ -10101,13 +10338,17 @@ function isTrivialGroup(name, extraTrivial) {
10101
10338
  function normalizeGroupName(name) {
10102
10339
  return name.toLowerCase().replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
10103
10340
  }
10104
- function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
10341
+ function extractModuleFromPath(id, extraTrivial, extraSkipSegments, extraGenericRoles) {
10105
10342
  const segments = id.split("/");
10106
10343
  const routeGroups = extractRouteGroups(id);
10107
10344
  const skipSegments = new Set(SKIP_SEGMENTS_BUILTIN);
10345
+ for (const r of GENERIC_ROLE_NAMES_BUILTIN) skipSegments.add(r);
10108
10346
  if (extraSkipSegments) {
10109
10347
  for (const s of extraSkipSegments) skipSegments.add(s);
10110
10348
  }
10349
+ if (extraGenericRoles) {
10350
+ for (const r of extraGenericRoles) skipSegments.add(r);
10351
+ }
10111
10352
  const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g, extraTrivial)).map(normalizeGroupName);
10112
10353
  if (moduleGroups.length > 0) {
10113
10354
  return moduleGroups[moduleGroups.length - 1];
@@ -12201,7 +12442,7 @@ function handleDetectProjectStack() {
12201
12442
  const descriptions = {
12202
12443
  "fetch-resolver": "Detects direct fetch()/api.get() calls with inline URLs",
12203
12444
  "api-annotations": "Scans for @api METHOD /path annotations in JSDoc/comments",
12204
- "url-literal-scanner": "Finds /api/... string literals as fallback detection",
12445
+ "url-literal-scanner": "Finds path-like string literals (any /\u2026) and resolves them to API routes",
12205
12446
  "static-ref-scanner": "Finds references to static values (enums, permissions, roles)"
12206
12447
  };
12207
12448
  const grouped = {};