@buoy-design/scanners 0.2.1 → 0.2.26

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.
Files changed (109) hide show
  1. package/dist/design-systems/index.d.ts +6 -0
  2. package/dist/design-systems/index.d.ts.map +1 -0
  3. package/dist/design-systems/index.js +6 -0
  4. package/dist/design-systems/index.js.map +1 -0
  5. package/dist/design-systems/library-detector.d.ts +53 -0
  6. package/dist/design-systems/library-detector.d.ts.map +1 -0
  7. package/dist/design-systems/library-detector.js +183 -0
  8. package/dist/design-systems/library-detector.js.map +1 -0
  9. package/dist/extractors/css-in-js.d.ts +20 -0
  10. package/dist/extractors/css-in-js.d.ts.map +1 -0
  11. package/dist/extractors/css-in-js.js +284 -0
  12. package/dist/extractors/css-in-js.js.map +1 -0
  13. package/dist/extractors/html-style.d.ts +1 -1
  14. package/dist/extractors/html-style.d.ts.map +1 -1
  15. package/dist/extractors/index.d.ts +1 -0
  16. package/dist/extractors/index.d.ts.map +1 -1
  17. package/dist/extractors/index.js +1 -0
  18. package/dist/extractors/index.js.map +1 -1
  19. package/dist/figma/client.d.ts +4 -0
  20. package/dist/figma/client.d.ts.map +1 -1
  21. package/dist/figma/client.js.map +1 -1
  22. package/dist/figma/component-scanner.d.ts +67 -1
  23. package/dist/figma/component-scanner.d.ts.map +1 -1
  24. package/dist/figma/component-scanner.js +162 -1
  25. package/dist/figma/component-scanner.js.map +1 -1
  26. package/dist/figma/index.d.ts +1 -1
  27. package/dist/figma/index.d.ts.map +1 -1
  28. package/dist/figma/index.js.map +1 -1
  29. package/dist/git/angular-scanner.d.ts +78 -1
  30. package/dist/git/angular-scanner.d.ts.map +1 -1
  31. package/dist/git/angular-scanner.js +249 -6
  32. package/dist/git/angular-scanner.js.map +1 -1
  33. package/dist/git/index.d.ts +4 -3
  34. package/dist/git/index.d.ts.map +1 -1
  35. package/dist/git/index.js +1 -0
  36. package/dist/git/index.js.map +1 -1
  37. package/dist/git/nextjs-scanner.d.ts +190 -0
  38. package/dist/git/nextjs-scanner.d.ts.map +1 -0
  39. package/dist/git/nextjs-scanner.js +979 -0
  40. package/dist/git/nextjs-scanner.js.map +1 -0
  41. package/dist/git/react-scanner.d.ts +74 -1
  42. package/dist/git/react-scanner.d.ts.map +1 -1
  43. package/dist/git/react-scanner.js +174 -5
  44. package/dist/git/react-scanner.js.map +1 -1
  45. package/dist/git/token-scanner.d.ts.map +1 -1
  46. package/dist/git/token-scanner.js +24 -2
  47. package/dist/git/token-scanner.js.map +1 -1
  48. package/dist/git/vue-scanner.d.ts +43 -1
  49. package/dist/git/vue-scanner.d.ts.map +1 -1
  50. package/dist/git/vue-scanner.js +130 -4
  51. package/dist/git/vue-scanner.js.map +1 -1
  52. package/dist/index.d.ts +2 -1
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +6 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/signals/extractors/arbitrary-value.d.ts +7 -0
  57. package/dist/signals/extractors/arbitrary-value.d.ts.map +1 -0
  58. package/dist/signals/extractors/arbitrary-value.js +57 -0
  59. package/dist/signals/extractors/arbitrary-value.js.map +1 -0
  60. package/dist/signals/extractors/breakpoint.d.ts +6 -0
  61. package/dist/signals/extractors/breakpoint.d.ts.map +1 -0
  62. package/dist/signals/extractors/breakpoint.js +41 -0
  63. package/dist/signals/extractors/breakpoint.js.map +1 -0
  64. package/dist/signals/extractors/index.d.ts +7 -0
  65. package/dist/signals/extractors/index.d.ts.map +1 -1
  66. package/dist/signals/extractors/index.js +7 -0
  67. package/dist/signals/extractors/index.js.map +1 -1
  68. package/dist/signals/extractors/inline-style.d.ts +6 -0
  69. package/dist/signals/extractors/inline-style.d.ts.map +1 -0
  70. package/dist/signals/extractors/inline-style.js +60 -0
  71. package/dist/signals/extractors/inline-style.js.map +1 -0
  72. package/dist/signals/extractors/radius.d.ts +3 -0
  73. package/dist/signals/extractors/radius.d.ts.map +1 -0
  74. package/dist/signals/extractors/radius.js +32 -0
  75. package/dist/signals/extractors/radius.js.map +1 -0
  76. package/dist/signals/extractors/shadow.d.ts +3 -0
  77. package/dist/signals/extractors/shadow.d.ts.map +1 -0
  78. package/dist/signals/extractors/shadow.js +41 -0
  79. package/dist/signals/extractors/shadow.js.map +1 -0
  80. package/dist/signals/extractors/sizing.d.ts +6 -0
  81. package/dist/signals/extractors/sizing.d.ts.map +1 -0
  82. package/dist/signals/extractors/sizing.js +60 -0
  83. package/dist/signals/extractors/sizing.js.map +1 -0
  84. package/dist/signals/extractors/z-index.d.ts +3 -0
  85. package/dist/signals/extractors/z-index.d.ts.map +1 -0
  86. package/dist/signals/extractors/z-index.js +30 -0
  87. package/dist/signals/extractors/z-index.js.map +1 -0
  88. package/dist/signals/scanner-integration.d.ts.map +1 -1
  89. package/dist/signals/scanner-integration.js +35 -0
  90. package/dist/signals/scanner-integration.js.map +1 -1
  91. package/dist/signals/types.d.ts +4 -4
  92. package/dist/signals/types.d.ts.map +1 -1
  93. package/dist/signals/types.js +4 -0
  94. package/dist/signals/types.js.map +1 -1
  95. package/dist/storybook/extractor.d.ts +106 -1
  96. package/dist/storybook/extractor.d.ts.map +1 -1
  97. package/dist/storybook/extractor.js +325 -5
  98. package/dist/storybook/extractor.js.map +1 -1
  99. package/dist/storybook/index.d.ts +1 -1
  100. package/dist/storybook/index.d.ts.map +1 -1
  101. package/dist/storybook/index.js.map +1 -1
  102. package/dist/tailwind/index.d.ts +1 -1
  103. package/dist/tailwind/index.d.ts.map +1 -1
  104. package/dist/tailwind/index.js.map +1 -1
  105. package/dist/tailwind/scanner.d.ts +32 -0
  106. package/dist/tailwind/scanner.d.ts.map +1 -1
  107. package/dist/tailwind/scanner.js +105 -0
  108. package/dist/tailwind/scanner.js.map +1 -1
  109. package/package.json +2 -2
