@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 +714 -214
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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)
|
|
1307
|
-
fullPath,
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
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
|
-
*
|
|
1320
|
-
*
|
|
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
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
|
1331
|
-
}
|
|
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(
|
|
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
|
-
|
|
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
|
|
1487
|
-
warnings.push(
|
|
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
|
|
1511
|
-
warnings.push(
|
|
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
|
|
1575
|
-
warnings.push(
|
|
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
|
|
1582
|
-
warnings.push(
|
|
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
|
|
1587
|
-
warnings.push(
|
|
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
|
|
1605
|
-
warnings.push(
|
|
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(
|
|
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
|
-
|
|
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
|
|
2304
|
-
warnings.push(
|
|
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
|
|
2332
|
-
warnings.push(
|
|
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
|
|
2336
|
-
warnings.push(
|
|
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
|
|
2372
|
-
warnings.push(
|
|
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
|
|
2390
|
-
warnings.push(
|
|
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
|
|
2681
|
+
const line = node.loc?.start.line;
|
|
2394
2682
|
const calleeName = callee.type === "Identifier" ? callee.name : "unknown";
|
|
2395
|
-
warnings.push(
|
|
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
|
|
2404
|
-
warnings.push(
|
|
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
|
|
2410
|
-
warnings.push(
|
|
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
|
|
2415
|
-
warnings.push(
|
|
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
|
|
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(
|
|
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
|
|
2428
|
-
warnings.push(
|
|
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
|
|
2433
|
-
warnings.push(
|
|
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
|
|
2446
|
-
warnings.push(
|
|
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
|
|
2451
|
-
warnings.push(
|
|
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(
|
|
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
|
|
2569
|
-
warnings.push(
|
|
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
|
|
2581
|
-
warnings.push(
|
|
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
|
|
2601
|
-
warnings.push(
|
|
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
|
|
2612
|
-
warnings.push(
|
|
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
|
|
2618
|
-
warnings.push(
|
|
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
|
|
2623
|
-
warnings.push(
|
|
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
|
|
2643
|
-
warnings.push(
|
|
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
|
|
2665
|
-
warnings.push(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
2868
|
-
warnings.push(
|
|
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
|
|
2890
|
-
warnings.push(
|
|
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(
|
|
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
|
|
2960
|
-
warnings.push(
|
|
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
|
|
2965
|
-
warnings.push(
|
|
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
|
|
2978
|
-
warnings.push(
|
|
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
|
|
2983
|
-
warnings.push(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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
|
-
|
|
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.
|
|
5018
|
-
logger.log("
|
|
5019
|
-
|
|
5020
|
-
|
|
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.
|
|
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.
|
|
5060
|
-
logger.
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
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
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
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
|
-
|
|
5111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
5237
|
-
logger.
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
logger.
|
|
5244
|
-
|
|
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) {
|