@screenbook/cli 1.2.0 → 1.3.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 +591 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { cli, define } from "gunshi";
|
|
4
3
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
4
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { cli, define } from "gunshi";
|
|
6
7
|
import { createJiti } from "jiti";
|
|
7
8
|
import { glob } from "tinyglobby";
|
|
8
9
|
import { defineConfig } from "@screenbook/core";
|
|
@@ -904,8 +905,8 @@ async function checkVersionCompatibility(cwd) {
|
|
|
904
905
|
status: "warn",
|
|
905
906
|
message: "Cannot check - packages not installed"
|
|
906
907
|
};
|
|
907
|
-
const extractMajor = (version) => {
|
|
908
|
-
return version.replace(/^[\^~>=<]+/, "").split(".")[0] ?? "0";
|
|
908
|
+
const extractMajor = (version$1) => {
|
|
909
|
+
return version$1.replace(/^[\^~>=<]+/, "").split(".")[0] ?? "0";
|
|
909
910
|
};
|
|
910
911
|
if (extractMajor(coreVersion) !== extractMajor(cliVersion)) return {
|
|
911
912
|
name: "Version compatibility",
|
|
@@ -1007,7 +1008,10 @@ function pathToScreenId(path) {
|
|
|
1007
1008
|
if (path === "/" || path === "") return "home";
|
|
1008
1009
|
return path.replace(/^\//, "").replace(/\/$/, "").split("/").map((segment) => {
|
|
1009
1010
|
if (segment.startsWith(":")) return segment.slice(1);
|
|
1010
|
-
if (segment.startsWith("*"))
|
|
1011
|
+
if (segment.startsWith("*")) {
|
|
1012
|
+
if (segment === "**") return "catchall";
|
|
1013
|
+
return segment.slice(1) || "catchall";
|
|
1014
|
+
}
|
|
1011
1015
|
return segment;
|
|
1012
1016
|
}).join(".");
|
|
1013
1017
|
}
|
|
@@ -1021,6 +1025,524 @@ function pathToScreenTitle(path) {
|
|
|
1021
1025
|
return (segments[segments.length - 1] || "Home").split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1022
1026
|
}
|
|
1023
1027
|
|
|
1028
|
+
//#endregion
|
|
1029
|
+
//#region src/utils/angularRouterParser.ts
|
|
1030
|
+
/**
|
|
1031
|
+
* Parse Angular Router configuration file and extract routes.
|
|
1032
|
+
* Supports both Standalone (Angular 14+) and NgModule patterns.
|
|
1033
|
+
*
|
|
1034
|
+
* Supported patterns:
|
|
1035
|
+
* - `export const routes: Routes = [...]`
|
|
1036
|
+
* - `const routes: Routes = [...]`
|
|
1037
|
+
* - `RouterModule.forRoot([...])`
|
|
1038
|
+
* - `RouterModule.forChild([...])`
|
|
1039
|
+
* - `export default [...]`
|
|
1040
|
+
* - `export default [...] satisfies Routes`
|
|
1041
|
+
*
|
|
1042
|
+
* @param filePath - Path to the router configuration file
|
|
1043
|
+
* @param preloadedContent - Optional pre-read file content. When provided, the file is not read from disk,
|
|
1044
|
+
* enabling testing with virtual content or avoiding duplicate file reads.
|
|
1045
|
+
* @returns ParseResult containing extracted routes and any warnings
|
|
1046
|
+
* @throws Error if the file cannot be read or contains syntax errors
|
|
1047
|
+
*/
|
|
1048
|
+
function parseAngularRouterConfig(filePath, preloadedContent) {
|
|
1049
|
+
const absolutePath = resolve(filePath);
|
|
1050
|
+
const routesFileDir = dirname(absolutePath);
|
|
1051
|
+
const warnings = [];
|
|
1052
|
+
let content;
|
|
1053
|
+
if (preloadedContent !== void 0) content = preloadedContent;
|
|
1054
|
+
else try {
|
|
1055
|
+
content = readFileSync(absolutePath, "utf-8");
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1058
|
+
throw new Error(`Failed to read routes file "${absolutePath}": ${message}`);
|
|
1059
|
+
}
|
|
1060
|
+
let ast;
|
|
1061
|
+
try {
|
|
1062
|
+
ast = parse(content, {
|
|
1063
|
+
sourceType: "module",
|
|
1064
|
+
plugins: ["typescript", ["decorators", { decoratorsBeforeExport: true }]]
|
|
1065
|
+
});
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
if (error instanceof SyntaxError) throw new Error(`Syntax error in routes file "${absolutePath}": ${error.message}`);
|
|
1068
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1069
|
+
throw new Error(`Failed to parse routes file "${absolutePath}": ${message}`);
|
|
1070
|
+
}
|
|
1071
|
+
const routes = [];
|
|
1072
|
+
for (const node of ast.program.body) {
|
|
1073
|
+
if (node.type === "VariableDeclaration") {
|
|
1074
|
+
for (const decl of node.declarations) if (decl.id.type === "Identifier" && decl.init?.type === "ArrayExpression") {
|
|
1075
|
+
const typeAnnotation = decl.id.typeAnnotation?.typeAnnotation;
|
|
1076
|
+
if (decl.id.name.toLowerCase().includes("route") || typeAnnotation?.type === "TSTypeReference" && typeAnnotation.typeName?.type === "Identifier" && typeAnnotation.typeName.name === "Routes") {
|
|
1077
|
+
const parsed = parseRoutesArray$3(decl.init, routesFileDir, warnings);
|
|
1078
|
+
routes.push(...parsed);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") {
|
|
1083
|
+
for (const decl of node.declaration.declarations) if (decl.id.type === "Identifier" && decl.init?.type === "ArrayExpression") {
|
|
1084
|
+
const typeAnnotation = decl.id.typeAnnotation?.typeAnnotation;
|
|
1085
|
+
if (decl.id.name.toLowerCase().includes("route") || typeAnnotation?.type === "TSTypeReference" && typeAnnotation.typeName?.type === "Identifier" && typeAnnotation.typeName.name === "Routes") {
|
|
1086
|
+
const parsed = parseRoutesArray$3(decl.init, routesFileDir, warnings);
|
|
1087
|
+
routes.push(...parsed);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "ArrayExpression") {
|
|
1092
|
+
const parsed = parseRoutesArray$3(node.declaration, routesFileDir, warnings);
|
|
1093
|
+
routes.push(...parsed);
|
|
1094
|
+
}
|
|
1095
|
+
if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "TSSatisfiesExpression" && node.declaration.expression.type === "ArrayExpression") {
|
|
1096
|
+
const parsed = parseRoutesArray$3(node.declaration.expression, routesFileDir, warnings);
|
|
1097
|
+
routes.push(...parsed);
|
|
1098
|
+
}
|
|
1099
|
+
let classNode = null;
|
|
1100
|
+
if (node.type === "ClassDeclaration") classNode = node;
|
|
1101
|
+
else if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "ClassDeclaration") classNode = node.declaration;
|
|
1102
|
+
if (classNode) {
|
|
1103
|
+
const decorators = classNode.decorators || [];
|
|
1104
|
+
for (const decorator of decorators) if (decorator.expression?.type === "CallExpression") {
|
|
1105
|
+
const routesFromDecorator = extractRoutesFromNgModule(decorator.expression, routesFileDir, warnings);
|
|
1106
|
+
routes.push(...routesFromDecorator);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
if (node.type === "ExpressionStatement") {
|
|
1110
|
+
const routesFromExpr = extractRoutesFromExpression(node.expression, routesFileDir, warnings);
|
|
1111
|
+
routes.push(...routesFromExpr);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
if (routes.length === 0) warnings.push("No routes found. Supported patterns: 'export const routes: Routes = [...]', 'RouterModule.forRoot([...])', or 'RouterModule.forChild([...])'");
|
|
1115
|
+
return {
|
|
1116
|
+
routes,
|
|
1117
|
+
warnings
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Extract routes from @NgModule decorator
|
|
1122
|
+
*/
|
|
1123
|
+
function extractRoutesFromNgModule(callExpr, baseDir, warnings) {
|
|
1124
|
+
const routes = [];
|
|
1125
|
+
if (callExpr.callee?.type === "Identifier" && callExpr.callee.name === "NgModule") {
|
|
1126
|
+
const arg = callExpr.arguments[0];
|
|
1127
|
+
if (arg?.type === "ObjectExpression") {
|
|
1128
|
+
for (const prop of arg.properties) if (prop.type === "ObjectProperty" && prop.key?.type === "Identifier" && prop.key.name === "imports") {
|
|
1129
|
+
if (prop.value?.type === "ArrayExpression") for (const element of prop.value.elements) {
|
|
1130
|
+
if (!element) continue;
|
|
1131
|
+
const extracted = extractRoutesFromExpression(element, baseDir, warnings);
|
|
1132
|
+
routes.push(...extracted);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return routes;
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Extract routes from RouterModule.forRoot/forChild call expressions
|
|
1141
|
+
*/
|
|
1142
|
+
function extractRoutesFromExpression(node, baseDir, warnings) {
|
|
1143
|
+
const routes = [];
|
|
1144
|
+
if (node?.type !== "CallExpression") return routes;
|
|
1145
|
+
const callee = node.callee;
|
|
1146
|
+
if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && callee.object.name === "RouterModule" && callee.property?.type === "Identifier" && (callee.property.name === "forRoot" || callee.property.name === "forChild")) {
|
|
1147
|
+
const routesArg = node.arguments[0];
|
|
1148
|
+
if (routesArg?.type === "ArrayExpression") {
|
|
1149
|
+
const parsed = parseRoutesArray$3(routesArg, baseDir, warnings);
|
|
1150
|
+
routes.push(...parsed);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return routes;
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Parse an array expression containing route objects
|
|
1157
|
+
*/
|
|
1158
|
+
function parseRoutesArray$3(arrayNode, baseDir, warnings) {
|
|
1159
|
+
const routes = [];
|
|
1160
|
+
for (const element of arrayNode.elements) {
|
|
1161
|
+
if (!element) continue;
|
|
1162
|
+
if (element.type === "SpreadElement") {
|
|
1163
|
+
const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
|
|
1164
|
+
warnings.push(`Spread operator detected${loc}. Routes from spread cannot be statically analyzed.`);
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
if (element.type === "ObjectExpression") {
|
|
1168
|
+
const parsedRoute = parseRouteObject$3(element, baseDir, warnings);
|
|
1169
|
+
if (parsedRoute) routes.push(parsedRoute);
|
|
1170
|
+
} else {
|
|
1171
|
+
const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
|
|
1172
|
+
warnings.push(`Non-object route element (${element.type})${loc}. Only object literals can be statically analyzed.`);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return routes;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Parse a single route object expression
|
|
1179
|
+
*/
|
|
1180
|
+
function parseRouteObject$3(objectNode, baseDir, warnings) {
|
|
1181
|
+
let path;
|
|
1182
|
+
let component;
|
|
1183
|
+
let children;
|
|
1184
|
+
let redirectTo;
|
|
1185
|
+
let hasPath = false;
|
|
1186
|
+
for (const prop of objectNode.properties) {
|
|
1187
|
+
if (prop.type !== "ObjectProperty") continue;
|
|
1188
|
+
if (prop.key.type !== "Identifier") continue;
|
|
1189
|
+
switch (prop.key.name) {
|
|
1190
|
+
case "path":
|
|
1191
|
+
if (prop.value.type === "StringLiteral") {
|
|
1192
|
+
path = prop.value.value;
|
|
1193
|
+
hasPath = true;
|
|
1194
|
+
} else {
|
|
1195
|
+
const loc = prop.loc ? ` at line ${prop.loc.start.line}` : "";
|
|
1196
|
+
warnings.push(`Dynamic path value (${prop.value.type})${loc}. Only string literal paths can be statically analyzed.`);
|
|
1197
|
+
}
|
|
1198
|
+
break;
|
|
1199
|
+
case "component":
|
|
1200
|
+
if (prop.value.type === "Identifier") component = prop.value.name;
|
|
1201
|
+
break;
|
|
1202
|
+
case "loadComponent":
|
|
1203
|
+
component = extractLazyComponent(prop.value, baseDir, warnings);
|
|
1204
|
+
break;
|
|
1205
|
+
case "loadChildren": {
|
|
1206
|
+
const lazyPath = extractLazyPath(prop.value, baseDir, warnings);
|
|
1207
|
+
if (lazyPath) component = `[lazy: ${lazyPath}]`;
|
|
1208
|
+
break;
|
|
1209
|
+
}
|
|
1210
|
+
case "children":
|
|
1211
|
+
if (prop.value.type === "ArrayExpression") children = parseRoutesArray$3(prop.value, baseDir, warnings);
|
|
1212
|
+
break;
|
|
1213
|
+
case "redirectTo":
|
|
1214
|
+
if (prop.value.type === "StringLiteral") redirectTo = prop.value.value;
|
|
1215
|
+
break;
|
|
1216
|
+
case "pathMatch":
|
|
1217
|
+
case "canActivate":
|
|
1218
|
+
case "canDeactivate":
|
|
1219
|
+
case "canMatch":
|
|
1220
|
+
case "resolve":
|
|
1221
|
+
case "data":
|
|
1222
|
+
case "title":
|
|
1223
|
+
case "providers":
|
|
1224
|
+
case "runGuardsAndResolvers":
|
|
1225
|
+
case "outlet": break;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (redirectTo && !component && !children) return null;
|
|
1229
|
+
if (!hasPath) {
|
|
1230
|
+
if (children && children.length > 0) return {
|
|
1231
|
+
path: "",
|
|
1232
|
+
component,
|
|
1233
|
+
children
|
|
1234
|
+
};
|
|
1235
|
+
return null;
|
|
1236
|
+
}
|
|
1237
|
+
return {
|
|
1238
|
+
path: path || "",
|
|
1239
|
+
component,
|
|
1240
|
+
children
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Extract component from lazy loadComponent pattern
|
|
1245
|
+
* loadComponent: () => import('./path').then(m => m.Component)
|
|
1246
|
+
*/
|
|
1247
|
+
function extractLazyComponent(node, baseDir, warnings) {
|
|
1248
|
+
if (node.type === "ArrowFunctionExpression") {
|
|
1249
|
+
const body = node.body;
|
|
1250
|
+
if (body.type === "CallExpression" && body.callee?.type === "MemberExpression" && body.callee.property?.type === "Identifier" && body.callee.property.name === "then") {
|
|
1251
|
+
const importCall = body.callee.object;
|
|
1252
|
+
const thenArg = body.arguments[0];
|
|
1253
|
+
if (importCall?.type === "CallExpression" && importCall.callee?.type === "Import") {
|
|
1254
|
+
if (importCall.arguments[0]?.type === "StringLiteral") {
|
|
1255
|
+
const importPath = resolveImportPath(importCall.arguments[0].value, baseDir);
|
|
1256
|
+
if (thenArg?.type === "ArrowFunctionExpression" && thenArg.body?.type === "MemberExpression" && thenArg.body.property?.type === "Identifier") return `${importPath}#${thenArg.body.property.name}`;
|
|
1257
|
+
return importPath;
|
|
1258
|
+
}
|
|
1259
|
+
const loc$1 = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1260
|
+
warnings.push(`Lazy loadComponent with dynamic path${loc$1}. Only string literal imports can be analyzed.`);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
if (body.type === "CallExpression" && body.callee?.type === "Import") {
|
|
1265
|
+
if (body.arguments[0]?.type === "StringLiteral") return resolveImportPath(body.arguments[0].value, baseDir);
|
|
1266
|
+
const loc$1 = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1267
|
+
warnings.push(`Lazy loadComponent with dynamic path${loc$1}. Only string literal imports can be analyzed.`);
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1272
|
+
warnings.push(`Unrecognized loadComponent pattern (${node.type})${loc}. Expected arrow function with import().then().`);
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Extract path from lazy loadChildren pattern
|
|
1276
|
+
* loadChildren: () => import('./path').then(m => m.routes)
|
|
1277
|
+
*/
|
|
1278
|
+
function extractLazyPath(node, baseDir, warnings) {
|
|
1279
|
+
if (node.type === "ArrowFunctionExpression") {
|
|
1280
|
+
const body = node.body;
|
|
1281
|
+
if (body.type === "CallExpression" && body.callee?.type === "MemberExpression" && body.callee.property?.type === "Identifier" && body.callee.property.name === "then") {
|
|
1282
|
+
const importCall = body.callee.object;
|
|
1283
|
+
if (importCall?.type === "CallExpression" && importCall.callee?.type === "Import" && importCall.arguments[0]?.type === "StringLiteral") return resolveImportPath(importCall.arguments[0].value, baseDir);
|
|
1284
|
+
}
|
|
1285
|
+
if (body.type === "CallExpression" && body.callee?.type === "Import") {
|
|
1286
|
+
if (body.arguments[0]?.type === "StringLiteral") return resolveImportPath(body.arguments[0].value, baseDir);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1290
|
+
warnings.push(`Unrecognized loadChildren pattern (${node.type})${loc}. Expected arrow function with import().`);
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Detect if content is Angular Router based on patterns.
|
|
1294
|
+
* Checks for @angular/router import, RouterModule patterns, or Routes type annotation.
|
|
1295
|
+
*/
|
|
1296
|
+
function isAngularRouterContent(content) {
|
|
1297
|
+
if (content.includes("@angular/router")) return true;
|
|
1298
|
+
if (content.includes("RouterModule.forRoot") || content.includes("RouterModule.forChild")) return true;
|
|
1299
|
+
if (/:\s*Routes\s*[=[]/.test(content)) return true;
|
|
1300
|
+
return false;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
//#endregion
|
|
1304
|
+
//#region src/utils/solidRouterParser.ts
|
|
1305
|
+
/**
|
|
1306
|
+
* Parse Solid Router configuration file and extract routes.
|
|
1307
|
+
* Supports various export patterns including `export const routes`, `export default`,
|
|
1308
|
+
* and TypeScript's `satisfies` operator.
|
|
1309
|
+
*
|
|
1310
|
+
* @param filePath - Path to the router configuration file
|
|
1311
|
+
* @param preloadedContent - Optional pre-read file content to avoid duplicate file reads
|
|
1312
|
+
* @returns ParseResult containing extracted routes and any warnings
|
|
1313
|
+
* @throws Error if the file cannot be read or contains syntax errors
|
|
1314
|
+
*/
|
|
1315
|
+
function parseSolidRouterConfig(filePath, preloadedContent) {
|
|
1316
|
+
const absolutePath = resolve(filePath);
|
|
1317
|
+
const routesFileDir = dirname(absolutePath);
|
|
1318
|
+
const warnings = [];
|
|
1319
|
+
let content;
|
|
1320
|
+
if (preloadedContent !== void 0) content = preloadedContent;
|
|
1321
|
+
else try {
|
|
1322
|
+
content = readFileSync(absolutePath, "utf-8");
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1325
|
+
throw new Error(`Failed to read routes file "${absolutePath}": ${message}`);
|
|
1326
|
+
}
|
|
1327
|
+
let ast;
|
|
1328
|
+
try {
|
|
1329
|
+
ast = parse(content, {
|
|
1330
|
+
sourceType: "module",
|
|
1331
|
+
plugins: ["typescript", "jsx"]
|
|
1332
|
+
});
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
if (error instanceof SyntaxError) throw new Error(`Syntax error in routes file "${absolutePath}": ${error.message}`);
|
|
1335
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1336
|
+
throw new Error(`Failed to parse routes file "${absolutePath}": ${message}`);
|
|
1337
|
+
}
|
|
1338
|
+
const routes = [];
|
|
1339
|
+
for (const node of ast.program.body) {
|
|
1340
|
+
if (node.type === "VariableDeclaration") {
|
|
1341
|
+
for (const decl of node.declarations) if (decl.id.type === "Identifier" && decl.id.name === "routes" && decl.init?.type === "ArrayExpression") {
|
|
1342
|
+
const parsed = parseRoutesArray$2(decl.init, routesFileDir, warnings);
|
|
1343
|
+
routes.push(...parsed);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") {
|
|
1347
|
+
for (const decl of node.declaration.declarations) if (decl.id.type === "Identifier" && decl.id.name === "routes" && decl.init?.type === "ArrayExpression") {
|
|
1348
|
+
const parsed = parseRoutesArray$2(decl.init, routesFileDir, warnings);
|
|
1349
|
+
routes.push(...parsed);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "ArrayExpression") {
|
|
1353
|
+
const parsed = parseRoutesArray$2(node.declaration, routesFileDir, warnings);
|
|
1354
|
+
routes.push(...parsed);
|
|
1355
|
+
}
|
|
1356
|
+
if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "TSSatisfiesExpression" && node.declaration.expression.type === "ArrayExpression") {
|
|
1357
|
+
const parsed = parseRoutesArray$2(node.declaration.expression, routesFileDir, warnings);
|
|
1358
|
+
routes.push(...parsed);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
if (routes.length === 0) warnings.push("No routes found. Supported patterns: 'export const routes = [...]' or 'export default [...]'");
|
|
1362
|
+
return {
|
|
1363
|
+
routes,
|
|
1364
|
+
warnings
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Parse an array expression containing route objects
|
|
1369
|
+
*/
|
|
1370
|
+
function parseRoutesArray$2(arrayNode, baseDir, warnings) {
|
|
1371
|
+
const routes = [];
|
|
1372
|
+
for (const element of arrayNode.elements) {
|
|
1373
|
+
if (!element) continue;
|
|
1374
|
+
if (element.type === "SpreadElement") {
|
|
1375
|
+
const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
|
|
1376
|
+
warnings.push(`Spread operator detected${loc}. Routes from spread cannot be statically analyzed.`);
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
if (element.type === "ObjectExpression") {
|
|
1380
|
+
const parsedRoutes = parseRouteObject$2(element, baseDir, warnings);
|
|
1381
|
+
routes.push(...parsedRoutes);
|
|
1382
|
+
} else {
|
|
1383
|
+
const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
|
|
1384
|
+
warnings.push(`Non-object route element (${element.type})${loc}. Only object literals can be statically analyzed.`);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return routes;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Parse a single route object expression
|
|
1391
|
+
* Returns array to handle multiple paths case: path: ["/a", "/b"]
|
|
1392
|
+
*/
|
|
1393
|
+
function parseRouteObject$2(objectNode, baseDir, warnings) {
|
|
1394
|
+
let paths = [];
|
|
1395
|
+
let component;
|
|
1396
|
+
let children;
|
|
1397
|
+
let hasPath = false;
|
|
1398
|
+
for (const prop of objectNode.properties) {
|
|
1399
|
+
if (prop.type !== "ObjectProperty") continue;
|
|
1400
|
+
if (prop.key.type !== "Identifier") continue;
|
|
1401
|
+
switch (prop.key.name) {
|
|
1402
|
+
case "path":
|
|
1403
|
+
if (prop.value.type === "StringLiteral") {
|
|
1404
|
+
paths = [prop.value.value];
|
|
1405
|
+
hasPath = true;
|
|
1406
|
+
} else if (prop.value.type === "ArrayExpression") {
|
|
1407
|
+
const arrayElementCount = prop.value.elements.filter(Boolean).length;
|
|
1408
|
+
paths = extractPathArray(prop.value, warnings);
|
|
1409
|
+
hasPath = paths.length > 0;
|
|
1410
|
+
if (arrayElementCount > 0 && paths.length === 0) {
|
|
1411
|
+
const loc = prop.loc ? ` at line ${prop.loc.start.line}` : "";
|
|
1412
|
+
warnings.push(`Path array contains only dynamic values${loc}. No static paths could be extracted.`);
|
|
1413
|
+
}
|
|
1414
|
+
} else {
|
|
1415
|
+
const loc = prop.loc ? ` at line ${prop.loc.start.line}` : "";
|
|
1416
|
+
warnings.push(`Dynamic path value (${prop.value.type})${loc}. Only string literal paths can be statically analyzed.`);
|
|
1417
|
+
}
|
|
1418
|
+
break;
|
|
1419
|
+
case "component":
|
|
1420
|
+
component = extractComponent(prop.value, baseDir, warnings);
|
|
1421
|
+
break;
|
|
1422
|
+
case "children":
|
|
1423
|
+
if (prop.value.type === "ArrayExpression") children = parseRoutesArray$2(prop.value, baseDir, warnings);
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
if (!hasPath) {
|
|
1428
|
+
if (children && children.length > 0) return [{
|
|
1429
|
+
path: "",
|
|
1430
|
+
component,
|
|
1431
|
+
children
|
|
1432
|
+
}];
|
|
1433
|
+
return [];
|
|
1434
|
+
}
|
|
1435
|
+
return paths.map((path) => ({
|
|
1436
|
+
path,
|
|
1437
|
+
component,
|
|
1438
|
+
children
|
|
1439
|
+
}));
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Extract paths from array expression
|
|
1443
|
+
* path: ["/login", "/register"] -> ["/login", "/register"]
|
|
1444
|
+
*/
|
|
1445
|
+
function extractPathArray(arrayNode, warnings) {
|
|
1446
|
+
const paths = [];
|
|
1447
|
+
for (const element of arrayNode.elements) {
|
|
1448
|
+
if (!element) continue;
|
|
1449
|
+
if (element.type === "StringLiteral") paths.push(element.value);
|
|
1450
|
+
else {
|
|
1451
|
+
const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
|
|
1452
|
+
warnings.push(`Non-string path in array (${element.type})${loc}. Only string literal paths can be analyzed.`);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return paths;
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Extract component from various patterns
|
|
1459
|
+
* - Direct identifier: component: Home
|
|
1460
|
+
* - Lazy component: component: lazy(() => import("./Home"))
|
|
1461
|
+
*/
|
|
1462
|
+
function extractComponent(node, baseDir, warnings) {
|
|
1463
|
+
if (node.type === "Identifier") return node.name;
|
|
1464
|
+
if (node.type === "CallExpression") {
|
|
1465
|
+
const callee = node.callee;
|
|
1466
|
+
if (callee.type === "Identifier" && callee.name === "lazy") {
|
|
1467
|
+
const lazyArg = node.arguments[0];
|
|
1468
|
+
if (lazyArg) return extractLazyImportPath$2(lazyArg, baseDir, warnings);
|
|
1469
|
+
const loc$1 = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1470
|
+
warnings.push(`lazy() called without arguments${loc$1}. Expected arrow function with import().`);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1474
|
+
const calleeName = callee.type === "Identifier" ? callee.name : "unknown";
|
|
1475
|
+
warnings.push(`Unrecognized component pattern: ${calleeName}(...)${loc}. Only 'lazy(() => import(...))' is supported.`);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
if (node.type === "ArrowFunctionExpression") {
|
|
1479
|
+
if (node.body.type === "JSXElement") {
|
|
1480
|
+
const openingElement = node.body.openingElement;
|
|
1481
|
+
if (openingElement?.name?.type === "JSXIdentifier") return openingElement.name.name;
|
|
1482
|
+
if (openingElement?.name?.type === "JSXMemberExpression") {
|
|
1483
|
+
const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1484
|
+
warnings.push(`Namespaced JSX component (e.g., <UI.Button />)${loc}. Component extraction not supported for member expressions. Consider using a direct component reference or create a wrapper component.`);
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
if (node.body.type === "BlockStatement") {
|
|
1489
|
+
const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1490
|
+
warnings.push(`Arrow function with block body${loc}. Only concise arrow functions returning JSX directly can be analyzed.`);
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
if (node.body.type === "JSXFragment") {
|
|
1494
|
+
const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1495
|
+
warnings.push(`JSX Fragment detected${loc}. Cannot extract component name from fragments.`);
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
if (node.body.type === "ConditionalExpression") {
|
|
1499
|
+
const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1500
|
+
let componentInfo = "";
|
|
1501
|
+
const consequent = node.body.consequent;
|
|
1502
|
+
const alternate = node.body.alternate;
|
|
1503
|
+
if (consequent?.type === "JSXElement" && alternate?.type === "JSXElement") componentInfo = ` (${consequent.openingElement?.name?.name || "unknown"} or ${alternate.openingElement?.name?.name || "unknown"})`;
|
|
1504
|
+
warnings.push(`Conditional component${componentInfo}${loc}. Only static JSX elements can be analyzed. Consider extracting to a separate component.`);
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
const arrowLoc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1508
|
+
warnings.push(`Unrecognized arrow function body (${node.body.type})${arrowLoc}. Component will not be extracted.`);
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
if (node) {
|
|
1512
|
+
const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1513
|
+
warnings.push(`Unrecognized component pattern (${node.type})${loc}. Component will not be extracted.`);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Extract import path from lazy function argument
|
|
1518
|
+
* () => import("./pages/Dashboard") -> resolved path
|
|
1519
|
+
*/
|
|
1520
|
+
function extractLazyImportPath$2(node, baseDir, warnings) {
|
|
1521
|
+
if (node.type === "ArrowFunctionExpression") {
|
|
1522
|
+
const body = node.body;
|
|
1523
|
+
if (body.type === "CallExpression" && body.callee.type === "Import") {
|
|
1524
|
+
if (body.arguments[0]?.type === "StringLiteral") return resolveImportPath(body.arguments[0].value, baseDir);
|
|
1525
|
+
const loc$1 = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1526
|
+
warnings.push(`Lazy import with dynamic path${loc$1}. Only string literal imports can be analyzed.`);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
1531
|
+
warnings.push(`Unrecognized lazy pattern (${node.type})${loc}. Expected arrow function with import().`);
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Detect if content is Solid Router based on patterns.
|
|
1535
|
+
* Note: Called by detectRouterType() in reactRouterParser.ts before React Router detection
|
|
1536
|
+
* because Solid Router and React Router share similar syntax patterns (both use `path` and `component`).
|
|
1537
|
+
* The detection order matters: TanStack Router -> Solid Router -> Angular Router -> React Router -> Vue Router.
|
|
1538
|
+
*/
|
|
1539
|
+
function isSolidRouterContent(content) {
|
|
1540
|
+
if (content.includes("@solidjs/router")) return true;
|
|
1541
|
+
if (content.includes("solid-app-router")) return true;
|
|
1542
|
+
if (content.includes("solid-js") && /\blazy\s*\(/.test(content) && /\bcomponent\s*:/.test(content) && /\bpath\s*:/.test(content)) return true;
|
|
1543
|
+
return false;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1024
1546
|
//#endregion
|
|
1025
1547
|
//#region src/utils/tanstackRouterParser.ts
|
|
1026
1548
|
/**
|
|
@@ -1513,7 +2035,15 @@ function extractComponentFromJSX(node, warnings) {
|
|
|
1513
2035
|
}
|
|
1514
2036
|
}
|
|
1515
2037
|
}
|
|
1516
|
-
if (node.type === "JSXFragment")
|
|
2038
|
+
if (node.type === "JSXFragment") {
|
|
2039
|
+
const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
2040
|
+
warnings.push(`JSX Fragment detected${loc}. Cannot extract component name from fragments. Consider wrapping in a named component.`);
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
if (node) {
|
|
2044
|
+
const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
|
|
2045
|
+
warnings.push(`Unrecognized element pattern (${node.type})${loc}. Component will not be extracted.`);
|
|
2046
|
+
}
|
|
1517
2047
|
}
|
|
1518
2048
|
/**
|
|
1519
2049
|
* Extract import path from lazy function
|
|
@@ -1549,10 +2079,14 @@ function isVueRouterContent(content) {
|
|
|
1549
2079
|
return false;
|
|
1550
2080
|
}
|
|
1551
2081
|
/**
|
|
1552
|
-
* Detect router type from file content
|
|
2082
|
+
* Detect router type from file content.
|
|
2083
|
+
* Detection order: TanStack Router -> Solid Router -> Angular Router -> React Router -> Vue Router.
|
|
2084
|
+
* This order ensures more specific patterns are checked before more generic ones.
|
|
1553
2085
|
*/
|
|
1554
2086
|
function detectRouterType(content) {
|
|
1555
2087
|
if (isTanStackRouterContent(content)) return "tanstack-router";
|
|
2088
|
+
if (isSolidRouterContent(content)) return "solid-router";
|
|
2089
|
+
if (isAngularRouterContent(content)) return "angular-router";
|
|
1556
2090
|
if (isReactRouterContent(content)) return "react-router";
|
|
1557
2091
|
if (isVueRouterContent(content)) return "vue-router";
|
|
1558
2092
|
return "unknown";
|
|
@@ -1746,14 +2280,21 @@ async function generateFromRoutesFile(routesFile, cwd, options) {
|
|
|
1746
2280
|
process.exit(1);
|
|
1747
2281
|
}
|
|
1748
2282
|
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";
|
|
2283
|
+
const routerTypeDisplay = routerType === "tanstack-router" ? "TanStack Router" : routerType === "solid-router" ? "Solid Router" : routerType === "angular-router" ? "Angular Router" : routerType === "react-router" ? "React Router" : routerType === "vue-router" ? "Vue Router" : "unknown";
|
|
1750
2284
|
logger.info(`Parsing routes from ${logger.path(routesFile)} (${routerTypeDisplay})...`);
|
|
1751
2285
|
logger.blank();
|
|
1752
2286
|
let parseResult;
|
|
1753
2287
|
try {
|
|
1754
2288
|
if (routerType === "tanstack-router") parseResult = parseTanStackRouterConfig(absoluteRoutesFile);
|
|
2289
|
+
else if (routerType === "solid-router") parseResult = parseSolidRouterConfig(absoluteRoutesFile);
|
|
2290
|
+
else if (routerType === "angular-router") parseResult = parseAngularRouterConfig(absoluteRoutesFile);
|
|
1755
2291
|
else if (routerType === "react-router") parseResult = parseReactRouterConfig(absoluteRoutesFile);
|
|
1756
|
-
else parseResult = parseVueRouterConfig(absoluteRoutesFile);
|
|
2292
|
+
else if (routerType === "vue-router") parseResult = parseVueRouterConfig(absoluteRoutesFile);
|
|
2293
|
+
else {
|
|
2294
|
+
logger.warn(`Could not auto-detect router type for ${logger.path(routesFile)}. Attempting to parse as Vue Router.`);
|
|
2295
|
+
logger.log(` ${logger.dim("If parsing fails, check that your router imports are explicit.")}`);
|
|
2296
|
+
parseResult = parseVueRouterConfig(absoluteRoutesFile);
|
|
2297
|
+
}
|
|
1757
2298
|
} catch (error) {
|
|
1758
2299
|
const message = error instanceof Error ? error.message : String(error);
|
|
1759
2300
|
logger.errorWithHelp(ERRORS.ROUTES_FILE_PARSE_ERROR(routesFile, message));
|
|
@@ -2345,6 +2886,34 @@ const FRAMEWORKS = [
|
|
|
2345
2886
|
routesPattern: "src/pages/**/*.astro",
|
|
2346
2887
|
metaPattern: "src/pages/**/screen.meta.ts"
|
|
2347
2888
|
},
|
|
2889
|
+
{
|
|
2890
|
+
name: "SolidStart",
|
|
2891
|
+
packages: ["@solidjs/start"],
|
|
2892
|
+
configFiles: ["app.config.ts", "app.config.js"],
|
|
2893
|
+
routesPattern: "src/routes/**/*.tsx",
|
|
2894
|
+
metaPattern: "src/routes/**/screen.meta.ts",
|
|
2895
|
+
check: (cwd) => existsSync(join(cwd, "src/routes"))
|
|
2896
|
+
},
|
|
2897
|
+
{
|
|
2898
|
+
name: "QwikCity",
|
|
2899
|
+
packages: ["@builder.io/qwik-city"],
|
|
2900
|
+
configFiles: [
|
|
2901
|
+
"vite.config.ts",
|
|
2902
|
+
"vite.config.js",
|
|
2903
|
+
"vite.config.mjs"
|
|
2904
|
+
],
|
|
2905
|
+
routesPattern: "src/routes/**/index.tsx",
|
|
2906
|
+
metaPattern: "src/routes/**/screen.meta.ts",
|
|
2907
|
+
check: (cwd) => existsSync(join(cwd, "src/routes"))
|
|
2908
|
+
},
|
|
2909
|
+
{
|
|
2910
|
+
name: "TanStack Start",
|
|
2911
|
+
packages: ["@tanstack/react-start", "@tanstack/start"],
|
|
2912
|
+
configFiles: ["app.config.ts", "app.config.js"],
|
|
2913
|
+
routesPattern: "src/routes/**/*.tsx",
|
|
2914
|
+
metaPattern: "src/routes/**/screen.meta.ts",
|
|
2915
|
+
check: (cwd) => existsSync(join(cwd, "src/routes/__root.tsx"))
|
|
2916
|
+
},
|
|
2348
2917
|
{
|
|
2349
2918
|
name: "Vite + Vue",
|
|
2350
2919
|
packages: ["vite", "vue"],
|
|
@@ -2379,7 +2948,8 @@ function readPackageJson(cwd) {
|
|
|
2379
2948
|
try {
|
|
2380
2949
|
const content = readFileSync(packageJsonPath, "utf-8");
|
|
2381
2950
|
return JSON.parse(content);
|
|
2382
|
-
} catch {
|
|
2951
|
+
} catch (error) {
|
|
2952
|
+
console.warn(`Warning: Failed to parse package.json at ${packageJsonPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2383
2953
|
return null;
|
|
2384
2954
|
}
|
|
2385
2955
|
}
|
|
@@ -2727,8 +3297,16 @@ async function lintRoutesFile(routesFile, cwd, config, adoption, allowCycles, st
|
|
|
2727
3297
|
const routerType = detectRouterType(content);
|
|
2728
3298
|
let parseResult;
|
|
2729
3299
|
if (routerType === "tanstack-router") parseResult = parseTanStackRouterConfig(absoluteRoutesFile, content);
|
|
3300
|
+
else if (routerType === "solid-router") parseResult = parseSolidRouterConfig(absoluteRoutesFile, content);
|
|
3301
|
+
else if (routerType === "angular-router") parseResult = parseAngularRouterConfig(absoluteRoutesFile, content);
|
|
2730
3302
|
else if (routerType === "react-router") parseResult = parseReactRouterConfig(absoluteRoutesFile, content);
|
|
2731
|
-
else parseResult = parseVueRouterConfig(absoluteRoutesFile, content);
|
|
3303
|
+
else if (routerType === "vue-router") parseResult = parseVueRouterConfig(absoluteRoutesFile, content);
|
|
3304
|
+
else {
|
|
3305
|
+
logger.warn(`Could not auto-detect router type for ${logger.path(routesFile)}. Attempting to parse as Vue Router.`);
|
|
3306
|
+
logger.log(` ${logger.dim("If parsing fails, check that your router imports are explicit.")}`);
|
|
3307
|
+
hasWarnings = true;
|
|
3308
|
+
parseResult = parseVueRouterConfig(absoluteRoutesFile, content);
|
|
3309
|
+
}
|
|
2732
3310
|
for (const warning of parseResult.warnings) {
|
|
2733
3311
|
logger.warn(warning);
|
|
2734
3312
|
hasWarnings = true;
|
|
@@ -3131,6 +3709,8 @@ const prImpactCommand = define({
|
|
|
3131
3709
|
|
|
3132
3710
|
//#endregion
|
|
3133
3711
|
//#region src/index.ts
|
|
3712
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
3713
|
+
const version = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")).version;
|
|
3134
3714
|
const mainCommand = define({
|
|
3135
3715
|
name: "screenbook",
|
|
3136
3716
|
description: "Screen catalog and navigation graph generator",
|
|
@@ -3152,7 +3732,7 @@ const mainCommand = define({
|
|
|
3152
3732
|
});
|
|
3153
3733
|
await cli(process.argv.slice(2), mainCommand, {
|
|
3154
3734
|
name: "screenbook",
|
|
3155
|
-
version
|
|
3735
|
+
version,
|
|
3156
3736
|
subCommands: {
|
|
3157
3737
|
init: initCommand,
|
|
3158
3738
|
generate: generateCommand,
|