@screenbook/cli 1.1.3 → 1.2.0
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/index.mjs +1137 -75
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { defineConfig } from "@screenbook/core";
|
|
|
9
9
|
import pc from "picocolors";
|
|
10
10
|
import { execSync, spawn } from "node:child_process";
|
|
11
11
|
import prompts from "prompts";
|
|
12
|
+
import { parse } from "@babel/parser";
|
|
12
13
|
import { minimatch } from "minimatch";
|
|
13
14
|
|
|
14
15
|
//#region src/utils/config.ts
|
|
@@ -166,14 +167,34 @@ function getCycleSummary(result) {
|
|
|
166
167
|
*/
|
|
167
168
|
const ERRORS = {
|
|
168
169
|
ROUTES_PATTERN_MISSING: {
|
|
169
|
-
title: "
|
|
170
|
-
suggestion: "Add routesPattern
|
|
170
|
+
title: "Routes configuration not found",
|
|
171
|
+
suggestion: "Add routesPattern (for file-based routing) or routesFile (for config-based routing) to your screenbook.config.ts.",
|
|
171
172
|
example: `import { defineConfig } from "@screenbook/core"
|
|
172
173
|
|
|
174
|
+
// Option 1: File-based routing (Next.js, Nuxt, etc.)
|
|
173
175
|
export default defineConfig({
|
|
174
|
-
routesPattern: "src/pages/**/page.tsx",
|
|
176
|
+
routesPattern: "src/pages/**/page.tsx",
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// Option 2: Config-based routing (Vue Router, React Router, etc.)
|
|
180
|
+
export default defineConfig({
|
|
181
|
+
routesFile: "src/router/routes.ts",
|
|
175
182
|
})`
|
|
176
183
|
},
|
|
184
|
+
ROUTES_FILE_NOT_FOUND: (filePath) => ({
|
|
185
|
+
title: `Routes file not found: ${filePath}`,
|
|
186
|
+
suggestion: "Check the routesFile path in your screenbook.config.ts. The file should export a routes array.",
|
|
187
|
+
example: `import { defineConfig } from "@screenbook/core"
|
|
188
|
+
|
|
189
|
+
export default defineConfig({
|
|
190
|
+
routesFile: "src/router/routes.ts", // Make sure this file exists
|
|
191
|
+
})`
|
|
192
|
+
}),
|
|
193
|
+
ROUTES_FILE_PARSE_ERROR: (filePath, error) => ({
|
|
194
|
+
title: `Failed to parse routes file: ${filePath}`,
|
|
195
|
+
message: error,
|
|
196
|
+
suggestion: "Ensure the file exports a valid routes array. Check for syntax errors or unsupported patterns."
|
|
197
|
+
}),
|
|
177
198
|
CONFIG_NOT_FOUND: {
|
|
178
199
|
title: "Configuration file not found",
|
|
179
200
|
suggestion: "Run 'screenbook init' to create a screenbook.config.ts file, or create one manually.",
|
|
@@ -943,6 +964,721 @@ function displayResults(results, verbose) {
|
|
|
943
964
|
}
|
|
944
965
|
}
|
|
945
966
|
|
|
967
|
+
//#endregion
|
|
968
|
+
//#region src/utils/routeParserUtils.ts
|
|
969
|
+
/**
|
|
970
|
+
* Resolve relative import path to absolute path
|
|
971
|
+
*/
|
|
972
|
+
function resolveImportPath(importPath, baseDir) {
|
|
973
|
+
if (importPath.startsWith(".")) return resolve(baseDir, importPath);
|
|
974
|
+
return importPath;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Flatten nested routes into a flat list with computed properties
|
|
978
|
+
*/
|
|
979
|
+
function flattenRoutes(routes, parentPath = "", depth = 0) {
|
|
980
|
+
const result = [];
|
|
981
|
+
for (const route of routes) {
|
|
982
|
+
if (route.redirect && !route.component) continue;
|
|
983
|
+
let fullPath;
|
|
984
|
+
if (route.path.startsWith("/")) fullPath = route.path;
|
|
985
|
+
else if (parentPath === "/") fullPath = `/${route.path}`;
|
|
986
|
+
else fullPath = parentPath ? `${parentPath}/${route.path}` : `/${route.path}`;
|
|
987
|
+
fullPath = fullPath.replace(/\/+/g, "/");
|
|
988
|
+
if (fullPath !== "/" && fullPath.endsWith("/")) fullPath = fullPath.slice(0, -1);
|
|
989
|
+
if (fullPath === "") fullPath = parentPath || "/";
|
|
990
|
+
if (route.component || !route.children) result.push({
|
|
991
|
+
fullPath,
|
|
992
|
+
name: route.name,
|
|
993
|
+
componentPath: route.component,
|
|
994
|
+
screenId: pathToScreenId(fullPath),
|
|
995
|
+
screenTitle: pathToScreenTitle(fullPath),
|
|
996
|
+
depth
|
|
997
|
+
});
|
|
998
|
+
if (route.children) result.push(...flattenRoutes(route.children, fullPath, depth + 1));
|
|
999
|
+
}
|
|
1000
|
+
return result;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Convert route path to screen ID
|
|
1004
|
+
* /user/:id/profile -> user.id.profile
|
|
1005
|
+
*/
|
|
1006
|
+
function pathToScreenId(path) {
|
|
1007
|
+
if (path === "/" || path === "") return "home";
|
|
1008
|
+
return path.replace(/^\//, "").replace(/\/$/, "").split("/").map((segment) => {
|
|
1009
|
+
if (segment.startsWith(":")) return segment.slice(1);
|
|
1010
|
+
if (segment.startsWith("*")) return segment.slice(1) || "catchall";
|
|
1011
|
+
return segment;
|
|
1012
|
+
}).join(".");
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Convert route path to screen title
|
|
1016
|
+
* /user/:id/profile -> Profile
|
|
1017
|
+
*/
|
|
1018
|
+
function pathToScreenTitle(path) {
|
|
1019
|
+
if (path === "/" || path === "") return "Home";
|
|
1020
|
+
const segments = path.replace(/^\//, "").replace(/\/$/, "").split("/").filter((s) => !s.startsWith(":") && !s.startsWith("*"));
|
|
1021
|
+
return (segments[segments.length - 1] || "Home").split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
//#endregion
|
|
1025
|
+
//#region src/utils/tanstackRouterParser.ts
|
|
1026
|
+
/**
|
|
1027
|
+
* Parse TanStack Router configuration file and extract routes
|
|
1028
|
+
*/
|
|
1029
|
+
function parseTanStackRouterConfig(filePath, preloadedContent) {
|
|
1030
|
+
const absolutePath = resolve(filePath);
|
|
1031
|
+
const routesFileDir = dirname(absolutePath);
|
|
1032
|
+
const warnings = [];
|
|
1033
|
+
let content;
|
|
1034
|
+
if (preloadedContent !== void 0) content = preloadedContent;
|
|
1035
|
+
else try {
|
|
1036
|
+
content = readFileSync(absolutePath, "utf-8");
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1039
|
+
throw new Error(`Failed to read routes file "${absolutePath}": ${message}`);
|
|
1040
|
+
}
|
|
1041
|
+
let ast;
|
|
1042
|
+
try {
|
|
1043
|
+
ast = parse(content, {
|
|
1044
|
+
sourceType: "module",
|
|
1045
|
+
plugins: ["typescript", "jsx"]
|
|
1046
|
+
});
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
if (error instanceof SyntaxError) throw new Error(`Syntax error in routes file "${absolutePath}": ${error.message}`);
|
|
1049
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1050
|
+
throw new Error(`Failed to parse routes file "${absolutePath}": ${message}`);
|
|
1051
|
+
}
|
|
1052
|
+
const routeMap = /* @__PURE__ */ new Map();
|
|
1053
|
+
for (const node of ast.program.body) collectRouteDefinitions(node, routeMap, routesFileDir, warnings);
|
|
1054
|
+
for (const node of ast.program.body) processAddChildrenCalls(node, routeMap, warnings);
|
|
1055
|
+
const routes = buildRouteTree(routeMap, warnings);
|
|
1056
|
+
if (routes.length === 0) warnings.push("No routes found. Supported patterns: 'createRootRoute()', 'createRoute()', and '.addChildren([...])'");
|
|
1057
|
+
return {
|
|
1058
|
+
routes,
|
|
1059
|
+
warnings
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Collect route definitions from AST nodes
|
|
1064
|
+
*/
|
|
1065
|
+
function collectRouteDefinitions(node, routeMap, baseDir, warnings) {
|
|
1066
|
+
if (node.type === "VariableDeclaration") for (const decl of node.declarations) {
|
|
1067
|
+
if (decl.id.type !== "Identifier") continue;
|
|
1068
|
+
const variableName = decl.id.name;
|
|
1069
|
+
if (decl.init?.type === "CallExpression") {
|
|
1070
|
+
const routeDef = extractRouteFromCallExpression(decl.init, variableName, baseDir, warnings);
|
|
1071
|
+
if (routeDef) routeMap.set(variableName, routeDef);
|
|
1072
|
+
}
|
|
1073
|
+
if (decl.init?.type === "CallExpression" && decl.init.callee?.type === "MemberExpression" && decl.init.callee.property?.type === "Identifier" && decl.init.callee.property.name === "lazy") {
|
|
1074
|
+
const createRouteCall = decl.init.callee.object;
|
|
1075
|
+
if (createRouteCall?.type === "CallExpression") {
|
|
1076
|
+
const routeDef = extractRouteFromCallExpression(createRouteCall, variableName, baseDir, warnings);
|
|
1077
|
+
if (routeDef) {
|
|
1078
|
+
const lazyArg = decl.init.arguments[0];
|
|
1079
|
+
if (lazyArg) {
|
|
1080
|
+
const lazyPath = extractLazyImportPath$1(lazyArg, baseDir, warnings);
|
|
1081
|
+
if (lazyPath) routeDef.component = lazyPath;
|
|
1082
|
+
}
|
|
1083
|
+
routeMap.set(variableName, routeDef);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") for (const decl of node.declaration.declarations) {
|
|
1089
|
+
if (decl.id.type !== "Identifier") continue;
|
|
1090
|
+
const variableName = decl.id.name;
|
|
1091
|
+
if (decl.init?.type === "CallExpression") {
|
|
1092
|
+
const routeDef = extractRouteFromCallExpression(decl.init, variableName, baseDir, warnings);
|
|
1093
|
+
if (routeDef) routeMap.set(variableName, routeDef);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Extract route definition from a CallExpression (createRoute or createRootRoute)
|
|
1099
|
+
*/
|
|
1100
|
+
function extractRouteFromCallExpression(callNode, variableName, baseDir, warnings) {
|
|
1101
|
+
const callee = callNode.callee;
|
|
1102
|
+
let isRoot = false;
|
|
1103
|
+
let optionsArg = callNode.arguments[0];
|
|
1104
|
+
if (callee.type === "Identifier") {
|
|
1105
|
+
if (callee.name === "createRootRoute") isRoot = true;
|
|
1106
|
+
else if (callee.name === "createRootRouteWithContext") isRoot = true;
|
|
1107
|
+
else if (callee.name !== "createRoute") return null;
|
|
1108
|
+
} else if (callee.type === "CallExpression") {
|
|
1109
|
+
const innerCallee = callee.callee;
|
|
1110
|
+
if (innerCallee?.type === "Identifier" && innerCallee.name === "createRootRouteWithContext") {
|
|
1111
|
+
isRoot = true;
|
|
1112
|
+
optionsArg = callNode.arguments[0];
|
|
1113
|
+
} else return null;
|
|
1114
|
+
} else return null;
|
|
1115
|
+
const routeDef = {
|
|
1116
|
+
variableName,
|
|
1117
|
+
isRoot
|
|
1118
|
+
};
|
|
1119
|
+
if (optionsArg?.type === "ObjectExpression") for (const prop of optionsArg.properties) {
|
|
1120
|
+
if (prop.type !== "ObjectProperty") continue;
|
|
1121
|
+
if (prop.key.type !== "Identifier") continue;
|
|
1122
|
+
switch (prop.key.name) {
|
|
1123
|
+
case "path":
|
|
1124
|
+
if (prop.value.type === "StringLiteral") routeDef.path = normalizeTanStackPath(prop.value.value);
|
|
1125
|
+
else {
|
|
1126
|
+
const loc = prop.loc ? ` at line ${prop.loc.start.line}` : "";
|
|
1127
|
+
warnings.push(`Dynamic path value (${prop.value.type})${loc}. Only string literal paths can be statically analyzed.`);
|
|
1128
|
+
}
|
|
1129
|
+
break;
|
|
1130
|
+
case "component":
|
|
1131
|
+
routeDef.component = extractComponentValue(prop.value, baseDir, warnings);
|
|
1132
|
+
break;
|
|
1133
|
+
case "getParentRoute":
|
|
1134
|
+
if (prop.value.type === "ArrowFunctionExpression") {
|
|
1135
|
+
const body = prop.value.body;
|
|
1136
|
+
if (body.type === "Identifier") routeDef.parentVariableName = body.name;
|
|
1137
|
+
else {
|
|
1138
|
+
const loc = prop.loc ? ` at line ${prop.loc.start.line}` : "";
|
|
1139
|
+
warnings.push(`Dynamic getParentRoute${loc}. Only static route references can be analyzed.`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
break;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return routeDef;
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Extract component value from different patterns
|
|
1149
|
+
* Returns undefined with a warning for unrecognized patterns
|
|
1150
|
+
*/
|
|
1151
|
+
function extractComponentValue(node, baseDir, warnings) {
|
|
1152
|
+
if (node.type === "Identifier") return node.name;
|
|
1153
|
+
if (node.type === "CallExpression") {
|
|
1154
|
+
const callee = node.callee;
|
|
1155
|
+
if (callee.type === "Identifier" && callee.name === "lazyRouteComponent") {
|
|
1156
|
+
const importArg = node.arguments[0];
|
|
1157
|
+
if (importArg) return extractLazyImportPath$1(importArg, baseDir, warnings);
|
|
1158
|
+
const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1159
|
+
warnings.push(`lazyRouteComponent called without arguments${loc}. Expected arrow function with import().`);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
if (node.type === "ArrowFunctionExpression") {
|
|
1165
|
+
if (node.body.type === "JSXElement") {
|
|
1166
|
+
const openingElement = node.body.openingElement;
|
|
1167
|
+
if (openingElement?.name?.type === "JSXIdentifier") return openingElement.name.name;
|
|
1168
|
+
if (openingElement?.name?.type === "JSXMemberExpression") {
|
|
1169
|
+
const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1170
|
+
warnings.push(`Namespaced JSX component${loc}. Component extraction not fully supported for member expressions.`);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
if (node.body.type === "BlockStatement") {
|
|
1175
|
+
const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1176
|
+
warnings.push(`Arrow function with block body${loc}. Only concise arrow functions returning JSX directly can be analyzed.`);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
if (node.body.type === "JSXFragment") {
|
|
1180
|
+
const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1181
|
+
warnings.push(`JSX Fragment detected${loc}. Cannot extract component name from fragments.`);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Extract import path from lazy function patterns
|
|
1188
|
+
*/
|
|
1189
|
+
function extractLazyImportPath$1(node, baseDir, warnings) {
|
|
1190
|
+
if (node.type === "ArrowFunctionExpression") {
|
|
1191
|
+
const body = node.body;
|
|
1192
|
+
if (body.type === "CallExpression" && body.callee.type === "Import") {
|
|
1193
|
+
if (body.arguments[0]?.type === "StringLiteral") return resolveImportPath(body.arguments[0].value, baseDir);
|
|
1194
|
+
}
|
|
1195
|
+
if (body.type === "CallExpression" && body.callee.type === "MemberExpression" && body.callee.object?.type === "CallExpression" && body.callee.object.callee?.type === "Import") {
|
|
1196
|
+
const importCall = body.callee.object;
|
|
1197
|
+
if (importCall.arguments[0]?.type === "StringLiteral") return resolveImportPath(importCall.arguments[0].value, baseDir);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1201
|
+
warnings.push(`Unrecognized lazy pattern (${node.type})${loc}. Expected arrow function with import().`);
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Process addChildren calls to establish parent-child relationships
|
|
1205
|
+
*/
|
|
1206
|
+
function processAddChildrenCalls(node, routeMap, warnings) {
|
|
1207
|
+
if (node.type === "VariableDeclaration") for (const decl of node.declarations) processAddChildrenExpression(decl.init, routeMap, warnings);
|
|
1208
|
+
if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") for (const decl of node.declaration.declarations) processAddChildrenExpression(decl.init, routeMap, warnings);
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Recursively process addChildren expressions
|
|
1212
|
+
*/
|
|
1213
|
+
function processAddChildrenExpression(expr, routeMap, warnings) {
|
|
1214
|
+
if (!expr) return void 0;
|
|
1215
|
+
if (expr.type === "CallExpression" && expr.callee?.type === "MemberExpression" && expr.callee.property?.type === "Identifier" && expr.callee.property.name === "addChildren") {
|
|
1216
|
+
let parentVarName;
|
|
1217
|
+
if (expr.callee.object?.type === "Identifier") parentVarName = expr.callee.object.name;
|
|
1218
|
+
else if (expr.callee.object?.type === "CallExpression") parentVarName = processAddChildrenExpression(expr.callee.object, routeMap, warnings);
|
|
1219
|
+
if (!parentVarName) return void 0;
|
|
1220
|
+
const parentDef = routeMap.get(parentVarName);
|
|
1221
|
+
if (!parentDef) {
|
|
1222
|
+
const loc = expr.loc ? ` at line ${expr.loc.start.line}` : "";
|
|
1223
|
+
warnings.push(`Parent route "${parentVarName}" not found${loc}. Ensure it's defined with createRoute/createRootRoute.`);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const childrenArg = expr.arguments[0];
|
|
1227
|
+
if (childrenArg?.type === "ArrayExpression") {
|
|
1228
|
+
const childNames = [];
|
|
1229
|
+
for (const element of childrenArg.elements) {
|
|
1230
|
+
if (!element) continue;
|
|
1231
|
+
if (element.type === "Identifier") childNames.push(element.name);
|
|
1232
|
+
else if (element.type === "CallExpression") {
|
|
1233
|
+
const nestedParent = processAddChildrenExpression(element, routeMap, warnings);
|
|
1234
|
+
if (nestedParent) childNames.push(nestedParent);
|
|
1235
|
+
} else if (element.type === "SpreadElement") {
|
|
1236
|
+
const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
|
|
1237
|
+
warnings.push(`Spread operator detected${loc}. Routes from spread cannot be statically analyzed.`);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
parentDef.children = childNames;
|
|
1241
|
+
}
|
|
1242
|
+
return parentVarName;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Build the route tree from collected definitions
|
|
1247
|
+
*/
|
|
1248
|
+
function buildRouteTree(routeMap, warnings) {
|
|
1249
|
+
const rootDefs = Array.from(routeMap.values()).filter((def) => def.isRoot);
|
|
1250
|
+
if (rootDefs.length === 0) return buildTreeFromParentRelations(routeMap, warnings);
|
|
1251
|
+
const routes = [];
|
|
1252
|
+
for (const rootDef of rootDefs) {
|
|
1253
|
+
const rootRoute = buildRouteFromDefinition(rootDef, routeMap, warnings, /* @__PURE__ */ new Set());
|
|
1254
|
+
if (rootRoute) {
|
|
1255
|
+
if (rootRoute.children && rootRoute.children.length > 0) routes.push(...rootRoute.children);
|
|
1256
|
+
else if (rootRoute.path) routes.push(rootRoute);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return routes;
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Build tree when there's no explicit root route
|
|
1263
|
+
* Falls back to finding routes that have no parent relationship defined
|
|
1264
|
+
*/
|
|
1265
|
+
function buildTreeFromParentRelations(routeMap, warnings) {
|
|
1266
|
+
const topLevelDefs = Array.from(routeMap.values()).filter((def) => !def.parentVariableName && !def.isRoot);
|
|
1267
|
+
const routes = [];
|
|
1268
|
+
for (const def of topLevelDefs) {
|
|
1269
|
+
const route = buildRouteFromDefinition(def, routeMap, warnings, /* @__PURE__ */ new Set());
|
|
1270
|
+
if (route) routes.push(route);
|
|
1271
|
+
}
|
|
1272
|
+
return routes;
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Build a ParsedRoute from a RouteDefinition
|
|
1276
|
+
* @param visited - Set of visited variable names for circular reference detection
|
|
1277
|
+
*/
|
|
1278
|
+
function buildRouteFromDefinition(def, routeMap, warnings, visited) {
|
|
1279
|
+
if (visited.has(def.variableName)) {
|
|
1280
|
+
warnings.push(`Circular reference detected: route "${def.variableName}" references itself in the route tree.`);
|
|
1281
|
+
return null;
|
|
1282
|
+
}
|
|
1283
|
+
visited.add(def.variableName);
|
|
1284
|
+
const route = {
|
|
1285
|
+
path: def.path ?? "",
|
|
1286
|
+
component: def.component
|
|
1287
|
+
};
|
|
1288
|
+
if (def.children && def.children.length > 0) {
|
|
1289
|
+
const children = [];
|
|
1290
|
+
for (const childName of def.children) {
|
|
1291
|
+
const childDef = routeMap.get(childName);
|
|
1292
|
+
if (childDef) {
|
|
1293
|
+
const childRoute = buildRouteFromDefinition(childDef, routeMap, warnings, visited);
|
|
1294
|
+
if (childRoute) children.push(childRoute);
|
|
1295
|
+
} else warnings.push(`Child route "${childName}" not found. Ensure it's defined with createRoute.`);
|
|
1296
|
+
}
|
|
1297
|
+
if (children.length > 0) route.children = children;
|
|
1298
|
+
}
|
|
1299
|
+
return route;
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Normalize TanStack Router path syntax to standard format
|
|
1303
|
+
* /$ (trailing catch-all) -> /*
|
|
1304
|
+
* $ (standalone splat) -> *
|
|
1305
|
+
* $param (dynamic segment) -> :param
|
|
1306
|
+
*/
|
|
1307
|
+
function normalizeTanStackPath(path) {
|
|
1308
|
+
return path.replace(/\/\$$/, "/*").replace(/^\$$/, "*").replace(/\$([a-zA-Z_][a-zA-Z0-9_]*)/g, ":$1");
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Detect if content is TanStack Router based on patterns
|
|
1312
|
+
*/
|
|
1313
|
+
function isTanStackRouterContent(content) {
|
|
1314
|
+
if (content.includes("@tanstack/react-router")) return true;
|
|
1315
|
+
if (content.includes("createRootRoute")) return true;
|
|
1316
|
+
if (content.includes("createRoute") && content.includes("getParentRoute")) return true;
|
|
1317
|
+
if (content.includes("lazyRouteComponent")) return true;
|
|
1318
|
+
if (/\.addChildren\s*\(/.test(content)) return true;
|
|
1319
|
+
return false;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
//#endregion
|
|
1323
|
+
//#region src/utils/reactRouterParser.ts
|
|
1324
|
+
/**
|
|
1325
|
+
* Router factory function names to detect
|
|
1326
|
+
*/
|
|
1327
|
+
const ROUTER_FACTORY_NAMES = [
|
|
1328
|
+
"createBrowserRouter",
|
|
1329
|
+
"createHashRouter",
|
|
1330
|
+
"createMemoryRouter"
|
|
1331
|
+
];
|
|
1332
|
+
/**
|
|
1333
|
+
* Parse React Router configuration file and extract routes
|
|
1334
|
+
*/
|
|
1335
|
+
function parseReactRouterConfig(filePath, preloadedContent) {
|
|
1336
|
+
const absolutePath = resolve(filePath);
|
|
1337
|
+
const routesFileDir = dirname(absolutePath);
|
|
1338
|
+
const warnings = [];
|
|
1339
|
+
let content;
|
|
1340
|
+
if (preloadedContent !== void 0) content = preloadedContent;
|
|
1341
|
+
else try {
|
|
1342
|
+
content = readFileSync(absolutePath, "utf-8");
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1345
|
+
throw new Error(`Failed to read routes file "${absolutePath}": ${message}`);
|
|
1346
|
+
}
|
|
1347
|
+
let ast;
|
|
1348
|
+
try {
|
|
1349
|
+
ast = parse(content, {
|
|
1350
|
+
sourceType: "module",
|
|
1351
|
+
plugins: ["typescript", "jsx"]
|
|
1352
|
+
});
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
if (error instanceof SyntaxError) throw new Error(`Syntax error in routes file "${absolutePath}": ${error.message}`);
|
|
1355
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1356
|
+
throw new Error(`Failed to parse routes file "${absolutePath}": ${message}`);
|
|
1357
|
+
}
|
|
1358
|
+
const routes = [];
|
|
1359
|
+
for (const node of ast.program.body) {
|
|
1360
|
+
if (node.type === "VariableDeclaration") {
|
|
1361
|
+
for (const decl of node.declarations) if (decl.init?.type === "CallExpression") {
|
|
1362
|
+
const callee = decl.init.callee;
|
|
1363
|
+
if (callee.type === "Identifier" && ROUTER_FACTORY_NAMES.includes(callee.name)) {
|
|
1364
|
+
const firstArg = decl.init.arguments[0];
|
|
1365
|
+
if (firstArg?.type === "ArrayExpression") {
|
|
1366
|
+
const parsed = parseRoutesArray$1(firstArg, routesFileDir, warnings);
|
|
1367
|
+
routes.push(...parsed);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") for (const decl of node.declaration.declarations) {
|
|
1373
|
+
if (decl.init?.type === "CallExpression") {
|
|
1374
|
+
const callee = decl.init.callee;
|
|
1375
|
+
if (callee.type === "Identifier" && ROUTER_FACTORY_NAMES.includes(callee.name)) {
|
|
1376
|
+
const firstArg = decl.init.arguments[0];
|
|
1377
|
+
if (firstArg?.type === "ArrayExpression") {
|
|
1378
|
+
const parsed = parseRoutesArray$1(firstArg, routesFileDir, warnings);
|
|
1379
|
+
routes.push(...parsed);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
if (decl.id.type === "Identifier" && decl.id.name === "routes" && decl.init?.type === "ArrayExpression") {
|
|
1384
|
+
const parsed = parseRoutesArray$1(decl.init, routesFileDir, warnings);
|
|
1385
|
+
routes.push(...parsed);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
if (node.type === "VariableDeclaration") {
|
|
1389
|
+
for (const decl of node.declarations) if (decl.id.type === "Identifier" && decl.id.name === "routes" && decl.init?.type === "ArrayExpression") {
|
|
1390
|
+
const parsed = parseRoutesArray$1(decl.init, routesFileDir, warnings);
|
|
1391
|
+
routes.push(...parsed);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "ArrayExpression") {
|
|
1395
|
+
const parsed = parseRoutesArray$1(node.declaration, routesFileDir, warnings);
|
|
1396
|
+
routes.push(...parsed);
|
|
1397
|
+
}
|
|
1398
|
+
if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "TSSatisfiesExpression" && node.declaration.expression.type === "ArrayExpression") {
|
|
1399
|
+
const parsed = parseRoutesArray$1(node.declaration.expression, routesFileDir, warnings);
|
|
1400
|
+
routes.push(...parsed);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
if (routes.length === 0) warnings.push("No routes found. Supported patterns: 'createBrowserRouter([...])', 'export const routes = [...]', or 'export default [...]'");
|
|
1404
|
+
return {
|
|
1405
|
+
routes,
|
|
1406
|
+
warnings
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Parse an array expression containing route objects
|
|
1411
|
+
*/
|
|
1412
|
+
function parseRoutesArray$1(arrayNode, baseDir, warnings) {
|
|
1413
|
+
const routes = [];
|
|
1414
|
+
for (const element of arrayNode.elements) {
|
|
1415
|
+
if (!element) continue;
|
|
1416
|
+
if (element.type === "SpreadElement") {
|
|
1417
|
+
const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
|
|
1418
|
+
warnings.push(`Spread operator detected${loc}. Routes from spread cannot be statically analyzed.`);
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
if (element.type === "ObjectExpression") {
|
|
1422
|
+
const route = parseRouteObject$1(element, baseDir, warnings);
|
|
1423
|
+
if (route) routes.push(route);
|
|
1424
|
+
} else {
|
|
1425
|
+
const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
|
|
1426
|
+
warnings.push(`Non-object route element (${element.type})${loc}. Only object literals can be statically analyzed.`);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return routes;
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Parse a single route object expression
|
|
1433
|
+
*/
|
|
1434
|
+
function parseRouteObject$1(objectNode, baseDir, warnings) {
|
|
1435
|
+
const route = { path: "" };
|
|
1436
|
+
let isIndexRoute = false;
|
|
1437
|
+
let hasPath = false;
|
|
1438
|
+
for (const prop of objectNode.properties) {
|
|
1439
|
+
if (prop.type !== "ObjectProperty") continue;
|
|
1440
|
+
if (prop.key.type !== "Identifier") continue;
|
|
1441
|
+
switch (prop.key.name) {
|
|
1442
|
+
case "path":
|
|
1443
|
+
if (prop.value.type === "StringLiteral") {
|
|
1444
|
+
route.path = prop.value.value;
|
|
1445
|
+
hasPath = true;
|
|
1446
|
+
} else {
|
|
1447
|
+
const loc = prop.loc ? ` at line ${prop.loc.start.line}` : "";
|
|
1448
|
+
warnings.push(`Dynamic path value (${prop.value.type})${loc}. Only string literal paths can be statically analyzed.`);
|
|
1449
|
+
}
|
|
1450
|
+
break;
|
|
1451
|
+
case "index":
|
|
1452
|
+
if (prop.value.type === "BooleanLiteral" && prop.value.value === true) isIndexRoute = true;
|
|
1453
|
+
break;
|
|
1454
|
+
case "element":
|
|
1455
|
+
route.component = extractComponentFromJSX(prop.value, warnings);
|
|
1456
|
+
break;
|
|
1457
|
+
case "Component":
|
|
1458
|
+
if (prop.value.type === "Identifier") route.component = prop.value.name;
|
|
1459
|
+
break;
|
|
1460
|
+
case "lazy": {
|
|
1461
|
+
const lazyPath = extractLazyImportPath(prop.value, baseDir, warnings);
|
|
1462
|
+
if (lazyPath) route.component = lazyPath;
|
|
1463
|
+
break;
|
|
1464
|
+
}
|
|
1465
|
+
case "children":
|
|
1466
|
+
if (prop.value.type === "ArrayExpression") route.children = parseRoutesArray$1(prop.value, baseDir, warnings);
|
|
1467
|
+
break;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
if (isIndexRoute) {
|
|
1471
|
+
route.path = "";
|
|
1472
|
+
return route;
|
|
1473
|
+
}
|
|
1474
|
+
if (!hasPath && !isIndexRoute) {
|
|
1475
|
+
if (route.children && route.children.length > 0) {
|
|
1476
|
+
route.path = "";
|
|
1477
|
+
return route;
|
|
1478
|
+
}
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
return route;
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Extract component name from JSX element
|
|
1485
|
+
* element: <Dashboard /> -> "Dashboard"
|
|
1486
|
+
*/
|
|
1487
|
+
function extractComponentFromJSX(node, warnings) {
|
|
1488
|
+
if (node.type === "JSXElement") {
|
|
1489
|
+
const openingElement = node.openingElement;
|
|
1490
|
+
if (openingElement?.name?.type === "JSXIdentifier") return openingElement.name.name;
|
|
1491
|
+
if (openingElement?.name?.type === "JSXMemberExpression") {
|
|
1492
|
+
const parts = [];
|
|
1493
|
+
let current = openingElement.name;
|
|
1494
|
+
while (current) {
|
|
1495
|
+
if (current.type === "JSXIdentifier") {
|
|
1496
|
+
parts.unshift(current.name);
|
|
1497
|
+
break;
|
|
1498
|
+
}
|
|
1499
|
+
if (current.type === "JSXMemberExpression") {
|
|
1500
|
+
if (current.property?.type === "JSXIdentifier") parts.unshift(current.property.name);
|
|
1501
|
+
current = current.object;
|
|
1502
|
+
} else break;
|
|
1503
|
+
}
|
|
1504
|
+
return parts.join(".");
|
|
1505
|
+
}
|
|
1506
|
+
if (node.children && node.children.length > 0 && openingElement?.name?.type === "JSXIdentifier") {
|
|
1507
|
+
const wrapperName = openingElement.name.name;
|
|
1508
|
+
for (const child of node.children) if (child.type === "JSXElement") {
|
|
1509
|
+
if (extractComponentFromJSX(child, warnings)) {
|
|
1510
|
+
warnings.push(`Wrapper component detected: <${wrapperName}>. Using wrapper name for screen.`);
|
|
1511
|
+
return wrapperName;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
if (node.type === "JSXFragment") return;
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Extract import path from lazy function
|
|
1520
|
+
* lazy: () => import("./pages/Dashboard") -> resolved path
|
|
1521
|
+
*/
|
|
1522
|
+
function extractLazyImportPath(node, baseDir, warnings) {
|
|
1523
|
+
if (node.type === "ArrowFunctionExpression") {
|
|
1524
|
+
const body = node.body;
|
|
1525
|
+
if (body.type === "CallExpression" && body.callee.type === "Import") {
|
|
1526
|
+
if (body.arguments[0]?.type === "StringLiteral") return resolveImportPath(body.arguments[0].value, baseDir);
|
|
1527
|
+
const loc$1 = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1528
|
+
warnings.push(`Lazy import with dynamic path${loc$1}. Only string literal imports can be statically analyzed.`);
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1533
|
+
warnings.push(`Unrecognized lazy pattern (${node.type})${loc}. Expected arrow function with import().`);
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Detect if content is React Router based on patterns
|
|
1537
|
+
*/
|
|
1538
|
+
function isReactRouterContent(content) {
|
|
1539
|
+
if (content.includes("createBrowserRouter") || content.includes("createHashRouter") || content.includes("createMemoryRouter") || content.includes("RouteObject")) return true;
|
|
1540
|
+
if (/element:\s*</.test(content)) return true;
|
|
1541
|
+
if (/Component:\s*[A-Z]/.test(content)) return true;
|
|
1542
|
+
return false;
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Detect if content is Vue Router based on patterns
|
|
1546
|
+
*/
|
|
1547
|
+
function isVueRouterContent(content) {
|
|
1548
|
+
if (content.includes("RouteRecordRaw") || content.includes("vue-router") || content.includes(".vue")) return true;
|
|
1549
|
+
return false;
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Detect router type from file content
|
|
1553
|
+
*/
|
|
1554
|
+
function detectRouterType(content) {
|
|
1555
|
+
if (isTanStackRouterContent(content)) return "tanstack-router";
|
|
1556
|
+
if (isReactRouterContent(content)) return "react-router";
|
|
1557
|
+
if (isVueRouterContent(content)) return "vue-router";
|
|
1558
|
+
return "unknown";
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
//#endregion
|
|
1562
|
+
//#region src/utils/vueRouterParser.ts
|
|
1563
|
+
/**
|
|
1564
|
+
* Parse Vue Router configuration file and extract routes
|
|
1565
|
+
*/
|
|
1566
|
+
function parseVueRouterConfig(filePath, preloadedContent) {
|
|
1567
|
+
const absolutePath = resolve(filePath);
|
|
1568
|
+
const routesFileDir = dirname(absolutePath);
|
|
1569
|
+
const warnings = [];
|
|
1570
|
+
let content;
|
|
1571
|
+
if (preloadedContent !== void 0) content = preloadedContent;
|
|
1572
|
+
else try {
|
|
1573
|
+
content = readFileSync(absolutePath, "utf-8");
|
|
1574
|
+
} catch (error) {
|
|
1575
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1576
|
+
throw new Error(`Failed to read routes file "${absolutePath}": ${message}`);
|
|
1577
|
+
}
|
|
1578
|
+
let ast;
|
|
1579
|
+
try {
|
|
1580
|
+
ast = parse(content, {
|
|
1581
|
+
sourceType: "module",
|
|
1582
|
+
plugins: ["typescript", "jsx"]
|
|
1583
|
+
});
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
if (error instanceof SyntaxError) throw new Error(`Syntax error in routes file "${absolutePath}": ${error.message}`);
|
|
1586
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1587
|
+
throw new Error(`Failed to parse routes file "${absolutePath}": ${message}`);
|
|
1588
|
+
}
|
|
1589
|
+
const routes = [];
|
|
1590
|
+
for (const node of ast.program.body) {
|
|
1591
|
+
if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") {
|
|
1592
|
+
for (const decl of node.declaration.declarations) if (decl.id.type === "Identifier" && decl.id.name === "routes" && decl.init?.type === "ArrayExpression") {
|
|
1593
|
+
const parsed = parseRoutesArray(decl.init, routesFileDir, warnings);
|
|
1594
|
+
routes.push(...parsed);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
if (node.type === "VariableDeclaration") {
|
|
1598
|
+
for (const decl of node.declarations) if (decl.id.type === "Identifier" && decl.id.name === "routes" && decl.init?.type === "ArrayExpression") {
|
|
1599
|
+
const parsed = parseRoutesArray(decl.init, routesFileDir, warnings);
|
|
1600
|
+
routes.push(...parsed);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "ArrayExpression") {
|
|
1604
|
+
const parsed = parseRoutesArray(node.declaration, routesFileDir, warnings);
|
|
1605
|
+
routes.push(...parsed);
|
|
1606
|
+
}
|
|
1607
|
+
if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "TSSatisfiesExpression" && node.declaration.expression.type === "ArrayExpression") {
|
|
1608
|
+
const parsed = parseRoutesArray(node.declaration.expression, routesFileDir, warnings);
|
|
1609
|
+
routes.push(...parsed);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
if (routes.length === 0) warnings.push("No routes array found. Supported patterns: 'export const routes = [...]', 'export default [...]', or 'export default [...] satisfies RouteRecordRaw[]'");
|
|
1613
|
+
return {
|
|
1614
|
+
routes,
|
|
1615
|
+
warnings
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Parse an array expression containing route objects
|
|
1620
|
+
*/
|
|
1621
|
+
function parseRoutesArray(arrayNode, baseDir, warnings) {
|
|
1622
|
+
const routes = [];
|
|
1623
|
+
for (const element of arrayNode.elements) {
|
|
1624
|
+
if (!element) continue;
|
|
1625
|
+
if (element.type === "SpreadElement") {
|
|
1626
|
+
const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
|
|
1627
|
+
warnings.push(`Spread operator detected${loc}. Routes from spread cannot be statically analyzed.`);
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
if (element.type === "ObjectExpression") {
|
|
1631
|
+
const route = parseRouteObject(element, baseDir, warnings);
|
|
1632
|
+
if (route) routes.push(route);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
return routes;
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Parse a single route object expression
|
|
1639
|
+
*/
|
|
1640
|
+
function parseRouteObject(objectNode, baseDir, warnings) {
|
|
1641
|
+
const route = { path: "" };
|
|
1642
|
+
for (const prop of objectNode.properties) {
|
|
1643
|
+
if (prop.type !== "ObjectProperty") continue;
|
|
1644
|
+
if (prop.key.type !== "Identifier") continue;
|
|
1645
|
+
switch (prop.key.name) {
|
|
1646
|
+
case "path":
|
|
1647
|
+
if (prop.value.type === "StringLiteral") route.path = prop.value.value;
|
|
1648
|
+
break;
|
|
1649
|
+
case "name":
|
|
1650
|
+
if (prop.value.type === "StringLiteral") route.name = prop.value.value;
|
|
1651
|
+
break;
|
|
1652
|
+
case "redirect":
|
|
1653
|
+
if (prop.value.type === "StringLiteral") route.redirect = prop.value.value;
|
|
1654
|
+
break;
|
|
1655
|
+
case "component":
|
|
1656
|
+
route.component = extractComponentPath(prop.value, baseDir);
|
|
1657
|
+
break;
|
|
1658
|
+
case "children":
|
|
1659
|
+
if (prop.value.type === "ArrayExpression") route.children = parseRoutesArray(prop.value, baseDir, warnings);
|
|
1660
|
+
break;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
if (!route.path) return null;
|
|
1664
|
+
return route;
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Extract component path from various component definitions
|
|
1668
|
+
*/
|
|
1669
|
+
function extractComponentPath(node, baseDir) {
|
|
1670
|
+
if (node.type === "Identifier") return;
|
|
1671
|
+
if (node.type === "ArrowFunctionExpression") {
|
|
1672
|
+
const body = node.body;
|
|
1673
|
+
if (body.type === "CallExpression" && body.callee.type === "Import") {
|
|
1674
|
+
if (body.arguments[0]?.type === "StringLiteral") return resolveImportPath(body.arguments[0].value, baseDir);
|
|
1675
|
+
}
|
|
1676
|
+
if (body.type === "CallExpression" && body.callee.type === "Import") {
|
|
1677
|
+
for (const arg of body.arguments) if (arg.type === "StringLiteral") return resolveImportPath(arg.value, baseDir);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
946
1682
|
//#endregion
|
|
947
1683
|
//#region src/commands/generate.ts
|
|
948
1684
|
const generateCommand = define({
|
|
@@ -979,92 +1715,229 @@ const generateCommand = define({
|
|
|
979
1715
|
const dryRun = ctx.values.dryRun ?? false;
|
|
980
1716
|
const force = ctx.values.force ?? false;
|
|
981
1717
|
const interactive = ctx.values.interactive ?? false;
|
|
982
|
-
if (!config.routesPattern) {
|
|
1718
|
+
if (!config.routesPattern && !config.routesFile) {
|
|
983
1719
|
logger.errorWithHelp(ERRORS.ROUTES_PATTERN_MISSING);
|
|
984
1720
|
process.exit(1);
|
|
985
1721
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1722
|
+
if (config.routesFile) {
|
|
1723
|
+
await generateFromRoutesFile(config.routesFile, cwd, {
|
|
1724
|
+
dryRun,
|
|
1725
|
+
force,
|
|
1726
|
+
interactive
|
|
1727
|
+
});
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
await generateFromRoutesPattern(config.routesPattern, cwd, {
|
|
1731
|
+
dryRun,
|
|
1732
|
+
force,
|
|
1733
|
+
interactive,
|
|
990
1734
|
ignore: config.ignore
|
|
991
1735
|
});
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
/**
|
|
1739
|
+
* Generate screen.meta.ts files from a router config file (Vue Router or React Router)
|
|
1740
|
+
*/
|
|
1741
|
+
async function generateFromRoutesFile(routesFile, cwd, options) {
|
|
1742
|
+
const { dryRun, force, interactive } = options;
|
|
1743
|
+
const absoluteRoutesFile = resolve(cwd, routesFile);
|
|
1744
|
+
if (!existsSync(absoluteRoutesFile)) {
|
|
1745
|
+
logger.errorWithHelp(ERRORS.ROUTES_FILE_NOT_FOUND(routesFile));
|
|
1746
|
+
process.exit(1);
|
|
1747
|
+
}
|
|
1748
|
+
const routerType = detectRouterType(readFileSync(absoluteRoutesFile, "utf-8"));
|
|
1749
|
+
const routerTypeDisplay = routerType === "tanstack-router" ? "TanStack Router" : routerType === "react-router" ? "React Router" : routerType === "vue-router" ? "Vue Router" : "unknown";
|
|
1750
|
+
logger.info(`Parsing routes from ${logger.path(routesFile)} (${routerTypeDisplay})...`);
|
|
1751
|
+
logger.blank();
|
|
1752
|
+
let parseResult;
|
|
1753
|
+
try {
|
|
1754
|
+
if (routerType === "tanstack-router") parseResult = parseTanStackRouterConfig(absoluteRoutesFile);
|
|
1755
|
+
else if (routerType === "react-router") parseResult = parseReactRouterConfig(absoluteRoutesFile);
|
|
1756
|
+
else parseResult = parseVueRouterConfig(absoluteRoutesFile);
|
|
1757
|
+
} catch (error) {
|
|
1758
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1759
|
+
logger.errorWithHelp(ERRORS.ROUTES_FILE_PARSE_ERROR(routesFile, message));
|
|
1760
|
+
process.exit(1);
|
|
1761
|
+
}
|
|
1762
|
+
for (const warning of parseResult.warnings) logger.warn(warning);
|
|
1763
|
+
const flatRoutes = flattenRoutes(parseResult.routes);
|
|
1764
|
+
if (flatRoutes.length === 0) {
|
|
1765
|
+
logger.warn("No routes found in the config file");
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
logger.log(`Found ${flatRoutes.length} routes`);
|
|
1769
|
+
logger.blank();
|
|
1770
|
+
let created = 0;
|
|
1771
|
+
let skipped = 0;
|
|
1772
|
+
for (const route of flatRoutes) {
|
|
1773
|
+
const metaPath = determineMetaPath(route, cwd);
|
|
1774
|
+
const absoluteMetaPath = resolve(cwd, metaPath);
|
|
1775
|
+
if (!force && existsSync(absoluteMetaPath)) {
|
|
1776
|
+
if (!interactive) {
|
|
1777
|
+
skipped++;
|
|
1778
|
+
continue;
|
|
1779
|
+
}
|
|
1780
|
+
logger.itemWarn(`Exists: ${logger.path(metaPath)} (use --force to overwrite)`);
|
|
1781
|
+
skipped++;
|
|
1782
|
+
continue;
|
|
995
1783
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
if (!interactive) {
|
|
1006
|
-
skipped++;
|
|
1007
|
-
continue;
|
|
1008
|
-
}
|
|
1009
|
-
logger.itemWarn(`Exists: ${logger.path(metaPath)} (use --force to overwrite)`);
|
|
1784
|
+
const screenMeta = {
|
|
1785
|
+
id: route.screenId,
|
|
1786
|
+
title: route.screenTitle,
|
|
1787
|
+
route: route.fullPath
|
|
1788
|
+
};
|
|
1789
|
+
if (interactive) {
|
|
1790
|
+
const result = await promptForScreen(route.fullPath, screenMeta);
|
|
1791
|
+
if (result.skip) {
|
|
1792
|
+
logger.itemWarn(`Skipped: ${logger.path(metaPath)}`);
|
|
1010
1793
|
skipped++;
|
|
1011
1794
|
continue;
|
|
1012
1795
|
}
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1796
|
+
const content = generateScreenMetaContent(result.meta, {
|
|
1797
|
+
owner: result.owner,
|
|
1798
|
+
tags: result.tags
|
|
1799
|
+
});
|
|
1800
|
+
if (dryRun) {
|
|
1801
|
+
logDryRunOutput(metaPath, result.meta, result.owner, result.tags);
|
|
1802
|
+
created++;
|
|
1803
|
+
} else if (safeWriteFile(absoluteMetaPath, metaPath, content)) created++;
|
|
1804
|
+
} else {
|
|
1805
|
+
const content = generateScreenMetaContent(screenMeta);
|
|
1806
|
+
if (dryRun) {
|
|
1807
|
+
logDryRunOutput(metaPath, screenMeta);
|
|
1808
|
+
created++;
|
|
1809
|
+
} else if (safeWriteFile(absoluteMetaPath, metaPath, content)) created++;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
logSummary(created, skipped, dryRun);
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Generate screen.meta.ts files from route files matching a glob pattern
|
|
1816
|
+
*/
|
|
1817
|
+
async function generateFromRoutesPattern(routesPattern, cwd, options) {
|
|
1818
|
+
const { dryRun, force, interactive, ignore } = options;
|
|
1819
|
+
logger.info("Scanning for route files...");
|
|
1820
|
+
logger.blank();
|
|
1821
|
+
const routeFiles = await glob(routesPattern, {
|
|
1822
|
+
cwd,
|
|
1823
|
+
ignore
|
|
1824
|
+
});
|
|
1825
|
+
if (routeFiles.length === 0) {
|
|
1826
|
+
logger.warn(`No route files found matching: ${routesPattern}`);
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
logger.log(`Found ${routeFiles.length} route files`);
|
|
1830
|
+
logger.blank();
|
|
1831
|
+
let created = 0;
|
|
1832
|
+
let skipped = 0;
|
|
1833
|
+
for (const routeFile of routeFiles) {
|
|
1834
|
+
const routeDir = dirname(routeFile);
|
|
1835
|
+
const metaPath = join(routeDir, "screen.meta.ts");
|
|
1836
|
+
const absoluteMetaPath = join(cwd, metaPath);
|
|
1837
|
+
if (!force && existsSync(absoluteMetaPath)) {
|
|
1838
|
+
if (!interactive) {
|
|
1839
|
+
skipped++;
|
|
1840
|
+
continue;
|
|
1049
1841
|
}
|
|
1050
|
-
|
|
1842
|
+
logger.itemWarn(`Exists: ${logger.path(metaPath)} (use --force to overwrite)`);
|
|
1843
|
+
skipped++;
|
|
1844
|
+
continue;
|
|
1051
1845
|
}
|
|
1846
|
+
const screenMeta = inferScreenMeta(routeDir, routesPattern);
|
|
1847
|
+
if (interactive) {
|
|
1848
|
+
const result = await promptForScreen(routeFile, screenMeta);
|
|
1849
|
+
if (result.skip) {
|
|
1850
|
+
logger.itemWarn(`Skipped: ${logger.path(metaPath)}`);
|
|
1851
|
+
skipped++;
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
const content = generateScreenMetaContent(result.meta, {
|
|
1855
|
+
owner: result.owner,
|
|
1856
|
+
tags: result.tags
|
|
1857
|
+
});
|
|
1858
|
+
if (dryRun) {
|
|
1859
|
+
logDryRunOutput(metaPath, result.meta, result.owner, result.tags);
|
|
1860
|
+
created++;
|
|
1861
|
+
} else if (safeWriteFile(absoluteMetaPath, metaPath, content)) created++;
|
|
1862
|
+
} else {
|
|
1863
|
+
const content = generateScreenMetaContent(screenMeta);
|
|
1864
|
+
if (dryRun) {
|
|
1865
|
+
logDryRunOutput(metaPath, screenMeta);
|
|
1866
|
+
created++;
|
|
1867
|
+
} else if (safeWriteFile(absoluteMetaPath, metaPath, content)) created++;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
logSummary(created, skipped, dryRun);
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Determine where to place screen.meta.ts for a route
|
|
1874
|
+
*/
|
|
1875
|
+
function determineMetaPath(route, cwd) {
|
|
1876
|
+
if (route.componentPath) {
|
|
1877
|
+
const relativePath = relative(cwd, dirname(route.componentPath));
|
|
1878
|
+
if (!relativePath.startsWith("..")) return join(relativePath, "screen.meta.ts");
|
|
1879
|
+
}
|
|
1880
|
+
return join("src", "screens", route.screenId.replace(/\./g, "/"), "screen.meta.ts");
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Ensure the directory for a file exists
|
|
1884
|
+
*/
|
|
1885
|
+
function ensureDirectoryExists(filePath) {
|
|
1886
|
+
const dir = dirname(filePath);
|
|
1887
|
+
if (!existsSync(dir)) try {
|
|
1888
|
+
mkdirSync(dir, { recursive: true });
|
|
1889
|
+
} catch (error) {
|
|
1890
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1891
|
+
throw new Error(`Failed to create directory "${dir}": ${message}`);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Safely write a file with error handling
|
|
1896
|
+
* Returns true if successful, false if failed
|
|
1897
|
+
*/
|
|
1898
|
+
function safeWriteFile(absolutePath, relativePath, content) {
|
|
1899
|
+
try {
|
|
1900
|
+
ensureDirectoryExists(absolutePath);
|
|
1901
|
+
writeFileSync(absolutePath, content);
|
|
1902
|
+
logger.itemSuccess(`Created: ${logger.path(relativePath)}`);
|
|
1903
|
+
return true;
|
|
1904
|
+
} catch (error) {
|
|
1905
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1906
|
+
logger.itemError(`Failed to create ${logger.path(relativePath)}: ${message}`);
|
|
1907
|
+
return false;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Log dry run output for a screen
|
|
1912
|
+
*/
|
|
1913
|
+
function logDryRunOutput(metaPath, meta, owner, tags) {
|
|
1914
|
+
logger.step(`Would create: ${logger.path(metaPath)}`);
|
|
1915
|
+
logger.log(` ${logger.dim(`id: "${meta.id}"`)}`);
|
|
1916
|
+
logger.log(` ${logger.dim(`title: "${meta.title}"`)}`);
|
|
1917
|
+
logger.log(` ${logger.dim(`route: "${meta.route}"`)}`);
|
|
1918
|
+
if (owner && owner.length > 0) logger.log(` ${logger.dim(`owner: [${owner.map((o) => `"${o}"`).join(", ")}]`)}`);
|
|
1919
|
+
if (tags && tags.length > 0) logger.log(` ${logger.dim(`tags: [${tags.map((t) => `"${t}"`).join(", ")}]`)}`);
|
|
1920
|
+
logger.blank();
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Log summary after generation
|
|
1924
|
+
*/
|
|
1925
|
+
function logSummary(created, skipped, dryRun) {
|
|
1926
|
+
logger.blank();
|
|
1927
|
+
if (dryRun) {
|
|
1928
|
+
logger.info(`Would create ${created} files (${skipped} already exist)`);
|
|
1052
1929
|
logger.blank();
|
|
1053
|
-
|
|
1054
|
-
|
|
1930
|
+
logger.log(`Run without ${logger.code("--dry-run")} to create files`);
|
|
1931
|
+
} else {
|
|
1932
|
+
logger.done(`Created ${created} files (${skipped} skipped)`);
|
|
1933
|
+
if (created > 0) {
|
|
1055
1934
|
logger.blank();
|
|
1056
|
-
logger.log(
|
|
1057
|
-
|
|
1058
|
-
logger.
|
|
1059
|
-
if (created > 0) {
|
|
1060
|
-
logger.blank();
|
|
1061
|
-
logger.log(logger.bold("Next steps:"));
|
|
1062
|
-
logger.log(" 1. Review and customize the generated screen.meta.ts files");
|
|
1063
|
-
logger.log(` 2. Run ${logger.code("screenbook dev")} to view your screen catalog`);
|
|
1064
|
-
}
|
|
1935
|
+
logger.log(logger.bold("Next steps:"));
|
|
1936
|
+
logger.log(" 1. Review and customize the generated screen.meta.ts files");
|
|
1937
|
+
logger.log(` 2. Run ${logger.code("screenbook dev")} to view your screen catalog`);
|
|
1065
1938
|
}
|
|
1066
1939
|
}
|
|
1067
|
-
}
|
|
1940
|
+
}
|
|
1068
1941
|
/**
|
|
1069
1942
|
* Parse comma-separated string into array
|
|
1070
1943
|
*/
|
|
@@ -1706,7 +2579,7 @@ const lintCommand = define({
|
|
|
1706
2579
|
const cwd = process.cwd();
|
|
1707
2580
|
const adoption = config.adoption ?? { mode: "full" };
|
|
1708
2581
|
let hasWarnings = false;
|
|
1709
|
-
if (!config.routesPattern) {
|
|
2582
|
+
if (!config.routesPattern && !config.routesFile) {
|
|
1710
2583
|
logger.errorWithHelp(ERRORS.ROUTES_PATTERN_MISSING);
|
|
1711
2584
|
process.exit(1);
|
|
1712
2585
|
}
|
|
@@ -1717,6 +2590,10 @@ const lintCommand = define({
|
|
|
1717
2590
|
if (adoption.minimumCoverage != null) logger.log(`Minimum coverage: ${adoption.minimumCoverage}%`);
|
|
1718
2591
|
}
|
|
1719
2592
|
logger.blank();
|
|
2593
|
+
if (config.routesFile) {
|
|
2594
|
+
await lintRoutesFile(config.routesFile, cwd, config, adoption, ctx.values.allowCycles ?? false, ctx.values.strict ?? false);
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
1720
2597
|
let routeFiles = await glob(config.routesPattern, {
|
|
1721
2598
|
cwd,
|
|
1722
2599
|
ignore: config.ignore
|
|
@@ -1821,6 +2698,9 @@ const lintCommand = define({
|
|
|
1821
2698
|
} else if (error instanceof Error) {
|
|
1822
2699
|
logger.warn(`Failed to analyze screens.json: ${error.message}`);
|
|
1823
2700
|
hasWarnings = true;
|
|
2701
|
+
} else {
|
|
2702
|
+
logger.warn(`Failed to analyze screens.json: ${String(error)}`);
|
|
2703
|
+
hasWarnings = true;
|
|
1824
2704
|
}
|
|
1825
2705
|
}
|
|
1826
2706
|
if (hasWarnings) {
|
|
@@ -1830,6 +2710,188 @@ const lintCommand = define({
|
|
|
1830
2710
|
}
|
|
1831
2711
|
});
|
|
1832
2712
|
/**
|
|
2713
|
+
* Lint screen.meta.ts coverage for routesFile mode (config-based routing)
|
|
2714
|
+
*/
|
|
2715
|
+
async function lintRoutesFile(routesFile, cwd, config, adoption, allowCycles, strict) {
|
|
2716
|
+
let hasWarnings = false;
|
|
2717
|
+
const absoluteRoutesFile = resolve(cwd, routesFile);
|
|
2718
|
+
if (!existsSync(absoluteRoutesFile)) {
|
|
2719
|
+
logger.errorWithHelp(ERRORS.ROUTES_FILE_NOT_FOUND(routesFile));
|
|
2720
|
+
process.exit(1);
|
|
2721
|
+
}
|
|
2722
|
+
logger.log(`Parsing routes from ${logger.path(routesFile)}...`);
|
|
2723
|
+
logger.blank();
|
|
2724
|
+
let flatRoutes;
|
|
2725
|
+
try {
|
|
2726
|
+
const content = readFileSync(absoluteRoutesFile, "utf-8");
|
|
2727
|
+
const routerType = detectRouterType(content);
|
|
2728
|
+
let parseResult;
|
|
2729
|
+
if (routerType === "tanstack-router") parseResult = parseTanStackRouterConfig(absoluteRoutesFile, content);
|
|
2730
|
+
else if (routerType === "react-router") parseResult = parseReactRouterConfig(absoluteRoutesFile, content);
|
|
2731
|
+
else parseResult = parseVueRouterConfig(absoluteRoutesFile, content);
|
|
2732
|
+
for (const warning of parseResult.warnings) {
|
|
2733
|
+
logger.warn(warning);
|
|
2734
|
+
hasWarnings = true;
|
|
2735
|
+
}
|
|
2736
|
+
flatRoutes = flattenRoutes(parseResult.routes);
|
|
2737
|
+
} catch (error) {
|
|
2738
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2739
|
+
logger.errorWithHelp(ERRORS.ROUTES_FILE_PARSE_ERROR(routesFile, message));
|
|
2740
|
+
process.exit(1);
|
|
2741
|
+
}
|
|
2742
|
+
if (flatRoutes.length === 0) {
|
|
2743
|
+
logger.warn("No routes found in the config file");
|
|
2744
|
+
return hasWarnings;
|
|
2745
|
+
}
|
|
2746
|
+
const metaFiles = await glob(config.metaPattern, {
|
|
2747
|
+
cwd,
|
|
2748
|
+
ignore: config.ignore
|
|
2749
|
+
});
|
|
2750
|
+
const metaDirs = /* @__PURE__ */ new Set();
|
|
2751
|
+
const metaDirsByName = /* @__PURE__ */ new Map();
|
|
2752
|
+
for (const metaFile of metaFiles) {
|
|
2753
|
+
const dir = dirname(metaFile);
|
|
2754
|
+
metaDirs.add(dir);
|
|
2755
|
+
const baseName = dir.split("/").pop()?.toLowerCase() || "";
|
|
2756
|
+
if (baseName) metaDirsByName.set(baseName, dir);
|
|
2757
|
+
}
|
|
2758
|
+
const missingMeta = [];
|
|
2759
|
+
const covered = [];
|
|
2760
|
+
for (const route of flatRoutes) {
|
|
2761
|
+
if (route.componentPath?.endsWith("Layout")) continue;
|
|
2762
|
+
let matched = false;
|
|
2763
|
+
const metaPath = determineMetaDir(route, cwd);
|
|
2764
|
+
if (metaDirs.has(metaPath)) matched = true;
|
|
2765
|
+
if (!matched && route.componentPath) {
|
|
2766
|
+
const componentName = route.componentPath.toLowerCase();
|
|
2767
|
+
if (metaDirsByName.has(componentName)) matched = true;
|
|
2768
|
+
if (!matched) {
|
|
2769
|
+
const parts = route.componentPath.split(/(?=[A-Z])/);
|
|
2770
|
+
const lastPart = parts[parts.length - 1];
|
|
2771
|
+
if (parts.length > 1 && lastPart) {
|
|
2772
|
+
if (metaDirsByName.has(lastPart.toLowerCase())) matched = true;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
if (!matched) {
|
|
2777
|
+
const screenPath = route.screenId.replace(/\./g, "/");
|
|
2778
|
+
for (const dir of metaDirs) if (dir.endsWith(screenPath)) {
|
|
2779
|
+
matched = true;
|
|
2780
|
+
break;
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
if (matched) covered.push(route);
|
|
2784
|
+
else missingMeta.push(route);
|
|
2785
|
+
}
|
|
2786
|
+
const total = covered.length + missingMeta.length;
|
|
2787
|
+
const coveredCount = covered.length;
|
|
2788
|
+
const missingCount = missingMeta.length;
|
|
2789
|
+
const coveragePercent = Math.round(coveredCount / total * 100);
|
|
2790
|
+
logger.log(`Found ${total} routes`);
|
|
2791
|
+
logger.log(`Coverage: ${coveredCount}/${total} (${coveragePercent}%)`);
|
|
2792
|
+
logger.blank();
|
|
2793
|
+
const minimumCoverage = adoption.minimumCoverage ?? 100;
|
|
2794
|
+
const passedCoverage = coveragePercent >= minimumCoverage;
|
|
2795
|
+
if (missingCount > 0) {
|
|
2796
|
+
logger.log(`Missing screen.meta.ts (${missingCount} routes):`);
|
|
2797
|
+
logger.blank();
|
|
2798
|
+
for (const route of missingMeta) {
|
|
2799
|
+
const suggestedMetaPath = determineSuggestedMetaPath(route, cwd);
|
|
2800
|
+
logger.itemError(`${route.fullPath} ${logger.dim(`(${route.screenId})`)}`);
|
|
2801
|
+
logger.log(` ${logger.dim("→")} ${logger.path(suggestedMetaPath)}`);
|
|
2802
|
+
}
|
|
2803
|
+
logger.blank();
|
|
2804
|
+
}
|
|
2805
|
+
if (!passedCoverage) {
|
|
2806
|
+
logger.error(`Lint failed: Coverage ${coveragePercent}% is below minimum ${minimumCoverage}%`);
|
|
2807
|
+
process.exit(1);
|
|
2808
|
+
} else if (missingCount > 0) {
|
|
2809
|
+
logger.success(`Coverage ${coveragePercent}% meets minimum ${minimumCoverage}%`);
|
|
2810
|
+
if (adoption.mode === "progressive") logger.log(` ${logger.dim("Tip:")} Increase minimumCoverage in config to gradually improve coverage`);
|
|
2811
|
+
} else logger.done("All routes have screen.meta.ts files");
|
|
2812
|
+
const screensPath = join(cwd, config.outDir, "screens.json");
|
|
2813
|
+
if (existsSync(screensPath)) try {
|
|
2814
|
+
const content = readFileSync(screensPath, "utf-8");
|
|
2815
|
+
const screens = JSON.parse(content);
|
|
2816
|
+
const orphans = findOrphanScreens(screens);
|
|
2817
|
+
if (orphans.length > 0) {
|
|
2818
|
+
hasWarnings = true;
|
|
2819
|
+
logger.blank();
|
|
2820
|
+
logger.warn(`Orphan screens detected (${orphans.length}):`);
|
|
2821
|
+
logger.blank();
|
|
2822
|
+
logger.log(" These screens have no entryPoints and are not");
|
|
2823
|
+
logger.log(" referenced in any other screen's 'next' array.");
|
|
2824
|
+
logger.blank();
|
|
2825
|
+
for (const orphan of orphans) logger.itemWarn(`${orphan.id} ${logger.dim(orphan.route)}`);
|
|
2826
|
+
logger.blank();
|
|
2827
|
+
logger.log(` ${logger.dim("Consider adding entryPoints or removing these screens.")}`);
|
|
2828
|
+
}
|
|
2829
|
+
if (!allowCycles) {
|
|
2830
|
+
const cycleResult = detectCycles(screens);
|
|
2831
|
+
if (cycleResult.hasCycles) {
|
|
2832
|
+
hasWarnings = true;
|
|
2833
|
+
logger.blank();
|
|
2834
|
+
logger.warn(getCycleSummary(cycleResult));
|
|
2835
|
+
logger.log(formatCycleWarnings(cycleResult.cycles));
|
|
2836
|
+
logger.blank();
|
|
2837
|
+
if (cycleResult.disallowedCycles.length > 0) {
|
|
2838
|
+
logger.log(` ${logger.dim("Use 'allowCycles: true' in screen.meta.ts to allow intentional cycles.")}`);
|
|
2839
|
+
if (strict) {
|
|
2840
|
+
logger.blank();
|
|
2841
|
+
logger.errorWithHelp(ERRORS.CYCLES_DETECTED(cycleResult.disallowedCycles.length));
|
|
2842
|
+
process.exit(1);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
const invalidNavs = findInvalidNavigations(screens);
|
|
2848
|
+
if (invalidNavs.length > 0) {
|
|
2849
|
+
hasWarnings = true;
|
|
2850
|
+
logger.blank();
|
|
2851
|
+
logger.warn(`Invalid navigation targets (${invalidNavs.length}):`);
|
|
2852
|
+
logger.blank();
|
|
2853
|
+
logger.log(" These navigation references point to non-existent screens.");
|
|
2854
|
+
logger.blank();
|
|
2855
|
+
for (const inv of invalidNavs) logger.itemWarn(`${inv.screenId} → ${logger.dim(inv.field)}: "${inv.target}"`);
|
|
2856
|
+
logger.blank();
|
|
2857
|
+
logger.log(` ${logger.dim("Check that these screen IDs exist in your codebase.")}`);
|
|
2858
|
+
}
|
|
2859
|
+
} catch (error) {
|
|
2860
|
+
if (error instanceof SyntaxError) {
|
|
2861
|
+
logger.warn("Failed to parse screens.json - file may be corrupted");
|
|
2862
|
+
logger.log(` ${logger.dim("Run 'screenbook build' to regenerate.")}`);
|
|
2863
|
+
hasWarnings = true;
|
|
2864
|
+
} else if (error instanceof Error) {
|
|
2865
|
+
logger.warn(`Failed to analyze screens.json: ${error.message}`);
|
|
2866
|
+
hasWarnings = true;
|
|
2867
|
+
} else {
|
|
2868
|
+
logger.warn(`Failed to analyze screens.json: ${String(error)}`);
|
|
2869
|
+
hasWarnings = true;
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
if (hasWarnings) {
|
|
2873
|
+
logger.blank();
|
|
2874
|
+
logger.warn("Lint completed with warnings.");
|
|
2875
|
+
}
|
|
2876
|
+
return hasWarnings;
|
|
2877
|
+
}
|
|
2878
|
+
/**
|
|
2879
|
+
* Determine the directory where screen.meta.ts should be for a route
|
|
2880
|
+
*/
|
|
2881
|
+
function determineMetaDir(route, cwd) {
|
|
2882
|
+
if (route.componentPath) {
|
|
2883
|
+
const relativePath = relative(cwd, dirname(route.componentPath));
|
|
2884
|
+
if (!relativePath.startsWith("..")) return relativePath;
|
|
2885
|
+
}
|
|
2886
|
+
return join("src", "screens", route.screenId.replace(/\./g, "/"));
|
|
2887
|
+
}
|
|
2888
|
+
/**
|
|
2889
|
+
* Determine the suggested screen.meta.ts path for a route
|
|
2890
|
+
*/
|
|
2891
|
+
function determineSuggestedMetaPath(route, cwd) {
|
|
2892
|
+
return join(determineMetaDir(route, cwd), "screen.meta.ts");
|
|
2893
|
+
}
|
|
2894
|
+
/**
|
|
1833
2895
|
* Find navigation references that point to non-existent screens.
|
|
1834
2896
|
* Checks `next`, `entryPoints` arrays and mock navigation targets.
|
|
1835
2897
|
*/
|