@screenbook/cli 1.7.1 → 1.8.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 CHANGED
@@ -51,7 +51,7 @@ const ERRORS = {
51
51
  ROUTES_PATTERN_MISSING: {
52
52
  title: "Routes configuration not found",
53
53
  suggestion: "Add routesPattern (for file-based routing) or routesFile (for config-based routing) to your screenbook.config.ts.",
54
- example: `import { defineConfig } from "@screenbook/core"
54
+ example: `import { defineConfig } from "screenbook"
55
55
 
56
56
  // Option 1: File-based routing (Next.js, Nuxt, etc.)
57
57
  export default defineConfig({
@@ -66,7 +66,7 @@ export default defineConfig({
66
66
  ROUTES_FILE_NOT_FOUND: (filePath) => ({
67
67
  title: `Routes file not found: ${filePath}`,
68
68
  suggestion: "Check the routesFile path in your screenbook.config.ts. The file should export a routes array.",
69
- example: `import { defineConfig } from "@screenbook/core"
69
+ example: `import { defineConfig } from "screenbook"
70
70
 
71
71
  export default defineConfig({
72
72
  routesFile: "src/router/routes.ts", // Make sure this file exists
@@ -80,7 +80,7 @@ export default defineConfig({
80
80
  CONFIG_NOT_FOUND: {
81
81
  title: "Configuration file not found",
82
82
  suggestion: "Run 'screenbook init' to create a screenbook.config.ts file, or create one manually.",
83
- example: `import { defineConfig } from "@screenbook/core"
83
+ example: `import { defineConfig } from "screenbook"
84
84
 
85
85
  export default defineConfig({
86
86
  metaPattern: "src/**/screen.meta.ts",
@@ -107,7 +107,7 @@ export default defineConfig({
107
107
  META_FILE_LOAD_ERROR: (filePath) => ({
108
108
  title: `Failed to load ${filePath}`,
109
109
  suggestion: "Check the file for syntax errors or missing exports. The file should export a 'screen' object.",
110
- example: `import { defineScreen } from "@screenbook/core"
110
+ example: `import { defineScreen } from "screenbook"
111
111
 
112
112
  export const screen = defineScreen({
113
113
  id: "example.screen",
@@ -194,6 +194,12 @@ function setVerbose(verbose) {
194
194
  verboseMode = verbose;
195
195
  }
196
196
  /**
197
+ * Check if verbose mode is enabled
198
+ */
199
+ function isVerbose() {
200
+ return verboseMode;
201
+ }
202
+ /**
197
203
  * Logger utility for consistent, color-coded CLI output
198
204
  */
199
205
  const logger = {
@@ -239,6 +245,25 @@ const logger = {
239
245
  }
240
246
  console.error();
241
247
  },
248
+ warnWithHelp: (options) => {
249
+ const { title, message, details, suggestions } = options;
250
+ console.log();
251
+ console.log(`${pc.yellow("⚠")} ${pc.yellow(`Warning: ${title}`)}`);
252
+ if (message) {
253
+ console.log();
254
+ console.log(` ${message}`);
255
+ }
256
+ if (details && details.length > 0) {
257
+ console.log(" This means:");
258
+ for (const detail of details) console.log(` ${pc.dim("•")} ${detail}`);
259
+ }
260
+ if (suggestions && suggestions.length > 0) {
261
+ console.log();
262
+ console.log(` ${pc.cyan("To fix this, either:")}`);
263
+ for (let i = 0; i < suggestions.length; i++) console.log(` ${i + 1}. ${suggestions[i]}`);
264
+ }
265
+ console.log();
266
+ },
242
267
  step: (msg) => {
243
268
  console.log(`${pc.dim("→")} ${msg}`);
244
269
  },
@@ -1283,6 +1308,22 @@ function displayResults(results, verbose) {
1283
1308
  //#endregion
1284
1309
  //#region src/utils/routeParserUtils.ts
1285
1310
  /**
1311
+ * Create a spread warning from an AST element.
1312
+ * Extracts line number and variable name for better error messages.
1313
+ * @param element - AST element (SpreadElement)
1314
+ * @returns SpreadWarning object
1315
+ */
1316
+ function createSpreadWarning(element) {
1317
+ const line = element.loc?.start.line;
1318
+ const variableName = element.argument?.type === "Identifier" ? element.argument.name : void 0;
1319
+ return {
1320
+ type: "spread",
1321
+ message: `Spread operator detected${line ? ` at line ${line}` : ""}`,
1322
+ line,
1323
+ variableName
1324
+ };
1325
+ }
1326
+ /**
1286
1327
  * Resolve relative import path to absolute path
1287
1328
  */
1288
1329
  function resolveImportPath(importPath, baseDir) {
@@ -1292,7 +1333,7 @@ function resolveImportPath(importPath, baseDir) {
1292
1333
  /**
1293
1334
  * Flatten nested routes into a flat list with computed properties
1294
1335
  */
1295
- function flattenRoutes(routes, parentPath = "", depth = 0) {
1336
+ function flattenRoutes(routes, parentPath = "", depth = 0, options = {}) {
1296
1337
  const result = [];
1297
1338
  for (const route of routes) {
1298
1339
  if (route.redirect && !route.component) continue;
@@ -1303,32 +1344,104 @@ function flattenRoutes(routes, parentPath = "", depth = 0) {
1303
1344
  fullPath = fullPath.replace(/\/+/g, "/");
1304
1345
  if (fullPath !== "/" && fullPath.endsWith("/")) fullPath = fullPath.slice(0, -1);
1305
1346
  if (fullPath === "") fullPath = parentPath || "/";
1306
- if (route.component || !route.children) result.push({
1307
- fullPath,
1308
- name: route.name,
1309
- componentPath: route.component,
1310
- screenId: pathToScreenId(fullPath),
1311
- screenTitle: pathToScreenTitle(fullPath),
1312
- depth
1313
- });
1314
- if (route.children) result.push(...flattenRoutes(route.children, fullPath, depth + 1));
1347
+ if (route.component || !route.children) {
1348
+ const { screenId, suggestions } = pathToScreenId(fullPath, options);
1349
+ result.push({
1350
+ fullPath,
1351
+ name: route.name,
1352
+ componentPath: route.component,
1353
+ screenId,
1354
+ screenTitle: pathToScreenTitle(fullPath),
1355
+ depth,
1356
+ suggestions
1357
+ });
1358
+ }
1359
+ if (route.children) result.push(...flattenRoutes(route.children, fullPath, depth + 1, options));
1315
1360
  }
1316
1361
  return result;
1317
1362
  }
1318
1363
  /**
1319
- * Convert route path to screen ID
1320
- * /user/:id/profile -> user.id.profile
1364
+ * Action segments that provide semantic context.
1365
+ * When a parameter is followed by one of these, the parameter is preserved.
1366
+ */
1367
+ const ACTION_SEGMENTS = new Set([
1368
+ "edit",
1369
+ "new",
1370
+ "create",
1371
+ "delete",
1372
+ "settings",
1373
+ "view",
1374
+ "update"
1375
+ ]);
1376
+ /**
1377
+ * Check if a segment is an action segment
1378
+ */
1379
+ function isActionSegment(segment) {
1380
+ return segment !== void 0 && ACTION_SEGMENTS.has(segment.toLowerCase());
1381
+ }
1382
+ /**
1383
+ * Infer semantic alternatives for a parameter
1321
1384
  */
1322
- function pathToScreenId(path) {
1323
- if (path === "/" || path === "") return "home";
1324
- return path.replace(/^\//, "").replace(/\/$/, "").split("/").map((segment) => {
1325
- if (segment.startsWith(":")) return segment.slice(1);
1326
- if (segment.startsWith("*")) {
1327
- if (segment === "**") return "catchall";
1328
- return segment.slice(1) || "catchall";
1385
+ function inferSemanticAlternatives(param, isLast) {
1386
+ const alternatives = [];
1387
+ if (isLast) alternatives.push("detail", "view");
1388
+ const entityMatch = param.match(/^:(\w+)Id$/i);
1389
+ if (entityMatch?.[1]) alternatives.push(entityMatch[1].toLowerCase());
1390
+ return alternatives;
1391
+ }
1392
+ /**
1393
+ * Resolve a parameter segment to a screen ID segment
1394
+ */
1395
+ function resolveParameter(segment, isLastSegment, nextSegment, options) {
1396
+ const mappingKey = segment;
1397
+ if (options.parameterMapping?.[mappingKey]) return { resolved: options.parameterMapping[mappingKey] };
1398
+ if (options.smartParameterNaming) {
1399
+ if (segment === ":id" && isLastSegment) return { resolved: "detail" };
1400
+ const entityMatch = segment.match(/^:(\w+)Id$/i);
1401
+ if (entityMatch?.[1] && isLastSegment) return { resolved: entityMatch[1].toLowerCase() };
1402
+ if (!isLastSegment && isActionSegment(nextSegment)) return { resolved: segment.slice(1) };
1403
+ if (segment === ":id" && !isActionSegment(nextSegment)) return { resolved: "detail" };
1404
+ }
1405
+ const cleanParam = segment.slice(1);
1406
+ switch (options.unmappedParameterStrategy) {
1407
+ case "detail": return { resolved: "detail" };
1408
+ case "warn": {
1409
+ const alternatives = inferSemanticAlternatives(segment, isLastSegment);
1410
+ return {
1411
+ resolved: cleanParam,
1412
+ suggestion: alternatives.length > 0 ? `Consider renaming to: ${alternatives.join(", ")}` : void 0
1413
+ };
1329
1414
  }
1330
- return segment;
1331
- }).join(".");
1415
+ default: return { resolved: cleanParam };
1416
+ }
1417
+ }
1418
+ /**
1419
+ * Convert route path to screen ID with options
1420
+ * /user/:id/profile -> user.id.profile (default)
1421
+ * /user/:id/profile -> user.detail.profile (with smartParameterNaming)
1422
+ */
1423
+ function pathToScreenId(path, options = {}) {
1424
+ if (path === "/" || path === "") return { screenId: "home" };
1425
+ const segments = path.replace(/^\//, "").replace(/\/$/, "").split("/");
1426
+ const resolvedSegments = [];
1427
+ const allSuggestions = [];
1428
+ for (let i = 0; i < segments.length; i++) {
1429
+ const segment = segments[i];
1430
+ if (!segment) continue;
1431
+ const isLast = i === segments.length - 1;
1432
+ const nextSegment = segments[i + 1];
1433
+ if (segment.startsWith(":")) {
1434
+ const { resolved, suggestion } = resolveParameter(segment, isLast, nextSegment, options);
1435
+ resolvedSegments.push(resolved);
1436
+ if (suggestion) allSuggestions.push(suggestion);
1437
+ } else if (segment.startsWith("*")) if (segment === "**") resolvedSegments.push("catchall");
1438
+ else resolvedSegments.push(segment.slice(1) || "catchall");
1439
+ else resolvedSegments.push(segment);
1440
+ }
1441
+ return {
1442
+ screenId: resolvedSegments.join("."),
1443
+ suggestions: allSuggestions.length > 0 ? allSuggestions : void 0
1444
+ };
1332
1445
  }
1333
1446
  /**
1334
1447
  * Convert route path to screen title
@@ -1426,7 +1539,10 @@ function parseAngularRouterConfig(filePath, preloadedContent) {
1426
1539
  routes.push(...routesFromExpr);
1427
1540
  }
1428
1541
  }
1429
- if (routes.length === 0) warnings.push("No routes found. Supported patterns: 'export const routes: Routes = [...]', 'RouterModule.forRoot([...])', or 'RouterModule.forChild([...])'");
1542
+ if (routes.length === 0) warnings.push({
1543
+ type: "general",
1544
+ message: "No routes found. Supported patterns: 'export const routes: Routes = [...]', 'RouterModule.forRoot([...])', or 'RouterModule.forChild([...])'"
1545
+ });
1430
1546
  return {
1431
1547
  routes,
1432
1548
  warnings
@@ -1475,16 +1591,19 @@ function parseRoutesArray$3(arrayNode, baseDir, warnings) {
1475
1591
  for (const element of arrayNode.elements) {
1476
1592
  if (!element) continue;
1477
1593
  if (element.type === "SpreadElement") {
1478
- const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
1479
- warnings.push(`Spread operator detected${loc}. Routes from spread cannot be statically analyzed.`);
1594
+ warnings.push(createSpreadWarning(element));
1480
1595
  continue;
1481
1596
  }
1482
1597
  if (element.type === "ObjectExpression") {
1483
1598
  const parsedRoute = parseRouteObject$3(element, baseDir, warnings);
1484
1599
  if (parsedRoute) routes.push(parsedRoute);
1485
1600
  } else {
1486
- const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
1487
- warnings.push(`Non-object route element (${element.type})${loc}. Only object literals can be statically analyzed.`);
1601
+ const line = element.loc?.start.line;
1602
+ warnings.push({
1603
+ type: "general",
1604
+ message: `Non-object route element (${element.type})${line ? ` at line ${line}` : ""}. Only object literals can be statically analyzed.`,
1605
+ line
1606
+ });
1488
1607
  }
1489
1608
  }
1490
1609
  return routes;
@@ -1507,8 +1626,12 @@ function parseRouteObject$3(objectNode, baseDir, warnings) {
1507
1626
  path = prop.value.value;
1508
1627
  hasPath = true;
1509
1628
  } else {
1510
- const loc = prop.loc ? ` at line ${prop.loc.start.line}` : "";
1511
- warnings.push(`Dynamic path value (${prop.value.type})${loc}. Only string literal paths can be statically analyzed.`);
1629
+ const line = prop.loc?.start.line;
1630
+ warnings.push({
1631
+ type: "general",
1632
+ message: `Dynamic path value (${prop.value.type})${line ? ` at line ${line}` : ""}. Only string literal paths can be statically analyzed.`,
1633
+ line
1634
+ });
1512
1635
  }
1513
1636
  break;
1514
1637
  case "component":
@@ -1571,20 +1694,32 @@ function extractLazyComponent(node, baseDir, warnings) {
1571
1694
  if (thenArg?.type === "ArrowFunctionExpression" && thenArg.body?.type === "MemberExpression" && thenArg.body.property?.type === "Identifier") return `${importPath}#${thenArg.body.property.name}`;
1572
1695
  return importPath;
1573
1696
  }
1574
- const loc$1 = node.loc ? ` at line ${node.loc.start.line}` : "";
1575
- warnings.push(`Lazy loadComponent with dynamic path${loc$1}. Only string literal imports can be analyzed.`);
1697
+ const line$1 = node.loc?.start.line;
1698
+ warnings.push({
1699
+ type: "general",
1700
+ message: `Lazy loadComponent with dynamic path${line$1 ? ` at line ${line$1}` : ""}. Only string literal imports can be analyzed.`,
1701
+ line: line$1
1702
+ });
1576
1703
  return;
1577
1704
  }
1578
1705
  }
1579
1706
  if (body.type === "CallExpression" && body.callee?.type === "Import") {
1580
1707
  if (body.arguments[0]?.type === "StringLiteral") return resolveImportPath(body.arguments[0].value, baseDir);
1581
- const loc$1 = node.loc ? ` at line ${node.loc.start.line}` : "";
1582
- warnings.push(`Lazy loadComponent with dynamic path${loc$1}. Only string literal imports can be analyzed.`);
1708
+ const line$1 = node.loc?.start.line;
1709
+ warnings.push({
1710
+ type: "general",
1711
+ message: `Lazy loadComponent with dynamic path${line$1 ? ` at line ${line$1}` : ""}. Only string literal imports can be analyzed.`,
1712
+ line: line$1
1713
+ });
1583
1714
  return;
1584
1715
  }
1585
1716
  }
1586
- const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
1587
- warnings.push(`Unrecognized loadComponent pattern (${node.type})${loc}. Expected arrow function with import().then().`);
1717
+ const line = node.loc?.start.line;
1718
+ warnings.push({
1719
+ type: "general",
1720
+ message: `Unrecognized loadComponent pattern (${node.type})${line ? ` at line ${line}` : ""}. Expected arrow function with import().then().`,
1721
+ line
1722
+ });
1588
1723
  }
1589
1724
  /**
1590
1725
  * Extract path from lazy loadChildren pattern
@@ -1601,8 +1736,12 @@ function extractLazyPath(node, baseDir, warnings) {
1601
1736
  if (body.arguments[0]?.type === "StringLiteral") return resolveImportPath(body.arguments[0].value, baseDir);
1602
1737
  }
1603
1738
  }
1604
- const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
1605
- warnings.push(`Unrecognized loadChildren pattern (${node.type})${loc}. Expected arrow function with import().`);
1739
+ const line = node.loc?.start.line;
1740
+ warnings.push({
1741
+ type: "general",
1742
+ message: `Unrecognized loadChildren pattern (${node.type})${line ? ` at line ${line}` : ""}. Expected arrow function with import().`,
1743
+ line
1744
+ });
1606
1745
  }
1607
1746
  /**
1608
1747
  * Detect if content is Angular Router based on patterns.
@@ -1625,7 +1764,7 @@ function createDetectedNavigation(path, type, line) {
1625
1764
  const pathWithoutQuery = path.split("?")[0] ?? path;
1626
1765
  return {
1627
1766
  path,
1628
- screenId: pathToScreenId(pathWithoutQuery.split("#")[0] ?? pathWithoutQuery),
1767
+ screenId: pathToScreenId(pathWithoutQuery.split("#")[0] ?? pathWithoutQuery).screenId,
1629
1768
  type,
1630
1769
  line
1631
1770
  };
@@ -2220,6 +2359,133 @@ function isMatchingPackage(source, clientPackages) {
2220
2359
  return false;
2221
2360
  }
2222
2361
 
2362
+ //#endregion
2363
+ //#region src/utils/constants.ts
2364
+ /**
2365
+ * Default patterns to exclude from route file scanning.
2366
+ * These directories typically contain reusable components, not navigable screens.
2367
+ * @see https://github.com/wadakatu/screenbook/issues/170
2368
+ * @see https://github.com/wadakatu/screenbook/issues/190
2369
+ */
2370
+ const DEFAULT_EXCLUDE_PATTERNS = [
2371
+ "**/components/**",
2372
+ "**/shared/**",
2373
+ "**/utils/**",
2374
+ "**/hooks/**",
2375
+ "**/composables/**",
2376
+ "**/stores/**",
2377
+ "**/services/**",
2378
+ "**/helpers/**",
2379
+ "**/lib/**",
2380
+ "**/common/**"
2381
+ ];
2382
+ /**
2383
+ * Check if a path matches any of the exclude patterns.
2384
+ * Used by both generate and lint commands for consistent behavior.
2385
+ */
2386
+ function matchesExcludePattern(filePath, excludePatterns) {
2387
+ for (const pattern of excludePatterns) {
2388
+ const cleanPattern = pattern.replace(/\*\*/g, "").replace(/\*/g, "").replace(/^\//, "").replace(/\/$/, "");
2389
+ if (cleanPattern && filePath.includes(`/${cleanPattern}/`)) return true;
2390
+ if (cleanPattern && filePath.startsWith(`${cleanPattern}/`)) return true;
2391
+ }
2392
+ return false;
2393
+ }
2394
+
2395
+ //#endregion
2396
+ //#region src/utils/displayWarnings.ts
2397
+ /**
2398
+ * Display structured warnings with appropriate formatting
2399
+ * @param warnings - Array of parsed warnings
2400
+ * @param options - Display options
2401
+ * @returns Result containing warning counts and whether any were displayed
2402
+ */
2403
+ function displayWarnings(warnings, options = {}) {
2404
+ const { spreadOperatorSetting = "warn" } = options;
2405
+ let spreadCount = 0;
2406
+ let generalCount = 0;
2407
+ for (const warning of warnings) if (warning.type === "spread") {
2408
+ if (spreadOperatorSetting === "off") continue;
2409
+ spreadCount++;
2410
+ const varPart = warning.variableName ? `'${warning.variableName}'` : "spread variable";
2411
+ if (spreadOperatorSetting === "error") {
2412
+ logger.error(warning.message);
2413
+ logger.blank();
2414
+ logger.log(" Routes from spread operators cannot be statically analyzed by screenbook.");
2415
+ logger.log(" This means:");
2416
+ logger.log(` ${logger.dim("•")} screenbook won't detect missing screen.meta.ts for routes in ${varPart}`);
2417
+ logger.log(` ${logger.dim("•")} These routes may appear to pass lint even without screen.meta.ts files`);
2418
+ logger.blank();
2419
+ } else logger.warnWithHelp({
2420
+ title: warning.message,
2421
+ message: "Routes from spread operators cannot be statically analyzed by screenbook.",
2422
+ details: [`screenbook won't detect missing screen.meta.ts for routes in ${varPart}`, "These routes may appear to pass lint even without screen.meta.ts files"],
2423
+ suggestions: [
2424
+ "Inline the routes directly in the main routes array",
2425
+ "Manually ensure screen.meta.ts files exist for the spread routes",
2426
+ "Ignore this warning if you're OK with manual coverage tracking"
2427
+ ]
2428
+ });
2429
+ } else {
2430
+ generalCount++;
2431
+ logger.warn(warning.message);
2432
+ }
2433
+ return {
2434
+ hasWarnings: spreadCount > 0 || generalCount > 0,
2435
+ spreadCount,
2436
+ generalCount,
2437
+ shouldFailLint: spreadOperatorSetting === "error" && spreadCount > 0
2438
+ };
2439
+ }
2440
+ /**
2441
+ * Display warnings for generate command (slightly different messaging)
2442
+ * @param warnings - Array of parsed warnings
2443
+ * @param options - Display options
2444
+ * @returns Result containing warning counts and whether any were displayed
2445
+ */
2446
+ function displayGenerateWarnings(warnings, options = {}) {
2447
+ const { spreadOperatorSetting = "warn" } = options;
2448
+ let spreadCount = 0;
2449
+ let generalCount = 0;
2450
+ let suppressedCount = 0;
2451
+ for (const warning of warnings) if (warning.type === "spread") {
2452
+ if (spreadOperatorSetting === "off") {
2453
+ suppressedCount++;
2454
+ continue;
2455
+ }
2456
+ spreadCount++;
2457
+ const varPart = warning.variableName ? `'${warning.variableName}'` : "spread variable";
2458
+ if (spreadOperatorSetting === "error") {
2459
+ logger.error(warning.message);
2460
+ logger.blank();
2461
+ logger.log(" Routes from spread operators cannot be statically analyzed by screenbook.");
2462
+ logger.log(" This means:");
2463
+ logger.log(` ${logger.dim("•")} screenbook won't auto-generate screen.meta.ts for routes in ${varPart}`);
2464
+ logger.log(` ${logger.dim("•")} You'll need to manually create screen.meta.ts files for these routes`);
2465
+ logger.blank();
2466
+ } else logger.warnWithHelp({
2467
+ title: warning.message,
2468
+ message: "Routes from spread operators cannot be statically analyzed by screenbook.",
2469
+ details: [`screenbook won't auto-generate screen.meta.ts for routes in ${varPart}`, "You'll need to manually create screen.meta.ts files for these routes"],
2470
+ suggestions: [
2471
+ "Inline the routes directly in the main routes array",
2472
+ "Manually create screen.meta.ts files for the spread routes",
2473
+ "Ignore this warning if you're OK with manual screen metadata"
2474
+ ]
2475
+ });
2476
+ } else {
2477
+ generalCount++;
2478
+ logger.warn(warning.message);
2479
+ }
2480
+ if (suppressedCount > 0) logger.log(logger.dim(` (${suppressedCount} spread operator warning(s) suppressed by configuration)`));
2481
+ return {
2482
+ hasWarnings: spreadCount > 0 || generalCount > 0,
2483
+ spreadCount,
2484
+ generalCount,
2485
+ shouldFailLint: spreadOperatorSetting === "error" && spreadCount > 0
2486
+ };
2487
+ }
2488
+
2223
2489
  //#endregion
2224
2490
  //#region src/utils/solidRouterParser.ts
2225
2491
  /**
@@ -2278,7 +2544,10 @@ function parseSolidRouterConfig(filePath, preloadedContent) {
2278
2544
  routes.push(...parsed);
2279
2545
  }
2280
2546
  }
2281
- if (routes.length === 0) warnings.push("No routes found. Supported patterns: 'export const routes = [...]' or 'export default [...]'");
2547
+ if (routes.length === 0) warnings.push({
2548
+ type: "general",
2549
+ message: "No routes found. Supported patterns: 'export const routes = [...]' or 'export default [...]'"
2550
+ });
2282
2551
  return {
2283
2552
  routes,
2284
2553
  warnings
@@ -2292,16 +2561,19 @@ function parseRoutesArray$2(arrayNode, baseDir, warnings) {
2292
2561
  for (const element of arrayNode.elements) {
2293
2562
  if (!element) continue;
2294
2563
  if (element.type === "SpreadElement") {
2295
- const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
2296
- warnings.push(`Spread operator detected${loc}. Routes from spread cannot be statically analyzed.`);
2564
+ warnings.push(createSpreadWarning(element));
2297
2565
  continue;
2298
2566
  }
2299
2567
  if (element.type === "ObjectExpression") {
2300
2568
  const parsedRoutes = parseRouteObject$2(element, baseDir, warnings);
2301
2569
  routes.push(...parsedRoutes);
2302
2570
  } else {
2303
- const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
2304
- warnings.push(`Non-object route element (${element.type})${loc}. Only object literals can be statically analyzed.`);
2571
+ const line = element.loc?.start.line;
2572
+ warnings.push({
2573
+ type: "general",
2574
+ message: `Non-object route element (${element.type})${line ? ` at line ${line}` : ""}. Only object literals can be statically analyzed.`,
2575
+ line
2576
+ });
2305
2577
  }
2306
2578
  }
2307
2579
  return routes;
@@ -2328,12 +2600,20 @@ function parseRouteObject$2(objectNode, baseDir, warnings) {
2328
2600
  paths = extractPathArray(prop.value, warnings);
2329
2601
  hasPath = paths.length > 0;
2330
2602
  if (arrayElementCount > 0 && paths.length === 0) {
2331
- const loc = prop.loc ? ` at line ${prop.loc.start.line}` : "";
2332
- warnings.push(`Path array contains only dynamic values${loc}. No static paths could be extracted.`);
2603
+ const line = prop.loc?.start.line;
2604
+ warnings.push({
2605
+ type: "general",
2606
+ message: `Path array contains only dynamic values${line ? ` at line ${line}` : ""}. No static paths could be extracted.`,
2607
+ line
2608
+ });
2333
2609
  }
2334
2610
  } else {
2335
- const loc = prop.loc ? ` at line ${prop.loc.start.line}` : "";
2336
- warnings.push(`Dynamic path value (${prop.value.type})${loc}. Only string literal paths can be statically analyzed.`);
2611
+ const line = prop.loc?.start.line;
2612
+ warnings.push({
2613
+ type: "general",
2614
+ message: `Dynamic path value (${prop.value.type})${line ? ` at line ${line}` : ""}. Only string literal paths can be statically analyzed.`,
2615
+ line
2616
+ });
2337
2617
  }
2338
2618
  break;
2339
2619
  case "component":
@@ -2368,8 +2648,12 @@ function extractPathArray(arrayNode, warnings) {
2368
2648
  if (!element) continue;
2369
2649
  if (element.type === "StringLiteral") paths.push(element.value);
2370
2650
  else {
2371
- const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
2372
- warnings.push(`Non-string path in array (${element.type})${loc}. Only string literal paths can be analyzed.`);
2651
+ const line = element.loc?.start.line;
2652
+ warnings.push({
2653
+ type: "general",
2654
+ message: `Non-string path in array (${element.type})${line ? ` at line ${line}` : ""}. Only string literal paths can be analyzed.`,
2655
+ line
2656
+ });
2373
2657
  }
2374
2658
  }
2375
2659
  return paths;
@@ -2386,13 +2670,21 @@ function extractComponent(node, baseDir, warnings) {
2386
2670
  if (callee.type === "Identifier" && callee.name === "lazy") {
2387
2671
  const lazyArg = node.arguments[0];
2388
2672
  if (lazyArg) return extractLazyImportPath$2(lazyArg, baseDir, warnings);
2389
- const loc$1 = node.loc ? ` at line ${node.loc.start.line}` : "";
2390
- warnings.push(`lazy() called without arguments${loc$1}. Expected arrow function with import().`);
2673
+ const line$1 = node.loc?.start.line;
2674
+ warnings.push({
2675
+ type: "general",
2676
+ message: `lazy() called without arguments${line$1 ? ` at line ${line$1}` : ""}. Expected arrow function with import().`,
2677
+ line: line$1
2678
+ });
2391
2679
  return;
2392
2680
  }
2393
- const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
2681
+ const line = node.loc?.start.line;
2394
2682
  const calleeName = callee.type === "Identifier" ? callee.name : "unknown";
2395
- warnings.push(`Unrecognized component pattern: ${calleeName}(...)${loc}. Only 'lazy(() => import(...))' is supported.`);
2683
+ warnings.push({
2684
+ type: "general",
2685
+ message: `Unrecognized component pattern: ${calleeName}(...)${line ? ` at line ${line}` : ""}. Only 'lazy(() => import(...))' is supported.`,
2686
+ line
2687
+ });
2396
2688
  return;
2397
2689
  }
2398
2690
  if (node.type === "ArrowFunctionExpression") {
@@ -2400,37 +2692,61 @@ function extractComponent(node, baseDir, warnings) {
2400
2692
  const openingElement = node.body.openingElement;
2401
2693
  if (openingElement?.name?.type === "JSXIdentifier") return openingElement.name.name;
2402
2694
  if (openingElement?.name?.type === "JSXMemberExpression") {
2403
- const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
2404
- 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.`);
2695
+ const line$1 = node.loc?.start.line;
2696
+ warnings.push({
2697
+ type: "general",
2698
+ message: `Namespaced JSX component (e.g., <UI.Button />)${line$1 ? ` at line ${line$1}` : ""}. Component extraction not supported for member expressions. Consider using a direct component reference or create a wrapper component.`,
2699
+ line: line$1
2700
+ });
2405
2701
  return;
2406
2702
  }
2407
2703
  }
2408
2704
  if (node.body.type === "BlockStatement") {
2409
- const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
2410
- warnings.push(`Arrow function with block body${loc}. Only concise arrow functions returning JSX directly can be analyzed.`);
2705
+ const line$1 = node.loc?.start.line;
2706
+ warnings.push({
2707
+ type: "general",
2708
+ message: `Arrow function with block body${line$1 ? ` at line ${line$1}` : ""}. Only concise arrow functions returning JSX directly can be analyzed.`,
2709
+ line: line$1
2710
+ });
2411
2711
  return;
2412
2712
  }
2413
2713
  if (node.body.type === "JSXFragment") {
2414
- const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
2415
- warnings.push(`JSX Fragment detected${loc}. Cannot extract component name from fragments.`);
2714
+ const line$1 = node.loc?.start.line;
2715
+ warnings.push({
2716
+ type: "general",
2717
+ message: `JSX Fragment detected${line$1 ? ` at line ${line$1}` : ""}. Cannot extract component name from fragments.`,
2718
+ line: line$1
2719
+ });
2416
2720
  return;
2417
2721
  }
2418
2722
  if (node.body.type === "ConditionalExpression") {
2419
- const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
2723
+ const line$1 = node.loc?.start.line;
2420
2724
  let componentInfo = "";
2421
2725
  const consequent = node.body.consequent;
2422
2726
  const alternate = node.body.alternate;
2423
2727
  if (consequent?.type === "JSXElement" && alternate?.type === "JSXElement") componentInfo = ` (${consequent.openingElement?.name?.name || "unknown"} or ${alternate.openingElement?.name?.name || "unknown"})`;
2424
- warnings.push(`Conditional component${componentInfo}${loc}. Only static JSX elements can be analyzed. Consider extracting to a separate component.`);
2728
+ warnings.push({
2729
+ type: "general",
2730
+ message: `Conditional component${componentInfo}${line$1 ? ` at line ${line$1}` : ""}. Only static JSX elements can be analyzed. Consider extracting to a separate component.`,
2731
+ line: line$1
2732
+ });
2425
2733
  return;
2426
2734
  }
2427
- const arrowLoc = node.loc ? ` at line ${node.loc.start.line}` : "";
2428
- warnings.push(`Unrecognized arrow function body (${node.body.type})${arrowLoc}. Component will not be extracted.`);
2735
+ const line = node.loc?.start.line;
2736
+ warnings.push({
2737
+ type: "general",
2738
+ message: `Unrecognized arrow function body (${node.body.type})${line ? ` at line ${line}` : ""}. Component will not be extracted.`,
2739
+ line
2740
+ });
2429
2741
  return;
2430
2742
  }
2431
2743
  if (node) {
2432
- const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
2433
- warnings.push(`Unrecognized component pattern (${node.type})${loc}. Component will not be extracted.`);
2744
+ const line = node.loc?.start.line;
2745
+ warnings.push({
2746
+ type: "general",
2747
+ message: `Unrecognized component pattern (${node.type})${line ? ` at line ${line}` : ""}. Component will not be extracted.`,
2748
+ line
2749
+ });
2434
2750
  }
2435
2751
  }
2436
2752
  /**
@@ -2442,13 +2758,21 @@ function extractLazyImportPath$2(node, baseDir, warnings) {
2442
2758
  const body = node.body;
2443
2759
  if (body.type === "CallExpression" && body.callee.type === "Import") {
2444
2760
  if (body.arguments[0]?.type === "StringLiteral") return resolveImportPath(body.arguments[0].value, baseDir);
2445
- const loc$1 = node.loc ? ` at line ${node.loc.start.line}` : "";
2446
- warnings.push(`Lazy import with dynamic path${loc$1}. Only string literal imports can be analyzed.`);
2761
+ const line$1 = node.loc?.start.line;
2762
+ warnings.push({
2763
+ type: "general",
2764
+ message: `Lazy import with dynamic path${line$1 ? ` at line ${line$1}` : ""}. Only string literal imports can be analyzed.`,
2765
+ line: line$1
2766
+ });
2447
2767
  return;
2448
2768
  }
2449
2769
  }
2450
- const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
2451
- warnings.push(`Unrecognized lazy pattern (${node.type})${loc}. Expected arrow function with import().`);
2770
+ const line = node.loc?.start.line;
2771
+ warnings.push({
2772
+ type: "general",
2773
+ message: `Unrecognized lazy pattern (${node.type})${line ? ` at line ${line}` : ""}. Expected arrow function with import().`,
2774
+ line
2775
+ });
2452
2776
  }
2453
2777
  /**
2454
2778
  * Detect if content is Solid Router based on patterns.
@@ -2495,7 +2819,10 @@ function parseTanStackRouterConfig(filePath, preloadedContent) {
2495
2819
  for (const node of ast.program.body) collectRouteDefinitions(node, routeMap, routesFileDir, warnings);
2496
2820
  for (const node of ast.program.body) processAddChildrenCalls(node, routeMap, warnings);
2497
2821
  const routes = buildRouteTree(routeMap, warnings);
2498
- if (routes.length === 0) warnings.push("No routes found. Supported patterns: 'createRootRoute()', 'createRoute()', and '.addChildren([...])'");
2822
+ if (routes.length === 0) warnings.push({
2823
+ type: "general",
2824
+ message: "No routes found. Supported patterns: 'createRootRoute()', 'createRoute()', and '.addChildren([...])'"
2825
+ });
2499
2826
  return {
2500
2827
  routes,
2501
2828
  warnings
@@ -2565,8 +2892,12 @@ function extractRouteFromCallExpression(callNode, variableName, baseDir, warning
2565
2892
  case "path":
2566
2893
  if (prop.value.type === "StringLiteral") routeDef.path = normalizeTanStackPath(prop.value.value);
2567
2894
  else {
2568
- const loc = prop.loc ? ` at line ${prop.loc.start.line}` : "";
2569
- warnings.push(`Dynamic path value (${prop.value.type})${loc}. Only string literal paths can be statically analyzed.`);
2895
+ const line = prop.loc?.start.line;
2896
+ warnings.push({
2897
+ type: "general",
2898
+ message: `Dynamic path value (${prop.value.type})${line ? ` at line ${line}` : ""}. Only string literal paths can be statically analyzed.`,
2899
+ line
2900
+ });
2570
2901
  }
2571
2902
  break;
2572
2903
  case "component":
@@ -2577,8 +2908,12 @@ function extractRouteFromCallExpression(callNode, variableName, baseDir, warning
2577
2908
  const body = prop.value.body;
2578
2909
  if (body.type === "Identifier") routeDef.parentVariableName = body.name;
2579
2910
  else {
2580
- const loc = prop.loc ? ` at line ${prop.loc.start.line}` : "";
2581
- warnings.push(`Dynamic getParentRoute${loc}. Only static route references can be analyzed.`);
2911
+ const line = prop.loc?.start.line;
2912
+ warnings.push({
2913
+ type: "general",
2914
+ message: `Dynamic getParentRoute${line ? ` at line ${line}` : ""}. Only static route references can be analyzed.`,
2915
+ line
2916
+ });
2582
2917
  }
2583
2918
  }
2584
2919
  break;
@@ -2597,8 +2932,12 @@ function extractComponentValue(node, baseDir, warnings) {
2597
2932
  if (callee.type === "Identifier" && callee.name === "lazyRouteComponent") {
2598
2933
  const importArg = node.arguments[0];
2599
2934
  if (importArg) return extractLazyImportPath$1(importArg, baseDir, warnings);
2600
- const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
2601
- warnings.push(`lazyRouteComponent called without arguments${loc}. Expected arrow function with import().`);
2935
+ const line = node.loc?.start.line;
2936
+ warnings.push({
2937
+ type: "general",
2938
+ message: `lazyRouteComponent called without arguments${line ? ` at line ${line}` : ""}. Expected arrow function with import().`,
2939
+ line
2940
+ });
2602
2941
  return;
2603
2942
  }
2604
2943
  return;
@@ -2608,19 +2947,31 @@ function extractComponentValue(node, baseDir, warnings) {
2608
2947
  const openingElement = node.body.openingElement;
2609
2948
  if (openingElement?.name?.type === "JSXIdentifier") return openingElement.name.name;
2610
2949
  if (openingElement?.name?.type === "JSXMemberExpression") {
2611
- const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
2612
- warnings.push(`Namespaced JSX component${loc}. Component extraction not fully supported for member expressions.`);
2950
+ const line = node.loc?.start.line;
2951
+ warnings.push({
2952
+ type: "general",
2953
+ message: `Namespaced JSX component${line ? ` at line ${line}` : ""}. Component extraction not fully supported for member expressions.`,
2954
+ line
2955
+ });
2613
2956
  return;
2614
2957
  }
2615
2958
  }
2616
2959
  if (node.body.type === "BlockStatement") {
2617
- const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
2618
- warnings.push(`Arrow function with block body${loc}. Only concise arrow functions returning JSX directly can be analyzed.`);
2960
+ const line = node.loc?.start.line;
2961
+ warnings.push({
2962
+ type: "general",
2963
+ message: `Arrow function with block body${line ? ` at line ${line}` : ""}. Only concise arrow functions returning JSX directly can be analyzed.`,
2964
+ line
2965
+ });
2619
2966
  return;
2620
2967
  }
2621
2968
  if (node.body.type === "JSXFragment") {
2622
- const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
2623
- warnings.push(`JSX Fragment detected${loc}. Cannot extract component name from fragments.`);
2969
+ const line = node.loc?.start.line;
2970
+ warnings.push({
2971
+ type: "general",
2972
+ message: `JSX Fragment detected${line ? ` at line ${line}` : ""}. Cannot extract component name from fragments.`,
2973
+ line
2974
+ });
2624
2975
  return;
2625
2976
  }
2626
2977
  }
@@ -2639,8 +2990,12 @@ function extractLazyImportPath$1(node, baseDir, warnings) {
2639
2990
  if (importCall.arguments[0]?.type === "StringLiteral") return resolveImportPath(importCall.arguments[0].value, baseDir);
2640
2991
  }
2641
2992
  }
2642
- const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
2643
- warnings.push(`Unrecognized lazy pattern (${node.type})${loc}. Expected arrow function with import().`);
2993
+ const line = node.loc?.start.line;
2994
+ warnings.push({
2995
+ type: "general",
2996
+ message: `Unrecognized lazy pattern (${node.type})${line ? ` at line ${line}` : ""}. Expected arrow function with import().`,
2997
+ line
2998
+ });
2644
2999
  }
2645
3000
  /**
2646
3001
  * Process addChildren calls to establish parent-child relationships
@@ -2661,8 +3016,12 @@ function processAddChildrenExpression(expr, routeMap, warnings) {
2661
3016
  if (!parentVarName) return void 0;
2662
3017
  const parentDef = routeMap.get(parentVarName);
2663
3018
  if (!parentDef) {
2664
- const loc = expr.loc ? ` at line ${expr.loc.start.line}` : "";
2665
- warnings.push(`Parent route "${parentVarName}" not found${loc}. Ensure it's defined with createRoute/createRootRoute.`);
3019
+ const line = expr.loc?.start.line;
3020
+ warnings.push({
3021
+ type: "general",
3022
+ message: `Parent route "${parentVarName}" not found${line ? ` at line ${line}` : ""}. Ensure it's defined with createRoute/createRootRoute.`,
3023
+ line
3024
+ });
2666
3025
  return;
2667
3026
  }
2668
3027
  const childrenArg = expr.arguments[0];
@@ -2674,10 +3033,7 @@ function processAddChildrenExpression(expr, routeMap, warnings) {
2674
3033
  else if (element.type === "CallExpression") {
2675
3034
  const nestedParent = processAddChildrenExpression(element, routeMap, warnings);
2676
3035
  if (nestedParent) childNames.push(nestedParent);
2677
- } else if (element.type === "SpreadElement") {
2678
- const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
2679
- warnings.push(`Spread operator detected${loc}. Routes from spread cannot be statically analyzed.`);
2680
- }
3036
+ } else if (element.type === "SpreadElement") warnings.push(createSpreadWarning(element));
2681
3037
  }
2682
3038
  parentDef.children = childNames;
2683
3039
  }
@@ -2719,7 +3075,10 @@ function buildTreeFromParentRelations(routeMap, warnings) {
2719
3075
  */
2720
3076
  function buildRouteFromDefinition(def, routeMap, warnings, visited) {
2721
3077
  if (visited.has(def.variableName)) {
2722
- warnings.push(`Circular reference detected: route "${def.variableName}" references itself in the route tree.`);
3078
+ warnings.push({
3079
+ type: "general",
3080
+ message: `Circular reference detected: route "${def.variableName}" references itself in the route tree.`
3081
+ });
2723
3082
  return null;
2724
3083
  }
2725
3084
  visited.add(def.variableName);
@@ -2734,7 +3093,10 @@ function buildRouteFromDefinition(def, routeMap, warnings, visited) {
2734
3093
  if (childDef) {
2735
3094
  const childRoute = buildRouteFromDefinition(childDef, routeMap, warnings, visited);
2736
3095
  if (childRoute) children.push(childRoute);
2737
- } else warnings.push(`Child route "${childName}" not found. Ensure it's defined with createRoute.`);
3096
+ } else warnings.push({
3097
+ type: "general",
3098
+ message: `Child route "${childName}" not found. Ensure it's defined with createRoute.`
3099
+ });
2738
3100
  }
2739
3101
  if (children.length > 0) route.children = children;
2740
3102
  }
@@ -2842,7 +3204,10 @@ function parseReactRouterConfig(filePath, preloadedContent) {
2842
3204
  routes.push(...parsed);
2843
3205
  }
2844
3206
  }
2845
- if (routes.length === 0) warnings.push("No routes found. Supported patterns: 'createBrowserRouter([...])', 'export const routes = [...]', or 'export default [...]'");
3207
+ if (routes.length === 0) warnings.push({
3208
+ type: "general",
3209
+ message: "No routes found. Supported patterns: 'createBrowserRouter([...])', 'export const routes = [...]', or 'export default [...]'"
3210
+ });
2846
3211
  return {
2847
3212
  routes,
2848
3213
  warnings
@@ -2856,16 +3221,19 @@ function parseRoutesArray$1(arrayNode, baseDir, warnings) {
2856
3221
  for (const element of arrayNode.elements) {
2857
3222
  if (!element) continue;
2858
3223
  if (element.type === "SpreadElement") {
2859
- const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
2860
- warnings.push(`Spread operator detected${loc}. Routes from spread cannot be statically analyzed.`);
3224
+ warnings.push(createSpreadWarning(element));
2861
3225
  continue;
2862
3226
  }
2863
3227
  if (element.type === "ObjectExpression") {
2864
3228
  const route = parseRouteObject$1(element, baseDir, warnings);
2865
3229
  if (route) routes.push(route);
2866
3230
  } else {
2867
- const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
2868
- warnings.push(`Non-object route element (${element.type})${loc}. Only object literals can be statically analyzed.`);
3231
+ const line = element.loc?.start.line;
3232
+ warnings.push({
3233
+ type: "general",
3234
+ message: `Non-object route element (${element.type})${line ? ` at line ${line}` : ""}. Only object literals can be statically analyzed.`,
3235
+ line
3236
+ });
2869
3237
  }
2870
3238
  }
2871
3239
  return routes;
@@ -2886,8 +3254,12 @@ function parseRouteObject$1(objectNode, baseDir, warnings) {
2886
3254
  route.path = prop.value.value;
2887
3255
  hasPath = true;
2888
3256
  } else {
2889
- const loc = prop.loc ? ` at line ${prop.loc.start.line}` : "";
2890
- warnings.push(`Dynamic path value (${prop.value.type})${loc}. Only string literal paths can be statically analyzed.`);
3257
+ const line = prop.loc?.start.line;
3258
+ warnings.push({
3259
+ type: "general",
3260
+ message: `Dynamic path value (${prop.value.type})${line ? ` at line ${line}` : ""}. Only string literal paths can be statically analyzed.`,
3261
+ line
3262
+ });
2891
3263
  }
2892
3264
  break;
2893
3265
  case "index":
@@ -2949,20 +3321,31 @@ function extractComponentFromJSX(node, warnings) {
2949
3321
  const wrapperName = openingElement.name.name;
2950
3322
  for (const child of node.children) if (child.type === "JSXElement") {
2951
3323
  if (extractComponentFromJSX(child, warnings)) {
2952
- warnings.push(`Wrapper component detected: <${wrapperName}>. Using wrapper name for screen.`);
3324
+ warnings.push({
3325
+ type: "general",
3326
+ message: `Wrapper component detected: <${wrapperName}>. Using wrapper name for screen.`
3327
+ });
2953
3328
  return wrapperName;
2954
3329
  }
2955
3330
  }
2956
3331
  }
2957
3332
  }
2958
3333
  if (node.type === "JSXFragment") {
2959
- const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
2960
- warnings.push(`JSX Fragment detected${loc}. Cannot extract component name from fragments. Consider wrapping in a named component.`);
3334
+ const line = node.loc?.start.line;
3335
+ warnings.push({
3336
+ type: "general",
3337
+ message: `JSX Fragment detected${line ? ` at line ${line}` : ""}. Cannot extract component name from fragments. Consider wrapping in a named component.`,
3338
+ line
3339
+ });
2961
3340
  return;
2962
3341
  }
2963
3342
  if (node) {
2964
- const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
2965
- warnings.push(`Unrecognized element pattern (${node.type})${loc}. Component will not be extracted.`);
3343
+ const line = node.loc?.start.line;
3344
+ warnings.push({
3345
+ type: "general",
3346
+ message: `Unrecognized element pattern (${node.type})${line ? ` at line ${line}` : ""}. Component will not be extracted.`,
3347
+ line
3348
+ });
2966
3349
  }
2967
3350
  }
2968
3351
  /**
@@ -2974,13 +3357,21 @@ function extractLazyImportPath(node, baseDir, warnings) {
2974
3357
  const body = node.body;
2975
3358
  if (body.type === "CallExpression" && body.callee.type === "Import") {
2976
3359
  if (body.arguments[0]?.type === "StringLiteral") return resolveImportPath(body.arguments[0].value, baseDir);
2977
- const loc$1 = node.loc ? ` at line ${node.loc.start.line}` : "";
2978
- warnings.push(`Lazy import with dynamic path${loc$1}. Only string literal imports can be statically analyzed.`);
3360
+ const line$1 = node.loc?.start.line;
3361
+ warnings.push({
3362
+ type: "general",
3363
+ message: `Lazy import with dynamic path${line$1 ? ` at line ${line$1}` : ""}. Only string literal imports can be statically analyzed.`,
3364
+ line: line$1
3365
+ });
2979
3366
  return;
2980
3367
  }
2981
3368
  }
2982
- const loc = node.loc ? ` at line ${node.loc.start.line}` : "";
2983
- warnings.push(`Unrecognized lazy pattern (${node.type})${loc}. Expected arrow function with import().`);
3369
+ const line = node.loc?.start.line;
3370
+ warnings.push({
3371
+ type: "general",
3372
+ message: `Unrecognized lazy pattern (${node.type})${line ? ` at line ${line}` : ""}. Expected arrow function with import().`,
3373
+ line
3374
+ });
2984
3375
  }
2985
3376
  /**
2986
3377
  * Detect if content is React Router based on patterns
@@ -3064,7 +3455,10 @@ function parseVueRouterConfig(filePath, preloadedContent) {
3064
3455
  routes.push(...parsed);
3065
3456
  }
3066
3457
  }
3067
- if (routes.length === 0) warnings.push("No routes array found. Supported patterns: 'export const routes = [...]', 'export default [...]', or 'export default [...] satisfies RouteRecordRaw[]'");
3458
+ if (routes.length === 0) warnings.push({
3459
+ type: "general",
3460
+ message: "No routes array found. Supported patterns: 'export const routes = [...]', 'export default [...]', or 'export default [...] satisfies RouteRecordRaw[]'"
3461
+ });
3068
3462
  return {
3069
3463
  routes,
3070
3464
  warnings
@@ -3094,8 +3488,7 @@ function parseRoutesArray(arrayNode, baseDir, warnings, importMap) {
3094
3488
  for (const element of arrayNode.elements) {
3095
3489
  if (!element) continue;
3096
3490
  if (element.type === "SpreadElement") {
3097
- const loc = element.loc ? ` at line ${element.loc.start.line}` : "";
3098
- warnings.push(`Spread operator detected${loc}. Routes from spread cannot be statically analyzed.`);
3491
+ warnings.push(createSpreadWarning(element));
3099
3492
  continue;
3100
3493
  }
3101
3494
  if (element.type === "ObjectExpression") {
@@ -3337,34 +3730,6 @@ const VUE_ROUTER_CONFIG_PATHS = [
3337
3730
  "router/index.ts"
3338
3731
  ];
3339
3732
  /**
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
3733
  * Detect Vue Router configuration file in the project
3369
3734
  */
3370
3735
  function detectVueRouterConfigFile(cwd) {
@@ -3480,7 +3845,9 @@ const generateCommand = define({
3480
3845
  interactive,
3481
3846
  detectApi,
3482
3847
  detectNavigation,
3483
- apiIntegration: config.apiIntegration
3848
+ apiIntegration: config.apiIntegration,
3849
+ spreadOperator: config.lint?.spreadOperator,
3850
+ generate: config.generate
3484
3851
  });
3485
3852
  return;
3486
3853
  }
@@ -3491,7 +3858,9 @@ const generateCommand = define({
3491
3858
  ignore: config.ignore,
3492
3859
  detectApi,
3493
3860
  detectNavigation,
3494
- apiIntegration: config.apiIntegration
3861
+ apiIntegration: config.apiIntegration,
3862
+ spreadOperator: config.lint?.spreadOperator,
3863
+ generate: config.generate
3495
3864
  });
3496
3865
  }
3497
3866
  });
@@ -3499,7 +3868,12 @@ const generateCommand = define({
3499
3868
  * Generate screen.meta.ts files from a router config file (Vue Router or React Router)
3500
3869
  */
3501
3870
  async function generateFromRoutesFile(routesFile, cwd, options) {
3502
- const { dryRun, force, interactive, detectApi, detectNavigation, apiIntegration } = options;
3871
+ const { dryRun, force, interactive, detectApi, detectNavigation, apiIntegration, spreadOperator, generate } = options;
3872
+ const screenIdOptions = {
3873
+ smartParameterNaming: generate?.smartParameterNaming,
3874
+ parameterMapping: generate?.parameterMapping,
3875
+ unmappedParameterStrategy: generate?.unmappedParameterStrategy
3876
+ };
3503
3877
  const absoluteRoutesFile = resolve(cwd, routesFile);
3504
3878
  if (!existsSync(absoluteRoutesFile)) {
3505
3879
  logger.errorWithHelp(ERRORS.ROUTES_FILE_NOT_FOUND(routesFile));
@@ -3526,8 +3900,8 @@ async function generateFromRoutesFile(routesFile, cwd, options) {
3526
3900
  logger.errorWithHelp(ERRORS.ROUTES_FILE_PARSE_ERROR(routesFile, message));
3527
3901
  process.exit(1);
3528
3902
  }
3529
- for (const warning of parseResult.warnings) logger.warn(warning);
3530
- const flatRoutes = flattenRoutes(parseResult.routes);
3903
+ displayGenerateWarnings(parseResult.warnings, { spreadOperatorSetting: spreadOperator });
3904
+ const flatRoutes = flattenRoutes(parseResult.routes, "", 0, screenIdOptions);
3531
3905
  if (flatRoutes.length === 0) {
3532
3906
  logger.warn("No routes found in the config file");
3533
3907
  return;
@@ -3609,7 +3983,8 @@ async function generateFromRoutesFile(routesFile, cwd, options) {
3609
3983
  owner: result.owner,
3610
3984
  tags: result.tags,
3611
3985
  dependsOn: detectedApis,
3612
- next: detectedNext
3986
+ next: detectedNext,
3987
+ suggestions: route.suggestions
3613
3988
  });
3614
3989
  if (dryRun) {
3615
3990
  logDryRunOutput(metaPath, result.meta, result.owner, result.tags, detectedApis, detectedNext);
@@ -3618,7 +3993,8 @@ async function generateFromRoutesFile(routesFile, cwd, options) {
3618
3993
  } else {
3619
3994
  const content = generateScreenMetaContent(screenMeta, {
3620
3995
  dependsOn: detectedApis,
3621
- next: detectedNext
3996
+ next: detectedNext,
3997
+ suggestions: route.suggestions
3622
3998
  });
3623
3999
  if (dryRun) {
3624
4000
  logDryRunOutput(metaPath, screenMeta, void 0, void 0, detectedApis, detectedNext);
@@ -3632,7 +4008,12 @@ async function generateFromRoutesFile(routesFile, cwd, options) {
3632
4008
  * Generate screen.meta.ts files from route files matching a glob pattern
3633
4009
  */
3634
4010
  async function generateFromRoutesPattern(routesPattern, cwd, options) {
3635
- const { dryRun, force, interactive, ignore, detectApi, detectNavigation, apiIntegration } = options;
4011
+ const { dryRun, force, interactive, ignore, detectApi, detectNavigation, apiIntegration, spreadOperator, generate } = options;
4012
+ const screenIdOptions = {
4013
+ smartParameterNaming: generate?.smartParameterNaming,
4014
+ parameterMapping: generate?.parameterMapping,
4015
+ unmappedParameterStrategy: generate?.unmappedParameterStrategy
4016
+ };
3636
4017
  logger.info("Scanning for route files...");
3637
4018
  logger.blank();
3638
4019
  const routeFiles = await glob(routesPattern, {
@@ -3649,12 +4030,12 @@ async function generateFromRoutesPattern(routesPattern, cwd, options) {
3649
4030
  const vueRouterConfig = detectVueRouterConfigFile(cwd);
3650
4031
  if (vueRouterConfig) try {
3651
4032
  const parseResult = parseVueRouterConfig(vueRouterConfig);
3652
- const flatRoutes = flattenRoutes(parseResult.routes);
4033
+ const flatRoutes = flattenRoutes(parseResult.routes, "", 0, screenIdOptions);
3653
4034
  if (flatRoutes.length > 0) {
3654
4035
  routeComponentMap = buildRouteComponentMap(flatRoutes, cwd);
3655
4036
  logger.log(` ${logger.dim(`(using routes from ${relative(cwd, vueRouterConfig)})`)}`);
3656
4037
  }
3657
- for (const warning of parseResult.warnings) logger.warn(warning);
4038
+ displayGenerateWarnings(parseResult.warnings, { spreadOperatorSetting: spreadOperator });
3658
4039
  } catch (error) {
3659
4040
  const message = error instanceof Error ? error.message : String(error);
3660
4041
  logger.warn(`Could not parse Vue Router config: ${message}`);
@@ -3728,7 +4109,8 @@ async function generateFromRoutesPattern(routesPattern, cwd, options) {
3728
4109
  owner: result.owner,
3729
4110
  tags: result.tags,
3730
4111
  dependsOn: detectedApis,
3731
- next: detectedNext
4112
+ next: detectedNext,
4113
+ suggestions: matchedRoute?.suggestions
3732
4114
  });
3733
4115
  if (dryRun) {
3734
4116
  logDryRunOutput(metaPath, result.meta, result.owner, result.tags, detectedApis, detectedNext);
@@ -3737,7 +4119,8 @@ async function generateFromRoutesPattern(routesPattern, cwd, options) {
3737
4119
  } else {
3738
4120
  const content = generateScreenMetaContent(screenMeta, {
3739
4121
  dependsOn: detectedApis,
3740
- next: detectedNext
4122
+ next: detectedNext,
4123
+ suggestions: matchedRoute?.suggestions
3741
4124
  });
3742
4125
  if (dryRun) {
3743
4126
  logDryRunOutput(metaPath, screenMeta, void 0, void 0, detectedApis, detectedNext);
@@ -3914,6 +4297,7 @@ function generateScreenMetaContent(meta, options) {
3914
4297
  const tags = options?.tags && options.tags.length > 0 ? options.tags : [meta.id.split(".")[0] || "general"];
3915
4298
  const dependsOn = options?.dependsOn ?? [];
3916
4299
  const next = options?.next ?? [];
4300
+ const suggestions = options?.suggestions ?? [];
3917
4301
  const ownerStr = owner.length > 0 ? `[${owner.map((o) => `"${o}"`).join(", ")}]` : "[]";
3918
4302
  const tagsStr = `[${tags.map((t) => `"${t}"`).join(", ")}]`;
3919
4303
  const dependsOnStr = dependsOn.length > 0 ? `[${dependsOn.map((d) => `"${d}"`).join(", ")}]` : "[]";
@@ -3921,10 +4305,10 @@ function generateScreenMetaContent(meta, options) {
3921
4305
  const dependsOnComment = dependsOn.length > 0 ? "// Auto-detected API dependencies (add more as needed)" : `// APIs/services this screen depends on (for impact analysis)
3922
4306
  // Example: ["UserAPI.getProfile", "PaymentService.checkout"]`;
3923
4307
  const nextComment = next.length > 0 ? "// Auto-detected navigation targets (add more as needed)" : "// Screen IDs this screen can navigate to";
3924
- return `import { defineScreen } from "@screenbook/core"
4308
+ return `import { defineScreen } from "screenbook"
3925
4309
 
3926
4310
  export const screen = defineScreen({
3927
- id: "${meta.id}",
4311
+ ${suggestions.length > 0 ? `// TODO: ${suggestions.join("; ")}\n\t` : ""}id: "${meta.id}",
3928
4312
  title: "${meta.title}",
3929
4313
  route: "${meta.route}",
3930
4314
 
@@ -4371,7 +4755,7 @@ function isInteractive() {
4371
4755
  //#endregion
4372
4756
  //#region src/commands/init.ts
4373
4757
  function generateConfigTemplate(framework) {
4374
- if (framework) return `import { defineConfig } from "@screenbook/core"
4758
+ if (framework) return `import { defineConfig } from "screenbook"
4375
4759
 
4376
4760
  export default defineConfig({
4377
4761
  // Auto-detected: ${framework.name}
@@ -4380,7 +4764,7 @@ export default defineConfig({
4380
4764
  outDir: ".screenbook",
4381
4765
  })
4382
4766
  `;
4383
- return `import { defineConfig } from "@screenbook/core"
4767
+ return `import { defineConfig } from "screenbook"
4384
4768
 
4385
4769
  export default defineConfig({
4386
4770
  // Glob pattern for screen metadata files
@@ -4940,6 +5324,7 @@ const lintCommand = define({
4940
5324
  const cwd = process.cwd();
4941
5325
  const adoption = config.adoption ?? { mode: "full" };
4942
5326
  let hasWarnings = false;
5327
+ let shouldFailLint = false;
4943
5328
  if (!config.routesPattern && !config.routesFile) {
4944
5329
  logger.errorWithHelp(ERRORS.ROUTES_PATTERN_MISSING);
4945
5330
  process.exit(1);
@@ -4959,6 +5344,20 @@ const lintCommand = define({
4959
5344
  cwd,
4960
5345
  ignore: config.ignore
4961
5346
  });
5347
+ const excludePatterns = config.excludePatterns ?? DEFAULT_EXCLUDE_PATTERNS;
5348
+ const excludedFiles = [];
5349
+ routeFiles = routeFiles.filter((file) => {
5350
+ if (matchesExcludePattern(file, excludePatterns)) {
5351
+ excludedFiles.push(file);
5352
+ return false;
5353
+ }
5354
+ return true;
5355
+ });
5356
+ if (isVerbose() && excludedFiles.length > 0) {
5357
+ logger.log(`Excluded by patterns (${excludedFiles.length} files):`);
5358
+ for (const file of excludedFiles) logger.log(` ${logger.dim(file)}`);
5359
+ logger.blank();
5360
+ }
4962
5361
  if (adoption.mode === "progressive" && adoption.includePatterns?.length) routeFiles = routeFiles.filter((file) => adoption.includePatterns?.some((pattern) => minimatch(file, pattern)));
4963
5362
  if (routeFiles.length === 0) {
4964
5363
  logger.warn(`No route files found matching: ${config.routesPattern}`);
@@ -5008,18 +5407,18 @@ const lintCommand = define({
5008
5407
  if (existsSync(screensPath)) try {
5009
5408
  const content = readFileSync(screensPath, "utf-8");
5010
5409
  const screens = JSON.parse(content);
5011
- const orphans = findOrphanScreens(screens);
5012
- if (orphans.length > 0) {
5013
- hasWarnings = true;
5014
- logger.blank();
5015
- logger.warn(`Orphan screens detected (${orphans.length}):`);
5410
+ if (!Array.isArray(screens)) {
5016
5411
  logger.blank();
5017
- logger.log(" These screens have no entryPoints and are not");
5018
- logger.log(" referenced in any other screen's 'next' array.");
5019
- logger.blank();
5020
- for (const orphan of orphans) logger.itemWarn(`${orphan.id} ${logger.dim(orphan.route)}`);
5412
+ logger.error("screens.json does not contain a valid array of screens");
5413
+ logger.log(` ${logger.dim("Run 'screenbook build' to regenerate.")}`);
5414
+ process.exit(1);
5415
+ }
5416
+ const orphanResult = checkAndReportOrphanScreens(screens, config.lint?.orphans ?? "warn");
5417
+ if (orphanResult.hasWarnings) hasWarnings = true;
5418
+ if (orphanResult.shouldFail) {
5021
5419
  logger.blank();
5022
- logger.log(` ${logger.dim("Consider adding entryPoints or removing these screens.")}`);
5420
+ logger.error("Lint failed: Orphan screens detected in strict mode");
5421
+ shouldFailLint = true;
5023
5422
  }
5024
5423
  if (!ctx.values.allowCycles) {
5025
5424
  const cycleResult = detectCycles(screens);
@@ -5056,17 +5455,26 @@ const lintCommand = define({
5056
5455
  }
5057
5456
  } catch (error) {
5058
5457
  if (error instanceof SyntaxError) {
5059
- logger.warn("Failed to parse screens.json - file may be corrupted");
5060
- logger.log(` ${logger.dim("Run 'screenbook build' to regenerate.")}`);
5061
- hasWarnings = true;
5062
- } else if (error instanceof Error) {
5063
- logger.warn(`Failed to analyze screens.json: ${error.message}`);
5064
- hasWarnings = true;
5065
- } else {
5066
- logger.warn(`Failed to analyze screens.json: ${String(error)}`);
5067
- hasWarnings = true;
5458
+ logger.blank();
5459
+ logger.error("Failed to parse screens.json");
5460
+ logger.log(` ${logger.dim("The file contains invalid JSON syntax.")}`);
5461
+ logger.log(` ${logger.dim("Run 'screenbook build' to regenerate the file.")}`);
5462
+ process.exit(1);
5068
5463
  }
5464
+ if (error instanceof Error && (error.message.includes("EACCES") || error.message.includes("EPERM"))) {
5465
+ logger.blank();
5466
+ logger.error("Permission denied reading screens.json");
5467
+ logger.log(` ${logger.dim(error.message)}`);
5468
+ logger.log(` ${logger.dim("Check file permissions on the .screenbook directory.")}`);
5469
+ process.exit(1);
5470
+ }
5471
+ logger.blank();
5472
+ logger.error("Unexpected error reading screens.json");
5473
+ if (error instanceof Error) logger.log(` ${logger.dim(error.message)}`);
5474
+ else logger.log(` ${logger.dim(String(error))}`);
5475
+ process.exit(1);
5069
5476
  }
5477
+ if (shouldFailLint) process.exit(1);
5070
5478
  if (hasWarnings) {
5071
5479
  logger.blank();
5072
5480
  logger.warn("Lint completed with warnings.");
@@ -5078,6 +5486,7 @@ const lintCommand = define({
5078
5486
  */
5079
5487
  async function lintRoutesFile(routesFile, cwd, config, adoption, allowCycles, strict) {
5080
5488
  let hasWarnings = false;
5489
+ let shouldFailLint = false;
5081
5490
  const absoluteRoutesFile = resolve(cwd, routesFile);
5082
5491
  if (!existsSync(absoluteRoutesFile)) {
5083
5492
  logger.errorWithHelp(ERRORS.ROUTES_FILE_NOT_FOUND(routesFile));
@@ -5090,25 +5499,43 @@ async function lintRoutesFile(routesFile, cwd, config, adoption, allowCycles, st
5090
5499
  const content = readFileSync(absoluteRoutesFile, "utf-8");
5091
5500
  const routerType = detectRouterType(content);
5092
5501
  let parseResult;
5093
- if (routerType === "tanstack-router") parseResult = parseTanStackRouterConfig(absoluteRoutesFile, content);
5094
- else if (routerType === "solid-router") parseResult = parseSolidRouterConfig(absoluteRoutesFile, content);
5095
- else if (routerType === "angular-router") parseResult = parseAngularRouterConfig(absoluteRoutesFile, content);
5096
- else if (routerType === "react-router") parseResult = parseReactRouterConfig(absoluteRoutesFile, content);
5097
- else if (routerType === "vue-router") parseResult = parseVueRouterConfig(absoluteRoutesFile, content);
5098
- else {
5099
- logger.warn(`Could not auto-detect router type for ${logger.path(routesFile)}. Attempting to parse as Vue Router.`);
5100
- logger.log(` ${logger.dim("If parsing fails, check that your router imports are explicit.")}`);
5101
- hasWarnings = true;
5102
- parseResult = parseVueRouterConfig(absoluteRoutesFile, content);
5103
- }
5104
- for (const warning of parseResult.warnings) {
5105
- logger.warn(warning);
5106
- hasWarnings = true;
5502
+ try {
5503
+ if (routerType === "tanstack-router") parseResult = parseTanStackRouterConfig(absoluteRoutesFile, content);
5504
+ else if (routerType === "solid-router") parseResult = parseSolidRouterConfig(absoluteRoutesFile, content);
5505
+ else if (routerType === "angular-router") parseResult = parseAngularRouterConfig(absoluteRoutesFile, content);
5506
+ else if (routerType === "react-router") parseResult = parseReactRouterConfig(absoluteRoutesFile, content);
5507
+ else if (routerType === "vue-router") parseResult = parseVueRouterConfig(absoluteRoutesFile, content);
5508
+ else {
5509
+ logger.warn(`Could not auto-detect router type for ${logger.path(routesFile)}. Attempting to parse as Vue Router.`);
5510
+ logger.log(` ${logger.dim("If parsing fails, check that your router imports are explicit.")}`);
5511
+ hasWarnings = true;
5512
+ parseResult = parseVueRouterConfig(absoluteRoutesFile, content);
5513
+ }
5514
+ } catch (parseError) {
5515
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
5516
+ logger.errorWithHelp(ERRORS.ROUTES_FILE_PARSE_ERROR(routesFile, message));
5517
+ process.exit(1);
5107
5518
  }
5519
+ const warningsResult = displayWarnings(parseResult.warnings, { spreadOperatorSetting: config.lint?.spreadOperator });
5520
+ if (warningsResult.hasWarnings) hasWarnings = true;
5521
+ if (warningsResult.shouldFailLint) shouldFailLint = true;
5108
5522
  flatRoutes = flattenRoutes(parseResult.routes);
5109
5523
  } catch (error) {
5110
- const message = error instanceof Error ? error.message : String(error);
5111
- logger.errorWithHelp(ERRORS.ROUTES_FILE_PARSE_ERROR(routesFile, message));
5524
+ if (error instanceof Error) if (error.message.includes("ENOENT")) logger.errorWithHelp(ERRORS.ROUTES_FILE_NOT_FOUND(routesFile));
5525
+ else if (error.message.includes("EACCES") || error.message.includes("EPERM")) {
5526
+ logger.blank();
5527
+ logger.error(`Permission denied reading ${routesFile}`);
5528
+ logger.log(` ${logger.dim(error.message)}`);
5529
+ logger.log(` ${logger.dim("Check file permissions.")}`);
5530
+ } else {
5531
+ logger.blank();
5532
+ logger.error(`Failed to read ${routesFile}`);
5533
+ logger.log(` ${logger.dim(error.message)}`);
5534
+ }
5535
+ else {
5536
+ logger.blank();
5537
+ logger.error(`Unexpected error: ${String(error)}`);
5538
+ }
5112
5539
  process.exit(1);
5113
5540
  }
5114
5541
  if (flatRoutes.length === 0) {
@@ -5129,11 +5556,20 @@ async function lintRoutesFile(routesFile, cwd, config, adoption, allowCycles, st
5129
5556
  }
5130
5557
  const missingMeta = [];
5131
5558
  const covered = [];
5559
+ const excludedRoutes = [];
5560
+ const excludePatterns = config.excludePatterns ?? DEFAULT_EXCLUDE_PATTERNS;
5132
5561
  for (const route of flatRoutes) {
5133
5562
  if (route.componentPath?.endsWith("Layout")) continue;
5563
+ const metaDir = determineMetaDir(route, cwd);
5564
+ if (matchesExcludePattern(metaDir, excludePatterns)) {
5565
+ excludedRoutes.push({
5566
+ route,
5567
+ metaDir
5568
+ });
5569
+ continue;
5570
+ }
5134
5571
  let matched = false;
5135
- const metaPath = determineMetaDir(route, cwd);
5136
- if (metaDirs.has(metaPath)) matched = true;
5572
+ if (metaDirs.has(metaDir)) matched = true;
5137
5573
  if (!matched && route.componentPath) {
5138
5574
  const componentName = route.componentPath.toLowerCase();
5139
5575
  if (metaDirsByName.has(componentName)) matched = true;
@@ -5155,6 +5591,11 @@ async function lintRoutesFile(routesFile, cwd, config, adoption, allowCycles, st
5155
5591
  if (matched) covered.push(route);
5156
5592
  else missingMeta.push(route);
5157
5593
  }
5594
+ if (isVerbose() && excludedRoutes.length > 0) {
5595
+ logger.log(`Excluded by patterns (${excludedRoutes.length} routes):`);
5596
+ for (const { route, metaDir } of excludedRoutes) logger.log(` ${logger.dim(`${route.fullPath} → ${metaDir}`)}`);
5597
+ logger.blank();
5598
+ }
5158
5599
  const total = covered.length + missingMeta.length;
5159
5600
  const coveredCount = covered.length;
5160
5601
  const missingCount = missingMeta.length;
@@ -5185,18 +5626,18 @@ async function lintRoutesFile(routesFile, cwd, config, adoption, allowCycles, st
5185
5626
  if (existsSync(screensPath)) try {
5186
5627
  const content = readFileSync(screensPath, "utf-8");
5187
5628
  const screens = JSON.parse(content);
5188
- const orphans = findOrphanScreens(screens);
5189
- if (orphans.length > 0) {
5190
- hasWarnings = true;
5191
- logger.blank();
5192
- logger.warn(`Orphan screens detected (${orphans.length}):`);
5193
- logger.blank();
5194
- logger.log(" These screens have no entryPoints and are not");
5195
- logger.log(" referenced in any other screen's 'next' array.");
5629
+ if (!Array.isArray(screens)) {
5196
5630
  logger.blank();
5197
- for (const orphan of orphans) logger.itemWarn(`${orphan.id} ${logger.dim(orphan.route)}`);
5631
+ logger.error("screens.json does not contain a valid array of screens");
5632
+ logger.log(` ${logger.dim("Run 'screenbook build' to regenerate.")}`);
5633
+ process.exit(1);
5634
+ }
5635
+ const orphanResult = checkAndReportOrphanScreens(screens, config.lint?.orphans ?? "warn");
5636
+ if (orphanResult.hasWarnings) hasWarnings = true;
5637
+ if (orphanResult.shouldFail) {
5198
5638
  logger.blank();
5199
- logger.log(` ${logger.dim("Consider adding entryPoints or removing these screens.")}`);
5639
+ logger.error("Lint failed: Orphan screens detected in strict mode");
5640
+ shouldFailLint = true;
5200
5641
  }
5201
5642
  if (!allowCycles) {
5202
5643
  const cycleResult = detectCycles(screens);
@@ -5233,17 +5674,26 @@ async function lintRoutesFile(routesFile, cwd, config, adoption, allowCycles, st
5233
5674
  }
5234
5675
  } catch (error) {
5235
5676
  if (error instanceof SyntaxError) {
5236
- logger.warn("Failed to parse screens.json - file may be corrupted");
5237
- logger.log(` ${logger.dim("Run 'screenbook build' to regenerate.")}`);
5238
- hasWarnings = true;
5239
- } else if (error instanceof Error) {
5240
- logger.warn(`Failed to analyze screens.json: ${error.message}`);
5241
- hasWarnings = true;
5242
- } else {
5243
- logger.warn(`Failed to analyze screens.json: ${String(error)}`);
5244
- hasWarnings = true;
5677
+ logger.blank();
5678
+ logger.error("Failed to parse screens.json");
5679
+ logger.log(` ${logger.dim("The file contains invalid JSON syntax.")}`);
5680
+ logger.log(` ${logger.dim("Run 'screenbook build' to regenerate the file.")}`);
5681
+ process.exit(1);
5682
+ }
5683
+ if (error instanceof Error && (error.message.includes("EACCES") || error.message.includes("EPERM"))) {
5684
+ logger.blank();
5685
+ logger.error("Permission denied reading screens.json");
5686
+ logger.log(` ${logger.dim(error.message)}`);
5687
+ logger.log(` ${logger.dim("Check file permissions on the .screenbook directory.")}`);
5688
+ process.exit(1);
5245
5689
  }
5690
+ logger.blank();
5691
+ logger.error("Unexpected error reading screens.json");
5692
+ if (error instanceof Error) logger.log(` ${logger.dim(error.message)}`);
5693
+ else logger.log(` ${logger.dim(String(error))}`);
5694
+ process.exit(1);
5246
5695
  }
5696
+ if (shouldFailLint) process.exit(1);
5247
5697
  if (hasWarnings) {
5248
5698
  logger.blank();
5249
5699
  logger.warn("Lint completed with warnings.");
@@ -5309,6 +5759,56 @@ function findOrphanScreens(screens) {
5309
5759
  return orphans;
5310
5760
  }
5311
5761
  /**
5762
+ * Check for orphan screens and report based on configuration.
5763
+ * When all screens are orphans (common after initial generation),
5764
+ * show a tip instead of warnings to avoid confusing new users.
5765
+ */
5766
+ function checkAndReportOrphanScreens(screens, orphanSetting) {
5767
+ if (orphanSetting === "off") return {
5768
+ hasWarnings: false,
5769
+ shouldFail: false
5770
+ };
5771
+ const orphans = findOrphanScreens(screens);
5772
+ if (orphans.length === 0) return {
5773
+ hasWarnings: false,
5774
+ shouldFail: false
5775
+ };
5776
+ if (orphans.length === screens.length) {
5777
+ logger.blank();
5778
+ logger.info("Tip: All screens are currently disconnected.");
5779
+ logger.blank();
5780
+ logger.log(" This is normal after initial setup. To connect screens:");
5781
+ logger.log(" 1. Add 'entryPoints' to define how users reach each screen");
5782
+ logger.log(" 2. Add 'next' to define navigation targets from each screen");
5783
+ logger.blank();
5784
+ if (orphanSetting === "error") return {
5785
+ hasWarnings: true,
5786
+ shouldFail: true
5787
+ };
5788
+ return {
5789
+ hasWarnings: false,
5790
+ shouldFail: false
5791
+ };
5792
+ }
5793
+ logger.blank();
5794
+ logger.warn(`Orphan screens detected (${orphans.length}):`);
5795
+ logger.blank();
5796
+ logger.log(" These screens have no entryPoints and are not");
5797
+ logger.log(" referenced in any other screen's 'next' array.");
5798
+ logger.blank();
5799
+ for (const orphan of orphans) logger.itemWarn(`${orphan.id} ${logger.dim(orphan.route)}`);
5800
+ logger.blank();
5801
+ logger.log(` ${logger.dim("Consider adding entryPoints or removing these screens.")}`);
5802
+ if (orphanSetting === "error") return {
5803
+ hasWarnings: true,
5804
+ shouldFail: true
5805
+ };
5806
+ return {
5807
+ hasWarnings: true,
5808
+ shouldFail: false
5809
+ };
5810
+ }
5811
+ /**
5312
5812
  * Validate dependsOn references against OpenAPI specifications
5313
5813
  */
5314
5814
  async function validateDependsOnAgainstOpenApi(screens, sources, cwd, strict) {