@screenbook/cli 1.6.1 → 1.7.1
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 +149 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -827,23 +827,22 @@ async function generateCoverageData(config, cwd, screens) {
|
|
|
827
827
|
cwd,
|
|
828
828
|
ignore: config.ignore
|
|
829
829
|
});
|
|
830
|
-
new Set(
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
830
|
+
const metaDirs = /* @__PURE__ */ new Set();
|
|
831
|
+
for (const screen of screens) if (screen.filePath) {
|
|
832
|
+
const absoluteDir = dirname(screen.filePath);
|
|
833
|
+
const relativeDir = absoluteDir.startsWith(cwd) ? absoluteDir.slice(cwd.length + 1) : absoluteDir;
|
|
834
|
+
metaDirs.add(relativeDir);
|
|
835
|
+
}
|
|
834
836
|
const missing = [];
|
|
835
837
|
for (const routeFile of routeFiles) {
|
|
836
838
|
const routeDir = dirname(routeFile);
|
|
837
|
-
if (!
|
|
838
|
-
const screenDir = s.id.replace(/\./g, "/");
|
|
839
|
-
return routeDir.includes(screenDir) || screenDir.includes(routeDir.replace(/^src\/pages\//, "").replace(/^app\//, ""));
|
|
840
|
-
})) missing.push({
|
|
839
|
+
if (!metaDirs.has(routeDir)) missing.push({
|
|
841
840
|
route: routeFile,
|
|
842
841
|
suggestedPath: join(dirname(routeFile), "screen.meta.ts")
|
|
843
842
|
});
|
|
844
843
|
}
|
|
845
844
|
const total = routeFiles.length > 0 ? routeFiles.length : screens.length;
|
|
846
|
-
const covered = screens.length;
|
|
845
|
+
const covered = routeFiles.length > 0 ? routeFiles.length - missing.length : screens.length;
|
|
847
846
|
const percentage = total > 0 ? Math.round(covered / total * 100) : 100;
|
|
848
847
|
const byOwner = {};
|
|
849
848
|
for (const screen of screens) {
|
|
@@ -3042,25 +3041,26 @@ function parseVueRouterConfig(filePath, preloadedContent) {
|
|
|
3042
3041
|
throw new Error(`Failed to parse routes file "${absolutePath}": ${message}`);
|
|
3043
3042
|
}
|
|
3044
3043
|
const routes = [];
|
|
3044
|
+
const importMap = buildImportMap(ast.program.body, routesFileDir);
|
|
3045
3045
|
for (const node of ast.program.body) {
|
|
3046
3046
|
if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") {
|
|
3047
3047
|
for (const decl of node.declaration.declarations) if (decl.id.type === "Identifier" && decl.id.name === "routes" && decl.init?.type === "ArrayExpression") {
|
|
3048
|
-
const parsed = parseRoutesArray(decl.init, routesFileDir, warnings);
|
|
3048
|
+
const parsed = parseRoutesArray(decl.init, routesFileDir, warnings, importMap);
|
|
3049
3049
|
routes.push(...parsed);
|
|
3050
3050
|
}
|
|
3051
3051
|
}
|
|
3052
3052
|
if (node.type === "VariableDeclaration") {
|
|
3053
3053
|
for (const decl of node.declarations) if (decl.id.type === "Identifier" && decl.id.name === "routes" && decl.init?.type === "ArrayExpression") {
|
|
3054
|
-
const parsed = parseRoutesArray(decl.init, routesFileDir, warnings);
|
|
3054
|
+
const parsed = parseRoutesArray(decl.init, routesFileDir, warnings, importMap);
|
|
3055
3055
|
routes.push(...parsed);
|
|
3056
3056
|
}
|
|
3057
3057
|
}
|
|
3058
3058
|
if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "ArrayExpression") {
|
|
3059
|
-
const parsed = parseRoutesArray(node.declaration, routesFileDir, warnings);
|
|
3059
|
+
const parsed = parseRoutesArray(node.declaration, routesFileDir, warnings, importMap);
|
|
3060
3060
|
routes.push(...parsed);
|
|
3061
3061
|
}
|
|
3062
3062
|
if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "TSSatisfiesExpression" && node.declaration.expression.type === "ArrayExpression") {
|
|
3063
|
-
const parsed = parseRoutesArray(node.declaration.expression, routesFileDir, warnings);
|
|
3063
|
+
const parsed = parseRoutesArray(node.declaration.expression, routesFileDir, warnings, importMap);
|
|
3064
3064
|
routes.push(...parsed);
|
|
3065
3065
|
}
|
|
3066
3066
|
}
|
|
@@ -3071,9 +3071,25 @@ function parseVueRouterConfig(filePath, preloadedContent) {
|
|
|
3071
3071
|
};
|
|
3072
3072
|
}
|
|
3073
3073
|
/**
|
|
3074
|
+
* Build a map of imported identifiers to their file paths
|
|
3075
|
+
*/
|
|
3076
|
+
function buildImportMap(body, baseDir) {
|
|
3077
|
+
const importMap = /* @__PURE__ */ new Map();
|
|
3078
|
+
for (const node of body) {
|
|
3079
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
3080
|
+
const source = node.source.value;
|
|
3081
|
+
if (typeof source !== "string") continue;
|
|
3082
|
+
const resolvedPath = resolveImportPath(source, baseDir);
|
|
3083
|
+
if (!resolvedPath) continue;
|
|
3084
|
+
for (const specifier of node.specifiers) if (specifier.type === "ImportDefaultSpecifier") importMap.set(specifier.local.name, resolvedPath);
|
|
3085
|
+
else if (specifier.type === "ImportSpecifier") importMap.set(specifier.local.name, resolvedPath);
|
|
3086
|
+
}
|
|
3087
|
+
return importMap;
|
|
3088
|
+
}
|
|
3089
|
+
/**
|
|
3074
3090
|
* Parse an array expression containing route objects
|
|
3075
3091
|
*/
|
|
3076
|
-
function parseRoutesArray(arrayNode, baseDir, warnings) {
|
|
3092
|
+
function parseRoutesArray(arrayNode, baseDir, warnings, importMap) {
|
|
3077
3093
|
const routes = [];
|
|
3078
3094
|
for (const element of arrayNode.elements) {
|
|
3079
3095
|
if (!element) continue;
|
|
@@ -3083,7 +3099,7 @@ function parseRoutesArray(arrayNode, baseDir, warnings) {
|
|
|
3083
3099
|
continue;
|
|
3084
3100
|
}
|
|
3085
3101
|
if (element.type === "ObjectExpression") {
|
|
3086
|
-
const route = parseRouteObject(element, baseDir, warnings);
|
|
3102
|
+
const route = parseRouteObject(element, baseDir, warnings, importMap);
|
|
3087
3103
|
if (route) routes.push(route);
|
|
3088
3104
|
}
|
|
3089
3105
|
}
|
|
@@ -3092,14 +3108,18 @@ function parseRoutesArray(arrayNode, baseDir, warnings) {
|
|
|
3092
3108
|
/**
|
|
3093
3109
|
* Parse a single route object expression
|
|
3094
3110
|
*/
|
|
3095
|
-
function parseRouteObject(objectNode, baseDir, warnings) {
|
|
3096
|
-
const route = {
|
|
3111
|
+
function parseRouteObject(objectNode, baseDir, warnings, importMap) {
|
|
3112
|
+
const route = {};
|
|
3113
|
+
let hasPath = false;
|
|
3097
3114
|
for (const prop of objectNode.properties) {
|
|
3098
3115
|
if (prop.type !== "ObjectProperty") continue;
|
|
3099
3116
|
if (prop.key.type !== "Identifier") continue;
|
|
3100
3117
|
switch (prop.key.name) {
|
|
3101
3118
|
case "path":
|
|
3102
|
-
if (prop.value.type === "StringLiteral")
|
|
3119
|
+
if (prop.value.type === "StringLiteral") {
|
|
3120
|
+
route.path = prop.value.value;
|
|
3121
|
+
hasPath = true;
|
|
3122
|
+
}
|
|
3103
3123
|
break;
|
|
3104
3124
|
case "name":
|
|
3105
3125
|
if (prop.value.type === "StringLiteral") route.name = prop.value.value;
|
|
@@ -3108,21 +3128,21 @@ function parseRouteObject(objectNode, baseDir, warnings) {
|
|
|
3108
3128
|
if (prop.value.type === "StringLiteral") route.redirect = prop.value.value;
|
|
3109
3129
|
break;
|
|
3110
3130
|
case "component":
|
|
3111
|
-
route.component = extractComponentPath(prop.value, baseDir);
|
|
3131
|
+
route.component = extractComponentPath(prop.value, baseDir, importMap);
|
|
3112
3132
|
break;
|
|
3113
3133
|
case "children":
|
|
3114
|
-
if (prop.value.type === "ArrayExpression") route.children = parseRoutesArray(prop.value, baseDir, warnings);
|
|
3134
|
+
if (prop.value.type === "ArrayExpression") route.children = parseRoutesArray(prop.value, baseDir, warnings, importMap);
|
|
3115
3135
|
break;
|
|
3116
3136
|
}
|
|
3117
3137
|
}
|
|
3118
|
-
if (!
|
|
3138
|
+
if (!hasPath) return null;
|
|
3119
3139
|
return route;
|
|
3120
3140
|
}
|
|
3121
3141
|
/**
|
|
3122
3142
|
* Extract component path from various component definitions
|
|
3123
3143
|
*/
|
|
3124
|
-
function extractComponentPath(node, baseDir) {
|
|
3125
|
-
if (node.type === "Identifier") return;
|
|
3144
|
+
function extractComponentPath(node, baseDir, importMap) {
|
|
3145
|
+
if (node.type === "Identifier") return importMap.get(node.name);
|
|
3126
3146
|
if (node.type === "ArrowFunctionExpression") {
|
|
3127
3147
|
const body = node.body;
|
|
3128
3148
|
if (body.type === "CallExpression" && body.callee.type === "Import") {
|
|
@@ -3305,6 +3325,83 @@ function walkTemplateNodes(nodes, callback) {
|
|
|
3305
3325
|
|
|
3306
3326
|
//#endregion
|
|
3307
3327
|
//#region src/commands/generate.ts
|
|
3328
|
+
/**
|
|
3329
|
+
* Common paths where Vue Router configuration files are typically located
|
|
3330
|
+
*/
|
|
3331
|
+
const VUE_ROUTER_CONFIG_PATHS = [
|
|
3332
|
+
"src/router/routes.ts",
|
|
3333
|
+
"src/router/index.ts",
|
|
3334
|
+
"src/router.ts",
|
|
3335
|
+
"src/routes.ts",
|
|
3336
|
+
"router/routes.ts",
|
|
3337
|
+
"router/index.ts"
|
|
3338
|
+
];
|
|
3339
|
+
/**
|
|
3340
|
+
* Default patterns to exclude from screen.meta.ts generation.
|
|
3341
|
+
* These directories typically contain reusable components, not navigable screens.
|
|
3342
|
+
* @see https://github.com/wadakatu/screenbook/issues/170
|
|
3343
|
+
*/
|
|
3344
|
+
const DEFAULT_EXCLUDE_PATTERNS = [
|
|
3345
|
+
"**/components/**",
|
|
3346
|
+
"**/shared/**",
|
|
3347
|
+
"**/utils/**",
|
|
3348
|
+
"**/hooks/**",
|
|
3349
|
+
"**/composables/**",
|
|
3350
|
+
"**/stores/**",
|
|
3351
|
+
"**/services/**",
|
|
3352
|
+
"**/helpers/**",
|
|
3353
|
+
"**/lib/**",
|
|
3354
|
+
"**/common/**"
|
|
3355
|
+
];
|
|
3356
|
+
/**
|
|
3357
|
+
* Check if a path matches any of the exclude patterns
|
|
3358
|
+
*/
|
|
3359
|
+
function matchesExcludePattern(filePath, excludePatterns) {
|
|
3360
|
+
for (const pattern of excludePatterns) {
|
|
3361
|
+
const cleanPattern = pattern.replace(/\*\*/g, "").replace(/\*/g, "").replace(/^\//, "").replace(/\/$/, "");
|
|
3362
|
+
if (cleanPattern && filePath.includes(`/${cleanPattern}/`)) return true;
|
|
3363
|
+
if (cleanPattern && filePath.startsWith(`${cleanPattern}/`)) return true;
|
|
3364
|
+
}
|
|
3365
|
+
return false;
|
|
3366
|
+
}
|
|
3367
|
+
/**
|
|
3368
|
+
* Detect Vue Router configuration file in the project
|
|
3369
|
+
*/
|
|
3370
|
+
function detectVueRouterConfigFile(cwd) {
|
|
3371
|
+
for (const configPath of VUE_ROUTER_CONFIG_PATHS) {
|
|
3372
|
+
const absolutePath = join(cwd, configPath);
|
|
3373
|
+
if (existsSync(absolutePath)) return absolutePath;
|
|
3374
|
+
}
|
|
3375
|
+
return null;
|
|
3376
|
+
}
|
|
3377
|
+
/**
|
|
3378
|
+
* Build a map from component file paths to their route information.
|
|
3379
|
+
* Normalizes paths for reliable matching.
|
|
3380
|
+
*/
|
|
3381
|
+
function buildRouteComponentMap(flatRoutes, cwd) {
|
|
3382
|
+
const map = /* @__PURE__ */ new Map();
|
|
3383
|
+
for (const route of flatRoutes) if (route.componentPath) {
|
|
3384
|
+
const componentDir = dirname(relative(cwd, route.componentPath));
|
|
3385
|
+
const componentNameWithoutExt = basename(route.componentPath).replace(/\.[^.]+$/, "");
|
|
3386
|
+
map.set(componentDir, route);
|
|
3387
|
+
map.set(basename(componentDir), route);
|
|
3388
|
+
map.set(componentNameWithoutExt, route);
|
|
3389
|
+
}
|
|
3390
|
+
return map;
|
|
3391
|
+
}
|
|
3392
|
+
/**
|
|
3393
|
+
* Find matching route from the component map for a given route file
|
|
3394
|
+
*/
|
|
3395
|
+
function findMatchingRoute(routeFile, routeComponentMap) {
|
|
3396
|
+
const routeDir = dirname(routeFile);
|
|
3397
|
+
const routeDirName = basename(routeDir);
|
|
3398
|
+
if (routeComponentMap.has(routeDir)) return routeComponentMap.get(routeDir) ?? null;
|
|
3399
|
+
if (routeComponentMap.has(routeDirName)) return routeComponentMap.get(routeDirName) ?? null;
|
|
3400
|
+
for (const [, route] of routeComponentMap) if (route.componentPath) {
|
|
3401
|
+
if (basename(dirname(route.componentPath)) === routeDirName) return route;
|
|
3402
|
+
}
|
|
3403
|
+
return null;
|
|
3404
|
+
}
|
|
3308
3405
|
const generateCommand = define({
|
|
3309
3406
|
name: "generate",
|
|
3310
3407
|
description: "Auto-generate screen.meta.ts files from route files",
|
|
@@ -3442,6 +3539,10 @@ async function generateFromRoutesFile(routesFile, cwd, options) {
|
|
|
3442
3539
|
for (const route of flatRoutes) {
|
|
3443
3540
|
const metaPath = determineMetaPath(route, cwd);
|
|
3444
3541
|
const absoluteMetaPath = resolve(cwd, metaPath);
|
|
3542
|
+
if (matchesExcludePattern(metaPath, DEFAULT_EXCLUDE_PATTERNS)) {
|
|
3543
|
+
skipped++;
|
|
3544
|
+
continue;
|
|
3545
|
+
}
|
|
3445
3546
|
if (!force && existsSync(absoluteMetaPath)) {
|
|
3446
3547
|
if (!interactive) {
|
|
3447
3548
|
skipped++;
|
|
@@ -3536,13 +3637,29 @@ async function generateFromRoutesPattern(routesPattern, cwd, options) {
|
|
|
3536
3637
|
logger.blank();
|
|
3537
3638
|
const routeFiles = await glob(routesPattern, {
|
|
3538
3639
|
cwd,
|
|
3539
|
-
ignore
|
|
3640
|
+
ignore: [...ignore, ...DEFAULT_EXCLUDE_PATTERNS]
|
|
3540
3641
|
});
|
|
3541
3642
|
if (routeFiles.length === 0) {
|
|
3542
3643
|
logger.warn(`No route files found matching: ${routesPattern}`);
|
|
3543
3644
|
return;
|
|
3544
3645
|
}
|
|
3545
3646
|
logger.log(`Found ${routeFiles.length} route files`);
|
|
3647
|
+
let routeComponentMap = null;
|
|
3648
|
+
if (routesPattern.includes(".vue")) {
|
|
3649
|
+
const vueRouterConfig = detectVueRouterConfigFile(cwd);
|
|
3650
|
+
if (vueRouterConfig) try {
|
|
3651
|
+
const parseResult = parseVueRouterConfig(vueRouterConfig);
|
|
3652
|
+
const flatRoutes = flattenRoutes(parseResult.routes);
|
|
3653
|
+
if (flatRoutes.length > 0) {
|
|
3654
|
+
routeComponentMap = buildRouteComponentMap(flatRoutes, cwd);
|
|
3655
|
+
logger.log(` ${logger.dim(`(using routes from ${relative(cwd, vueRouterConfig)})`)}`);
|
|
3656
|
+
}
|
|
3657
|
+
for (const warning of parseResult.warnings) logger.warn(warning);
|
|
3658
|
+
} catch (error) {
|
|
3659
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3660
|
+
logger.warn(`Could not parse Vue Router config: ${message}`);
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3546
3663
|
logger.blank();
|
|
3547
3664
|
let created = 0;
|
|
3548
3665
|
let skipped = 0;
|
|
@@ -3559,7 +3676,14 @@ async function generateFromRoutesPattern(routesPattern, cwd, options) {
|
|
|
3559
3676
|
skipped++;
|
|
3560
3677
|
continue;
|
|
3561
3678
|
}
|
|
3562
|
-
|
|
3679
|
+
let screenMeta;
|
|
3680
|
+
const matchedRoute = routeComponentMap ? findMatchingRoute(routeFile, routeComponentMap) : null;
|
|
3681
|
+
if (matchedRoute) screenMeta = {
|
|
3682
|
+
id: matchedRoute.screenId,
|
|
3683
|
+
title: matchedRoute.screenTitle,
|
|
3684
|
+
route: matchedRoute.fullPath
|
|
3685
|
+
};
|
|
3686
|
+
else screenMeta = inferScreenMeta(routeDir, routesPattern);
|
|
3563
3687
|
let detectedApis = [];
|
|
3564
3688
|
if (detectApi && apiIntegration) {
|
|
3565
3689
|
const absoluteRouteFile = join(cwd, routeFile);
|