@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.
- package/dist/server/chart-serve.js +336 -95
- package/dist/server/cli.js +337 -96
- package/dist/server/graph/queries/classify.scm +8 -0
- package/dist/server/graph/queries/db-calls.scm +21 -0
- package/dist/server/graph/queries/deep/jsx-semantic.scm +13 -2
- package/dist/server/graph/queries/navigations.scm +28 -12
- package/dist/server/graph-mcp-entry.js +341 -96
- package/package.json +1 -1
|
@@ -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 =
|
|
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
|
-
|
|
1023
|
-
|
|
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
|
-
|
|
1112
|
+
i++;
|
|
1113
|
+
j++;
|
|
1034
1114
|
continue;
|
|
1035
1115
|
}
|
|
1036
1116
|
return -1;
|
|
1037
1117
|
}
|
|
1038
|
-
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1309
|
+
type: isServerAction ? "server-action" : "endpoint",
|
|
1310
|
+
// For HTTP routes: name = URL path. For Server Actions: name = file id +
|
|
1311
|
+
// exported action names — the callable surface, not a URL.
|
|
1312
|
+
name: isServerAction ? actions.length > 0 ? `${id} (${actions.join(", ")})` : id : routePath,
|
|
1213
1313
|
layer: "api",
|
|
1214
1314
|
path: routePath,
|
|
1315
|
+
// null for Server Actions
|
|
1215
1316
|
methods,
|
|
1216
1317
|
handler: id,
|
|
1217
1318
|
mutates,
|
|
1319
|
+
...isServerAction ? { actions } : {},
|
|
1218
1320
|
auth: authStrategy.length > 0 ? authStrategy : ["public"],
|
|
1219
1321
|
db_models: [...new Set(dbCalls.map((c) => c.model))],
|
|
1220
1322
|
db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
|
|
@@ -1228,6 +1330,8 @@ function generate(rootDir) {
|
|
|
1228
1330
|
} else {
|
|
1229
1331
|
const route = extractRoute(id);
|
|
1230
1332
|
const deep = extractDeep(absPath);
|
|
1333
|
+
const dbCalls = extractDbCallsTS(absPath);
|
|
1334
|
+
const authWrappers = type === "page" || type === "layout" ? [...extractAuthWrappersTS(absPath)] : [];
|
|
1231
1335
|
uiNodes.push({
|
|
1232
1336
|
id,
|
|
1233
1337
|
type,
|
|
@@ -1238,7 +1342,9 @@ function generate(rootDir) {
|
|
|
1238
1342
|
elements: deep.elements,
|
|
1239
1343
|
stateVars: deep.stateVars,
|
|
1240
1344
|
conditions: deep.conditions,
|
|
1241
|
-
variables: deep.variables
|
|
1345
|
+
variables: deep.variables,
|
|
1346
|
+
...authWrappers.length > 0 ? { auth: authWrappers } : {},
|
|
1347
|
+
...dbCalls.length > 0 ? { _dbCalls: dbCalls } : {}
|
|
1242
1348
|
});
|
|
1243
1349
|
if (route) routeToNodeId.set(route, id);
|
|
1244
1350
|
}
|
|
@@ -1262,6 +1368,29 @@ function generate(rootDir) {
|
|
|
1262
1368
|
uiEdges.push(...edges);
|
|
1263
1369
|
uiFlagged.push(...flagged);
|
|
1264
1370
|
}
|
|
1371
|
+
const layoutsById = /* @__PURE__ */ new Set();
|
|
1372
|
+
for (const n of uiNodes) {
|
|
1373
|
+
if (n.type === "layout") layoutsById.add(n.id);
|
|
1374
|
+
}
|
|
1375
|
+
function findClosestLayout(pageId) {
|
|
1376
|
+
let dir = pageId.replace(/\/page\.tsx$/, "");
|
|
1377
|
+
while (dir.length > 0) {
|
|
1378
|
+
const candidate = `${dir}/layout.tsx`;
|
|
1379
|
+
if (layoutsById.has(candidate)) return candidate;
|
|
1380
|
+
const slash = dir.lastIndexOf("/");
|
|
1381
|
+
if (slash < 0) break;
|
|
1382
|
+
dir = dir.slice(0, slash);
|
|
1383
|
+
}
|
|
1384
|
+
if (layoutsById.has("app/layout.tsx")) return "app/layout.tsx";
|
|
1385
|
+
return null;
|
|
1386
|
+
}
|
|
1387
|
+
for (const n of uiNodes) {
|
|
1388
|
+
if (n.type !== "page") continue;
|
|
1389
|
+
const layoutId = findClosestLayout(n.id);
|
|
1390
|
+
if (layoutId && layoutId !== n.id) {
|
|
1391
|
+
uiEdges.push({ source: layoutId, target: n.id, type: "wraps" });
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1265
1394
|
const fetchCallEntries = [];
|
|
1266
1395
|
for (const absPath of fileSet) {
|
|
1267
1396
|
const sourceId = toNodeId(srcDir, rootDir, absPath);
|
|
@@ -1343,17 +1472,37 @@ function generate(rootDir) {
|
|
|
1343
1472
|
if (!dbCalls) continue;
|
|
1344
1473
|
const seenModels = /* @__PURE__ */ new Set();
|
|
1345
1474
|
for (const call of dbCalls) {
|
|
1346
|
-
|
|
1347
|
-
seenModels.
|
|
1475
|
+
const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
|
|
1476
|
+
if (seenModels.has(target)) continue;
|
|
1477
|
+
seenModels.add(target);
|
|
1348
1478
|
apiCrossRefs.push({
|
|
1349
1479
|
source: node.id,
|
|
1350
|
-
target
|
|
1480
|
+
target,
|
|
1351
1481
|
type: call.isMutation ? "mutates" : "reads",
|
|
1352
1482
|
layer: "db"
|
|
1353
1483
|
});
|
|
1354
1484
|
}
|
|
1355
1485
|
delete node._dbCalls;
|
|
1356
1486
|
}
|
|
1487
|
+
const uiCrossRefs = [];
|
|
1488
|
+
for (const node of uiNodes) {
|
|
1489
|
+
const dbCalls = node._dbCalls;
|
|
1490
|
+
if (!dbCalls) continue;
|
|
1491
|
+
const seenModels = /* @__PURE__ */ new Set();
|
|
1492
|
+
for (const call of dbCalls) {
|
|
1493
|
+
const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
|
|
1494
|
+
if (seenModels.has(target)) continue;
|
|
1495
|
+
seenModels.add(target);
|
|
1496
|
+
uiCrossRefs.push({
|
|
1497
|
+
source: node.id,
|
|
1498
|
+
target,
|
|
1499
|
+
type: call.isMutation ? "mutates" : "reads",
|
|
1500
|
+
layer: "db"
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
delete node._dbCalls;
|
|
1504
|
+
}
|
|
1505
|
+
uiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1357
1506
|
const apiNodeIds = new Set(apiNodes.map((n) => n.id));
|
|
1358
1507
|
const apiEdges = [];
|
|
1359
1508
|
const uiOnlyEdges = [];
|
|
@@ -1415,7 +1564,7 @@ function generate(rootDir) {
|
|
|
1415
1564
|
},
|
|
1416
1565
|
nodes: stripLayer(uiNodes),
|
|
1417
1566
|
edges: uiOnlyEdges,
|
|
1418
|
-
cross_refs:
|
|
1567
|
+
cross_refs: uiCrossRefs,
|
|
1419
1568
|
contradictions: [],
|
|
1420
1569
|
warnings: [],
|
|
1421
1570
|
flagged_edges: dedupedFlagged,
|
|
@@ -2261,26 +2410,54 @@ function normalizeFetchUrl(raw) {
|
|
|
2261
2410
|
return { path: s || "/", hadInterpolation };
|
|
2262
2411
|
}
|
|
2263
2412
|
function scoreApiRouteMatch(candidate, known) {
|
|
2264
|
-
if (candidate.length !== known.length) return -1;
|
|
2265
2413
|
let score = 0;
|
|
2266
|
-
|
|
2414
|
+
let i = 0, j = 0;
|
|
2415
|
+
while (i < candidate.length && j < known.length) {
|
|
2267
2416
|
const a = candidate[i];
|
|
2268
|
-
const b = known[
|
|
2417
|
+
const b = known[j];
|
|
2418
|
+
if (b.startsWith("*") && b.endsWith("?")) {
|
|
2419
|
+
score += 1;
|
|
2420
|
+
return score;
|
|
2421
|
+
}
|
|
2422
|
+
if (b.startsWith("*")) {
|
|
2423
|
+
const remaining = candidate.length - i;
|
|
2424
|
+
if (remaining < 1) return -1;
|
|
2425
|
+
score += 1 + remaining;
|
|
2426
|
+
return score;
|
|
2427
|
+
}
|
|
2269
2428
|
if (a === b) {
|
|
2270
2429
|
score += 3;
|
|
2430
|
+
i++;
|
|
2431
|
+
j++;
|
|
2271
2432
|
continue;
|
|
2272
2433
|
}
|
|
2273
2434
|
if (a.startsWith(":") && b.startsWith(":")) {
|
|
2274
2435
|
score += 2;
|
|
2436
|
+
i++;
|
|
2437
|
+
j++;
|
|
2275
2438
|
continue;
|
|
2276
2439
|
}
|
|
2277
2440
|
if (a.startsWith(":") || b.startsWith(":")) {
|
|
2278
2441
|
score += 1;
|
|
2442
|
+
i++;
|
|
2443
|
+
j++;
|
|
2279
2444
|
continue;
|
|
2280
2445
|
}
|
|
2281
2446
|
return -1;
|
|
2282
2447
|
}
|
|
2283
|
-
|
|
2448
|
+
if (i === candidate.length) {
|
|
2449
|
+
while (j < known.length) {
|
|
2450
|
+
const b = known[j];
|
|
2451
|
+
if (b.startsWith("*") && b.endsWith("?")) {
|
|
2452
|
+
score += 1;
|
|
2453
|
+
j++;
|
|
2454
|
+
continue;
|
|
2455
|
+
}
|
|
2456
|
+
return -1;
|
|
2457
|
+
}
|
|
2458
|
+
return score;
|
|
2459
|
+
}
|
|
2460
|
+
return -1;
|
|
2284
2461
|
}
|
|
2285
2462
|
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
2286
2463
|
const raw = call.url;
|
|
@@ -2437,48 +2614,58 @@ var fetchResolverParser = {
|
|
|
2437
2614
|
// src/server/graph/parsers/crosslayer/api-annotations.ts
|
|
2438
2615
|
var import_node_fs8 = require("node:fs");
|
|
2439
2616
|
var import_node_path7 = require("node:path");
|
|
2617
|
+
init_config();
|
|
2440
2618
|
var API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
|
|
2441
|
-
function
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2446
|
-
const full = (0, import_node_path7.join)(dir, entry.name);
|
|
2447
|
-
if (entry.isDirectory()) {
|
|
2448
|
-
results.push(...walk2(full, exts));
|
|
2449
|
-
} else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
|
|
2450
|
-
results.push(full);
|
|
2451
|
-
}
|
|
2619
|
+
function toNodeId2(srcDir, rootDir, absPath) {
|
|
2620
|
+
const relFromSrc = (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
2621
|
+
if (relFromSrc.startsWith("..")) {
|
|
2622
|
+
return (0, import_node_path7.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
2452
2623
|
}
|
|
2453
|
-
return
|
|
2454
|
-
}
|
|
2455
|
-
function toNodeId2(srcDir, absPath) {
|
|
2456
|
-
return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
2624
|
+
return relFromSrc;
|
|
2457
2625
|
}
|
|
2458
2626
|
var apiAnnotationsParser = {
|
|
2459
2627
|
id: "api-annotations",
|
|
2460
2628
|
layer: "crosslayer",
|
|
2461
2629
|
concern: "api-binding",
|
|
2462
2630
|
detect(rootDir) {
|
|
2463
|
-
return (
|
|
2631
|
+
return resolveProjectPaths(rootDir, loadConfig(rootDir)) !== null;
|
|
2464
2632
|
},
|
|
2465
2633
|
generate(rootDir, layerOutputs) {
|
|
2466
2634
|
const apiOutput = layerOutputs.get("api");
|
|
2467
2635
|
if (!apiOutput) {
|
|
2468
2636
|
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2469
2637
|
}
|
|
2638
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2639
|
+
if (!paths) {
|
|
2640
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2641
|
+
}
|
|
2470
2642
|
const uiOutput = layerOutputs.get("ui");
|
|
2471
2643
|
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
2472
2644
|
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
2473
2645
|
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
2474
|
-
const srcDir =
|
|
2475
|
-
const
|
|
2646
|
+
const srcDir = paths.srcDir;
|
|
2647
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2648
|
+
const files = [];
|
|
2649
|
+
for (const root of paths.srcRoots) {
|
|
2650
|
+
for (const f of walk(root, [".ts", ".tsx"])) {
|
|
2651
|
+
if (!seen.has(f)) {
|
|
2652
|
+
seen.add(f);
|
|
2653
|
+
files.push(f);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
for (const conv of paths.conventionFiles) {
|
|
2658
|
+
if (!seen.has(conv)) {
|
|
2659
|
+
seen.add(conv);
|
|
2660
|
+
files.push(conv);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2476
2663
|
const crossRefs = [];
|
|
2477
2664
|
const flaggedEdges = [];
|
|
2478
|
-
const
|
|
2665
|
+
const seenEdge = /* @__PURE__ */ new Set();
|
|
2479
2666
|
for (const absPath of files) {
|
|
2480
2667
|
const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
|
|
2481
|
-
const sourceId = toNodeId2(srcDir, absPath);
|
|
2668
|
+
const sourceId = toNodeId2(srcDir, rootDir, absPath);
|
|
2482
2669
|
if (!uiNodeIds.has(sourceId)) continue;
|
|
2483
2670
|
let match;
|
|
2484
2671
|
API_ANNOTATION_RE.lastIndex = 0;
|
|
@@ -2488,8 +2675,8 @@ var apiAnnotationsParser = {
|
|
|
2488
2675
|
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
2489
2676
|
if (result.kind === "resolved" && result.nodeId) {
|
|
2490
2677
|
const key = `${sourceId}|${result.nodeId}|calls_api`;
|
|
2491
|
-
if (
|
|
2492
|
-
|
|
2678
|
+
if (seenEdge.has(key)) continue;
|
|
2679
|
+
seenEdge.add(key);
|
|
2493
2680
|
crossRefs.push({
|
|
2494
2681
|
source: sourceId,
|
|
2495
2682
|
target: result.nodeId,
|
|
@@ -2524,23 +2711,13 @@ var apiAnnotationsParser = {
|
|
|
2524
2711
|
var import_node_fs9 = require("node:fs");
|
|
2525
2712
|
var import_node_path8 = require("node:path");
|
|
2526
2713
|
init_config();
|
|
2527
|
-
var URL_LITERAL_RE = /['"`](\/
|
|
2528
|
-
function
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2533
|
-
const full = (0, import_node_path8.join)(dir, entry.name);
|
|
2534
|
-
if (entry.isDirectory()) {
|
|
2535
|
-
results.push(...walk3(full, exts));
|
|
2536
|
-
} else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
|
|
2537
|
-
results.push(full);
|
|
2538
|
-
}
|
|
2714
|
+
var URL_LITERAL_RE = /['"`](\/[a-zA-Z][^'"`\s]*?)['"`]/g;
|
|
2715
|
+
function toNodeId3(srcDir, rootDir, absPath) {
|
|
2716
|
+
const relFromSrc = (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
2717
|
+
if (relFromSrc.startsWith("..")) {
|
|
2718
|
+
return (0, import_node_path8.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
2539
2719
|
}
|
|
2540
|
-
return
|
|
2541
|
-
}
|
|
2542
|
-
function toNodeId3(srcDir, absPath) {
|
|
2543
|
-
return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
2720
|
+
return relFromSrc;
|
|
2544
2721
|
}
|
|
2545
2722
|
var urlLiteralScannerParser = {
|
|
2546
2723
|
id: "url-literal-scanner",
|
|
@@ -2561,15 +2738,26 @@ var urlLiteralScannerParser = {
|
|
|
2561
2738
|
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
2562
2739
|
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2563
2740
|
const srcDir = paths.srcDir;
|
|
2564
|
-
const
|
|
2565
|
-
const files = [
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2741
|
+
const seenFile = /* @__PURE__ */ new Set();
|
|
2742
|
+
const files = [];
|
|
2743
|
+
for (const root of paths.srcRoots) {
|
|
2744
|
+
for (const f of walk(root, [".ts", ".tsx"])) {
|
|
2745
|
+
if (!seenFile.has(f)) {
|
|
2746
|
+
seenFile.add(f);
|
|
2747
|
+
files.push(f);
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
for (const conv of paths.conventionFiles) {
|
|
2752
|
+
if (!seenFile.has(conv)) {
|
|
2753
|
+
seenFile.add(conv);
|
|
2754
|
+
files.push(conv);
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2569
2757
|
const crossRefs = [];
|
|
2570
2758
|
const seen = /* @__PURE__ */ new Set();
|
|
2571
2759
|
for (const absPath of files) {
|
|
2572
|
-
const sourceId = toNodeId3(srcDir, absPath);
|
|
2760
|
+
const sourceId = toNodeId3(srcDir, rootDir, absPath);
|
|
2573
2761
|
if (!uiNodeIds.has(sourceId)) continue;
|
|
2574
2762
|
const content = (0, import_node_fs9.readFileSync)(absPath, "utf-8");
|
|
2575
2763
|
let match;
|
|
@@ -2604,6 +2792,7 @@ var urlLiteralScannerParser = {
|
|
|
2604
2792
|
// src/server/graph/parsers/static/static-values.ts
|
|
2605
2793
|
var import_node_fs10 = require("node:fs");
|
|
2606
2794
|
var import_node_path9 = require("node:path");
|
|
2795
|
+
init_config();
|
|
2607
2796
|
var parseCode = null;
|
|
2608
2797
|
function tryLoadTreeSitter() {
|
|
2609
2798
|
if (parseCode) return true;
|
|
@@ -2635,10 +2824,15 @@ function classifyScope(source, model) {
|
|
|
2635
2824
|
function extractEnumValues(rootDir) {
|
|
2636
2825
|
const nodes = [];
|
|
2637
2826
|
const edges = [];
|
|
2638
|
-
const
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2827
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2828
|
+
const schemaPaths = [];
|
|
2829
|
+
if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath) {
|
|
2830
|
+
schemaPaths.push(paths.dbConfig.schemaPath);
|
|
2831
|
+
schemaPaths.push((0, import_node_path9.join)((0, import_node_path9.dirname)(paths.dbConfig.schemaPath), "schema"));
|
|
2832
|
+
} else {
|
|
2833
|
+
schemaPaths.push((0, import_node_path9.join)(rootDir, "prisma", "schema.prisma"));
|
|
2834
|
+
schemaPaths.push((0, import_node_path9.join)(rootDir, "prisma", "schema"));
|
|
2835
|
+
}
|
|
2642
2836
|
let content = "";
|
|
2643
2837
|
for (const p of schemaPaths) {
|
|
2644
2838
|
if ((0, import_node_fs10.existsSync)(p)) {
|
|
@@ -2722,7 +2916,7 @@ function extractStringArrayFromNode(node) {
|
|
|
2722
2916
|
return values;
|
|
2723
2917
|
}
|
|
2724
2918
|
function findArrayDecl(root, varName) {
|
|
2725
|
-
function
|
|
2919
|
+
function walk2(node) {
|
|
2726
2920
|
if (node.type === "variable_declarator") {
|
|
2727
2921
|
const nameNode = node.childForFieldName("name");
|
|
2728
2922
|
const valueNode = node.childForFieldName("value");
|
|
@@ -2735,12 +2929,12 @@ function findArrayDecl(root, varName) {
|
|
|
2735
2929
|
}
|
|
2736
2930
|
}
|
|
2737
2931
|
for (const child of node.namedChildren) {
|
|
2738
|
-
const found =
|
|
2932
|
+
const found = walk2(child);
|
|
2739
2933
|
if (found) return found;
|
|
2740
2934
|
}
|
|
2741
2935
|
return null;
|
|
2742
2936
|
}
|
|
2743
|
-
return
|
|
2937
|
+
return walk2(root);
|
|
2744
2938
|
}
|
|
2745
2939
|
function extractObjectPropsRegex(objStr) {
|
|
2746
2940
|
const props = {};
|
|
@@ -2803,11 +2997,26 @@ function modelToNodeType(model) {
|
|
|
2803
2997
|
function extractSeedData(rootDir) {
|
|
2804
2998
|
const nodes = [];
|
|
2805
2999
|
const edges = [];
|
|
2806
|
-
const
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
(0, import_node_path9.join)(
|
|
2810
|
-
|
|
3000
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
3001
|
+
const candidates = [];
|
|
3002
|
+
if (paths?.dbDir) {
|
|
3003
|
+
candidates.push((0, import_node_path9.join)(paths.dbDir, "seed.ts"));
|
|
3004
|
+
candidates.push((0, import_node_path9.join)(paths.dbDir, "seed.js"));
|
|
3005
|
+
} else {
|
|
3006
|
+
candidates.push((0, import_node_path9.join)(rootDir, "prisma", "seed.ts"));
|
|
3007
|
+
candidates.push((0, import_node_path9.join)(rootDir, "prisma", "seed.js"));
|
|
3008
|
+
}
|
|
3009
|
+
const baseRoots = paths?.srcRoots ?? [(0, import_node_path9.join)(rootDir, "src")];
|
|
3010
|
+
for (const root of baseRoots) {
|
|
3011
|
+
candidates.push((0, import_node_path9.join)(root, "server", "lib", "system-tags.ts"));
|
|
3012
|
+
}
|
|
3013
|
+
const seedFiles = candidates.filter((p) => {
|
|
3014
|
+
try {
|
|
3015
|
+
return (0, import_node_fs10.existsSync)(p) && (0, import_node_fs10.statSync)(p).isFile();
|
|
3016
|
+
} catch {
|
|
3017
|
+
return false;
|
|
3018
|
+
}
|
|
3019
|
+
});
|
|
2811
3020
|
const useTreeSitter = tryLoadTreeSitter();
|
|
2812
3021
|
for (const filePath of seedFiles) {
|
|
2813
3022
|
const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
|
|
@@ -2917,9 +3126,20 @@ function walkDir(dir, exts) {
|
|
|
2917
3126
|
}
|
|
2918
3127
|
function extractConstants(rootDir) {
|
|
2919
3128
|
const nodes = [];
|
|
2920
|
-
const
|
|
2921
|
-
|
|
2922
|
-
|
|
3129
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
3130
|
+
const roots = paths?.srcRoots ?? [(0, import_node_path9.join)(rootDir, "src")];
|
|
3131
|
+
const seenFile = /* @__PURE__ */ new Set();
|
|
3132
|
+
const allFiles = [];
|
|
3133
|
+
for (const root of roots) {
|
|
3134
|
+
for (const f of walkDir(root, [".ts", ".tsx"])) {
|
|
3135
|
+
if (!seenFile.has(f)) {
|
|
3136
|
+
seenFile.add(f);
|
|
3137
|
+
allFiles.push(f);
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
if (allFiles.length === 0) return { nodes };
|
|
3142
|
+
for (const filePath of allFiles) {
|
|
2923
3143
|
const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
|
|
2924
3144
|
const relPath = (0, import_node_path9.relative)(rootDir, filePath);
|
|
2925
3145
|
const constArrayRe = /export\s+const\s+([A-Z][A-Z_0-9]+)\s*(?::[^=]+)?\s*=\s*\[/g;
|
|
@@ -2954,6 +3174,13 @@ function extractConstants(rootDir) {
|
|
|
2954
3174
|
return { nodes };
|
|
2955
3175
|
}
|
|
2956
3176
|
function detect4(rootDir) {
|
|
3177
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
3178
|
+
if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath && (0, import_node_fs10.existsSync)(paths.dbConfig.schemaPath)) {
|
|
3179
|
+
return true;
|
|
3180
|
+
}
|
|
3181
|
+
if (paths?.dbDir) {
|
|
3182
|
+
if ((0, import_node_fs10.existsSync)((0, import_node_path9.join)(paths.dbDir, "seed.ts")) || (0, import_node_fs10.existsSync)((0, import_node_path9.join)(paths.dbDir, "seed.js"))) return true;
|
|
3183
|
+
}
|
|
2957
3184
|
return (0, import_node_fs10.existsSync)((0, import_node_path9.join)(rootDir, "prisma", "schema.prisma")) || (0, import_node_fs10.existsSync)((0, import_node_path9.join)(rootDir, "prisma", "seed.ts"));
|
|
2958
3185
|
}
|
|
2959
3186
|
function generate4(rootDir) {
|
|
@@ -3173,13 +3400,22 @@ var staticRefScannerParser = {
|
|
|
3173
3400
|
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
3174
3401
|
if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
3175
3402
|
const srcDir = paths.srcDir;
|
|
3176
|
-
const
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3403
|
+
const seenFile = /* @__PURE__ */ new Set();
|
|
3404
|
+
const files = [];
|
|
3405
|
+
for (const root of paths.srcRoots) {
|
|
3406
|
+
for (const f of walkWithIgnore(root, [".ts", ".tsx"])) {
|
|
3407
|
+
if (!seenFile.has(f)) {
|
|
3408
|
+
seenFile.add(f);
|
|
3409
|
+
files.push(f);
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
for (const conv of paths.conventionFiles) {
|
|
3414
|
+
if (!seenFile.has(conv)) {
|
|
3415
|
+
seenFile.add(conv);
|
|
3416
|
+
files.push(conv);
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3183
3419
|
const uiOutput = layerOutputs.get("ui");
|
|
3184
3420
|
const apiOutput = layerOutputs.get("api");
|
|
3185
3421
|
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
@@ -3196,7 +3432,8 @@ var staticRefScannerParser = {
|
|
|
3196
3432
|
const seen = /* @__PURE__ */ new Set();
|
|
3197
3433
|
let filesScanned = 0;
|
|
3198
3434
|
for (const absPath of files) {
|
|
3199
|
-
const
|
|
3435
|
+
const relFromSrc = (0, import_node_path10.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
3436
|
+
const sourceId = relFromSrc.startsWith("..") ? (0, import_node_path10.relative)(rootDir, absPath).replace(/\\/g, "/") : relFromSrc;
|
|
3200
3437
|
const sourceLayer = uiNodeIds.has(sourceId) ? "ui" : apiNodeIds.has(sourceId) ? "api" : null;
|
|
3201
3438
|
if (!sourceLayer) continue;
|
|
3202
3439
|
const content = (0, import_node_fs11.readFileSync)(absPath, "utf-8");
|
|
@@ -3744,13 +3981,17 @@ function isTrivialGroup(name, extraTrivial) {
|
|
|
3744
3981
|
function normalizeGroupName(name) {
|
|
3745
3982
|
return name.toLowerCase().replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
|
|
3746
3983
|
}
|
|
3747
|
-
function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
|
|
3984
|
+
function extractModuleFromPath(id, extraTrivial, extraSkipSegments, extraGenericRoles) {
|
|
3748
3985
|
const segments = id.split("/");
|
|
3749
3986
|
const routeGroups = extractRouteGroups(id);
|
|
3750
3987
|
const skipSegments = new Set(SKIP_SEGMENTS_BUILTIN);
|
|
3988
|
+
for (const r of GENERIC_ROLE_NAMES_BUILTIN) skipSegments.add(r);
|
|
3751
3989
|
if (extraSkipSegments) {
|
|
3752
3990
|
for (const s of extraSkipSegments) skipSegments.add(s);
|
|
3753
3991
|
}
|
|
3992
|
+
if (extraGenericRoles) {
|
|
3993
|
+
for (const r of extraGenericRoles) skipSegments.add(r);
|
|
3994
|
+
}
|
|
3754
3995
|
const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g, extraTrivial)).map(normalizeGroupName);
|
|
3755
3996
|
if (moduleGroups.length > 0) {
|
|
3756
3997
|
return moduleGroups[moduleGroups.length - 1];
|