@@ -0,0 +1,979 @@
1
+ import { SignalAwareScanner } from "../base/index.js";
2
+ import { createComponentId } from "@buoy-design/core";
3
+ import * as ts from "typescript";
4
+ import { readFile } from "fs/promises";
5
+ import { readFileSync, existsSync, statSync } from "fs";
6
+ import { relative, join, dirname, basename } from "path";
7
+ import { glob } from "glob";
8
+ import { createScannerSignalCollector, } from "../signals/scanner-integration.js";
9
+ import { getHardcodedValueType } from "../patterns/index.js";
10
+ // Color patterns for CSS scanning
11
+ const COLOR_PATTERNS = [
12
+ /^#[0-9a-fA-F]{3,8}$/,
13
+ /^rgb\s*\(/i,
14
+ /^rgba\s*\(/i,
15
+ /^hsl\s*\(/i,
16
+ /^hsla\s*\(/i,
17
+ /^oklch\s*\(/i,
18
+ ];
19
+ // Spacing patterns for CSS scanning
20
+ const SPACING_PATTERNS = [
21
+ /^\d+(\.\d+)?(px|rem|em|vh|vw|%)$/,
22
+ ];
23
+ // App Router special file patterns
24
+ const APP_ROUTER_FILES = {
25
+ page: /^page\.(tsx?|jsx?)$/,
26
+ layout: /^layout\.(tsx?|jsx?)$/,
27
+ loading: /^loading\.(tsx?|jsx?)$/,
28
+ error: /^error\.(tsx?|jsx?)$/,
29
+ "not-found": /^not-found\.(tsx?|jsx?)$/,
30
+ template: /^template\.(tsx?|jsx?)$/,
31
+ default: /^default\.(tsx?|jsx?)$/,
32
+ route: /^route\.(tsx?|jsx?)$/,
33
+ };
34
+ export class NextJSScanner extends SignalAwareScanner {
35
+ static DEFAULT_PATTERNS = ["**/*.tsx", "**/*.jsx", "**/*.ts", "**/*.js"];
36
+ static CSS_MODULE_PATTERNS = ["**/*.module.css", "**/*.module.scss"];
37
+ async scan() {
38
+ this.clearSignals();
39
+ const result = {
40
+ items: [],
41
+ serverComponents: [],
42
+ clientComponents: [],
43
+ cssModules: [],
44
+ imageUsage: [],
45
+ routes: [],
46
+ routeGroups: [],
47
+ errors: [],
48
+ stats: {
49
+ filesScanned: 0,
50
+ itemsFound: 0,
51
+ duration: 0,
52
+ },
53
+ };
54
+ const startTime = Date.now();
55
+ // Detect App Router structure
56
+ const appDir = join(this.config.projectRoot, "app");
57
+ let hasAppRouter = false;
58
+ try {
59
+ hasAppRouter = existsSync(appDir) && statSync(appDir).isDirectory();
60
+ }
61
+ catch {
62
+ // App directory not accessible
63
+ }
64
+ if (hasAppRouter && this.config.appRouter !== false) {
65
+ // Scan App Router routes
66
+ const routeResult = await this.scanAppRouter(appDir);
67
+ result.routes = routeResult.routes;
68
+ result.routeGroups = routeResult.routeGroups;
69
+ }
70
+ // Scan components
71
+ let componentResult;
72
+ if (this.config.cache) {
73
+ componentResult = await this.runScanWithCache((file) => this.parseFile(file), NextJSScanner.DEFAULT_PATTERNS);
74
+ }
75
+ else {
76
+ componentResult = await this.runScan((file) => this.parseFile(file), NextJSScanner.DEFAULT_PATTERNS);
77
+ }
78
+ result.items = componentResult.items;
79
+ result.errors = componentResult.errors;
80
+ result.stats = {
81
+ ...componentResult.stats,
82
+ duration: Date.now() - startTime,
83
+ };
84
+ // Categorize server vs client components based on tags
85
+ for (const comp of result.items) {
86
+ const tags = comp.metadata.tags || [];
87
+ if (tags.includes("client-component")) {
88
+ result.clientComponents.push(comp);
89
+ }
90
+ else if (tags.includes("server-component")) {
91
+ result.serverComponents.push(comp);
92
+ }
93
+ }
94
+ // Scan CSS modules
95
+ if (this.config.cssModules !== false) {
96
+ result.cssModules = await this.scanCSSModules();
97
+ }
98
+ // Scan next/image usage
99
+ if (this.config.validateImage !== false) {
100
+ result.imageUsage = await this.scanImageUsage();
101
+ }
102
+ return result;
103
+ }
104
+ getSourceType() {
105
+ return "nextjs";
106
+ }
107
+ /**
108
+ * Scan the App Router directory structure
109
+ */
110
+ async scanAppRouter(appDir) {
111
+ const routes = [];
112
+ const routeGroups = new Set();
113
+ const scanDirectory = async (dir, routePath, currentGroup) => {
114
+ const entries = await this.readDirectory(dir);
115
+ // Check for route group (parentheses directory)
116
+ const dirName = basename(dir);
117
+ let groupName = currentGroup;
118
+ if (dirName.startsWith("(") && dirName.endsWith(")")) {
119
+ groupName = dirName.slice(1, -1);
120
+ routeGroups.add(groupName);
121
+ }
122
+ // Detect dynamic segments
123
+ const isDynamic = dirName.startsWith("[") && dirName.endsWith("]");
124
+ let segmentName;
125
+ if (isDynamic) {
126
+ segmentName = dirName.slice(1, -1);
127
+ // Handle catch-all routes [...slug] and optional catch-all [[...slug]]
128
+ if (segmentName.startsWith("...")) {
129
+ segmentName = segmentName.slice(3);
130
+ }
131
+ else if (segmentName.startsWith("[...")) {
132
+ segmentName = segmentName.slice(4, -1);
133
+ }
134
+ }
135
+ // Build route path (skip route group names in path)
136
+ let currentRoutePath = routePath;
137
+ if (!dirName.startsWith("(")) {
138
+ if (isDynamic && segmentName) {
139
+ currentRoutePath = routePath + `/[${segmentName}]`;
140
+ }
141
+ else if (dirName !== "app") {
142
+ currentRoutePath = routePath + "/" + dirName;
143
+ }
144
+ }
145
+ const route = {
146
+ path: currentRoutePath || "/",
147
+ isDynamic: isDynamic,
148
+ dynamicSegments: [],
149
+ routeGroup: groupName,
150
+ };
151
+ // Check for special App Router files
152
+ for (const entry of entries) {
153
+ const entryPath = join(dir, entry);
154
+ // Gracefully handle files that don't exist or are inaccessible
155
+ let stat;
156
+ try {
157
+ stat = statSync(entryPath);
158
+ }
159
+ catch {
160
+ // Skip entries that can't be accessed (symlinks, permissions, etc.)
161
+ continue;
162
+ }
163
+ if (stat.isFile()) {
164
+ for (const [fileType, pattern] of Object.entries(APP_ROUTER_FILES)) {
165
+ if (pattern.test(entry)) {
166
+ const relativePath = relative(this.config.projectRoot, entryPath);
167
+ switch (fileType) {
168
+ case "page":
169
+ route.pageFile = relativePath;
170
+ break;
171
+ case "layout":
172
+ route.layoutFile = relativePath;
173
+ break;
174
+ case "loading":
175
+ route.loadingFile = relativePath;
176
+ break;
177
+ case "error":
178
+ route.errorFile = relativePath;
179
+ break;
180
+ }
181
+ }
182
+ }
183
+ }
184
+ else if (stat.isDirectory()) {
185
+ await scanDirectory(entryPath, currentRoutePath, groupName);
186
+ }
187
+ }
188
+ // Only add route if it has at least a page
189
+ if (route.pageFile) {
190
+ routes.push(route);
191
+ }
192
+ };
193
+ await scanDirectory(appDir, "", undefined);
194
+ return { routes, routeGroups: Array.from(routeGroups) };
195
+ }
196
+ /**
197
+ * Read directory entries
198
+ */
199
+ async readDirectory(dir) {
200
+ try {
201
+ const { readdir } = await import("fs/promises");
202
+ return await readdir(dir);
203
+ }
204
+ catch {
205
+ return [];
206
+ }
207
+ }
208
+ /**
209
+ * Parse a single file for components
210
+ */
211
+ async parseFile(filePath) {
212
+ const content = await readFile(filePath, "utf-8");
213
+ const relativePath = relative(this.config.projectRoot, filePath);
214
+ // Determine if this is a client or server component
215
+ const isClientComponent = this.hasUseClientDirective(content);
216
+ const isInAppDir = relativePath.startsWith("app/") || relativePath.startsWith("app\\");
217
+ const isServerComponent = isInAppDir && !isClientComponent;
218
+ // Detect App Router file type
219
+ const fileName = basename(filePath);
220
+ let appRouterFileType;
221
+ for (const [fileType, pattern] of Object.entries(APP_ROUTER_FILES)) {
222
+ if (pattern.test(fileName)) {
223
+ appRouterFileType = fileType;
224
+ break;
225
+ }
226
+ }
227
+ // Detect route group from path
228
+ const routeGroup = this.extractRouteGroup(relativePath);
229
+ // Detect dynamic segments from path
230
+ const dynamicSegments = this.extractDynamicSegments(relativePath);
231
+ // Parse TypeScript/JavaScript
232
+ let scriptKind;
233
+ if (filePath.endsWith(".tsx")) {
234
+ scriptKind = ts.ScriptKind.TSX;
235
+ }
236
+ else if (filePath.endsWith(".jsx") || filePath.endsWith(".js")) {
237
+ scriptKind = ts.ScriptKind.JSX;
238
+ }
239
+ else {
240
+ scriptKind = ts.ScriptKind.TS;
241
+ }
242
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKind);
243
+ const components = [];
244
+ // Use 'react' as the signal collector type since Next.js is React-based
245
+ const signalCollector = createScannerSignalCollector("react", relativePath);
246
+ const visit = (node) => {
247
+ // Function declarations
248
+ if (ts.isFunctionDeclaration(node) && node.name) {
249
+ if (this.isAtModuleScope(node) && this.isReactComponent(node, sourceFile)) {
250
+ const comp = this.extractComponent(node, sourceFile, relativePath, isClientComponent, isServerComponent, appRouterFileType, routeGroup, dynamicSegments, signalCollector);
251
+ if (comp)
252
+ components.push(comp);
253
+ }
254
+ }
255
+ // Variable declarations (arrow functions, etc.)
256
+ if (ts.isVariableStatement(node) && this.isAtModuleScope(node)) {
257
+ for (const decl of node.declarationList.declarations) {
258
+ if (ts.isIdentifier(decl.name) && decl.initializer) {
259
+ if (this.isReactComponentExpression(decl.initializer, sourceFile)) {
260
+ const comp = this.extractVariableComponent(decl, sourceFile, relativePath, isClientComponent, isServerComponent, appRouterFileType, routeGroup, dynamicSegments, signalCollector);
261
+ if (comp)
262
+ components.push(comp);
263
+ }
264
+ }
265
+ }
266
+ }
267
+ // Default exports (common in Next.js pages/layouts)
268
+ if (ts.isExportAssignment(node) && !node.isExportEquals) {
269
+ const expr = node.expression;
270
+ if (ts.isIdentifier(expr)) {
271
+ // export default ComponentName - already handled by function/variable declarations
272
+ }
273
+ else if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {
274
+ // export default () => {} or export default function() {}
275
+ const comp = this.extractDefaultExportComponent(node, sourceFile, relativePath, isClientComponent, isServerComponent, appRouterFileType, routeGroup, dynamicSegments, signalCollector);
276
+ if (comp)
277
+ components.push(comp);
278
+ }
279
+ }
280
+ ts.forEachChild(node, visit);
281
+ };
282
+ ts.forEachChild(sourceFile, visit);
283
+ // Add signals
284
+ this.addSignals(relativePath, signalCollector.getEmitter());
285
+ return components;
286
+ }
287
+ /**
288
+ * Check if content has 'use client' directive
289
+ */
290
+ hasUseClientDirective(content) {
291
+ // Check first few lines for 'use client'
292
+ const lines = content.split("\n").slice(0, 10);
293
+ for (const line of lines) {
294
+ const trimmed = line.trim();
295
+ if (trimmed === '"use client"' || trimmed === "'use client'" || trimmed === '"use client";' || trimmed === "'use client';") {
296
+ return true;
297
+ }
298
+ // Stop checking if we hit actual code (not comments or empty lines)
299
+ if (trimmed && !trimmed.startsWith("//") && !trimmed.startsWith("/*") && !trimmed.startsWith("*")) {
300
+ if (!trimmed.includes("use client")) {
301
+ break;
302
+ }
303
+ }
304
+ }
305
+ return false;
306
+ }
307
+ /**
308
+ * Extract route group from file path
309
+ */
310
+ extractRouteGroup(filePath) {
311
+ const match = filePath.match(/\(([^)]+)\)/);
312
+ return match ? match[1] : undefined;
313
+ }
314
+ /**
315
+ * Extract dynamic segments from file path
316
+ */
317
+ extractDynamicSegments(filePath) {
318
+ const segments = [];
319
+ const matches = filePath.matchAll(/\[([^\]]+)\]/g);
320
+ for (const match of matches) {
321
+ let segment = match[1];
322
+ if (segment) {
323
+ // Handle catch-all routes
324
+ if (segment.startsWith("...")) {
325
+ segment = segment.slice(3);
326
+ }
327
+ segments.push(segment);
328
+ }
329
+ }
330
+ return segments;
331
+ }
332
+ /**
333
+ * Check if node is at module scope
334
+ */
335
+ isAtModuleScope(node) {
336
+ let current = node.parent;
337
+ while (current) {
338
+ if (ts.isFunctionDeclaration(current) ||
339
+ ts.isFunctionExpression(current) ||
340
+ ts.isArrowFunction(current) ||
341
+ ts.isMethodDeclaration(current)) {
342
+ return false;
343
+ }
344
+ if (ts.isSourceFile(current)) {
345
+ return true;
346
+ }
347
+ current = current.parent;
348
+ }
349
+ return false;
350
+ }
351
+ /**
352
+ * Check if a function declaration is a React component
353
+ */
354
+ isReactComponent(node, sourceFile) {
355
+ if (!node.name)
356
+ return false;
357
+ const name = node.name.getText(sourceFile);
358
+ if (!/^[A-Z]/.test(name))
359
+ return false;
360
+ // Check for JSX
361
+ if (this.returnsJsx(node))
362
+ return true;
363
+ // Check return type
364
+ if (node.type) {
365
+ const returnType = node.type.getText(sourceFile);
366
+ if (returnType.includes("ReactNode") ||
367
+ returnType.includes("ReactElement") ||
368
+ returnType.includes("JSX.Element")) {
369
+ return true;
370
+ }
371
+ }
372
+ return false;
373
+ }
374
+ /**
375
+ * Check if an expression is a React component
376
+ */
377
+ isReactComponentExpression(node, sourceFile) {
378
+ if (ts.isAsExpression(node)) {
379
+ return this.isReactComponentExpression(node.expression, sourceFile);
380
+ }
381
+ if (ts.isParenthesizedExpression(node)) {
382
+ return this.isReactComponentExpression(node.expression, sourceFile);
383
+ }
384
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
385
+ if (this.returnsJsx(node))
386
+ return true;
387
+ if (node.type) {
388
+ const returnType = node.type.getText(sourceFile);
389
+ if (returnType.includes("ReactNode") ||
390
+ returnType.includes("ReactElement") ||
391
+ returnType.includes("JSX.Element")) {
392
+ return true;
393
+ }
394
+ }
395
+ return false;
396
+ }
397
+ if (ts.isCallExpression(node)) {
398
+ const callText = node.expression.getText(sourceFile);
399
+ if (callText.includes("forwardRef") ||
400
+ callText.includes("memo") ||
401
+ callText === "lazy" ||
402
+ callText === "React.lazy") {
403
+ return true;
404
+ }
405
+ }
406
+ return false;
407
+ }
408
+ /**
409
+ * Check if a function returns JSX
410
+ */
411
+ returnsJsx(node) {
412
+ let hasJsx = false;
413
+ const checkNode = (n) => {
414
+ if (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n) || ts.isJsxFragment(n)) {
415
+ hasJsx = true;
416
+ return;
417
+ }
418
+ ts.forEachChild(n, checkNode);
419
+ };
420
+ if (node.body) {
421
+ checkNode(node.body);
422
+ }
423
+ return hasJsx;
424
+ }
425
+ /**
426
+ * Extract component from function declaration
427
+ */
428
+ extractComponent(node, sourceFile, relativePath, isClientComponent, isServerComponent, appRouterFileType, routeGroup, _dynamicSegments, signalCollector) {
429
+ if (!node.name)
430
+ return null;
431
+ const name = node.name.getText(sourceFile);
432
+ const line = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
433
+ const props = this.extractProps(node.parameters, sourceFile);
434
+ const hardcodedValues = this.extractHardcodedValues(node, sourceFile, signalCollector);
435
+ // Build tags
436
+ const tags = [];
437
+ if (isClientComponent)
438
+ tags.push("client-component");
439
+ if (isServerComponent)
440
+ tags.push("server-component");
441
+ if (appRouterFileType)
442
+ tags.push(`app-router-${appRouterFileType}`);
443
+ if (routeGroup)
444
+ tags.push(`route-group-${routeGroup}`);
445
+ signalCollector.collectComponentDef(name, line, {
446
+ isClientComponent,
447
+ isServerComponent,
448
+ appRouterFileType,
449
+ });
450
+ // Use react source type for compatibility
451
+ const source = {
452
+ type: "react",
453
+ path: relativePath,
454
+ exportName: name,
455
+ line,
456
+ };
457
+ return {
458
+ id: createComponentId(source, name),
459
+ name,
460
+ source,
461
+ props,
462
+ variants: [],
463
+ tokens: [],
464
+ dependencies: this.extractDependencies(node, sourceFile, signalCollector),
465
+ metadata: {
466
+ tags,
467
+ hardcodedValues: hardcodedValues.length > 0 ? hardcodedValues : undefined,
468
+ },
469
+ scannedAt: new Date(),
470
+ };
471
+ }
472
+ /**
473
+ * Extract component from variable declaration
474
+ */
475
+ extractVariableComponent(node, sourceFile, relativePath, isClientComponent, isServerComponent, appRouterFileType, routeGroup, _dynamicSegments, signalCollector) {
476
+ if (!ts.isIdentifier(node.name))
477
+ return null;
478
+ const name = node.name.getText(sourceFile);
479
+ if (!/^[A-Z]/.test(name))
480
+ return null;
481
+ const line = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
482
+ const hardcodedValues = this.extractHardcodedValues(node, sourceFile, signalCollector);
483
+ let props = [];
484
+ const init = node.initializer;
485
+ if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) {
486
+ props = this.extractProps(init.parameters, sourceFile);
487
+ }
488
+ const tags = [];
489
+ if (isClientComponent)
490
+ tags.push("client-component");
491
+ if (isServerComponent)
492
+ tags.push("server-component");
493
+ if (appRouterFileType)
494
+ tags.push(`app-router-${appRouterFileType}`);
495
+ if (routeGroup)
496
+ tags.push(`route-group-${routeGroup}`);
497
+ signalCollector.collectComponentDef(name, line, {
498
+ isClientComponent,
499
+ isServerComponent,
500
+ appRouterFileType,
501
+ });
502
+ const source = {
503
+ type: "react",
504
+ path: relativePath,
505
+ exportName: name,
506
+ line,
507
+ };
508
+ return {
509
+ id: createComponentId(source, name),
510
+ name,
511
+ source,
512
+ props,
513
+ variants: [],
514
+ tokens: [],
515
+ dependencies: [],
516
+ metadata: {
517
+ tags,
518
+ hardcodedValues: hardcodedValues.length > 0 ? hardcodedValues : undefined,
519
+ },
520
+ scannedAt: new Date(),
521
+ };
522
+ }
523
+ /**
524
+ * Extract component from default export
525
+ */
526
+ extractDefaultExportComponent(node, sourceFile, relativePath, isClientComponent, isServerComponent, appRouterFileType, routeGroup, _dynamicSegments, signalCollector) {
527
+ const line = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
528
+ // Generate name from file path for default exports
529
+ const fileName = basename(relativePath, ".tsx").replace(/\.jsx?$/, "");
530
+ let name;
531
+ if (appRouterFileType) {
532
+ // Use the route segment + file type for App Router files
533
+ const pathParts = dirname(relativePath).split("/").filter(p => p && p !== "app");
534
+ const routeSegment = pathParts[pathParts.length - 1] || "Root";
535
+ name = this.toPascalCase(routeSegment) + this.toPascalCase(appRouterFileType);
536
+ }
537
+ else {
538
+ name = this.toPascalCase(fileName);
539
+ }
540
+ const expr = node.expression;
541
+ let props = [];
542
+ if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {
543
+ props = this.extractProps(expr.parameters, sourceFile);
544
+ }
545
+ const hardcodedValues = this.extractHardcodedValues(node, sourceFile, signalCollector);
546
+ const tags = ["default-export"];
547
+ if (isClientComponent)
548
+ tags.push("client-component");
549
+ if (isServerComponent)
550
+ tags.push("server-component");
551
+ if (appRouterFileType)
552
+ tags.push(`app-router-${appRouterFileType}`);
553
+ if (routeGroup)
554
+ tags.push(`route-group-${routeGroup}`);
555
+ signalCollector.collectComponentDef(name, line, {
556
+ isClientComponent,
557
+ isServerComponent,
558
+ appRouterFileType,
559
+ isDefaultExport: true,
560
+ });
561
+ const source = {
562
+ type: "react",
563
+ path: relativePath,
564
+ exportName: "default",
565
+ line,
566
+ };
567
+ return {
568
+ id: createComponentId(source, name),
569
+ name,
570
+ source,
571
+ props,
572
+ variants: [],
573
+ tokens: [],
574
+ dependencies: [],
575
+ metadata: {
576
+ tags,
577
+ hardcodedValues: hardcodedValues.length > 0 ? hardcodedValues : undefined,
578
+ },
579
+ scannedAt: new Date(),
580
+ };
581
+ }
582
+ /**
583
+ * Convert string to PascalCase
584
+ */
585
+ toPascalCase(str) {
586
+ return str
587
+ .replace(/[-_](.)/g, (_, c) => c.toUpperCase())
588
+ .replace(/^(.)/, (_, c) => c.toUpperCase());
589
+ }
590
+ /**
591
+ * Extract props from parameters
592
+ */
593
+ extractProps(parameters, sourceFile) {
594
+ const props = [];
595
+ const propsParam = parameters[0];
596
+ if (!propsParam)
597
+ return props;
598
+ const typeNode = propsParam.type;
599
+ if (typeNode && ts.isTypeLiteralNode(typeNode)) {
600
+ for (const member of typeNode.members) {
601
+ if (ts.isPropertySignature(member) && member.name) {
602
+ props.push({
603
+ name: member.name.getText(sourceFile),
604
+ type: member.type ? member.type.getText(sourceFile) : "unknown",
605
+ required: !member.questionToken,
606
+ });
607
+ }
608
+ }
609
+ }
610
+ else if (typeNode && ts.isTypeReferenceNode(typeNode)) {
611
+ props.push({
612
+ name: "props",
613
+ type: typeNode.getText(sourceFile),
614
+ required: true,
615
+ });
616
+ }
617
+ if (ts.isObjectBindingPattern(propsParam.name)) {
618
+ for (const element of propsParam.name.elements) {
619
+ if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
620
+ props.push({
621
+ name: element.name.getText(sourceFile),
622
+ type: "unknown",
623
+ required: !element.initializer,
624
+ defaultValue: element.initializer ? element.initializer.getText(sourceFile) : undefined,
625
+ });
626
+ }
627
+ }
628
+ }
629
+ return props;
630
+ }
631
+ /**
632
+ * Extract dependencies (component usage)
633
+ */
634
+ extractDependencies(node, sourceFile, signalCollector) {
635
+ const deps = new Set();
636
+ const visit = (n) => {
637
+ if (ts.isJsxOpeningElement(n) || ts.isJsxSelfClosingElement(n)) {
638
+ const tagName = n.tagName.getText(sourceFile);
639
+ if (/^[A-Z]/.test(tagName)) {
640
+ deps.add(tagName);
641
+ if (signalCollector) {
642
+ const depLine = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)).line + 1;
643
+ signalCollector.collectComponentUsage(tagName, depLine);
644
+ }
645
+ }
646
+ }
647
+ ts.forEachChild(n, visit);
648
+ };
649
+ visit(node);
650
+ return Array.from(deps);
651
+ }
652
+ /**
653
+ * Extract hardcoded values from a node
654
+ */
655
+ extractHardcodedValues(node, sourceFile, signalCollector) {
656
+ const hardcoded = [];
657
+ const visit = (n) => {
658
+ if (ts.isJsxAttribute(n)) {
659
+ const attrName = n.name.getText(sourceFile);
660
+ // Style prop
661
+ if (attrName === "style" && n.initializer) {
662
+ const styleValues = this.extractStyleObjectValues(n.initializer, sourceFile, signalCollector);
663
+ hardcoded.push(...styleValues);
664
+ }
665
+ // Direct color/spacing props
666
+ if (["color", "bg", "backgroundColor", "fill", "stroke"].includes(attrName)) {
667
+ const value = this.getJsxAttributeValue(n, sourceFile);
668
+ if (value && this.isHardcodedColor(value)) {
669
+ const attrLine = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)).line + 1;
670
+ hardcoded.push({
671
+ type: "color",
672
+ value,
673
+ property: attrName,
674
+ location: `line ${attrLine}`,
675
+ });
676
+ signalCollector?.collectFromValue(value, attrName, attrLine);
677
+ }
678
+ }
679
+ }
680
+ ts.forEachChild(n, visit);
681
+ };
682
+ visit(node);
683
+ // Deduplicate
684
+ const seen = new Set();
685
+ return hardcoded.filter((h) => {
686
+ const key = `${h.property}:${h.value}`;
687
+ if (seen.has(key))
688
+ return false;
689
+ seen.add(key);
690
+ return true;
691
+ });
692
+ }
693
+ /**
694
+ * Extract hardcoded values from style object
695
+ */
696
+ extractStyleObjectValues(initializer, sourceFile, signalCollector) {
697
+ const values = [];
698
+ const styleProperties = {
699
+ color: "color",
700
+ backgroundColor: "color",
701
+ background: "color",
702
+ borderColor: "color",
703
+ padding: "spacing",
704
+ paddingTop: "spacing",
705
+ paddingRight: "spacing",
706
+ paddingBottom: "spacing",
707
+ paddingLeft: "spacing",
708
+ margin: "spacing",
709
+ marginTop: "spacing",
710
+ marginRight: "spacing",
711
+ marginBottom: "spacing",
712
+ marginLeft: "spacing",
713
+ fontSize: "fontSize",
714
+ };
715
+ const processObject = (obj) => {
716
+ for (const prop of obj.properties) {
717
+ if (ts.isPropertyAssignment(prop) && prop.name) {
718
+ const propName = prop.name.getText(sourceFile);
719
+ const valueType = styleProperties[propName];
720
+ if (valueType && prop.initializer) {
721
+ const value = this.getLiteralValue(prop.initializer, sourceFile);
722
+ if (value && this.isHardcodedValue(value, valueType)) {
723
+ const propLine = sourceFile.getLineAndCharacterOfPosition(prop.getStart(sourceFile)).line + 1;
724
+ values.push({
725
+ type: valueType,
726
+ value,
727
+ property: propName,
728
+ location: `line ${propLine}`,
729
+ });
730
+ signalCollector?.collectFromValue(value, propName, propLine);
731
+ }
732
+ }
733
+ }
734
+ }
735
+ };
736
+ if (ts.isJsxExpression(initializer) && initializer.expression) {
737
+ if (ts.isObjectLiteralExpression(initializer.expression)) {
738
+ processObject(initializer.expression);
739
+ }
740
+ }
741
+ return values;
742
+ }
743
+ /**
744
+ * Get JSX attribute value
745
+ */
746
+ getJsxAttributeValue(attr, sourceFile) {
747
+ if (!attr.initializer)
748
+ return null;
749
+ if (ts.isStringLiteral(attr.initializer)) {
750
+ return attr.initializer.text;
751
+ }
752
+ if (ts.isJsxExpression(attr.initializer) && attr.initializer.expression) {
753
+ return this.getLiteralValue(attr.initializer.expression, sourceFile);
754
+ }
755
+ return null;
756
+ }
757
+ /**
758
+ * Get literal value from expression
759
+ */
760
+ getLiteralValue(node, _sourceFile) {
761
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
762
+ return node.text;
763
+ }
764
+ if (ts.isNumericLiteral(node)) {
765
+ return node.text;
766
+ }
767
+ return null;
768
+ }
769
+ /**
770
+ * Check if value is hardcoded
771
+ */
772
+ isHardcodedValue(value, type) {
773
+ if (value.includes("var(--") || value.includes("theme.") || value.includes("tokens.")) {
774
+ return false;
775
+ }
776
+ switch (type) {
777
+ case "color":
778
+ return this.isHardcodedColor(value);
779
+ case "spacing":
780
+ case "fontSize":
781
+ return this.isHardcodedSpacing(value);
782
+ default:
783
+ return false;
784
+ }
785
+ }
786
+ /**
787
+ * Check if value is a hardcoded color
788
+ */
789
+ isHardcodedColor(value) {
790
+ if (value.includes("var(--") || value.includes("theme.") || value.includes("tokens.")) {
791
+ return false;
792
+ }
793
+ if (["inherit", "transparent", "currentColor", "initial", "unset"].includes(value)) {
794
+ return false;
795
+ }
796
+ return COLOR_PATTERNS.some((p) => p.test(value));
797
+ }
798
+ /**
799
+ * Check if value is hardcoded spacing
800
+ */
801
+ isHardcodedSpacing(value) {
802
+ if (value.includes("var(--") || value.includes("theme.") || value.includes("tokens.")) {
803
+ return false;
804
+ }
805
+ if (["auto", "inherit", "0", "100%", "50%"].includes(value)) {
806
+ return false;
807
+ }
808
+ return SPACING_PATTERNS.some((p) => p.test(value));
809
+ }
810
+ /**
811
+ * Scan CSS modules for hardcoded values
812
+ */
813
+ async scanCSSModules() {
814
+ const results = [];
815
+ for (const pattern of NextJSScanner.CSS_MODULE_PATTERNS) {
816
+ try {
817
+ const files = await glob(pattern, {
818
+ cwd: this.config.projectRoot,
819
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.next/**"],
820
+ absolute: true,
821
+ });
822
+ for (const file of files) {
823
+ const analysis = this.analyzeCSSModule(file);
824
+ if (analysis) {
825
+ results.push(analysis);
826
+ }
827
+ }
828
+ }
829
+ catch {
830
+ // Continue on error
831
+ }
832
+ }
833
+ return results;
834
+ }
835
+ /**
836
+ * Analyze a single CSS module file
837
+ */
838
+ analyzeCSSModule(filePath) {
839
+ try {
840
+ const content = readFileSync(filePath, "utf-8");
841
+ const relativePath = relative(this.config.projectRoot, filePath);
842
+ const analysis = {
843
+ path: relativePath,
844
+ classNames: [],
845
+ hardcodedValues: [],
846
+ cssVariables: [],
847
+ };
848
+ // Extract class names
849
+ const classNameRegex = /\.([a-zA-Z_][\w-]*)/g;
850
+ let match;
851
+ while ((match = classNameRegex.exec(content)) !== null) {
852
+ const className = match[1];
853
+ if (className && !analysis.classNames.includes(className)) {
854
+ analysis.classNames.push(className);
855
+ }
856
+ }
857
+ // Extract CSS variables used
858
+ const varRegex = /var\(--([^)]+)\)/g;
859
+ while ((match = varRegex.exec(content)) !== null) {
860
+ const varName = match[1];
861
+ if (varName && !analysis.cssVariables.includes(varName)) {
862
+ analysis.cssVariables.push(varName);
863
+ }
864
+ }
865
+ // Find hardcoded values
866
+ let lineNum = 1;
867
+ const lines = content.split("\n");
868
+ for (const lineContent of lines) {
869
+ const localRegex = /([a-z-]+)\s*:\s*([^;{}]+)/g;
870
+ let propMatch;
871
+ while ((propMatch = localRegex.exec(lineContent)) !== null) {
872
+ const property = propMatch[1];
873
+ const value = propMatch[2]?.trim();
874
+ if (!property || !value)
875
+ continue;
876
+ // Skip if using CSS variable
877
+ if (value.includes("var(--"))
878
+ continue;
879
+ const hardcodedType = getHardcodedValueType(property, value);
880
+ if (hardcodedType) {
881
+ analysis.hardcodedValues.push({
882
+ type: hardcodedType,
883
+ value,
884
+ property,
885
+ location: `${relativePath}:${lineNum}`,
886
+ });
887
+ }
888
+ }
889
+ lineNum++;
890
+ }
891
+ return analysis;
892
+ }
893
+ catch {
894
+ return null;
895
+ }
896
+ }
897
+ /**
898
+ * Scan for next/image usage
899
+ */
900
+ async scanImageUsage() {
901
+ const results = [];
902
+ try {
903
+ const files = await glob("**/*.{tsx,jsx}", {
904
+ cwd: this.config.projectRoot,
905
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.next/**"],
906
+ absolute: true,
907
+ });
908
+ for (const file of files) {
909
+ const content = await readFile(file, "utf-8");
910
+ const relativePath = relative(this.config.projectRoot, file);
911
+ // Check if file imports next/image
912
+ if (!content.includes("next/image") && !content.includes("from 'next/image'") && !content.includes('from "next/image"')) {
913
+ continue;
914
+ }
915
+ // Parse and find Image usage
916
+ const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
917
+ const visit = (node) => {
918
+ if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) {
919
+ const tagName = node.tagName.getText(sourceFile);
920
+ if (tagName === "Image" || tagName === "NextImage") {
921
+ const imageLine = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
922
+ const usage = this.analyzeImageUsage(node, sourceFile, relativePath, imageLine);
923
+ results.push(usage);
924
+ }
925
+ }
926
+ ts.forEachChild(node, visit);
927
+ };
928
+ ts.forEachChild(sourceFile, visit);
929
+ }
930
+ }
931
+ catch {
932
+ // Continue on error
933
+ }
934
+ return results;
935
+ }
936
+ /**
937
+ * Analyze a single Image component usage
938
+ */
939
+ analyzeImageUsage(node, sourceFile, file, line) {
940
+ const usage = {
941
+ file,
942
+ line,
943
+ hasAlt: false,
944
+ hasWidth: false,
945
+ hasHeight: false,
946
+ hasFill: false,
947
+ styleIssues: [],
948
+ };
949
+ const attributes = node.attributes;
950
+ for (const attr of attributes.properties) {
951
+ if (ts.isJsxAttribute(attr) && attr.name) {
952
+ const attrName = attr.name.getText(sourceFile);
953
+ switch (attrName) {
954
+ case "alt":
955
+ usage.hasAlt = true;
956
+ break;
957
+ case "width":
958
+ usage.hasWidth = true;
959
+ break;
960
+ case "height":
961
+ usage.hasHeight = true;
962
+ break;
963
+ case "fill":
964
+ usage.hasFill = true;
965
+ break;
966
+ case "style":
967
+ // Check for hardcoded values in style
968
+ if (attr.initializer) {
969
+ const styleValues = this.extractStyleObjectValues(attr.initializer, sourceFile);
970
+ usage.styleIssues.push(...styleValues);
971
+ }
972
+ break;
973
+ }
974
+ }
975
+ }
976
+ return usage;
977
+ }
978
+ }
979
+ //# sourceMappingURL=nextjs-scanner.js.map