@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 CHANGED
@@ -827,23 +827,22 @@ async function generateCoverageData(config, cwd, screens) {
827
827
  cwd,
828
828
  ignore: config.ignore
829
829
  });
830
- new Set(screens.map((s) => {
831
- const parts = s.id.split(".");
832
- return parts.slice(0, -1).join("/") || parts[0];
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 (!screens.some((s) => {
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 = { path: "" };
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") route.path = prop.value.value;
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 (!route.path) return null;
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
- const screenMeta = inferScreenMeta(routeDir, routesPattern);
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);