@wtdlee/repomap 0.1.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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +527 -0
  3. package/dist/analyzers/base-analyzer.d.ts +46 -0
  4. package/dist/analyzers/base-analyzer.d.ts.map +1 -0
  5. package/dist/analyzers/base-analyzer.js +48 -0
  6. package/dist/analyzers/base-analyzer.js.map +1 -0
  7. package/dist/analyzers/dataflow-analyzer.d.ts +30 -0
  8. package/dist/analyzers/dataflow-analyzer.d.ts.map +1 -0
  9. package/dist/analyzers/dataflow-analyzer.js +426 -0
  10. package/dist/analyzers/dataflow-analyzer.js.map +1 -0
  11. package/dist/analyzers/graphql-analyzer.d.ts +23 -0
  12. package/dist/analyzers/graphql-analyzer.d.ts.map +1 -0
  13. package/dist/analyzers/graphql-analyzer.js +387 -0
  14. package/dist/analyzers/graphql-analyzer.js.map +1 -0
  15. package/dist/analyzers/index.d.ts +6 -0
  16. package/dist/analyzers/index.d.ts.map +1 -0
  17. package/dist/analyzers/index.js +6 -0
  18. package/dist/analyzers/index.js.map +1 -0
  19. package/dist/analyzers/pages-analyzer.d.ts +85 -0
  20. package/dist/analyzers/pages-analyzer.d.ts.map +1 -0
  21. package/dist/analyzers/pages-analyzer.js +1696 -0
  22. package/dist/analyzers/pages-analyzer.js.map +1 -0
  23. package/dist/analyzers/rails/index.d.ts +47 -0
  24. package/dist/analyzers/rails/index.d.ts.map +1 -0
  25. package/dist/analyzers/rails/index.js +146 -0
  26. package/dist/analyzers/rails/index.js.map +1 -0
  27. package/dist/analyzers/rails/rails-controller-analyzer.d.ts +83 -0
  28. package/dist/analyzers/rails/rails-controller-analyzer.d.ts.map +1 -0
  29. package/dist/analyzers/rails/rails-controller-analyzer.js +479 -0
  30. package/dist/analyzers/rails/rails-controller-analyzer.js.map +1 -0
  31. package/dist/analyzers/rails/rails-grpc-analyzer.d.ts +45 -0
  32. package/dist/analyzers/rails/rails-grpc-analyzer.d.ts.map +1 -0
  33. package/dist/analyzers/rails/rails-grpc-analyzer.js +263 -0
  34. package/dist/analyzers/rails/rails-grpc-analyzer.js.map +1 -0
  35. package/dist/analyzers/rails/rails-model-analyzer.d.ts +89 -0
  36. package/dist/analyzers/rails/rails-model-analyzer.d.ts.map +1 -0
  37. package/dist/analyzers/rails/rails-model-analyzer.js +494 -0
  38. package/dist/analyzers/rails/rails-model-analyzer.js.map +1 -0
  39. package/dist/analyzers/rails/rails-react-analyzer.d.ts +42 -0
  40. package/dist/analyzers/rails/rails-react-analyzer.d.ts.map +1 -0
  41. package/dist/analyzers/rails/rails-react-analyzer.js +530 -0
  42. package/dist/analyzers/rails/rails-react-analyzer.js.map +1 -0
  43. package/dist/analyzers/rails/rails-routes-analyzer.d.ts +63 -0
  44. package/dist/analyzers/rails/rails-routes-analyzer.d.ts.map +1 -0
  45. package/dist/analyzers/rails/rails-routes-analyzer.js +541 -0
  46. package/dist/analyzers/rails/rails-routes-analyzer.js.map +1 -0
  47. package/dist/analyzers/rails/rails-view-analyzer.d.ts +50 -0
  48. package/dist/analyzers/rails/rails-view-analyzer.d.ts.map +1 -0
  49. package/dist/analyzers/rails/rails-view-analyzer.js +387 -0
  50. package/dist/analyzers/rails/rails-view-analyzer.js.map +1 -0
  51. package/dist/analyzers/rails/ruby-parser.d.ts +64 -0
  52. package/dist/analyzers/rails/ruby-parser.d.ts.map +1 -0
  53. package/dist/analyzers/rails/ruby-parser.js +213 -0
  54. package/dist/analyzers/rails/ruby-parser.js.map +1 -0
  55. package/dist/analyzers/rest-api-analyzer.d.ts +66 -0
  56. package/dist/analyzers/rest-api-analyzer.d.ts.map +1 -0
  57. package/dist/analyzers/rest-api-analyzer.js +480 -0
  58. package/dist/analyzers/rest-api-analyzer.js.map +1 -0
  59. package/dist/cli.d.ts +3 -0
  60. package/dist/cli.d.ts.map +1 -0
  61. package/dist/cli.js +550 -0
  62. package/dist/cli.js.map +1 -0
  63. package/dist/core/cache.d.ts +48 -0
  64. package/dist/core/cache.d.ts.map +1 -0
  65. package/dist/core/cache.js +152 -0
  66. package/dist/core/cache.js.map +1 -0
  67. package/dist/core/engine.d.ts +47 -0
  68. package/dist/core/engine.d.ts.map +1 -0
  69. package/dist/core/engine.js +320 -0
  70. package/dist/core/engine.js.map +1 -0
  71. package/dist/core/index.d.ts +3 -0
  72. package/dist/core/index.d.ts.map +1 -0
  73. package/dist/core/index.js +3 -0
  74. package/dist/core/index.js.map +1 -0
  75. package/dist/generators/assets/common.css +187 -0
  76. package/dist/generators/assets/docs.css +363 -0
  77. package/dist/generators/assets/page-map.css +305 -0
  78. package/dist/generators/assets/rails-map.css +473 -0
  79. package/dist/generators/index.d.ts +4 -0
  80. package/dist/generators/index.d.ts.map +1 -0
  81. package/dist/generators/index.js +4 -0
  82. package/dist/generators/index.js.map +1 -0
  83. package/dist/generators/markdown-generator.d.ts +26 -0
  84. package/dist/generators/markdown-generator.d.ts.map +1 -0
  85. package/dist/generators/markdown-generator.js +783 -0
  86. package/dist/generators/markdown-generator.js.map +1 -0
  87. package/dist/generators/mermaid-generator.d.ts +36 -0
  88. package/dist/generators/mermaid-generator.d.ts.map +1 -0
  89. package/dist/generators/mermaid-generator.js +365 -0
  90. package/dist/generators/mermaid-generator.js.map +1 -0
  91. package/dist/generators/page-map-generator.d.ts +23 -0
  92. package/dist/generators/page-map-generator.d.ts.map +1 -0
  93. package/dist/generators/page-map-generator.js +3563 -0
  94. package/dist/generators/page-map-generator.js.map +1 -0
  95. package/dist/generators/rails-map-generator.d.ts +22 -0
  96. package/dist/generators/rails-map-generator.d.ts.map +1 -0
  97. package/dist/generators/rails-map-generator.js +909 -0
  98. package/dist/generators/rails-map-generator.js.map +1 -0
  99. package/dist/index.d.ts +11 -0
  100. package/dist/index.d.ts.map +1 -0
  101. package/dist/index.js +12 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/server/doc-server.d.ts +31 -0
  104. package/dist/server/doc-server.d.ts.map +1 -0
  105. package/dist/server/doc-server.js +1233 -0
  106. package/dist/server/doc-server.js.map +1 -0
  107. package/dist/server/index.d.ts +2 -0
  108. package/dist/server/index.d.ts.map +1 -0
  109. package/dist/server/index.js +2 -0
  110. package/dist/server/index.js.map +1 -0
  111. package/dist/types.d.ts +294 -0
  112. package/dist/types.d.ts.map +1 -0
  113. package/dist/types.js +6 -0
  114. package/dist/types.js.map +1 -0
  115. package/dist/utils/env-detector.d.ts +32 -0
  116. package/dist/utils/env-detector.d.ts.map +1 -0
  117. package/dist/utils/env-detector.js +189 -0
  118. package/dist/utils/env-detector.js.map +1 -0
  119. package/dist/utils/parallel.d.ts +24 -0
  120. package/dist/utils/parallel.d.ts.map +1 -0
  121. package/dist/utils/parallel.js +71 -0
  122. package/dist/utils/parallel.js.map +1 -0
  123. package/package.json +131 -0
