@screenbook/cli 1.2.0 → 1.4.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 +810 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -5
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";
|
|
@@ -601,7 +602,7 @@ const devCommand = define({
|
|
|
601
602
|
const cwd = process.cwd();
|
|
602
603
|
logger.info("Starting Screenbook development server...");
|
|
603
604
|
await buildScreens(config, cwd);
|
|
604
|
-
const uiPackagePath = resolveUiPackage();
|
|
605
|
+
const uiPackagePath = resolveUiPackage$1();
|
|
605
606
|
if (!uiPackagePath) {
|
|
606
607
|
logger.errorWithHelp({
|
|
607
608
|
title: "Could not find @screenbook/ui package",
|
|
@@ -677,7 +678,7 @@ async function buildScreens(config, cwd) {
|
|
|
677
678
|
logger.blank();
|
|
678
679
|
logger.success(`Generated ${logger.path(outputPath)}`);
|
|
679
680
|
}
|
|
680
|
-
function resolveUiPackage() {
|
|
681
|
+
function resolveUiPackage$1() {
|
|
681
682
|
try {
|
|
682
683
|
return dirname(createRequire(import.meta.url).resolve("@screenbook/ui/package.json"));
|
|
683
684
|
} catch {
|
|
@@ -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
|
}
|
|
@@ -2434,6 +3004,18 @@ async function promptFrameworkSelection() {
|
|
|
2434
3004
|
};
|
|
2435
3005
|
}
|
|
2436
3006
|
|
|
3007
|
+
//#endregion
|
|
3008
|
+
//#region src/utils/isInteractive.ts
|
|
3009
|
+
/**
|
|
3010
|
+
* Check if the current environment supports interactive prompts.
|
|
3011
|
+
* Returns false in CI environments or when stdin is not a TTY.
|
|
3012
|
+
*/
|
|
3013
|
+
function isInteractive() {
|
|
3014
|
+
if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) return false;
|
|
3015
|
+
if (process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.JENKINS_URL) return false;
|
|
3016
|
+
return process.stdin.isTTY === true;
|
|
3017
|
+
}
|
|
3018
|
+
|
|
2437
3019
|
//#endregion
|
|
2438
3020
|
//#region src/commands/init.ts
|
|
2439
3021
|
function generateConfigTemplate(framework) {
|
|
@@ -2490,6 +3072,123 @@ function printNextSteps(hasRoutesPattern) {
|
|
|
2490
3072
|
logger.log(logger.dim(" page.tsx # Your route file"));
|
|
2491
3073
|
logger.log(logger.dim(" screen.meta.ts # Auto-generated, customize as needed"));
|
|
2492
3074
|
}
|
|
3075
|
+
/**
|
|
3076
|
+
* Resolve a boolean option with priority order:
|
|
3077
|
+
* 1. Explicit flag (e.g., --generate or --no-generate) takes precedence
|
|
3078
|
+
* 2. -y flag enables all optional features
|
|
3079
|
+
* 3. Non-interactive environments (CI mode or no TTY) fall back to ciDefault
|
|
3080
|
+
* 4. Otherwise, prompt the user interactively
|
|
3081
|
+
*/
|
|
3082
|
+
async function resolveOption(params) {
|
|
3083
|
+
const { explicitValue, yesAll, ciMode, ciDefault, promptMessage } = params;
|
|
3084
|
+
if (explicitValue !== void 0) return explicitValue;
|
|
3085
|
+
if (yesAll) return true;
|
|
3086
|
+
if (ciMode || !isInteractive()) return ciDefault;
|
|
3087
|
+
const response = await prompts({
|
|
3088
|
+
type: "confirm",
|
|
3089
|
+
name: "value",
|
|
3090
|
+
message: promptMessage,
|
|
3091
|
+
initial: true
|
|
3092
|
+
});
|
|
3093
|
+
if (response.value === void 0) {
|
|
3094
|
+
logger.blank();
|
|
3095
|
+
logger.info("Operation cancelled");
|
|
3096
|
+
process.exit(0);
|
|
3097
|
+
}
|
|
3098
|
+
return response.value;
|
|
3099
|
+
}
|
|
3100
|
+
async function countRouteFiles(routesPattern, cwd) {
|
|
3101
|
+
return (await glob(routesPattern, { cwd })).length;
|
|
3102
|
+
}
|
|
3103
|
+
async function runGenerate(routesPattern, cwd) {
|
|
3104
|
+
await generateFromRoutesPattern(routesPattern, cwd, {
|
|
3105
|
+
dryRun: false,
|
|
3106
|
+
force: false,
|
|
3107
|
+
interactive: false,
|
|
3108
|
+
ignore: ["**/node_modules/**"]
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
async function buildScreensForDev(metaPattern, outDir, cwd) {
|
|
3112
|
+
const files = await glob(metaPattern, {
|
|
3113
|
+
cwd,
|
|
3114
|
+
ignore: ["**/node_modules/**"]
|
|
3115
|
+
});
|
|
3116
|
+
if (files.length === 0) {
|
|
3117
|
+
logger.warn(`No screen.meta.ts files found matching: ${metaPattern}`);
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
const jiti = createJiti(cwd);
|
|
3121
|
+
const screens = [];
|
|
3122
|
+
for (const file of files) {
|
|
3123
|
+
const absolutePath = resolve(cwd, file);
|
|
3124
|
+
try {
|
|
3125
|
+
const module = await jiti.import(absolutePath);
|
|
3126
|
+
if (module.screen) screens.push({
|
|
3127
|
+
...module.screen,
|
|
3128
|
+
filePath: absolutePath
|
|
3129
|
+
});
|
|
3130
|
+
} catch (error) {
|
|
3131
|
+
logger.itemWarn(`Failed to load ${file}`);
|
|
3132
|
+
if (error instanceof Error) logger.log(` ${logger.dim(error.message)}`);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
const outputPath = join(cwd, outDir, "screens.json");
|
|
3136
|
+
const outputDir = dirname(outputPath);
|
|
3137
|
+
if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
|
|
3138
|
+
writeFileSync(outputPath, JSON.stringify(screens, null, 2));
|
|
3139
|
+
}
|
|
3140
|
+
function resolveUiPackage() {
|
|
3141
|
+
try {
|
|
3142
|
+
return dirname(createRequire(import.meta.url).resolve("@screenbook/ui/package.json"));
|
|
3143
|
+
} catch {
|
|
3144
|
+
const possiblePaths = [
|
|
3145
|
+
join(process.cwd(), "node_modules", "@screenbook", "ui"),
|
|
3146
|
+
join(process.cwd(), "..", "ui"),
|
|
3147
|
+
join(process.cwd(), "packages", "ui")
|
|
3148
|
+
];
|
|
3149
|
+
for (const p of possiblePaths) if (existsSync(join(p, "package.json"))) return p;
|
|
3150
|
+
return null;
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
async function startDevServer(metaPattern, outDir, cwd, port) {
|
|
3154
|
+
await buildScreensForDev(metaPattern, outDir, cwd);
|
|
3155
|
+
const uiPackagePath = resolveUiPackage();
|
|
3156
|
+
if (!uiPackagePath) {
|
|
3157
|
+
logger.warn("Could not find @screenbook/ui package");
|
|
3158
|
+
logger.log(` Run ${logger.code("npm install @screenbook/ui")} to install it`);
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
3161
|
+
const screensJsonPath = join(cwd, outDir, "screens.json");
|
|
3162
|
+
const uiScreensDir = join(uiPackagePath, ".screenbook");
|
|
3163
|
+
if (!existsSync(uiScreensDir)) mkdirSync(uiScreensDir, { recursive: true });
|
|
3164
|
+
if (existsSync(screensJsonPath)) copyFileSync(screensJsonPath, join(uiScreensDir, "screens.json"));
|
|
3165
|
+
logger.blank();
|
|
3166
|
+
logger.info(`Starting UI server on ${logger.highlight(`http://localhost:${port}`)}`);
|
|
3167
|
+
logger.blank();
|
|
3168
|
+
const astroProcess = spawn("npx", [
|
|
3169
|
+
"astro",
|
|
3170
|
+
"dev",
|
|
3171
|
+
"--port",
|
|
3172
|
+
port
|
|
3173
|
+
], {
|
|
3174
|
+
cwd: uiPackagePath,
|
|
3175
|
+
stdio: "inherit",
|
|
3176
|
+
shell: true
|
|
3177
|
+
});
|
|
3178
|
+
astroProcess.on("error", (error) => {
|
|
3179
|
+
logger.error(`Failed to start server: ${error.message}`);
|
|
3180
|
+
process.exit(1);
|
|
3181
|
+
});
|
|
3182
|
+
astroProcess.on("close", (code) => {
|
|
3183
|
+
process.exit(code ?? 0);
|
|
3184
|
+
});
|
|
3185
|
+
process.on("SIGINT", () => {
|
|
3186
|
+
astroProcess.kill("SIGINT");
|
|
3187
|
+
});
|
|
3188
|
+
process.on("SIGTERM", () => {
|
|
3189
|
+
astroProcess.kill("SIGTERM");
|
|
3190
|
+
});
|
|
3191
|
+
}
|
|
2493
3192
|
const initCommand = define({
|
|
2494
3193
|
name: "init",
|
|
2495
3194
|
description: "Initialize Screenbook in a project",
|
|
@@ -2504,12 +3203,46 @@ const initCommand = define({
|
|
|
2504
3203
|
type: "boolean",
|
|
2505
3204
|
description: "Skip framework auto-detection",
|
|
2506
3205
|
default: false
|
|
3206
|
+
},
|
|
3207
|
+
generate: {
|
|
3208
|
+
type: "boolean",
|
|
3209
|
+
description: "Auto-generate screen.meta.ts files (--no-generate to skip)",
|
|
3210
|
+
default: void 0,
|
|
3211
|
+
negatable: true
|
|
3212
|
+
},
|
|
3213
|
+
dev: {
|
|
3214
|
+
type: "boolean",
|
|
3215
|
+
description: "Start development server after init (--no-dev to skip)",
|
|
3216
|
+
default: void 0,
|
|
3217
|
+
negatable: true
|
|
3218
|
+
},
|
|
3219
|
+
yes: {
|
|
3220
|
+
type: "boolean",
|
|
3221
|
+
short: "y",
|
|
3222
|
+
description: "Answer yes to all prompts",
|
|
3223
|
+
default: false
|
|
3224
|
+
},
|
|
3225
|
+
ci: {
|
|
3226
|
+
type: "boolean",
|
|
3227
|
+
description: "CI mode (no prompts, generate only)",
|
|
3228
|
+
default: false
|
|
3229
|
+
},
|
|
3230
|
+
port: {
|
|
3231
|
+
type: "string",
|
|
3232
|
+
short: "p",
|
|
3233
|
+
description: "Port for the dev server",
|
|
3234
|
+
default: "4321"
|
|
2507
3235
|
}
|
|
2508
3236
|
},
|
|
2509
3237
|
run: async (ctx) => {
|
|
2510
3238
|
const cwd = process.cwd();
|
|
2511
3239
|
const force = ctx.values.force ?? false;
|
|
2512
3240
|
const skipDetect = ctx.values.skipDetect ?? false;
|
|
3241
|
+
const generateFlag = ctx.values.generate;
|
|
3242
|
+
const devFlag = ctx.values.dev;
|
|
3243
|
+
const yesAll = ctx.values.yes ?? false;
|
|
3244
|
+
const ciMode = ctx.values.ci ?? false;
|
|
3245
|
+
const port = ctx.values.port ?? "4321";
|
|
2513
3246
|
logger.info("Initializing Screenbook...");
|
|
2514
3247
|
logger.blank();
|
|
2515
3248
|
let framework = null;
|
|
@@ -2518,11 +3251,13 @@ const initCommand = define({
|
|
|
2518
3251
|
if (framework) logger.itemSuccess(`Detected: ${framework.name}`);
|
|
2519
3252
|
else {
|
|
2520
3253
|
logger.log(" Could not auto-detect framework");
|
|
2521
|
-
|
|
2522
|
-
framework = await promptFrameworkSelection();
|
|
2523
|
-
if (framework) {
|
|
3254
|
+
if (!ciMode && isInteractive()) {
|
|
2524
3255
|
logger.blank();
|
|
2525
|
-
|
|
3256
|
+
framework = await promptFrameworkSelection();
|
|
3257
|
+
if (framework) {
|
|
3258
|
+
logger.blank();
|
|
3259
|
+
logger.itemSuccess(`Selected: ${framework.name}`);
|
|
3260
|
+
}
|
|
2526
3261
|
}
|
|
2527
3262
|
}
|
|
2528
3263
|
}
|
|
@@ -2546,8 +3281,54 @@ const initCommand = define({
|
|
|
2546
3281
|
}
|
|
2547
3282
|
logger.blank();
|
|
2548
3283
|
logger.done("Screenbook initialized successfully!");
|
|
2549
|
-
|
|
2550
|
-
|
|
3284
|
+
if (!framework?.routesPattern) {
|
|
3285
|
+
printValueProposition();
|
|
3286
|
+
printNextSteps(false);
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
const routeFileCount = await countRouteFiles(framework.routesPattern, cwd);
|
|
3290
|
+
if (routeFileCount === 0) {
|
|
3291
|
+
printValueProposition();
|
|
3292
|
+
printNextSteps(true);
|
|
3293
|
+
return;
|
|
3294
|
+
}
|
|
3295
|
+
logger.blank();
|
|
3296
|
+
if (!await resolveOption({
|
|
3297
|
+
explicitValue: generateFlag,
|
|
3298
|
+
yesAll,
|
|
3299
|
+
ciMode,
|
|
3300
|
+
ciDefault: true,
|
|
3301
|
+
promptMessage: `Found ${routeFileCount} route files. Generate screen.meta.ts files?`
|
|
3302
|
+
})) {
|
|
3303
|
+
printValueProposition();
|
|
3304
|
+
printNextSteps(true);
|
|
3305
|
+
return;
|
|
3306
|
+
}
|
|
3307
|
+
logger.blank();
|
|
3308
|
+
logger.info("Generating screen metadata...");
|
|
3309
|
+
logger.blank();
|
|
3310
|
+
await runGenerate(framework.routesPattern, cwd);
|
|
3311
|
+
if (ciMode) {
|
|
3312
|
+
logger.blank();
|
|
3313
|
+
logger.done("Initialization complete!");
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
3316
|
+
logger.blank();
|
|
3317
|
+
if (!await resolveOption({
|
|
3318
|
+
explicitValue: devFlag,
|
|
3319
|
+
yesAll,
|
|
3320
|
+
ciMode,
|
|
3321
|
+
ciDefault: false,
|
|
3322
|
+
promptMessage: "Start the development server?"
|
|
3323
|
+
})) {
|
|
3324
|
+
logger.blank();
|
|
3325
|
+
logger.log(logger.bold("Next step:"));
|
|
3326
|
+
logger.log(` Run ${logger.code("screenbook dev")} to start the UI server`);
|
|
3327
|
+
return;
|
|
3328
|
+
}
|
|
3329
|
+
logger.blank();
|
|
3330
|
+
logger.info("Starting development server...");
|
|
3331
|
+
await startDevServer(framework.metaPattern, ".screenbook", cwd, port);
|
|
2551
3332
|
}
|
|
2552
3333
|
});
|
|
2553
3334
|
|
|
@@ -2727,8 +3508,16 @@ async function lintRoutesFile(routesFile, cwd, config, adoption, allowCycles, st
|
|
|
2727
3508
|
const routerType = detectRouterType(content);
|
|
2728
3509
|
let parseResult;
|
|
2729
3510
|
if (routerType === "tanstack-router") parseResult = parseTanStackRouterConfig(absoluteRoutesFile, content);
|
|
3511
|
+
else if (routerType === "solid-router") parseResult = parseSolidRouterConfig(absoluteRoutesFile, content);
|
|
3512
|
+
else if (routerType === "angular-router") parseResult = parseAngularRouterConfig(absoluteRoutesFile, content);
|
|
2730
3513
|
else if (routerType === "react-router") parseResult = parseReactRouterConfig(absoluteRoutesFile, content);
|
|
2731
|
-
else parseResult = parseVueRouterConfig(absoluteRoutesFile, content);
|
|
3514
|
+
else if (routerType === "vue-router") parseResult = parseVueRouterConfig(absoluteRoutesFile, content);
|
|
3515
|
+
else {
|
|
3516
|
+
logger.warn(`Could not auto-detect router type for ${logger.path(routesFile)}. Attempting to parse as Vue Router.`);
|
|
3517
|
+
logger.log(` ${logger.dim("If parsing fails, check that your router imports are explicit.")}`);
|
|
3518
|
+
hasWarnings = true;
|
|
3519
|
+
parseResult = parseVueRouterConfig(absoluteRoutesFile, content);
|
|
3520
|
+
}
|
|
2732
3521
|
for (const warning of parseResult.warnings) {
|
|
2733
3522
|
logger.warn(warning);
|
|
2734
3523
|
hasWarnings = true;
|
|
@@ -3131,6 +3920,8 @@ const prImpactCommand = define({
|
|
|
3131
3920
|
|
|
3132
3921
|
//#endregion
|
|
3133
3922
|
//#region src/index.ts
|
|
3923
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
3924
|
+
const version = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")).version;
|
|
3134
3925
|
const mainCommand = define({
|
|
3135
3926
|
name: "screenbook",
|
|
3136
3927
|
description: "Screen catalog and navigation graph generator",
|
|
@@ -3152,7 +3943,7 @@ const mainCommand = define({
|
|
|
3152
3943
|
});
|
|
3153
3944
|
await cli(process.argv.slice(2), mainCommand, {
|
|
3154
3945
|
name: "screenbook",
|
|
3155
|
-
version
|
|
3946
|
+
version,
|
|
3156
3947
|
subCommands: {
|
|
3157
3948
|
init: initCommand,
|
|
3158
3949
|
generate: generateCommand,
|