@@ -0,0 +1,1696 @@
1
+ import { Project, SyntaxKind, Node } from 'ts-morph';
2
+ import fg from 'fast-glob';
3
+ import * as path from 'path';
4
+ import * as fs from 'fs';
5
+ import { BaseAnalyzer } from './base-analyzer.js';
6
+ import { parallelMapSafe } from '../utils/parallel.js';
7
+ /**
8
+ * Analyzer for Next.js pages
9
+ * Next.jsページの分析器
10
+ */
11
+ export class PagesAnalyzer extends BaseAnalyzer {
12
+ project;
13
+ // Codegen Document → Operation name mapping
14
+ codegenMap = new Map();
15
+ constructor(config) {
16
+ super(config);
17
+ this.project = new Project({
18
+ tsConfigFilePath: this.resolvePath('tsconfig.json'),
19
+ skipAddingFilesFromTsConfig: true,
20
+ });
21
+ }
22
+ /**
23
+ * Extract GraphQL operation name from a gql template literal or function call
24
+ * Handles multiple patterns:
25
+ * - gql`query Name { ... }`
26
+ * - gql(/‌* GraphQL *‌/ `query Name { ... }`)
27
+ * - graphql(`query Name { ... }`)
28
+ */
29
+ extractOperationNameFromGql(text) {
30
+ // Pattern 1: Direct operation keyword with name
31
+ const directMatch = text.match(/(?:query|mutation|subscription)\s+(\w+)/);
32
+ if (directMatch && directMatch[1]) {
33
+ return directMatch[1];
34
+ }
35
+ // Pattern 2: After backtick (with possible whitespace/newlines)
36
+ const backtickMatch = text.match(/`\s*(?:query|mutation|subscription)\s+(\w+)/);
37
+ if (backtickMatch && backtickMatch[1]) {
38
+ return backtickMatch[1];
39
+ }
40
+ // Pattern 3: After GraphQL comment
41
+ const commentMatch = text.match(/GraphQL[^`]*`\s*(?:\n\s*)?(?:query|mutation|subscription)\s+(\w+)/);
42
+ if (commentMatch && commentMatch[1]) {
43
+ return commentMatch[1];
44
+ }
45
+ return null;
46
+ }
47
+ /**
48
+ * Find and extract operation name from a variable declaration in source file
49
+ * Handles cases like: const Query = gql(comment `query ActualName...`)
50
+ */
51
+ findOperationNameFromVariable(sourceFile, variableName) {
52
+ // Method 1: Try to find variable declaration directly
53
+ let varDecl = sourceFile.getVariableDeclaration(variableName);
54
+ // Method 2: Try exported declarations
55
+ if (!varDecl) {
56
+ const exportedDecls = sourceFile.getExportedDeclarations();
57
+ const exported = exportedDecls.get(variableName);
58
+ if (exported && exported.length > 0) {
59
+ const firstExport = exported[0];
60
+ if (Node.isVariableDeclaration(firstExport)) {
61
+ varDecl = firstExport;
62
+ }
63
+ }
64
+ }
65
+ // Method 3: Search all variable declarations
66
+ if (!varDecl) {
67
+ const allVarDecls = sourceFile.getVariableDeclarations();
68
+ varDecl = allVarDecls.find((v) => v.getName() === variableName);
69
+ }
70
+ // Method 4: Full text search for pattern (most comprehensive)
71
+ // This handles cases where the variable might not be found through AST
72
+ if (!varDecl) {
73
+ const fullText = sourceFile.getFullText();
74
+ // Match: const Query = gql(/* GraphQL */ `query ActualName...
75
+ // Or: const Query = gql`query ActualName...
76
+ const patterns = [
77
+ // gql(/* GraphQL */ `query Name...
78
+ new RegExp(`(?:const|let|var)\\s+${variableName}\\s*=\\s*gql\\s*\\(\\s*/\\*[^*]*\\*/\\s*\`\\s*(?:query|mutation|subscription)\\s+(\\w+)`, 's'),
79
+ // gql`query Name...
80
+ new RegExp(`(?:const|let|var)\\s+${variableName}\\s*=\\s*gql\`\\s*(?:query|mutation|subscription)\\s+(\\w+)`, 's'),
81
+ // gql(`query Name... (without comment)
82
+ new RegExp(`(?:const|let|var)\\s+${variableName}\\s*=\\s*gql\\s*\\(\`\\s*(?:query|mutation|subscription)\\s+(\\w+)`, 's'),
83
+ // graphql(`query Name...
84
+ new RegExp(`(?:const|let|var)\\s+${variableName}\\s*=\\s*graphql\\s*\\(\`\\s*(?:query|mutation|subscription)\\s+(\\w+)`, 's'),
85
+ ];
86
+ for (const pattern of patterns) {
87
+ const match = fullText.match(pattern);
88
+ if (match && match[1]) {
89
+ return match[1];
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+ // Extract from initializer
95
+ const initializer = varDecl.getInitializer();
96
+ if (!initializer)
97
+ return null;
98
+ const text = initializer.getText();
99
+ return this.extractOperationNameFromGql(text);
100
+ }
101
+ getName() {
102
+ return 'PagesAnalyzer';
103
+ }
104
+ async analyze() {
105
+ this.log('Starting page analysis...');
106
+ // Load codegen mapping if available
107
+ await this.loadCodegenMapping();
108
+ // Analyze _app.tsx for global providers/context
109
+ await this.analyzeAppFile();
110
+ // Find page files from multiple possible locations
111
+ const pageFiles = await this.findPageFiles();
112
+ this.log(`Found ${pageFiles.length} page files`);
113
+ // Add all files to project first (sequential for ts-morph safety)
114
+ for (const filePath of pageFiles) {
115
+ try {
116
+ this.project.addSourceFileAtPath(filePath);
117
+ }
118
+ catch {
119
+ // Ignore files that can't be added
120
+ }
121
+ }
122
+ // Analyze pages in parallel (using already-added source files)
123
+ const pages = await parallelMapSafe(pageFiles, async (filePath) => {
124
+ // Determine the correct pagesPath based on the file location
125
+ const pagesPath = this.detectPagesRoot(filePath);
126
+ return this.analyzePageFile(filePath, pagesPath);
127
+ }, 4 // Limit concurrency for ts-morph stability
128
+ );
129
+ // Filter out null results
130
+ const validPages = pages.filter((p) => p !== null);
131
+ this.log(`Analyzed ${validPages.length} pages successfully`);
132
+ return { pages: validPages };
133
+ }
134
+ async analyzePageFile(filePath, pagesPath) {
135
+ const sourceFile = this.project.getSourceFile(filePath);
136
+ if (!sourceFile)
137
+ return null;
138
+ const relativePath = path.relative(pagesPath, filePath);
139
+ const routePath = this.filePathToRoutePath(relativePath);
140
+ // Extract page component
141
+ const pageComponent = this.findPageComponent(sourceFile);
142
+ if (!pageComponent) {
143
+ return null;
144
+ }
145
+ // Extract various page information
146
+ const params = this.extractRouteParams(routePath);
147
+ const layout = this.extractLayout(sourceFile);
148
+ const authentication = this.extractAuthRequirement(sourceFile);
149
+ const permissions = this.extractPermissions(sourceFile);
150
+ const dataFetching = this.extractDataFetching(sourceFile);
151
+ const navigation = this.extractNavigation(sourceFile);
152
+ const linkedPages = this.extractLinkedPages(sourceFile);
153
+ const steps = this.extractSteps(sourceFile);
154
+ return {
155
+ path: routePath,
156
+ filePath: relativePath,
157
+ component: pageComponent,
158
+ params,
159
+ layout,
160
+ authentication,
161
+ permissions,
162
+ dataFetching,
163
+ navigation,
164
+ linkedPages,
165
+ steps: steps.length > 0 ? steps : undefined,
166
+ };
167
+ }
168
+ /**
169
+ * Detect the pages root directory from a file path
170
+ * e.g., /project/src/pages/users/index.tsx -> /project/src/pages
171
+ */
172
+ detectPagesRoot(filePath) {
173
+ // Common pages directory patterns to look for (exclude components/pages - those are reusable components)
174
+ const pagesPatterns = [
175
+ '/src/pages/',
176
+ '/pages/',
177
+ '/src/app/',
178
+ '/app/',
179
+ '/frontend/src/pages/',
180
+ '/app/javascript/pages/',
181
+ ];
182
+ for (const pattern of pagesPatterns) {
183
+ const idx = filePath.indexOf(pattern);
184
+ if (idx !== -1) {
185
+ // Return the path up to and including the pages directory
186
+ return filePath.substring(0, idx + pattern.length - 1);
187
+ }
188
+ }
189
+ // Fallback: use basePath
190
+ return this.basePath;
191
+ }
192
+ filePathToRoutePath(filePath) {
193
+ return ('/' +
194
+ filePath
195
+ .replace(/\.tsx?$/, '')
196
+ .replace(/\/index$/, '')
197
+ .replace(/\[\.\.\.(\w+)\]/g, '*')
198
+ .replace(/\[(\w+)\]/g, ':$1'));
199
+ }
200
+ extractRouteParams(routePath) {
201
+ const params = [];
202
+ const paramRegex = /:(\w+)/g;
203
+ let match;
204
+ while ((match = paramRegex.exec(routePath)) !== null) {
205
+ params.push(match[1]);
206
+ }
207
+ return params;
208
+ }
209
+ findPageComponent(sourceFile) {
210
+ // Find default export
211
+ const defaultExport = sourceFile.getDefaultExportSymbol();
212
+ if (defaultExport) {
213
+ const name = defaultExport.getName();
214
+ // Handle 'default' export name
215
+ if (name !== 'default') {
216
+ return name;
217
+ }
218
+ }
219
+ // Check for export default function/const
220
+ const exportAssignment = sourceFile.getExportAssignment((e) => !e.isExportEquals());
221
+ if (exportAssignment) {
222
+ const expr = exportAssignment.getExpression();
223
+ if (expr) {
224
+ const text = expr.getText();
225
+ // If it's a simple identifier, return it
226
+ if (/^[A-Z][a-zA-Z0-9]*$/.test(text)) {
227
+ return text;
228
+ }
229
+ // If it's a function expression, try to find the function name
230
+ if (Node.isFunctionExpression(expr) || Node.isArrowFunction(expr)) {
231
+ return 'default';
232
+ }
233
+ }
234
+ }
235
+ // Find export default function declaration
236
+ const functions = sourceFile.getFunctions();
237
+ for (const func of functions) {
238
+ if (func.isDefaultExport()) {
239
+ return func.getName() || 'default';
240
+ }
241
+ }
242
+ // Find Page variable
243
+ const pageVar = sourceFile.getVariableDeclaration('Page');
244
+ if (pageVar) {
245
+ return 'Page';
246
+ }
247
+ // Find NextPage typed variable (even without default export)
248
+ const varDeclarations = sourceFile.getVariableDeclarations();
249
+ for (const varDecl of varDeclarations) {
250
+ const typeNode = varDecl.getTypeNode();
251
+ if (typeNode) {
252
+ const typeText = typeNode.getText();
253
+ if (typeText.includes('NextPage') ||
254
+ typeText.includes('FC') ||
255
+ typeText.includes('React.FC')) {
256
+ return varDecl.getName();
257
+ }
258
+ }
259
+ }
260
+ // Find any PascalCase exported function/const that looks like a component
261
+ for (const varDecl of varDeclarations) {
262
+ const name = varDecl.getName();
263
+ if (/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
264
+ const init = varDecl.getInitializer();
265
+ if (init && (Node.isArrowFunction(init) || Node.isFunctionExpression(init))) {
266
+ // Check if it returns JSX
267
+ const text = init.getText();
268
+ if (text.includes('return') && (text.includes('<') || text.includes('jsx'))) {
269
+ return name;
270
+ }
271
+ }
272
+ }
273
+ }
274
+ return null;
275
+ }
276
+ extractLayout(sourceFile) {
277
+ // Look for getLayout property
278
+ const getLayoutAssignment = sourceFile
279
+ .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)
280
+ .find((node) => node.getName() === 'getLayout');
281
+ if (getLayoutAssignment) {
282
+ const parent = getLayoutAssignment.getParent();
283
+ if (Node.isBinaryExpression(parent)) {
284
+ const right = parent.getRight();
285
+ // Extract layout component name from the function
286
+ const jsxElements = right.getDescendantsOfKind(SyntaxKind.JsxOpeningElement);
287
+ if (jsxElements.length > 0) {
288
+ return jsxElements[0].getTagNameNode().getText();
289
+ }
290
+ }
291
+ }
292
+ return undefined;
293
+ }
294
+ extractAuthRequirement(sourceFile) {
295
+ const filePath = sourceFile.getFilePath();
296
+ const fileName = filePath.split('/').pop() || '';
297
+ // Pages that don't require authentication (exceptions)
298
+ const publicPages = [
299
+ '404.tsx',
300
+ 'permission-denied.tsx',
301
+ '_app.tsx',
302
+ '_document.tsx',
303
+ '_error.tsx',
304
+ ];
305
+ const isPublicPage = publicPages.some((p) => fileName === p);
306
+ // Default: ALL pages require authentication (because _app.tsx wraps everything with RequireAuthorization)
307
+ const result = {
308
+ required: !isPublicPage,
309
+ };
310
+ try {
311
+ // Look for common auth/permission wrapper components (generic patterns)
312
+ const authPatterns = [
313
+ 'RequiredCondition',
314
+ 'ProtectedRoute',
315
+ 'AuthGuard',
316
+ 'PrivateRoute',
317
+ 'WithAuth',
318
+ 'RequireAuth',
319
+ 'Authenticated',
320
+ 'Authorized',
321
+ ];
322
+ const authWrapper = sourceFile
323
+ .getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
324
+ .find((node) => {
325
+ const tagName = node.getTagNameNode().getText();
326
+ return authPatterns.some((pattern) => tagName.includes(pattern));
327
+ });
328
+ if (authWrapper) {
329
+ // Has additional permission requirements beyond basic auth
330
+ result.condition = 'Additional permissions required';
331
+ // Extract condition/roles - safely iterate attributes
332
+ const attributes = authWrapper.getAttributes();
333
+ for (const attr of attributes) {
334
+ if (attr.isKind(SyntaxKind.JsxAttribute)) {
335
+ try {
336
+ const name = attr.getNameNode().getText();
337
+ // Common attribute names for conditions/roles
338
+ if (['condition', 'roles', 'permissions', 'requiredRoles', 'allowedRoles'].includes(name)) {
339
+ const initializer = attr.getInitializer();
340
+ if (initializer) {
341
+ result.condition = initializer.getText();
342
+ // Extract roles from condition
343
+ const roles = this.extractRolesFromCondition(initializer.getText());
344
+ if (roles.length > 0) {
345
+ result.roles = roles;
346
+ }
347
+ }
348
+ }
349
+ }
350
+ catch {
351
+ // Skip this attribute
352
+ }
353
+ }
354
+ }
355
+ }
356
+ }
357
+ catch {
358
+ // Return default on error
359
+ }
360
+ return result;
361
+ }
362
+ extractRolesFromCondition(condition) {
363
+ const roles = [];
364
+ // Generic patterns for role extraction:
365
+ // - EnumName.RoleName (e.g., UserRole.Admin, MembershipRole.Owner)
366
+ // - 'role-name' or "role-name" string literals
367
+ // - ROLE_NAME constants
368
+ // Pattern 1: Enum-style roles (SomeEnum.RoleName)
369
+ const enumRoleRegex = /(\w+Role|\w+Permission)\.(\w+)/g;
370
+ let match;
371
+ while ((match = enumRoleRegex.exec(condition)) !== null) {
372
+ roles.push(match[2]);
373
+ }
374
+ // Pattern 2: String literals containing 'admin', 'user', 'owner', etc.
375
+ const stringRoleRegex = /['"]([a-zA-Z_-]+)['"]/g;
376
+ while ((match = stringRoleRegex.exec(condition)) !== null) {
377
+ const val = match[1];
378
+ // Only add if it looks like a role
379
+ if (/admin|user|owner|member|guest|manager|editor|viewer/i.test(val)) {
380
+ roles.push(val);
381
+ }
382
+ }
383
+ // Pattern 3: UPPER_CASE constants
384
+ const constRoleRegex = /\b(ROLE_\w+|[A-Z]+_ROLE)\b/g;
385
+ while ((match = constRoleRegex.exec(condition)) !== null) {
386
+ roles.push(match[1]);
387
+ }
388
+ return [...new Set(roles)]; // Remove duplicates
389
+ }
390
+ extractPermissions(sourceFile) {
391
+ const permissions = [];
392
+ // Look for permission checks in the code
393
+ const permissionChecks = sourceFile
394
+ .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)
395
+ .filter((node) => {
396
+ const text = node.getText();
397
+ return text.includes('Permission') || text.includes('Role') || text.includes('isAdmin');
398
+ });
399
+ for (const check of permissionChecks) {
400
+ const text = check.getText();
401
+ if (!permissions.includes(text)) {
402
+ permissions.push(text);
403
+ }
404
+ }
405
+ return permissions;
406
+ }
407
+ extractDataFetching(sourceFile) {
408
+ const dataFetching = [];
409
+ // Build a map of imported GraphQL hooks (including aliases)
410
+ // e.g., import { useQuery as getQuery } from '@apollo/client'
411
+ const apolloHookAliases = new Map();
412
+ const apolloHooks = ['useQuery', 'useMutation', 'useLazyQuery', 'useSubscription'];
413
+ for (const imp of sourceFile.getImportDeclarations()) {
414
+ const moduleSpec = imp.getModuleSpecifierValue();
415
+ if (moduleSpec.includes('@apollo/client') || moduleSpec.includes('apollo')) {
416
+ for (const named of imp.getNamedImports()) {
417
+ const originalName = named.getName();
418
+ const alias = named.getAliasNode()?.getText() || originalName;
419
+ if (apolloHooks.includes(originalName)) {
420
+ apolloHookAliases.set(alias, originalName);
421
+ }
422
+ }
423
+ }
424
+ }
425
+ // Check if this file uses Apollo Client
426
+ const hasApolloImport = apolloHookAliases.size > 0 ||
427
+ sourceFile.getImportDeclarations().some((imp) => {
428
+ const moduleSpecifier = imp.getModuleSpecifierValue();
429
+ return moduleSpecifier.includes('@apollo/client') || moduleSpecifier.includes('apollo');
430
+ });
431
+ // Find GraphQL hook calls - including aliases and custom hooks that wrap Apollo hooks
432
+ const graphqlHookCalls = sourceFile
433
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
434
+ .filter((call) => {
435
+ const expression = call.getExpression().getText();
436
+ // Direct Apollo hook or alias
437
+ if (apolloHookAliases.has(expression) || apolloHooks.includes(expression)) {
438
+ return true;
439
+ }
440
+ // Custom hooks pattern: use*Query, use*Mutation (e.g., useUserQuery, useFetchPosts)
441
+ // But exclude non-GraphQL hooks like useQueryParams, useQueryString
442
+ if (/^use[A-Z].*Query$/.test(expression) &&
443
+ !expression.includes('Params') &&
444
+ !expression.includes('String')) {
445
+ return true;
446
+ }
447
+ if (/^use[A-Z].*Mutation$/.test(expression)) {
448
+ return true;
449
+ }
450
+ return false;
451
+ });
452
+ for (const call of graphqlHookCalls) {
453
+ const hookName = call.getExpression().getText();
454
+ // Determine the actual type (resolve alias to original name)
455
+ let resolvedType;
456
+ if (apolloHookAliases.has(hookName)) {
457
+ resolvedType = apolloHookAliases.get(hookName);
458
+ }
459
+ else if (hookName.includes('Mutation')) {
460
+ resolvedType = 'useMutation';
461
+ }
462
+ else if (hookName.includes('Lazy')) {
463
+ resolvedType = 'useLazyQuery';
464
+ }
465
+ else {
466
+ resolvedType = 'useQuery';
467
+ }
468
+ const args = call.getArguments();
469
+ // Custom hooks might not have arguments (they encapsulate the query)
470
+ if (args.length === 0) {
471
+ // For custom hooks like useUserQuery(), extract name from hook name
472
+ if (/^use[A-Z]/.test(hookName)) {
473
+ const operationName = hookName.replace(/^use/, '').replace(/Query$|Mutation$/, '');
474
+ dataFetching.push({ type: resolvedType, operationName, variables: [] });
475
+ }
476
+ continue;
477
+ }
478
+ const firstArg = args[0];
479
+ const firstArgText = firstArg.getText();
480
+ // Skip if first argument is:
481
+ // - An array literal: ['key', ...]
482
+ // - An object literal: { queryKey: ... }
483
+ // - A string literal: 'queryKey'
484
+ // These are React Query/TanStack Query patterns, not Apollo
485
+ if (firstArgText.startsWith('[') ||
486
+ firstArgText.startsWith('{') ||
487
+ firstArgText.startsWith("'") ||
488
+ firstArgText.startsWith('"') ||
489
+ firstArgText.startsWith('`')) {
490
+ continue;
491
+ }
492
+ // Apollo Client pattern: first arg should be a Document identifier
493
+ // Valid patterns: GetUserDocument, GET_USER_QUERY, gql`...`
494
+ const isApolloPattern = hasApolloImport ||
495
+ firstArgText.endsWith('Document') ||
496
+ firstArgText.endsWith('Query') ||
497
+ firstArgText.endsWith('Mutation') ||
498
+ firstArgText.includes('gql') ||
499
+ /^[A-Z_]+$/.test(firstArgText); // SCREAMING_CASE constant
500
+ if (!isApolloPattern) {
501
+ continue;
502
+ }
503
+ const operationName = firstArgText.replace(/Document$/, '').replace(/Query$|Mutation$/, '');
504
+ const variables = [];
505
+ if (args.length > 1) {
506
+ const optionsArg = args[1];
507
+ const variablesProperty = optionsArg
508
+ .getDescendantsOfKind(SyntaxKind.PropertyAssignment)
509
+ .find((prop) => {
510
+ try {
511
+ return prop.getName() === 'variables';
512
+ }
513
+ catch {
514
+ return false;
515
+ }
516
+ });
517
+ if (variablesProperty) {
518
+ const initializer = variablesProperty.getInitializer();
519
+ if (initializer) {
520
+ // Extract variable names
521
+ const props = initializer.getDescendantsOfKind(SyntaxKind.PropertyAssignment);
522
+ for (const prop of props) {
523
+ try {
524
+ variables.push(prop.getName());
525
+ }
526
+ catch {
527
+ // Skip
528
+ }
529
+ }
530
+ }
531
+ }
532
+ }
533
+ dataFetching.push({ type: resolvedType, operationName, variables });
534
+ }
535
+ // Find getServerSideProps and extract GraphQL queries
536
+ const getServerSidePropsVar = sourceFile.getVariableDeclaration('getServerSideProps');
537
+ const getServerSidePropsFunc = sourceFile.getFunction('getServerSideProps');
538
+ const ssrNode = getServerSidePropsVar || getServerSidePropsFunc;
539
+ if (ssrNode) {
540
+ // Look for imported Document patterns (e.g., GetUserOnboardingUserDocument)
541
+ const imports = sourceFile.getImportDeclarations();
542
+ for (const imp of imports) {
543
+ const namedImports = imp.getNamedImports();
544
+ for (const named of namedImports) {
545
+ const name = named.getName();
546
+ if (name.endsWith('Document')) {
547
+ // Check if this document is used in the file
548
+ const usages = sourceFile
549
+ .getDescendantsOfKind(SyntaxKind.Identifier)
550
+ .filter((id) => id.getText() === name);
551
+ if (usages.length > 0) {
552
+ const operationName = name.replace(/Document$/, '');
553
+ dataFetching.push({
554
+ type: 'getServerSideProps',
555
+ operationName: `→ ${operationName}`,
556
+ });
557
+ }
558
+ }
559
+ }
560
+ }
561
+ // Also look for inline query patterns: graphqlClient.query({ query: ... })
562
+ const text = ssrNode.getText();
563
+ const queryMatches = text.match(/query:\s*(\w+)/g);
564
+ if (queryMatches) {
565
+ for (const match of queryMatches) {
566
+ const docName = match.replace(/query:\s*/, '');
567
+ if (!dataFetching.some((d) => d.operationName?.includes(docName.replace(/Document$/, '')))) {
568
+ dataFetching.push({
569
+ type: 'getServerSideProps',
570
+ operationName: `→ ${docName.replace(/Document$/, '')}`,
571
+ });
572
+ }
573
+ }
574
+ }
575
+ }
576
+ // Find getStaticProps
577
+ const getStaticPropsVar = sourceFile.getVariableDeclaration('getStaticProps');
578
+ const getStaticPropsFunc = sourceFile.getFunction('getStaticProps');
579
+ if (getStaticPropsVar || getStaticPropsFunc) {
580
+ dataFetching.push({
581
+ type: 'getStaticProps',
582
+ operationName: 'getStaticProps',
583
+ });
584
+ }
585
+ // Track component imports from relative paths and analyze their GraphQL usage
586
+ const imports = sourceFile.getImportDeclarations();
587
+ const sourceFilePath = sourceFile.getFilePath();
588
+ const sourceFileDir = path.dirname(sourceFilePath);
589
+ for (const imp of imports) {
590
+ const moduleSpec = imp.getModuleSpecifierValue();
591
+ // Skip external packages (node_modules) - only track relative imports
592
+ const isRelativeImport = moduleSpec.startsWith('.') || moduleSpec.startsWith('/');
593
+ const isInternalAlias = !moduleSpec.includes('node_modules') &&
594
+ !moduleSpec.startsWith('@types/') &&
595
+ moduleSpec.startsWith('@') === false; // Skip scoped packages
596
+ if (isRelativeImport || isInternalAlias) {
597
+ // Skip codegen/generated folders - these contain GraphQL types, not components
598
+ if (moduleSpec.includes('__generated__') || moduleSpec.includes('/generated/')) {
599
+ continue;
600
+ }
601
+ // Collect component names from this import
602
+ const componentNames = [];
603
+ // Check named imports for component-like names (PascalCase)
604
+ const namedImports = imp.getNamedImports();
605
+ for (const named of namedImports) {
606
+ const name = named.getName();
607
+ if (this.isComponentName(name)) {
608
+ componentNames.push(name);
609
+ }
610
+ }
611
+ // Check default import
612
+ const defaultImport = imp.getDefaultImport();
613
+ if (defaultImport) {
614
+ const name = defaultImport.getText();
615
+ if (this.isComponentName(name)) {
616
+ componentNames.push(name);
617
+ }
618
+ }
619
+ // For each component, try to analyze the imported file for GraphQL
620
+ for (const componentName of componentNames) {
621
+ // Try to resolve and analyze the imported component file
622
+ const importedQueries = this.analyzeImportedComponent(sourceFileDir, moduleSpec, componentName);
623
+ if (importedQueries.length > 0) {
624
+ // Add the queries found in the component with a reference marker
625
+ for (const query of importedQueries) {
626
+ dataFetching.push({
627
+ type: query.type,
628
+ operationName: query.operationName.startsWith('→')
629
+ ? `→ ${query.operationName} (${componentName})`
630
+ : `→ ${query.operationName} (${componentName})`,
631
+ variables: query.variables,
632
+ });
633
+ }
634
+ }
635
+ else {
636
+ // No queries found, just mark as component reference
637
+ dataFetching.push({
638
+ type: 'component',
639
+ operationName: componentName,
640
+ variables: [],
641
+ });
642
+ }
643
+ }
644
+ }
645
+ }
646
+ return dataFetching;
647
+ }
648
+ // Symbol tracking cache to avoid re-analyzing the same files
649
+ symbolTraceCache = new Map();
650
+ /**
651
+ * Analyze an imported component file for GraphQL queries with full symbol tracing
652
+ * 임포트된 컴포넌트를 재귀적으로 완전 추적
653
+ */
654
+ analyzeImportedComponent(sourceFileDir, moduleSpec, componentName, visited = new Set(), depth = 0) {
655
+ const MAX_DEPTH = 10; // 무한 루프 방지
656
+ if (depth > MAX_DEPTH)
657
+ return [];
658
+ const queries = [];
659
+ try {
660
+ // Resolve the import path
661
+ const resolvedPath = path.resolve(sourceFileDir, moduleSpec);
662
+ // Create a unique key for cycle detection
663
+ const cacheKey = `${resolvedPath}:${componentName}`;
664
+ if (visited.has(cacheKey)) {
665
+ return []; // 순환 참조 방지
666
+ }
667
+ visited.add(cacheKey);
668
+ // Check cache
669
+ const cachedResult = this.symbolTraceCache.get(cacheKey);
670
+ if (cachedResult !== undefined) {
671
+ return cachedResult;
672
+ }
673
+ // Try different file extensions and index files
674
+ const possiblePaths = [
675
+ `${resolvedPath}.tsx`,
676
+ `${resolvedPath}.ts`,
677
+ `${resolvedPath}/index.tsx`,
678
+ `${resolvedPath}/index.ts`,
679
+ `${resolvedPath}/${componentName}.tsx`,
680
+ `${resolvedPath}/${componentName}.ts`,
681
+ ];
682
+ let componentFile;
683
+ let componentFilePath;
684
+ for (const tryPath of possiblePaths) {
685
+ try {
686
+ componentFile = this.project.addSourceFileAtPath(tryPath);
687
+ if (componentFile) {
688
+ componentFilePath = tryPath;
689
+ break;
690
+ }
691
+ }
692
+ catch {
693
+ // File doesn't exist, try next
694
+ }
695
+ }
696
+ if (!componentFile || !componentFilePath)
697
+ return queries;
698
+ // If this is an index file, follow re-exports to find the actual component file
699
+ if (componentFilePath.endsWith('index.tsx') || componentFilePath.endsWith('index.ts')) {
700
+ const actualFile = this.followReExport(componentFile, componentName, path.dirname(componentFilePath));
701
+ if (actualFile) {
702
+ componentFile = actualFile;
703
+ }
704
+ }
705
+ // Check for Apollo Client imports (also check for gql imports which indicate GraphQL usage)
706
+ // Also check for graphql.macro, graphql-tag which are commonly used
707
+ const hasGraphQLImport = componentFile.getImportDeclarations().some((imp) => {
708
+ const spec = imp.getModuleSpecifierValue();
709
+ return (spec.includes('@apollo/client') ||
710
+ spec.includes('apollo') ||
711
+ spec.includes('gql') ||
712
+ spec.includes('graphql') || // graphql.macro, graphql-tag
713
+ spec.includes('__generated__'));
714
+ });
715
+ // Get all relative imports for full symbol tracing
716
+ const relativeImports = componentFile.getImportDeclarations().filter((imp) => {
717
+ const spec = imp.getModuleSpecifierValue();
718
+ return spec.startsWith('./') || spec.startsWith('../');
719
+ });
720
+ // 1. Analyze custom hooks (use* pattern)
721
+ for (const imp of relativeImports) {
722
+ const hookSpec = imp.getModuleSpecifierValue();
723
+ const hookNames = imp
724
+ .getNamedImports()
725
+ .map((n) => n.getName())
726
+ .filter((n) => /^use[A-Z]/.test(n));
727
+ for (const hookName of hookNames) {
728
+ const hookQueries = this.analyzeCustomHook(path.dirname(componentFile.getFilePath()), hookSpec, hookName, visited, depth + 1);
729
+ queries.push(...hookQueries);
730
+ }
731
+ }
732
+ // 2. Deep trace all imported components (not just Container/Page)
733
+ for (const imp of relativeImports) {
734
+ const nestedSpec = imp.getModuleSpecifierValue();
735
+ const namedImports = imp.getNamedImports().map((n) => n.getName());
736
+ const defaultImport = imp.getDefaultImport()?.getText();
737
+ // Analyze ALL PascalCase imports (components)
738
+ const componentImports = namedImports.filter((n) => /^[A-Z]/.test(n) && this.isComponentName(n));
739
+ for (const nestedComponentName of componentImports) {
740
+ const nestedQueries = this.analyzeImportedComponent(path.dirname(componentFile.getFilePath()), nestedSpec, nestedComponentName, visited, depth + 1);
741
+ queries.push(...nestedQueries);
742
+ }
743
+ // Also check default imports
744
+ if (defaultImport && /^[A-Z]/.test(defaultImport) && this.isComponentName(defaultImport)) {
745
+ const nestedQueries = this.analyzeImportedComponent(path.dirname(componentFile.getFilePath()), nestedSpec, defaultImport, visited, depth + 1);
746
+ queries.push(...nestedQueries);
747
+ }
748
+ }
749
+ // 3. If has GraphQL imports, look for direct hook calls
750
+ if (hasGraphQLImport) {
751
+ // Find GraphQL hook calls in the component
752
+ const hookCalls = componentFile
753
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
754
+ .filter((call) => {
755
+ const expression = call.getExpression().getText();
756
+ return ['useQuery', 'useMutation', 'useLazyQuery', 'useSubscription'].includes(expression);
757
+ });
758
+ for (const call of hookCalls) {
759
+ const hookName = call.getExpression().getText();
760
+ const args = call.getArguments();
761
+ if (args.length === 0)
762
+ continue;
763
+ const firstArg = args[0];
764
+ const firstArgText = firstArg.getText();
765
+ // Extract operation name from the first argument
766
+ let operationName = firstArgText;
767
+ let operationType = null;
768
+ // Try codegen mapping first
769
+ const codegenInfo = this.resolveDocumentName(firstArgText);
770
+ if (codegenInfo) {
771
+ operationName = codegenInfo.operationName;
772
+ operationType = codegenInfo.operationType;
773
+ }
774
+ // Handle codegen Document pattern: SomeQueryDocument → SomeQuery
775
+ else if (firstArgText.endsWith('Document')) {
776
+ operationName = firstArgText.replace(/Document$/, '');
777
+ }
778
+ // If it's a variable reference, try to find the actual query name
779
+ else if (/^[A-Za-z]/.test(firstArgText)) {
780
+ // Use the helper function to find operation name from variable
781
+ const extractedName = this.findOperationNameFromVariable(componentFile, firstArgText);
782
+ if (extractedName) {
783
+ operationName = extractedName;
784
+ }
785
+ // If still using the original variable name and it's a common pattern,
786
+ // try to extract operation name from it (e.g., ProfilePreviewQuery → ProfilePreview)
787
+ if (operationName === firstArgText &&
788
+ firstArgText !== 'Query' &&
789
+ firstArgText !== 'Mutation') {
790
+ const nameMatch = firstArgText.match(/^(.+?)(Query|Mutation|Subscription)$/);
791
+ if (nameMatch) {
792
+ operationName = nameMatch[1];
793
+ }
794
+ }
795
+ }
796
+ // Clean up operation name - but don't remove if it would become empty
797
+ // Also don't clean if it's just "Query" or "Mutation" (common variable names)
798
+ if (operationName !== 'Query' && operationName !== 'Mutation') {
799
+ const cleanedName = operationName
800
+ .replace(/Document$/, '')
801
+ .replace(/Query$|Mutation$/, '');
802
+ operationName = cleanedName || operationName;
803
+ }
804
+ // If operationName is still a generic name like "Query", try to extract from variable name pattern
805
+ // e.g., from useQuery(Query, ...) where Query = gql`query ActualName { ... }`
806
+ if (operationName === 'Query' || operationName === 'Mutation' || operationName === '') {
807
+ // The variable lookup already happened above but may have failed
808
+ // In this case, keep the original but mark it needs the file context
809
+ if (operationName === '') {
810
+ operationName = firstArgText || 'Unknown';
811
+ }
812
+ }
813
+ const type = operationType
814
+ ? operationType === 'mutation'
815
+ ? 'useMutation'
816
+ : operationType === 'subscription'
817
+ ? 'useSubscription'
818
+ : hookName.includes('Lazy')
819
+ ? 'useLazyQuery'
820
+ : 'useQuery'
821
+ : hookName.includes('Mutation')
822
+ ? 'useMutation'
823
+ : hookName.includes('Lazy')
824
+ ? 'useLazyQuery'
825
+ : 'useQuery';
826
+ queries.push({
827
+ type: type,
828
+ operationName,
829
+ variables: [],
830
+ });
831
+ }
832
+ }
833
+ // Cache the results
834
+ this.symbolTraceCache.set(cacheKey, queries);
835
+ }
836
+ catch {
837
+ // Failed to analyze imported component, skip
838
+ }
839
+ return queries;
840
+ }
841
+ /**
842
+ * Analyze a custom hook file for GraphQL queries with recursive tracing
843
+ */
844
+ analyzeCustomHook(sourceFileDir, moduleSpec, hookName, visited = new Set(), depth = 0) {
845
+ const MAX_DEPTH = 10;
846
+ if (depth > MAX_DEPTH)
847
+ return [];
848
+ const queries = [];
849
+ try {
850
+ // Resolve the import path
851
+ const resolvedPath = path.resolve(sourceFileDir, moduleSpec);
852
+ // Create a unique key for cycle detection
853
+ const cacheKey = `hook:${resolvedPath}:${hookName}`;
854
+ if (visited.has(cacheKey))
855
+ return [];
856
+ visited.add(cacheKey);
857
+ // Check cache
858
+ const cachedHookResult = this.symbolTraceCache.get(cacheKey);
859
+ if (cachedHookResult !== undefined) {
860
+ return cachedHookResult;
861
+ }
862
+ const possiblePaths = [
863
+ `${resolvedPath}.tsx`,
864
+ `${resolvedPath}.ts`,
865
+ `${resolvedPath}/${hookName}.tsx`,
866
+ `${resolvedPath}/${hookName}.ts`,
867
+ `${resolvedPath}/index.tsx`,
868
+ `${resolvedPath}/index.ts`,
869
+ ];
870
+ let hookFile;
871
+ for (const tryPath of possiblePaths) {
872
+ try {
873
+ hookFile = this.project.addSourceFileAtPath(tryPath);
874
+ if (hookFile)
875
+ break;
876
+ }
877
+ catch {
878
+ // File doesn't exist, try next
879
+ }
880
+ }
881
+ if (!hookFile)
882
+ return queries;
883
+ // Check for Apollo Client imports
884
+ const hasGraphQLImport = hookFile.getImportDeclarations().some((imp) => {
885
+ const spec = imp.getModuleSpecifierValue();
886
+ return (spec.includes('@apollo/client') ||
887
+ spec.includes('apollo') ||
888
+ spec.includes('graphql') ||
889
+ spec.includes('__generated__'));
890
+ });
891
+ // Also trace nested custom hooks even if no direct GraphQL import
892
+ const relativeImports = hookFile.getImportDeclarations().filter((imp) => {
893
+ const spec = imp.getModuleSpecifierValue();
894
+ return spec.startsWith('./') || spec.startsWith('../');
895
+ });
896
+ for (const imp of relativeImports) {
897
+ const nestedSpec = imp.getModuleSpecifierValue();
898
+ const nestedHookNames = imp
899
+ .getNamedImports()
900
+ .map((n) => n.getName())
901
+ .filter((n) => /^use[A-Z]/.test(n));
902
+ for (const nestedHookName of nestedHookNames) {
903
+ const nestedQueries = this.analyzeCustomHook(path.dirname(hookFile.getFilePath()), nestedSpec, nestedHookName, visited, depth + 1);
904
+ queries.push(...nestedQueries);
905
+ }
906
+ }
907
+ if (!hasGraphQLImport && queries.length === 0) {
908
+ return queries;
909
+ }
910
+ // Find useQuery/useMutation calls in the hook
911
+ const hookCalls = hookFile.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => {
912
+ const expression = call.getExpression().getText();
913
+ return ['useQuery', 'useMutation', 'useLazyQuery', 'useSubscription'].includes(expression);
914
+ });
915
+ for (const call of hookCalls) {
916
+ const callHookName = call.getExpression().getText();
917
+ const args = call.getArguments();
918
+ if (args.length === 0)
919
+ continue;
920
+ const firstArgText = args[0].getText();
921
+ let operationName = firstArgText;
922
+ let operationType = null;
923
+ // Try codegen mapping first
924
+ const codegenInfo = this.resolveDocumentName(firstArgText);
925
+ if (codegenInfo) {
926
+ operationName = codegenInfo.operationName;
927
+ operationType = codegenInfo.operationType;
928
+ }
929
+ // Handle codegen Document pattern: SomeQueryDocument → SomeQuery
930
+ else if (firstArgText.endsWith('Document')) {
931
+ operationName = firstArgText.replace(/Document$/, '');
932
+ }
933
+ // If it's a variable reference, try to find the actual query name
934
+ else if (/^[A-Za-z]/.test(firstArgText)) {
935
+ // Use the helper function to find operation name from variable
936
+ const extractedName = this.findOperationNameFromVariable(hookFile, firstArgText);
937
+ if (extractedName) {
938
+ operationName = extractedName;
939
+ }
940
+ // If still using the original variable name and it's a common pattern,
941
+ // try to extract operation name from it (e.g., ProfilePreviewQuery → ProfilePreview)
942
+ if (operationName === firstArgText &&
943
+ firstArgText !== 'Query' &&
944
+ firstArgText !== 'Mutation') {
945
+ const nameMatch = firstArgText.match(/^(.+?)(Query|Mutation|Subscription)$/);
946
+ if (nameMatch) {
947
+ operationName = nameMatch[1];
948
+ }
949
+ }
950
+ }
951
+ // Clean up operation name - but don't remove if it would become empty
952
+ // Also don't clean if it's just "Query" or "Mutation" (common variable names)
953
+ if (operationName !== 'Query' && operationName !== 'Mutation') {
954
+ const cleanedName = operationName
955
+ .replace(/Document$/, '')
956
+ .replace(/Query$|Mutation$/, '');
957
+ operationName = cleanedName || operationName;
958
+ }
959
+ // Prevent empty operation names
960
+ if (operationName === '') {
961
+ operationName = firstArgText || 'Unknown';
962
+ }
963
+ const type = operationType
964
+ ? operationType === 'mutation'
965
+ ? 'useMutation'
966
+ : operationType === 'subscription'
967
+ ? 'useSubscription'
968
+ : callHookName.includes('Lazy')
969
+ ? 'useLazyQuery'
970
+ : 'useQuery'
971
+ : callHookName.includes('Mutation')
972
+ ? 'useMutation'
973
+ : callHookName.includes('Lazy')
974
+ ? 'useLazyQuery'
975
+ : 'useQuery';
976
+ queries.push({
977
+ type: type,
978
+ operationName: `→ ${operationName} (via ${hookName})`,
979
+ variables: [],
980
+ });
981
+ }
982
+ // Cache the results
983
+ this.symbolTraceCache.set(cacheKey, queries);
984
+ }
985
+ catch {
986
+ // Failed to analyze hook
987
+ }
988
+ return queries;
989
+ }
990
+ // Global context providers and their GraphQL queries
991
+ globalContextQueries = [];
992
+ /**
993
+ * Find page files from multiple possible locations
994
+ * Next.js, React, Rails+React 구조 모두 지원
995
+ */
996
+ async findPageFiles() {
997
+ const pagesDir = this.getSetting('pagesDir', 'src/pages');
998
+ const allFiles = [];
999
+ // 1. Check Next.js standard directories (deduplicate)
1000
+ const nextjsDirsSet = new Set([pagesDir, 'pages', 'src/pages', 'app', 'src/app']);
1001
+ const nextjsDirs = [...nextjsDirsSet];
1002
+ for (const dir of nextjsDirs) {
1003
+ // Skip Rails 'app' directory (contains controllers, models, views - not React pages)
1004
+ if (dir === 'app' || dir === 'src/app') {
1005
+ const railsIndicators = ['controllers', 'models', 'views', 'helpers'];
1006
+ const dirPath = this.resolvePath(dir);
1007
+ const hasRailsStructure = railsIndicators.some((subdir) => {
1008
+ try {
1009
+ return fs.existsSync(path.join(dirPath, subdir));
1010
+ }
1011
+ catch {
1012
+ return false;
1013
+ }
1014
+ });
1015
+ if (hasRailsStructure) {
1016
+ continue; // Skip Rails app directory
1017
+ }
1018
+ }
1019
+ const dirPath = this.resolvePath(dir);
1020
+ try {
1021
+ const files = await fg(['**/*.tsx', '**/*.ts', '**/*.jsx', '**/*.js'], {
1022
+ cwd: dirPath,
1023
+ ignore: [
1024
+ '_app.tsx',
1025
+ '_app.ts',
1026
+ '_app.jsx',
1027
+ '_app.js',
1028
+ '_document.tsx',
1029
+ '_document.ts',
1030
+ '_document.jsx',
1031
+ '_document.js',
1032
+ '_error.tsx',
1033
+ '_error.ts',
1034
+ '_error.jsx',
1035
+ '_error.js',
1036
+ 'api/**',
1037
+ '**/*.test.*',
1038
+ '**/*.spec.*',
1039
+ '**/node_modules/**',
1040
+ '**/components/pages/**', // Reusable page components, not routes
1041
+ ],
1042
+ absolute: true,
1043
+ });
1044
+ allFiles.push(...files);
1045
+ if (files.length > 0) {
1046
+ this.log(`Found ${files.length} pages in ${dir}`);
1047
+ }
1048
+ }
1049
+ catch {
1050
+ // Directory doesn't exist
1051
+ }
1052
+ }
1053
+ // 2. Check Rails + React structures (exclude components/pages - those are reusable components, not routes)
1054
+ const railsReactDirs = ['frontend/src/**/pages', 'app/javascript/**/pages'];
1055
+ for (const pattern of railsReactDirs) {
1056
+ try {
1057
+ const files = await fg([
1058
+ `${pattern}/**/*.tsx`,
1059
+ `${pattern}/**/*.ts`,
1060
+ `${pattern}/**/*.jsx`,
1061
+ `${pattern}/**/*.js`,
1062
+ ], {
1063
+ cwd: this.basePath,
1064
+ ignore: [
1065
+ '**/*.test.*',
1066
+ '**/*.spec.*',
1067
+ '**/node_modules/**',
1068
+ '**/vendor/**',
1069
+ '**/components/pages/**', // Exclude reusable page components (not actual routes)
1070
+ '**/stories/**', // Exclude storybook files
1071
+ ],
1072
+ absolute: true,
1073
+ });
1074
+ allFiles.push(...files);
1075
+ if (files.length > 0) {
1076
+ this.log(`Found ${files.length} React pages in ${pattern}`);
1077
+ }
1078
+ }
1079
+ catch {
1080
+ // Pattern doesn't match
1081
+ }
1082
+ }
1083
+ // 3. Check for entry point files (Rails with Webpacker/Shakapacker)
1084
+ const entryPatterns = [
1085
+ 'frontend/src/**/index.tsx',
1086
+ 'frontend/src/**/App.tsx',
1087
+ 'app/javascript/packs/*.tsx',
1088
+ 'app/javascript/packs/*.jsx',
1089
+ ];
1090
+ for (const pattern of entryPatterns) {
1091
+ try {
1092
+ const files = await fg([pattern], {
1093
+ cwd: this.basePath,
1094
+ ignore: ['**/node_modules/**', '**/vendor/**'],
1095
+ absolute: true,
1096
+ });
1097
+ // Don't add duplicates
1098
+ for (const file of files) {
1099
+ if (!allFiles.includes(file)) {
1100
+ allFiles.push(file);
1101
+ }
1102
+ }
1103
+ }
1104
+ catch {
1105
+ // Pattern doesn't match
1106
+ }
1107
+ }
1108
+ // Remove duplicates
1109
+ return [...new Set(allFiles)];
1110
+ }
1111
+ /**
1112
+ * Analyze _app.tsx for global providers that use GraphQL
1113
+ * _app.tsx에서 전역 Context Provider의 GraphQL 분석
1114
+ */
1115
+ async analyzeAppFile() {
1116
+ const pagesDir = this.getSetting('pagesDir', 'src/pages');
1117
+ const possiblePaths = [
1118
+ this.resolvePath(`${pagesDir}/_app.tsx`),
1119
+ this.resolvePath(`${pagesDir}/_app.ts`),
1120
+ ];
1121
+ for (const appPath of possiblePaths) {
1122
+ try {
1123
+ const appFile = this.project.addSourceFileAtPath(appPath);
1124
+ if (!appFile)
1125
+ continue;
1126
+ // Find Provider components used in _app
1127
+ const jsxElements = appFile.getDescendantsOfKind(SyntaxKind.JsxElement);
1128
+ const jsxSelfClosing = appFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement);
1129
+ const providerNames = new Set();
1130
+ for (const el of [...jsxElements, ...jsxSelfClosing]) {
1131
+ const tagName = el.getFirstDescendantByKind(SyntaxKind.Identifier)?.getText();
1132
+ if (tagName && (tagName.includes('Provider') || tagName.includes('Context'))) {
1133
+ providerNames.add(tagName);
1134
+ }
1135
+ }
1136
+ // Find imports for these providers
1137
+ for (const imp of appFile.getImportDeclarations()) {
1138
+ const spec = imp.getModuleSpecifierValue();
1139
+ if (!spec.startsWith('./') && !spec.startsWith('../'))
1140
+ continue;
1141
+ const namedImports = imp.getNamedImports().map((n) => n.getName());
1142
+ const defaultImport = imp.getDefaultImport()?.getText();
1143
+ for (const providerName of providerNames) {
1144
+ if (namedImports.includes(providerName) || defaultImport === providerName) {
1145
+ // Analyze the provider file for GraphQL usage
1146
+ const providerQueries = this.analyzeImportedComponent(path.dirname(appPath), spec, providerName, new Set(), 0);
1147
+ for (const q of providerQueries) {
1148
+ // Mark as global context
1149
+ this.globalContextQueries.push({
1150
+ ...q,
1151
+ operationName: `[Global] ${q.operationName}`,
1152
+ });
1153
+ }
1154
+ }
1155
+ }
1156
+ }
1157
+ if (this.globalContextQueries.length > 0) {
1158
+ this.log(`Found ${this.globalContextQueries.length} global context queries from _app`);
1159
+ }
1160
+ return;
1161
+ }
1162
+ catch {
1163
+ // File doesn't exist, continue
1164
+ }
1165
+ }
1166
+ }
1167
+ /**
1168
+ * Load codegen mapping from __generated__ folders (optional)
1169
+ * 코드젠 폴더가 있으면 Document → Operation name 매핑 로드
1170
+ */
1171
+ async loadCodegenMapping() {
1172
+ const possibleDirs = [
1173
+ '__generated__',
1174
+ 'src/__generated__',
1175
+ 'src/__generated__/gql-graphql-gateway',
1176
+ 'generated',
1177
+ 'src/generated',
1178
+ ];
1179
+ for (const dir of possibleDirs) {
1180
+ const generatedPath = this.resolvePath(dir);
1181
+ try {
1182
+ const files = await fg(['**/*.ts', '**/*.tsx'], {
1183
+ cwd: generatedPath,
1184
+ absolute: true,
1185
+ onlyFiles: true,
1186
+ });
1187
+ for (const file of files) {
1188
+ try {
1189
+ const sourceFile = this.project.addSourceFileAtPath(file);
1190
+ // Look for DocumentNode exports with operation names
1191
+ // Pattern 1: export const SomeQueryDocument = gql`query SomeName { ... }`
1192
+ // Pattern 2: export type SomeQueryDocument = DocumentNode
1193
+ const varDecls = sourceFile.getVariableDeclarations();
1194
+ for (const varDecl of varDecls) {
1195
+ const name = varDecl.getName();
1196
+ if (name.endsWith('Document')) {
1197
+ const initializer = varDecl.getInitializer()?.getText() ?? '';
1198
+ // Extract operation name and type from gql template
1199
+ const operationMatch = initializer.match(/(?:query|mutation|subscription)\s+(\w+)/);
1200
+ const typeMatch = initializer.match(/(query|mutation|subscription)\s+/);
1201
+ if (operationMatch) {
1202
+ this.codegenMap.set(name, {
1203
+ operationName: operationMatch[1],
1204
+ operationType: typeMatch ? typeMatch[1] : 'query',
1205
+ });
1206
+ }
1207
+ }
1208
+ }
1209
+ // Pattern 3: Look at type definitions
1210
+ // export type SomeQuery = { ... }
1211
+ // This indicates SomeQueryDocument exists
1212
+ const typeAliases = sourceFile.getTypeAliases();
1213
+ for (const typeAlias of typeAliases) {
1214
+ const name = typeAlias.getName();
1215
+ if ((name.endsWith('Query') ||
1216
+ name.endsWith('Mutation') ||
1217
+ name.endsWith('Subscription')) &&
1218
+ !name.endsWith('Variables')) {
1219
+ const docName = name + 'Document';
1220
+ if (!this.codegenMap.has(docName)) {
1221
+ const operationType = name.endsWith('Mutation')
1222
+ ? 'mutation'
1223
+ : name.endsWith('Subscription')
1224
+ ? 'subscription'
1225
+ : 'query';
1226
+ this.codegenMap.set(docName, {
1227
+ operationName: name,
1228
+ operationType,
1229
+ });
1230
+ }
1231
+ }
1232
+ }
1233
+ }
1234
+ catch {
1235
+ // Skip file on error
1236
+ }
1237
+ }
1238
+ if (this.codegenMap.size > 0) {
1239
+ this.log(`Loaded ${this.codegenMap.size} codegen mappings from ${dir}`);
1240
+ return; // Found and loaded, no need to check other dirs
1241
+ }
1242
+ }
1243
+ catch {
1244
+ // Directory doesn't exist, continue
1245
+ }
1246
+ }
1247
+ }
1248
+ /**
1249
+ * Resolve Document name to operation name using codegen map
1250
+ */
1251
+ resolveDocumentName(documentName) {
1252
+ // Skip generic GraphQL type names - these are not actual operation names
1253
+ const genericTypeNames = new Set(['Query', 'Mutation', 'Subscription']);
1254
+ if (genericTypeNames.has(documentName)) {
1255
+ return null;
1256
+ }
1257
+ // Direct lookup
1258
+ const directResult = this.codegenMap.get(documentName);
1259
+ if (directResult !== undefined) {
1260
+ return directResult;
1261
+ }
1262
+ // Try with Document suffix
1263
+ const withSuffix = documentName.endsWith('Document') ? documentName : documentName + 'Document';
1264
+ const suffixResult = this.codegenMap.get(withSuffix);
1265
+ if (suffixResult !== undefined) {
1266
+ return suffixResult;
1267
+ }
1268
+ return null;
1269
+ }
1270
+ /**
1271
+ * Follow re-export in index file to find the actual component file
1272
+ */
1273
+ followReExport(indexFile, componentName, indexDir) {
1274
+ try {
1275
+ // Look for export declarations that export the component
1276
+ // e.g., export { ProfilePreviewContainer } from "./ProfilePreviewContainer"
1277
+ const exportDecls = indexFile.getExportDeclarations();
1278
+ let firstExportFile = null;
1279
+ for (const exportDecl of exportDecls) {
1280
+ const namedExports = exportDecl.getNamedExports();
1281
+ for (const named of namedExports) {
1282
+ const moduleSpec = exportDecl.getModuleSpecifierValue();
1283
+ if (!moduleSpec)
1284
+ continue;
1285
+ const resolvedPath = path.resolve(indexDir, moduleSpec);
1286
+ const possiblePaths = [
1287
+ `${resolvedPath}.tsx`,
1288
+ `${resolvedPath}.ts`,
1289
+ `${resolvedPath}/index.tsx`,
1290
+ `${resolvedPath}/index.ts`,
1291
+ ];
1292
+ // Try to load the file
1293
+ let file;
1294
+ for (const tryPath of possiblePaths) {
1295
+ try {
1296
+ file = this.project.addSourceFileAtPath(tryPath);
1297
+ if (file)
1298
+ break;
1299
+ }
1300
+ catch {
1301
+ // Try next path
1302
+ }
1303
+ }
1304
+ if (!file)
1305
+ continue;
1306
+ // Store first export as fallback (for default imports with different names)
1307
+ if (!firstExportFile) {
1308
+ firstExportFile = file;
1309
+ }
1310
+ // Check if this is the exact component we're looking for
1311
+ if (named.getName() === componentName ||
1312
+ named.getAliasNode()?.getText() === componentName) {
1313
+ return file;
1314
+ }
1315
+ }
1316
+ }
1317
+ // If exact match not found, return first export (handles default imports)
1318
+ if (firstExportFile) {
1319
+ return firstExportFile;
1320
+ }
1321
+ // Also check for: export * from "./SomeModule"
1322
+ for (const exportDecl of indexFile.getExportDeclarations()) {
1323
+ if (exportDecl.isNamespaceExport()) {
1324
+ const moduleSpec = exportDecl.getModuleSpecifierValue();
1325
+ if (moduleSpec) {
1326
+ const resolvedPath = path.resolve(indexDir, moduleSpec);
1327
+ const possiblePaths = [`${resolvedPath}.tsx`, `${resolvedPath}.ts`];
1328
+ for (const tryPath of possiblePaths) {
1329
+ try {
1330
+ const file = this.project.addSourceFileAtPath(tryPath);
1331
+ if (file) {
1332
+ // Check if this file exports the component
1333
+ const hasExport = file.getExportedDeclarations().has(componentName);
1334
+ if (hasExport)
1335
+ return file;
1336
+ }
1337
+ }
1338
+ catch {
1339
+ // Try next path
1340
+ }
1341
+ }
1342
+ }
1343
+ }
1344
+ }
1345
+ }
1346
+ catch {
1347
+ // Failed to follow re-export
1348
+ }
1349
+ return null;
1350
+ }
1351
+ /**
1352
+ * Check if a name looks like a React component (PascalCase with common suffixes)
1353
+ */
1354
+ isComponentName(name) {
1355
+ // Must be PascalCase (start with uppercase)
1356
+ if (!/^[A-Z]/.test(name))
1357
+ return false;
1358
+ // Exclude GraphQL-related type names (not actual components)
1359
+ // These are generated types from codegen or GraphQL operations
1360
+ if (name.endsWith('Query') ||
1361
+ name.endsWith('Mutation') ||
1362
+ name.endsWith('Subscription') ||
1363
+ name.endsWith('Fragment') ||
1364
+ name.endsWith('Document') ||
1365
+ name.endsWith('Variables') ||
1366
+ name === 'Query' ||
1367
+ name === 'Mutation' ||
1368
+ name === 'Subscription') {
1369
+ return false;
1370
+ }
1371
+ // Exclude React/Next.js type definitions (not actual components)
1372
+ const typeDefinitions = new Set([
1373
+ 'NextPage',
1374
+ 'NextPageContext',
1375
+ 'NextApiRequest',
1376
+ 'NextApiResponse',
1377
+ 'GetServerSideProps',
1378
+ 'GetStaticProps',
1379
+ 'GetStaticPaths',
1380
+ 'InferGetServerSidePropsType',
1381
+ 'InferGetStaticPropsType',
1382
+ 'FC',
1383
+ 'FunctionComponent',
1384
+ 'VFC',
1385
+ 'Component',
1386
+ 'PureComponent',
1387
+ 'ReactNode',
1388
+ 'ReactElement',
1389
+ 'PropsWithChildren',
1390
+ 'ComponentProps',
1391
+ 'ComponentType',
1392
+ 'ElementType',
1393
+ 'RefObject',
1394
+ 'MutableRefObject',
1395
+ 'Dispatch',
1396
+ 'SetStateAction',
1397
+ 'ChangeEvent',
1398
+ 'MouseEvent',
1399
+ 'KeyboardEvent',
1400
+ 'FormEvent',
1401
+ 'SyntheticEvent',
1402
+ ]);
1403
+ if (typeDefinitions.has(name)) {
1404
+ return false;
1405
+ }
1406
+ // Common component suffixes that likely contain data fetching
1407
+ const componentSuffixes = [
1408
+ 'Container',
1409
+ 'Page',
1410
+ 'Screen',
1411
+ 'View',
1412
+ 'Form',
1413
+ 'Modal',
1414
+ 'Dialog',
1415
+ 'Panel',
1416
+ 'Root',
1417
+ 'Provider',
1418
+ 'Wrapper',
1419
+ ];
1420
+ // Check for suffix match
1421
+ if (componentSuffixes.some((suffix) => name.endsWith(suffix))) {
1422
+ return true;
1423
+ }
1424
+ // Also match if it ends with Page-like patterns
1425
+ if (/Page[A-Z]?\w*$/.test(name) || /Container[A-Z]?\w*$/.test(name)) {
1426
+ return true;
1427
+ }
1428
+ // Match PascalCase names that look like feature components
1429
+ // e.g., CancellationEngagement, UserProfile, ProjectSettings
1430
+ if (/^[A-Z][a-z]+[A-Z][a-z]+/.test(name)) {
1431
+ return true;
1432
+ }
1433
+ return false;
1434
+ }
1435
+ extractNavigation(sourceFile) {
1436
+ const result = {
1437
+ visible: true,
1438
+ currentNavItem: null,
1439
+ };
1440
+ try {
1441
+ // Find globalNavigationStyle assignment
1442
+ const navStyleAssignment = sourceFile
1443
+ .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)
1444
+ .find((node) => {
1445
+ try {
1446
+ return node.getName() === 'globalNavigationStyle';
1447
+ }
1448
+ catch {
1449
+ return false;
1450
+ }
1451
+ });
1452
+ if (navStyleAssignment) {
1453
+ const parent = navStyleAssignment.getParent();
1454
+ if (Node.isBinaryExpression(parent)) {
1455
+ const right = parent.getRight();
1456
+ // Extract visible
1457
+ const visibleProp = right
1458
+ .getDescendantsOfKind(SyntaxKind.PropertyAssignment)
1459
+ .find((prop) => {
1460
+ try {
1461
+ return prop.getName() === 'visible';
1462
+ }
1463
+ catch {
1464
+ return false;
1465
+ }
1466
+ });
1467
+ if (visibleProp) {
1468
+ result.visible = visibleProp.getInitializer()?.getText() === 'true';
1469
+ }
1470
+ // Extract currentNavItem
1471
+ const navItemProp = right
1472
+ .getDescendantsOfKind(SyntaxKind.PropertyAssignment)
1473
+ .find((prop) => {
1474
+ try {
1475
+ return prop.getName() === 'currentNavItem';
1476
+ }
1477
+ catch {
1478
+ return false;
1479
+ }
1480
+ });
1481
+ if (navItemProp) {
1482
+ const value = navItemProp.getInitializer()?.getText();
1483
+ result.currentNavItem = value && value !== 'null' ? value.replace(/['"]/g, '') : null;
1484
+ }
1485
+ // Extract mini
1486
+ const miniProp = right
1487
+ .getDescendantsOfKind(SyntaxKind.PropertyAssignment)
1488
+ .find((prop) => {
1489
+ try {
1490
+ return prop.getName() === 'mini';
1491
+ }
1492
+ catch {
1493
+ return false;
1494
+ }
1495
+ });
1496
+ if (miniProp) {
1497
+ result.mini = miniProp.getInitializer()?.getText() === 'true';
1498
+ }
1499
+ }
1500
+ }
1501
+ }
1502
+ catch {
1503
+ // Return default on error
1504
+ }
1505
+ return result;
1506
+ }
1507
+ /**
1508
+ * Extract multi-step flow information (wizard, stepper, onboarding)
1509
+ */
1510
+ extractSteps(sourceFile) {
1511
+ const steps = [];
1512
+ // Pattern 1: useState with step-like variable names
1513
+ const useStateCalls = sourceFile
1514
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
1515
+ .filter((call) => call.getExpression().getText() === 'useState');
1516
+ for (const call of useStateCalls) {
1517
+ const parent = call.getParent();
1518
+ if (!parent)
1519
+ continue;
1520
+ const parentText = parent.getText();
1521
+ // Match: const [step, setStep] = useState(0) or [currentStep, setCurrentStep]
1522
+ const stepMatch = parentText.match(/\[\s*(step|currentStep|activeStep|page|currentPage|phase|stage)\s*,/i);
1523
+ if (stepMatch) {
1524
+ // Found a step state, now look for step-related JSX or switch cases
1525
+ const stepVarName = stepMatch[1];
1526
+ // Look for switch statements or conditional rendering
1527
+ const switchStatements = sourceFile.getDescendantsOfKind(SyntaxKind.SwitchStatement);
1528
+ for (const switchStmt of switchStatements) {
1529
+ const expression = switchStmt.getExpression().getText();
1530
+ if (expression.includes(stepVarName)) {
1531
+ const caseBlocks = switchStmt.getClauses();
1532
+ caseBlocks.forEach((clause, idx) => {
1533
+ if (clause.isKind(SyntaxKind.CaseClause)) {
1534
+ const caseExpr = clause.getExpression()?.getText() || String(idx);
1535
+ // Try to find component name in case block
1536
+ const jsxElements = clause.getDescendantsOfKind(SyntaxKind.JsxOpeningElement);
1537
+ const componentName = jsxElements.length > 0 ? jsxElements[0].getTagNameNode().getText() : undefined;
1538
+ steps.push({
1539
+ id: caseExpr.replace(/['"]/g, ''),
1540
+ name: `Step ${caseExpr.replace(/['"]/g, '')}`,
1541
+ component: componentName,
1542
+ });
1543
+ }
1544
+ });
1545
+ }
1546
+ }
1547
+ // Look for array of steps/components
1548
+ const arrayLiterals = sourceFile.getDescendantsOfKind(SyntaxKind.ArrayLiteralExpression);
1549
+ for (const arr of arrayLiterals) {
1550
+ const parentVar = arr.getParent();
1551
+ if (parentVar && parentVar.getText().match(/steps|pages|screens|views|components/i)) {
1552
+ const elements = arr.getElements();
1553
+ elements.forEach((el, idx) => {
1554
+ const elText = el.getText();
1555
+ // Could be component reference or object
1556
+ if (elText.startsWith('{')) {
1557
+ // Object literal, try to extract name/label
1558
+ const nameMatch = elText.match(/(?:name|label|title)\s*:\s*['"]([^'"]+)['"]/);
1559
+ const compMatch = elText.match(/(?:component|content)\s*:\s*<?\s*(\w+)/);
1560
+ steps.push({
1561
+ id: idx + 1,
1562
+ name: nameMatch ? nameMatch[1] : `Step ${idx + 1}`,
1563
+ component: compMatch ? compMatch[1] : undefined,
1564
+ });
1565
+ }
1566
+ else if (/^[A-Z]/.test(elText)) {
1567
+ // Component reference
1568
+ steps.push({
1569
+ id: idx + 1,
1570
+ name: elText,
1571
+ component: elText,
1572
+ });
1573
+ }
1574
+ });
1575
+ }
1576
+ }
1577
+ }
1578
+ }
1579
+ // Pattern 2: Stepper/Wizard component usage
1580
+ const jsxElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement);
1581
+ for (const jsx of jsxElements) {
1582
+ const tagName = jsx.getTagNameNode().getText();
1583
+ if (tagName.match(/Stepper|Wizard|Steps|TabPanel|FormStep/i)) {
1584
+ // Find Step children
1585
+ const parent = jsx.getParent();
1586
+ if (parent && parent.isKind(SyntaxKind.JsxElement)) {
1587
+ const children = parent.getJsxChildren();
1588
+ children.forEach((child, idx) => {
1589
+ if (child.isKind(SyntaxKind.JsxElement) ||
1590
+ child.isKind(SyntaxKind.JsxSelfClosingElement)) {
1591
+ const childTag = child.isKind(SyntaxKind.JsxElement)
1592
+ ? child.getOpeningElement().getTagNameNode().getText()
1593
+ : child.getTagNameNode().getText();
1594
+ // Get label/title attribute
1595
+ const attrs = child.isKind(SyntaxKind.JsxElement)
1596
+ ? child.getOpeningElement().getAttributes()
1597
+ : child.getAttributes();
1598
+ let stepName = childTag;
1599
+ for (const attr of attrs) {
1600
+ if (attr.isKind(SyntaxKind.JsxAttribute)) {
1601
+ const name = attr.getNameNode().getText();
1602
+ if (name === 'label' || name === 'title' || name === 'name') {
1603
+ const value = attr.getInitializer()?.getText();
1604
+ if (value) {
1605
+ stepName = value.replace(/['"{}]/g, '');
1606
+ break;
1607
+ }
1608
+ }
1609
+ }
1610
+ }
1611
+ steps.push({
1612
+ id: idx + 1,
1613
+ name: stepName,
1614
+ component: childTag,
1615
+ });
1616
+ }
1617
+ });
1618
+ }
1619
+ }
1620
+ }
1621
+ // Pattern 3: Conditional rendering with step variable
1622
+ const conditionalExprs = sourceFile.getDescendantsOfKind(SyntaxKind.ConditionalExpression);
1623
+ for (const cond of conditionalExprs) {
1624
+ const condition = cond.getCondition().getText();
1625
+ if (condition.match(/step\s*===?\s*\d+|currentStep|activeStep/i)) {
1626
+ // Extract step number and components
1627
+ const whenTrue = cond.getWhenTrue();
1628
+ // Note: whenFalse (cond.getWhenFalse()) could be used for nested step detection
1629
+ const stepNumMatch = condition.match(/===?\s*(\d+)/);
1630
+ if (stepNumMatch && steps.length === 0) {
1631
+ // Only add if we haven't found steps through other patterns
1632
+ const trueJsx = whenTrue.getDescendantsOfKind(SyntaxKind.JsxOpeningElement);
1633
+ if (trueJsx.length > 0) {
1634
+ steps.push({
1635
+ id: parseInt(stepNumMatch[1]),
1636
+ component: trueJsx[0].getTagNameNode().getText(),
1637
+ });
1638
+ }
1639
+ }
1640
+ }
1641
+ }
1642
+ return steps;
1643
+ }
1644
+ extractLinkedPages(sourceFile) {
1645
+ const linkedPages = [];
1646
+ // Find router.push/replace calls
1647
+ const routerCalls = sourceFile
1648
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
1649
+ .filter((call) => {
1650
+ const text = call.getExpression().getText();
1651
+ return (text.includes('router.push') || text.includes('router.replace') || text.includes('Link'));
1652
+ });
1653
+ for (const call of routerCalls) {
1654
+ const args = call.getArguments();
1655
+ if (args.length > 0) {
1656
+ const pathArg = args[0].getText();
1657
+ // Extract path string
1658
+ const pathMatch = pathArg.match(/['"`]([^'"`]+)['"`]/);
1659
+ if (pathMatch && !linkedPages.includes(pathMatch[1])) {
1660
+ linkedPages.push(pathMatch[1]);
1661
+ }
1662
+ }
1663
+ }
1664
+ // Find Link components
1665
+ const linkElements = sourceFile
1666
+ .getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
1667
+ .filter((node) => node.getTagNameNode().getText() === 'Link');
1668
+ for (const link of linkElements) {
1669
+ try {
1670
+ // Get all attributes and filter for JsxAttribute type (not JsxSpreadAttribute)
1671
+ const attributes = link.getAttributes();
1672
+ for (const attr of attributes) {
1673
+ // Check if it's a JsxAttribute
1674
+ if (attr.isKind(SyntaxKind.JsxAttribute)) {
1675
+ const nameNode = attr.getNameNode();
1676
+ const name = nameNode.getText();
1677
+ if (name === 'href') {
1678
+ const value = attr.getInitializer()?.getText();
1679
+ if (value) {
1680
+ const pathMatch = value.match(/['"`]([^'"`]+)['"`]/);
1681
+ if (pathMatch && !linkedPages.includes(pathMatch[1])) {
1682
+ linkedPages.push(pathMatch[1]);
1683
+ }
1684
+ }
1685
+ }
1686
+ }
1687
+ }
1688
+ }
1689
+ catch {
1690
+ // Skip if attribute extraction fails
1691
+ }
1692
+ }
1693
+ return linkedPages;
1694
+ }
1695
+ }
1696
+ //# sourceMappingURL=pages-analyzer.js.map