@wtdlee/repomap 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/analyzers/index.d.ts +69 -5
  2. package/dist/analyzers/index.js +1 -5
  3. package/dist/chunk-3PWXDB7B.js +153 -0
  4. package/dist/{generators/page-map-generator.js → chunk-3YFXZAP7.js} +322 -358
  5. package/dist/chunk-6F4PWJZI.js +1 -0
  6. package/dist/{generators/rails-map-generator.js → chunk-E4WRODSI.js} +86 -94
  7. package/dist/chunk-GNBMJMET.js +2519 -0
  8. package/dist/{server/doc-server.js → chunk-M6YNU536.js} +702 -290
  9. package/dist/chunk-OWM6WNLE.js +2610 -0
  10. package/dist/chunk-SSU6QFTX.js +1058 -0
  11. package/dist/cli.d.ts +0 -1
  12. package/dist/cli.js +348 -452
  13. package/dist/dataflow-analyzer-BfAiqVUp.d.ts +180 -0
  14. package/dist/env-detector-EEMVUEIA.js +1 -0
  15. package/dist/generators/index.d.ts +431 -3
  16. package/dist/generators/index.js +2 -3
  17. package/dist/index.d.ts +53 -10
  18. package/dist/index.js +8 -11
  19. package/dist/page-map-generator-6MJGPBVA.js +1 -0
  20. package/dist/rails-UWSDRS33.js +1 -0
  21. package/dist/rails-map-generator-D2URLMVJ.js +2 -0
  22. package/dist/server/index.d.ts +33 -1
  23. package/dist/server/index.js +7 -1
  24. package/dist/types.d.ts +39 -37
  25. package/dist/types.js +1 -5
  26. package/package.json +5 -3
  27. package/dist/analyzers/base-analyzer.d.ts +0 -45
  28. package/dist/analyzers/base-analyzer.js +0 -47
  29. package/dist/analyzers/dataflow-analyzer.d.ts +0 -29
  30. package/dist/analyzers/dataflow-analyzer.js +0 -425
  31. package/dist/analyzers/graphql-analyzer.d.ts +0 -22
  32. package/dist/analyzers/graphql-analyzer.js +0 -386
  33. package/dist/analyzers/pages-analyzer.d.ts +0 -84
  34. package/dist/analyzers/pages-analyzer.js +0 -1695
  35. package/dist/analyzers/rails/index.d.ts +0 -46
  36. package/dist/analyzers/rails/index.js +0 -145
  37. package/dist/analyzers/rails/rails-controller-analyzer.d.ts +0 -82
  38. package/dist/analyzers/rails/rails-controller-analyzer.js +0 -478
  39. package/dist/analyzers/rails/rails-grpc-analyzer.d.ts +0 -44
  40. package/dist/analyzers/rails/rails-grpc-analyzer.js +0 -262
  41. package/dist/analyzers/rails/rails-model-analyzer.d.ts +0 -88
  42. package/dist/analyzers/rails/rails-model-analyzer.js +0 -493
  43. package/dist/analyzers/rails/rails-react-analyzer.d.ts +0 -41
  44. package/dist/analyzers/rails/rails-react-analyzer.js +0 -529
  45. package/dist/analyzers/rails/rails-routes-analyzer.d.ts +0 -62
  46. package/dist/analyzers/rails/rails-routes-analyzer.js +0 -540
  47. package/dist/analyzers/rails/rails-view-analyzer.d.ts +0 -49
  48. package/dist/analyzers/rails/rails-view-analyzer.js +0 -386
  49. package/dist/analyzers/rails/ruby-parser.d.ts +0 -63
  50. package/dist/analyzers/rails/ruby-parser.js +0 -212
  51. package/dist/analyzers/rest-api-analyzer.d.ts +0 -65
  52. package/dist/analyzers/rest-api-analyzer.js +0 -479
  53. package/dist/core/cache.d.ts +0 -47
  54. package/dist/core/cache.js +0 -151
  55. package/dist/core/engine.d.ts +0 -46
  56. package/dist/core/engine.js +0 -319
  57. package/dist/core/index.d.ts +0 -2
  58. package/dist/core/index.js +0 -2
  59. package/dist/generators/markdown-generator.d.ts +0 -25
  60. package/dist/generators/markdown-generator.js +0 -782
  61. package/dist/generators/mermaid-generator.d.ts +0 -35
  62. package/dist/generators/mermaid-generator.js +0 -364
  63. package/dist/generators/page-map-generator.d.ts +0 -22
  64. package/dist/generators/rails-map-generator.d.ts +0 -21
  65. package/dist/server/doc-server.d.ts +0 -30
  66. package/dist/utils/env-detector.d.ts +0 -31
  67. package/dist/utils/env-detector.js +0 -188
  68. package/dist/utils/parallel.d.ts +0 -23
  69. package/dist/utils/parallel.js +0 -70
@@ -0,0 +1,2519 @@
1
+ import { Project, Node, SyntaxKind } from 'ts-morph';
2
+ import fg from 'fast-glob';
3
+ import * as path from 'path';
4
+ import * as fs from 'fs';
5
+ import * as fs2 from 'fs/promises';
6
+ import { parse } from 'graphql';
7
+
8
+ // src/analyzers/base-analyzer.ts
9
+ var BaseAnalyzer = class {
10
+ config;
11
+ basePath;
12
+ constructor(config) {
13
+ this.config = config;
14
+ this.basePath = config.path;
15
+ }
16
+ /**
17
+ * Resolve path relative to repository root
18
+ * リポジトリルートからの相対パスを解決
19
+ */
20
+ resolvePath(relativePath) {
21
+ return `${this.basePath}/${relativePath}`;
22
+ }
23
+ /**
24
+ * Get setting value with fallback
25
+ * 設定値を取得(フォールバック付き)
26
+ */
27
+ getSetting(key, defaultValue = "") {
28
+ return this.config.settings[key] ?? defaultValue;
29
+ }
30
+ /**
31
+ * Log analysis progress
32
+ * 分析進捗をログ出力
33
+ */
34
+ log(message) {
35
+ console.log(`[${this.getName()}] ${message}`);
36
+ }
37
+ /**
38
+ * Log warning
39
+ * 警告をログ出力
40
+ */
41
+ warn(message) {
42
+ console.warn(`[${this.getName()}] \u26A0\uFE0F ${message}`);
43
+ }
44
+ /**
45
+ * Log error
46
+ * エラーをログ出力
47
+ */
48
+ error(message, error) {
49
+ console.error(`[${this.getName()}] \u274C ${message}`, error?.message || "");
50
+ }
51
+ };
52
+
53
+ // src/utils/parallel.ts
54
+ async function parallelMapSafe(items, fn, concurrency = 8) {
55
+ const results = new Array(items.length).fill(null);
56
+ let currentIndex = 0;
57
+ async function worker() {
58
+ while (currentIndex < items.length) {
59
+ const index = currentIndex++;
60
+ if (index < items.length) {
61
+ try {
62
+ results[index] = await fn(items[index], index);
63
+ } catch {
64
+ results[index] = null;
65
+ }
66
+ }
67
+ }
68
+ }
69
+ const workers = Array(Math.min(concurrency, items.length)).fill(null).map(() => worker());
70
+ await Promise.all(workers);
71
+ return results.filter((r) => r !== null);
72
+ }
73
+
74
+ // src/analyzers/pages-analyzer.ts
75
+ var PagesAnalyzer = class extends BaseAnalyzer {
76
+ project;
77
+ // Codegen Document → Operation name mapping
78
+ codegenMap = /* @__PURE__ */ new Map();
79
+ constructor(config) {
80
+ super(config);
81
+ this.project = new Project({
82
+ tsConfigFilePath: this.resolvePath("tsconfig.json"),
83
+ skipAddingFilesFromTsConfig: true
84
+ });
85
+ }
86
+ /**
87
+ * Extract GraphQL operation name from a gql template literal or function call
88
+ * Handles multiple patterns:
89
+ * - gql`query Name { ... }`
90
+ * - gql(/‌* GraphQL *‌/ `query Name { ... }`)
91
+ * - graphql(`query Name { ... }`)
92
+ */
93
+ extractOperationNameFromGql(text) {
94
+ const directMatch = text.match(/(?:query|mutation|subscription)\s+(\w+)/);
95
+ if (directMatch && directMatch[1]) {
96
+ return directMatch[1];
97
+ }
98
+ const backtickMatch = text.match(/`\s*(?:query|mutation|subscription)\s+(\w+)/);
99
+ if (backtickMatch && backtickMatch[1]) {
100
+ return backtickMatch[1];
101
+ }
102
+ const commentMatch = text.match(
103
+ /GraphQL[^`]*`\s*(?:\n\s*)?(?:query|mutation|subscription)\s+(\w+)/
104
+ );
105
+ if (commentMatch && commentMatch[1]) {
106
+ return commentMatch[1];
107
+ }
108
+ return null;
109
+ }
110
+ /**
111
+ * Find and extract operation name from a variable declaration in source file
112
+ * Handles cases like: const Query = gql(comment `query ActualName...`)
113
+ */
114
+ findOperationNameFromVariable(sourceFile, variableName) {
115
+ let varDecl = sourceFile.getVariableDeclaration(variableName);
116
+ if (!varDecl) {
117
+ const exportedDecls = sourceFile.getExportedDeclarations();
118
+ const exported = exportedDecls.get(variableName);
119
+ if (exported && exported.length > 0) {
120
+ const firstExport = exported[0];
121
+ if (Node.isVariableDeclaration(firstExport)) {
122
+ varDecl = firstExport;
123
+ }
124
+ }
125
+ }
126
+ if (!varDecl) {
127
+ const allVarDecls = sourceFile.getVariableDeclarations();
128
+ varDecl = allVarDecls.find((v) => v.getName() === variableName);
129
+ }
130
+ if (!varDecl) {
131
+ const fullText = sourceFile.getFullText();
132
+ const patterns = [
133
+ // gql(/* GraphQL */ `query Name...
134
+ new RegExp(
135
+ `(?:const|let|var)\\s+${variableName}\\s*=\\s*gql\\s*\\(\\s*/\\*[^*]*\\*/\\s*\`\\s*(?:query|mutation|subscription)\\s+(\\w+)`,
136
+ "s"
137
+ ),
138
+ // gql`query Name...
139
+ new RegExp(
140
+ `(?:const|let|var)\\s+${variableName}\\s*=\\s*gql\`\\s*(?:query|mutation|subscription)\\s+(\\w+)`,
141
+ "s"
142
+ ),
143
+ // gql(`query Name... (without comment)
144
+ new RegExp(
145
+ `(?:const|let|var)\\s+${variableName}\\s*=\\s*gql\\s*\\(\`\\s*(?:query|mutation|subscription)\\s+(\\w+)`,
146
+ "s"
147
+ ),
148
+ // graphql(`query Name...
149
+ new RegExp(
150
+ `(?:const|let|var)\\s+${variableName}\\s*=\\s*graphql\\s*\\(\`\\s*(?:query|mutation|subscription)\\s+(\\w+)`,
151
+ "s"
152
+ )
153
+ ];
154
+ for (const pattern of patterns) {
155
+ const match = fullText.match(pattern);
156
+ if (match && match[1]) {
157
+ return match[1];
158
+ }
159
+ }
160
+ return null;
161
+ }
162
+ const initializer = varDecl.getInitializer();
163
+ if (!initializer) return null;
164
+ const text = initializer.getText();
165
+ return this.extractOperationNameFromGql(text);
166
+ }
167
+ getName() {
168
+ return "PagesAnalyzer";
169
+ }
170
+ async analyze() {
171
+ this.log("Starting page analysis...");
172
+ await this.loadCodegenMapping();
173
+ await this.analyzeAppFile();
174
+ const pageFiles = await this.findPageFiles();
175
+ this.log(`Found ${pageFiles.length} page files`);
176
+ for (const filePath of pageFiles) {
177
+ try {
178
+ this.project.addSourceFileAtPath(filePath);
179
+ } catch {
180
+ }
181
+ }
182
+ const pages = await parallelMapSafe(
183
+ pageFiles,
184
+ async (filePath) => {
185
+ const pagesPath = this.detectPagesRoot(filePath);
186
+ return this.analyzePageFile(filePath, pagesPath);
187
+ },
188
+ 4
189
+ // Limit concurrency for ts-morph stability
190
+ );
191
+ const validPages = pages.filter((p) => p !== null);
192
+ this.log(`Analyzed ${validPages.length} pages successfully`);
193
+ return { pages: validPages };
194
+ }
195
+ async analyzePageFile(filePath, pagesPath) {
196
+ const sourceFile = this.project.getSourceFile(filePath);
197
+ if (!sourceFile) return null;
198
+ const relativePath = path.relative(pagesPath, filePath);
199
+ const routePath = this.filePathToRoutePath(relativePath);
200
+ const pageComponent = this.findPageComponent(sourceFile);
201
+ if (!pageComponent) {
202
+ return null;
203
+ }
204
+ const params = this.extractRouteParams(routePath);
205
+ const layout = this.extractLayout(sourceFile);
206
+ const authentication = this.extractAuthRequirement(sourceFile);
207
+ const permissions = this.extractPermissions(sourceFile);
208
+ const dataFetching = this.extractDataFetching(sourceFile);
209
+ const navigation = this.extractNavigation(sourceFile);
210
+ const linkedPages = this.extractLinkedPages(sourceFile);
211
+ const steps = this.extractSteps(sourceFile);
212
+ return {
213
+ path: routePath,
214
+ filePath: relativePath,
215
+ component: pageComponent,
216
+ params,
217
+ layout,
218
+ authentication,
219
+ permissions,
220
+ dataFetching,
221
+ navigation,
222
+ linkedPages,
223
+ steps: steps.length > 0 ? steps : void 0
224
+ };
225
+ }
226
+ /**
227
+ * Detect the pages root directory from a file path
228
+ * e.g., /project/src/pages/users/index.tsx -> /project/src/pages
229
+ */
230
+ detectPagesRoot(filePath) {
231
+ const pagesPatterns = [
232
+ "/src/pages/",
233
+ "/pages/",
234
+ "/src/app/",
235
+ "/app/",
236
+ "/frontend/src/pages/",
237
+ "/app/javascript/pages/"
238
+ ];
239
+ for (const pattern of pagesPatterns) {
240
+ const idx = filePath.indexOf(pattern);
241
+ if (idx !== -1) {
242
+ return filePath.substring(0, idx + pattern.length - 1);
243
+ }
244
+ }
245
+ return this.basePath;
246
+ }
247
+ filePathToRoutePath(filePath) {
248
+ return "/" + filePath.replace(/\.tsx?$/, "").replace(/\/index$/, "").replace(/\[\.\.\.(\w+)\]/g, "*").replace(/\[(\w+)\]/g, ":$1");
249
+ }
250
+ extractRouteParams(routePath) {
251
+ const params = [];
252
+ const paramRegex = /:(\w+)/g;
253
+ let match;
254
+ while ((match = paramRegex.exec(routePath)) !== null) {
255
+ params.push(match[1]);
256
+ }
257
+ return params;
258
+ }
259
+ findPageComponent(sourceFile) {
260
+ const defaultExport = sourceFile.getDefaultExportSymbol();
261
+ if (defaultExport) {
262
+ const name = defaultExport.getName();
263
+ if (name !== "default") {
264
+ return name;
265
+ }
266
+ }
267
+ const exportAssignment = sourceFile.getExportAssignment((e) => !e.isExportEquals());
268
+ if (exportAssignment) {
269
+ const expr = exportAssignment.getExpression();
270
+ if (expr) {
271
+ const text = expr.getText();
272
+ if (/^[A-Z][a-zA-Z0-9]*$/.test(text)) {
273
+ return text;
274
+ }
275
+ if (Node.isFunctionExpression(expr) || Node.isArrowFunction(expr)) {
276
+ return "default";
277
+ }
278
+ }
279
+ }
280
+ const functions = sourceFile.getFunctions();
281
+ for (const func of functions) {
282
+ if (func.isDefaultExport()) {
283
+ return func.getName() || "default";
284
+ }
285
+ }
286
+ const pageVar = sourceFile.getVariableDeclaration("Page");
287
+ if (pageVar) {
288
+ return "Page";
289
+ }
290
+ const varDeclarations = sourceFile.getVariableDeclarations();
291
+ for (const varDecl of varDeclarations) {
292
+ const typeNode = varDecl.getTypeNode();
293
+ if (typeNode) {
294
+ const typeText = typeNode.getText();
295
+ if (typeText.includes("NextPage") || typeText.includes("FC") || typeText.includes("React.FC")) {
296
+ return varDecl.getName();
297
+ }
298
+ }
299
+ }
300
+ for (const varDecl of varDeclarations) {
301
+ const name = varDecl.getName();
302
+ if (/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
303
+ const init = varDecl.getInitializer();
304
+ if (init && (Node.isArrowFunction(init) || Node.isFunctionExpression(init))) {
305
+ const text = init.getText();
306
+ if (text.includes("return") && (text.includes("<") || text.includes("jsx"))) {
307
+ return name;
308
+ }
309
+ }
310
+ }
311
+ }
312
+ return null;
313
+ }
314
+ extractLayout(sourceFile) {
315
+ const getLayoutAssignment = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression).find((node) => node.getName() === "getLayout");
316
+ if (getLayoutAssignment) {
317
+ const parent = getLayoutAssignment.getParent();
318
+ if (Node.isBinaryExpression(parent)) {
319
+ const right = parent.getRight();
320
+ const jsxElements = right.getDescendantsOfKind(SyntaxKind.JsxOpeningElement);
321
+ if (jsxElements.length > 0) {
322
+ return jsxElements[0].getTagNameNode().getText();
323
+ }
324
+ }
325
+ }
326
+ return void 0;
327
+ }
328
+ extractAuthRequirement(sourceFile) {
329
+ const filePath = sourceFile.getFilePath();
330
+ const fileName = filePath.split("/").pop() || "";
331
+ const publicPages = [
332
+ "404.tsx",
333
+ "permission-denied.tsx",
334
+ "_app.tsx",
335
+ "_document.tsx",
336
+ "_error.tsx"
337
+ ];
338
+ const isPublicPage = publicPages.some((p) => fileName === p);
339
+ const result = {
340
+ required: !isPublicPage
341
+ };
342
+ try {
343
+ const authPatterns = [
344
+ "RequiredCondition",
345
+ "ProtectedRoute",
346
+ "AuthGuard",
347
+ "PrivateRoute",
348
+ "WithAuth",
349
+ "RequireAuth",
350
+ "Authenticated",
351
+ "Authorized"
352
+ ];
353
+ const authWrapper = sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement).find((node) => {
354
+ const tagName = node.getTagNameNode().getText();
355
+ return authPatterns.some((pattern) => tagName.includes(pattern));
356
+ });
357
+ if (authWrapper) {
358
+ result.condition = "Additional permissions required";
359
+ const attributes = authWrapper.getAttributes();
360
+ for (const attr of attributes) {
361
+ if (attr.isKind(SyntaxKind.JsxAttribute)) {
362
+ try {
363
+ const name = attr.getNameNode().getText();
364
+ if (["condition", "roles", "permissions", "requiredRoles", "allowedRoles"].includes(
365
+ name
366
+ )) {
367
+ const initializer = attr.getInitializer();
368
+ if (initializer) {
369
+ result.condition = initializer.getText();
370
+ const roles = this.extractRolesFromCondition(initializer.getText());
371
+ if (roles.length > 0) {
372
+ result.roles = roles;
373
+ }
374
+ }
375
+ }
376
+ } catch {
377
+ }
378
+ }
379
+ }
380
+ }
381
+ } catch {
382
+ }
383
+ return result;
384
+ }
385
+ extractRolesFromCondition(condition) {
386
+ const roles = [];
387
+ const enumRoleRegex = /(\w+Role|\w+Permission)\.(\w+)/g;
388
+ let match;
389
+ while ((match = enumRoleRegex.exec(condition)) !== null) {
390
+ roles.push(match[2]);
391
+ }
392
+ const stringRoleRegex = /['"]([a-zA-Z_-]+)['"]/g;
393
+ while ((match = stringRoleRegex.exec(condition)) !== null) {
394
+ const val = match[1];
395
+ if (/admin|user|owner|member|guest|manager|editor|viewer/i.test(val)) {
396
+ roles.push(val);
397
+ }
398
+ }
399
+ const constRoleRegex = /\b(ROLE_\w+|[A-Z]+_ROLE)\b/g;
400
+ while ((match = constRoleRegex.exec(condition)) !== null) {
401
+ roles.push(match[1]);
402
+ }
403
+ return [...new Set(roles)];
404
+ }
405
+ extractPermissions(sourceFile) {
406
+ const permissions = [];
407
+ const permissionChecks = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression).filter((node) => {
408
+ const text = node.getText();
409
+ return text.includes("Permission") || text.includes("Role") || text.includes("isAdmin");
410
+ });
411
+ for (const check of permissionChecks) {
412
+ const text = check.getText();
413
+ if (!permissions.includes(text)) {
414
+ permissions.push(text);
415
+ }
416
+ }
417
+ return permissions;
418
+ }
419
+ extractDataFetching(sourceFile) {
420
+ const dataFetching = [];
421
+ const apolloHookAliases = /* @__PURE__ */ new Map();
422
+ const apolloHooks = ["useQuery", "useMutation", "useLazyQuery", "useSubscription"];
423
+ for (const imp of sourceFile.getImportDeclarations()) {
424
+ const moduleSpec = imp.getModuleSpecifierValue();
425
+ if (moduleSpec.includes("@apollo/client") || moduleSpec.includes("apollo")) {
426
+ for (const named of imp.getNamedImports()) {
427
+ const originalName = named.getName();
428
+ const alias = named.getAliasNode()?.getText() || originalName;
429
+ if (apolloHooks.includes(originalName)) {
430
+ apolloHookAliases.set(alias, originalName);
431
+ }
432
+ }
433
+ }
434
+ }
435
+ const hasApolloImport = apolloHookAliases.size > 0 || sourceFile.getImportDeclarations().some((imp) => {
436
+ const moduleSpecifier = imp.getModuleSpecifierValue();
437
+ return moduleSpecifier.includes("@apollo/client") || moduleSpecifier.includes("apollo");
438
+ });
439
+ const graphqlHookCalls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => {
440
+ const expression = call.getExpression().getText();
441
+ if (apolloHookAliases.has(expression) || apolloHooks.includes(expression)) {
442
+ return true;
443
+ }
444
+ if (/^use[A-Z].*Query$/.test(expression) && !expression.includes("Params") && !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
+ let resolvedType;
455
+ if (apolloHookAliases.has(hookName)) {
456
+ resolvedType = apolloHookAliases.get(hookName);
457
+ } else if (hookName.includes("Mutation")) {
458
+ resolvedType = "useMutation";
459
+ } else if (hookName.includes("Lazy")) {
460
+ resolvedType = "useLazyQuery";
461
+ } else {
462
+ resolvedType = "useQuery";
463
+ }
464
+ const args = call.getArguments();
465
+ if (args.length === 0) {
466
+ if (/^use[A-Z]/.test(hookName)) {
467
+ const operationName2 = hookName.replace(/^use/, "").replace(/Query$|Mutation$/, "");
468
+ dataFetching.push({ type: resolvedType, operationName: operationName2, variables: [] });
469
+ }
470
+ continue;
471
+ }
472
+ const firstArg = args[0];
473
+ const firstArgText = firstArg.getText();
474
+ if (firstArgText.startsWith("[") || firstArgText.startsWith("{") || firstArgText.startsWith("'") || firstArgText.startsWith('"') || firstArgText.startsWith("`")) {
475
+ continue;
476
+ }
477
+ const isApolloPattern = hasApolloImport || firstArgText.endsWith("Document") || firstArgText.endsWith("Query") || firstArgText.endsWith("Mutation") || firstArgText.includes("gql") || /^[A-Z_]+$/.test(firstArgText);
478
+ if (!isApolloPattern) {
479
+ continue;
480
+ }
481
+ const operationName = firstArgText.replace(/Document$/, "").replace(/Query$|Mutation$/, "");
482
+ const variables = [];
483
+ if (args.length > 1) {
484
+ const optionsArg = args[1];
485
+ const variablesProperty = optionsArg.getDescendantsOfKind(SyntaxKind.PropertyAssignment).find((prop) => {
486
+ try {
487
+ return prop.getName() === "variables";
488
+ } catch {
489
+ return false;
490
+ }
491
+ });
492
+ if (variablesProperty) {
493
+ const initializer = variablesProperty.getInitializer();
494
+ if (initializer) {
495
+ const props = initializer.getDescendantsOfKind(SyntaxKind.PropertyAssignment);
496
+ for (const prop of props) {
497
+ try {
498
+ variables.push(prop.getName());
499
+ } catch {
500
+ }
501
+ }
502
+ }
503
+ }
504
+ }
505
+ dataFetching.push({ type: resolvedType, operationName, variables });
506
+ }
507
+ const getServerSidePropsVar = sourceFile.getVariableDeclaration("getServerSideProps");
508
+ const getServerSidePropsFunc = sourceFile.getFunction("getServerSideProps");
509
+ const ssrNode = getServerSidePropsVar || getServerSidePropsFunc;
510
+ if (ssrNode) {
511
+ const imports2 = sourceFile.getImportDeclarations();
512
+ for (const imp of imports2) {
513
+ const namedImports = imp.getNamedImports();
514
+ for (const named of namedImports) {
515
+ const name = named.getName();
516
+ if (name.endsWith("Document")) {
517
+ const usages = sourceFile.getDescendantsOfKind(SyntaxKind.Identifier).filter((id) => id.getText() === name);
518
+ if (usages.length > 0) {
519
+ const operationName = name.replace(/Document$/, "");
520
+ dataFetching.push({
521
+ type: "getServerSideProps",
522
+ operationName: `\u2192 ${operationName}`
523
+ });
524
+ }
525
+ }
526
+ }
527
+ }
528
+ const text = ssrNode.getText();
529
+ const queryMatches = text.match(/query:\s*(\w+)/g);
530
+ if (queryMatches) {
531
+ for (const match of queryMatches) {
532
+ const docName = match.replace(/query:\s*/, "");
533
+ if (!dataFetching.some((d) => d.operationName?.includes(docName.replace(/Document$/, "")))) {
534
+ dataFetching.push({
535
+ type: "getServerSideProps",
536
+ operationName: `\u2192 ${docName.replace(/Document$/, "")}`
537
+ });
538
+ }
539
+ }
540
+ }
541
+ }
542
+ const getStaticPropsVar = sourceFile.getVariableDeclaration("getStaticProps");
543
+ const getStaticPropsFunc = sourceFile.getFunction("getStaticProps");
544
+ if (getStaticPropsVar || getStaticPropsFunc) {
545
+ dataFetching.push({
546
+ type: "getStaticProps",
547
+ operationName: "getStaticProps"
548
+ });
549
+ }
550
+ const imports = sourceFile.getImportDeclarations();
551
+ const sourceFilePath = sourceFile.getFilePath();
552
+ const sourceFileDir = path.dirname(sourceFilePath);
553
+ for (const imp of imports) {
554
+ const moduleSpec = imp.getModuleSpecifierValue();
555
+ const isRelativeImport = moduleSpec.startsWith(".") || moduleSpec.startsWith("/");
556
+ const isInternalAlias = !moduleSpec.includes("node_modules") && !moduleSpec.startsWith("@types/") && moduleSpec.startsWith("@") === false;
557
+ if (isRelativeImport || isInternalAlias) {
558
+ if (moduleSpec.includes("__generated__") || moduleSpec.includes("/generated/")) {
559
+ continue;
560
+ }
561
+ const componentNames = [];
562
+ const namedImports = imp.getNamedImports();
563
+ for (const named of namedImports) {
564
+ const name = named.getName();
565
+ if (this.isComponentName(name)) {
566
+ componentNames.push(name);
567
+ }
568
+ }
569
+ const defaultImport = imp.getDefaultImport();
570
+ if (defaultImport) {
571
+ const name = defaultImport.getText();
572
+ if (this.isComponentName(name)) {
573
+ componentNames.push(name);
574
+ }
575
+ }
576
+ for (const componentName of componentNames) {
577
+ const importedQueries = this.analyzeImportedComponent(
578
+ sourceFileDir,
579
+ moduleSpec,
580
+ componentName
581
+ );
582
+ if (importedQueries.length > 0) {
583
+ for (const query of importedQueries) {
584
+ dataFetching.push({
585
+ type: query.type,
586
+ operationName: query.operationName.startsWith("\u2192") ? `\u2192 ${query.operationName} (${componentName})` : `\u2192 ${query.operationName} (${componentName})`,
587
+ variables: query.variables
588
+ });
589
+ }
590
+ } else {
591
+ dataFetching.push({
592
+ type: "component",
593
+ operationName: componentName,
594
+ variables: []
595
+ });
596
+ }
597
+ }
598
+ }
599
+ }
600
+ return dataFetching;
601
+ }
602
+ // Symbol tracking cache to avoid re-analyzing the same files
603
+ symbolTraceCache = /* @__PURE__ */ new Map();
604
+ /**
605
+ * Analyze an imported component file for GraphQL queries with full symbol tracing
606
+ * 임포트된 컴포넌트를 재귀적으로 완전 추적
607
+ */
608
+ analyzeImportedComponent(sourceFileDir, moduleSpec, componentName, visited = /* @__PURE__ */ new Set(), depth = 0) {
609
+ const MAX_DEPTH = 10;
610
+ if (depth > MAX_DEPTH) return [];
611
+ const queries = [];
612
+ try {
613
+ const resolvedPath = path.resolve(sourceFileDir, moduleSpec);
614
+ const cacheKey = `${resolvedPath}:${componentName}`;
615
+ if (visited.has(cacheKey)) {
616
+ return [];
617
+ }
618
+ visited.add(cacheKey);
619
+ const cachedResult = this.symbolTraceCache.get(cacheKey);
620
+ if (cachedResult !== void 0) {
621
+ return cachedResult;
622
+ }
623
+ const possiblePaths = [
624
+ `${resolvedPath}.tsx`,
625
+ `${resolvedPath}.ts`,
626
+ `${resolvedPath}/index.tsx`,
627
+ `${resolvedPath}/index.ts`,
628
+ `${resolvedPath}/${componentName}.tsx`,
629
+ `${resolvedPath}/${componentName}.ts`
630
+ ];
631
+ let componentFile;
632
+ let componentFilePath;
633
+ for (const tryPath of possiblePaths) {
634
+ try {
635
+ componentFile = this.project.addSourceFileAtPath(tryPath);
636
+ if (componentFile) {
637
+ componentFilePath = tryPath;
638
+ break;
639
+ }
640
+ } catch {
641
+ }
642
+ }
643
+ if (!componentFile || !componentFilePath) return queries;
644
+ if (componentFilePath.endsWith("index.tsx") || componentFilePath.endsWith("index.ts")) {
645
+ const actualFile = this.followReExport(
646
+ componentFile,
647
+ componentName,
648
+ path.dirname(componentFilePath)
649
+ );
650
+ if (actualFile) {
651
+ componentFile = actualFile;
652
+ }
653
+ }
654
+ const hasGraphQLImport = componentFile.getImportDeclarations().some((imp) => {
655
+ const spec = imp.getModuleSpecifierValue();
656
+ return spec.includes("@apollo/client") || spec.includes("apollo") || spec.includes("gql") || spec.includes("graphql") || // graphql.macro, graphql-tag
657
+ spec.includes("__generated__");
658
+ });
659
+ const relativeImports = componentFile.getImportDeclarations().filter((imp) => {
660
+ const spec = imp.getModuleSpecifierValue();
661
+ return spec.startsWith("./") || spec.startsWith("../");
662
+ });
663
+ for (const imp of relativeImports) {
664
+ const hookSpec = imp.getModuleSpecifierValue();
665
+ const hookNames = imp.getNamedImports().map((n) => n.getName()).filter((n) => /^use[A-Z]/.test(n));
666
+ for (const hookName of hookNames) {
667
+ const hookQueries = this.analyzeCustomHook(
668
+ path.dirname(componentFile.getFilePath()),
669
+ hookSpec,
670
+ hookName,
671
+ visited,
672
+ depth + 1
673
+ );
674
+ queries.push(...hookQueries);
675
+ }
676
+ }
677
+ for (const imp of relativeImports) {
678
+ const nestedSpec = imp.getModuleSpecifierValue();
679
+ const namedImports = imp.getNamedImports().map((n) => n.getName());
680
+ const defaultImport = imp.getDefaultImport()?.getText();
681
+ const componentImports = namedImports.filter(
682
+ (n) => /^[A-Z]/.test(n) && this.isComponentName(n)
683
+ );
684
+ for (const nestedComponentName of componentImports) {
685
+ const nestedQueries = this.analyzeImportedComponent(
686
+ path.dirname(componentFile.getFilePath()),
687
+ nestedSpec,
688
+ nestedComponentName,
689
+ visited,
690
+ depth + 1
691
+ );
692
+ queries.push(...nestedQueries);
693
+ }
694
+ if (defaultImport && /^[A-Z]/.test(defaultImport) && this.isComponentName(defaultImport)) {
695
+ const nestedQueries = this.analyzeImportedComponent(
696
+ path.dirname(componentFile.getFilePath()),
697
+ nestedSpec,
698
+ defaultImport,
699
+ visited,
700
+ depth + 1
701
+ );
702
+ queries.push(...nestedQueries);
703
+ }
704
+ }
705
+ if (hasGraphQLImport) {
706
+ const hookCalls = componentFile.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => {
707
+ const expression = call.getExpression().getText();
708
+ return ["useQuery", "useMutation", "useLazyQuery", "useSubscription"].includes(
709
+ expression
710
+ );
711
+ });
712
+ for (const call of hookCalls) {
713
+ const hookName = call.getExpression().getText();
714
+ const args = call.getArguments();
715
+ if (args.length === 0) continue;
716
+ const firstArg = args[0];
717
+ const firstArgText = firstArg.getText();
718
+ let operationName = firstArgText;
719
+ let operationType = null;
720
+ const codegenInfo = this.resolveDocumentName(firstArgText);
721
+ if (codegenInfo) {
722
+ operationName = codegenInfo.operationName;
723
+ operationType = codegenInfo.operationType;
724
+ } else if (firstArgText.endsWith("Document")) {
725
+ operationName = firstArgText.replace(/Document$/, "");
726
+ } else if (/^[A-Za-z]/.test(firstArgText)) {
727
+ const extractedName = this.findOperationNameFromVariable(componentFile, firstArgText);
728
+ if (extractedName) {
729
+ operationName = extractedName;
730
+ }
731
+ if (operationName === firstArgText && firstArgText !== "Query" && firstArgText !== "Mutation") {
732
+ const nameMatch = firstArgText.match(/^(.+?)(Query|Mutation|Subscription)$/);
733
+ if (nameMatch) {
734
+ operationName = nameMatch[1];
735
+ }
736
+ }
737
+ }
738
+ if (operationName !== "Query" && operationName !== "Mutation") {
739
+ const cleanedName = operationName.replace(/Document$/, "").replace(/Query$|Mutation$/, "");
740
+ operationName = cleanedName || operationName;
741
+ }
742
+ if (operationName === "Query" || operationName === "Mutation" || operationName === "") {
743
+ if (operationName === "") {
744
+ operationName = firstArgText || "Unknown";
745
+ }
746
+ }
747
+ const type = operationType ? operationType === "mutation" ? "useMutation" : operationType === "subscription" ? "useSubscription" : hookName.includes("Lazy") ? "useLazyQuery" : "useQuery" : hookName.includes("Mutation") ? "useMutation" : hookName.includes("Lazy") ? "useLazyQuery" : "useQuery";
748
+ queries.push({
749
+ type,
750
+ operationName,
751
+ variables: []
752
+ });
753
+ }
754
+ }
755
+ this.symbolTraceCache.set(cacheKey, queries);
756
+ } catch {
757
+ }
758
+ return queries;
759
+ }
760
+ /**
761
+ * Analyze a custom hook file for GraphQL queries with recursive tracing
762
+ */
763
+ analyzeCustomHook(sourceFileDir, moduleSpec, hookName, visited = /* @__PURE__ */ new Set(), depth = 0) {
764
+ const MAX_DEPTH = 10;
765
+ if (depth > MAX_DEPTH) return [];
766
+ const queries = [];
767
+ try {
768
+ const resolvedPath = path.resolve(sourceFileDir, moduleSpec);
769
+ const cacheKey = `hook:${resolvedPath}:${hookName}`;
770
+ if (visited.has(cacheKey)) return [];
771
+ visited.add(cacheKey);
772
+ const cachedHookResult = this.symbolTraceCache.get(cacheKey);
773
+ if (cachedHookResult !== void 0) {
774
+ return cachedHookResult;
775
+ }
776
+ const possiblePaths = [
777
+ `${resolvedPath}.tsx`,
778
+ `${resolvedPath}.ts`,
779
+ `${resolvedPath}/${hookName}.tsx`,
780
+ `${resolvedPath}/${hookName}.ts`,
781
+ `${resolvedPath}/index.tsx`,
782
+ `${resolvedPath}/index.ts`
783
+ ];
784
+ let hookFile;
785
+ for (const tryPath of possiblePaths) {
786
+ try {
787
+ hookFile = this.project.addSourceFileAtPath(tryPath);
788
+ if (hookFile) break;
789
+ } catch {
790
+ }
791
+ }
792
+ if (!hookFile) return queries;
793
+ const hasGraphQLImport = hookFile.getImportDeclarations().some((imp) => {
794
+ const spec = imp.getModuleSpecifierValue();
795
+ return spec.includes("@apollo/client") || spec.includes("apollo") || spec.includes("graphql") || spec.includes("__generated__");
796
+ });
797
+ const relativeImports = hookFile.getImportDeclarations().filter((imp) => {
798
+ const spec = imp.getModuleSpecifierValue();
799
+ return spec.startsWith("./") || spec.startsWith("../");
800
+ });
801
+ for (const imp of relativeImports) {
802
+ const nestedSpec = imp.getModuleSpecifierValue();
803
+ const nestedHookNames = imp.getNamedImports().map((n) => n.getName()).filter((n) => /^use[A-Z]/.test(n));
804
+ for (const nestedHookName of nestedHookNames) {
805
+ const nestedQueries = this.analyzeCustomHook(
806
+ path.dirname(hookFile.getFilePath()),
807
+ nestedSpec,
808
+ nestedHookName,
809
+ visited,
810
+ depth + 1
811
+ );
812
+ queries.push(...nestedQueries);
813
+ }
814
+ }
815
+ if (!hasGraphQLImport && queries.length === 0) {
816
+ return queries;
817
+ }
818
+ const hookCalls = hookFile.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => {
819
+ const expression = call.getExpression().getText();
820
+ return ["useQuery", "useMutation", "useLazyQuery", "useSubscription"].includes(expression);
821
+ });
822
+ for (const call of hookCalls) {
823
+ const callHookName = call.getExpression().getText();
824
+ const args = call.getArguments();
825
+ if (args.length === 0) continue;
826
+ const firstArgText = args[0].getText();
827
+ let operationName = firstArgText;
828
+ let operationType = null;
829
+ const codegenInfo = this.resolveDocumentName(firstArgText);
830
+ if (codegenInfo) {
831
+ operationName = codegenInfo.operationName;
832
+ operationType = codegenInfo.operationType;
833
+ } else if (firstArgText.endsWith("Document")) {
834
+ operationName = firstArgText.replace(/Document$/, "");
835
+ } else if (/^[A-Za-z]/.test(firstArgText)) {
836
+ const extractedName = this.findOperationNameFromVariable(hookFile, firstArgText);
837
+ if (extractedName) {
838
+ operationName = extractedName;
839
+ }
840
+ if (operationName === firstArgText && firstArgText !== "Query" && firstArgText !== "Mutation") {
841
+ const nameMatch = firstArgText.match(/^(.+?)(Query|Mutation|Subscription)$/);
842
+ if (nameMatch) {
843
+ operationName = nameMatch[1];
844
+ }
845
+ }
846
+ }
847
+ if (operationName !== "Query" && operationName !== "Mutation") {
848
+ const cleanedName = operationName.replace(/Document$/, "").replace(/Query$|Mutation$/, "");
849
+ operationName = cleanedName || operationName;
850
+ }
851
+ if (operationName === "") {
852
+ operationName = firstArgText || "Unknown";
853
+ }
854
+ const type = operationType ? operationType === "mutation" ? "useMutation" : operationType === "subscription" ? "useSubscription" : callHookName.includes("Lazy") ? "useLazyQuery" : "useQuery" : callHookName.includes("Mutation") ? "useMutation" : callHookName.includes("Lazy") ? "useLazyQuery" : "useQuery";
855
+ queries.push({
856
+ type,
857
+ operationName: `\u2192 ${operationName} (via ${hookName})`,
858
+ variables: []
859
+ });
860
+ }
861
+ this.symbolTraceCache.set(cacheKey, queries);
862
+ } catch {
863
+ }
864
+ return queries;
865
+ }
866
+ // Global context providers and their GraphQL queries
867
+ globalContextQueries = [];
868
+ /**
869
+ * Find page files from multiple possible locations
870
+ * Next.js, React, Rails+React 구조 모두 지원
871
+ */
872
+ async findPageFiles() {
873
+ const pagesDir = this.getSetting("pagesDir", "src/pages");
874
+ const allFiles = [];
875
+ const nextjsDirsSet = /* @__PURE__ */ new Set([pagesDir, "pages", "src/pages", "app", "src/app"]);
876
+ const nextjsDirs = [...nextjsDirsSet];
877
+ for (const dir of nextjsDirs) {
878
+ if (dir === "app" || dir === "src/app") {
879
+ const railsIndicators = ["controllers", "models", "views", "helpers"];
880
+ const dirPath2 = this.resolvePath(dir);
881
+ const hasRailsStructure = railsIndicators.some((subdir) => {
882
+ try {
883
+ return fs.existsSync(path.join(dirPath2, subdir));
884
+ } catch {
885
+ return false;
886
+ }
887
+ });
888
+ if (hasRailsStructure) {
889
+ continue;
890
+ }
891
+ }
892
+ const dirPath = this.resolvePath(dir);
893
+ try {
894
+ const files = await fg(["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"], {
895
+ cwd: dirPath,
896
+ ignore: [
897
+ "_app.tsx",
898
+ "_app.ts",
899
+ "_app.jsx",
900
+ "_app.js",
901
+ "_document.tsx",
902
+ "_document.ts",
903
+ "_document.jsx",
904
+ "_document.js",
905
+ "_error.tsx",
906
+ "_error.ts",
907
+ "_error.jsx",
908
+ "_error.js",
909
+ "api/**",
910
+ "**/*.test.*",
911
+ "**/*.spec.*",
912
+ "**/node_modules/**",
913
+ "**/components/pages/**"
914
+ // Reusable page components, not routes
915
+ ],
916
+ absolute: true
917
+ });
918
+ allFiles.push(...files);
919
+ if (files.length > 0) {
920
+ this.log(`Found ${files.length} pages in ${dir}`);
921
+ }
922
+ } catch {
923
+ }
924
+ }
925
+ const railsReactDirs = ["frontend/src/**/pages", "app/javascript/**/pages"];
926
+ for (const pattern of railsReactDirs) {
927
+ try {
928
+ const files = await fg(
929
+ [
930
+ `${pattern}/**/*.tsx`,
931
+ `${pattern}/**/*.ts`,
932
+ `${pattern}/**/*.jsx`,
933
+ `${pattern}/**/*.js`
934
+ ],
935
+ {
936
+ cwd: this.basePath,
937
+ ignore: [
938
+ "**/*.test.*",
939
+ "**/*.spec.*",
940
+ "**/node_modules/**",
941
+ "**/vendor/**",
942
+ "**/components/pages/**",
943
+ // Exclude reusable page components (not actual routes)
944
+ "**/stories/**"
945
+ // Exclude storybook files
946
+ ],
947
+ absolute: true
948
+ }
949
+ );
950
+ allFiles.push(...files);
951
+ if (files.length > 0) {
952
+ this.log(`Found ${files.length} React pages in ${pattern}`);
953
+ }
954
+ } catch {
955
+ }
956
+ }
957
+ const entryPatterns = [
958
+ "frontend/src/**/index.tsx",
959
+ "frontend/src/**/App.tsx",
960
+ "app/javascript/packs/*.tsx",
961
+ "app/javascript/packs/*.jsx"
962
+ ];
963
+ for (const pattern of entryPatterns) {
964
+ try {
965
+ const files = await fg([pattern], {
966
+ cwd: this.basePath,
967
+ ignore: ["**/node_modules/**", "**/vendor/**"],
968
+ absolute: true
969
+ });
970
+ for (const file of files) {
971
+ if (!allFiles.includes(file)) {
972
+ allFiles.push(file);
973
+ }
974
+ }
975
+ } catch {
976
+ }
977
+ }
978
+ return [...new Set(allFiles)];
979
+ }
980
+ /**
981
+ * Analyze _app.tsx for global providers that use GraphQL
982
+ * _app.tsx에서 전역 Context Provider의 GraphQL 분석
983
+ */
984
+ async analyzeAppFile() {
985
+ const pagesDir = this.getSetting("pagesDir", "src/pages");
986
+ const possiblePaths = [
987
+ this.resolvePath(`${pagesDir}/_app.tsx`),
988
+ this.resolvePath(`${pagesDir}/_app.ts`)
989
+ ];
990
+ for (const appPath of possiblePaths) {
991
+ try {
992
+ const appFile = this.project.addSourceFileAtPath(appPath);
993
+ if (!appFile) continue;
994
+ const jsxElements = appFile.getDescendantsOfKind(SyntaxKind.JsxElement);
995
+ const jsxSelfClosing = appFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement);
996
+ const providerNames = /* @__PURE__ */ new Set();
997
+ for (const el of [...jsxElements, ...jsxSelfClosing]) {
998
+ const tagName = el.getFirstDescendantByKind(SyntaxKind.Identifier)?.getText();
999
+ if (tagName && (tagName.includes("Provider") || tagName.includes("Context"))) {
1000
+ providerNames.add(tagName);
1001
+ }
1002
+ }
1003
+ for (const imp of appFile.getImportDeclarations()) {
1004
+ const spec = imp.getModuleSpecifierValue();
1005
+ if (!spec.startsWith("./") && !spec.startsWith("../")) continue;
1006
+ const namedImports = imp.getNamedImports().map((n) => n.getName());
1007
+ const defaultImport = imp.getDefaultImport()?.getText();
1008
+ for (const providerName of providerNames) {
1009
+ if (namedImports.includes(providerName) || defaultImport === providerName) {
1010
+ const providerQueries = this.analyzeImportedComponent(
1011
+ path.dirname(appPath),
1012
+ spec,
1013
+ providerName,
1014
+ /* @__PURE__ */ new Set(),
1015
+ 0
1016
+ );
1017
+ for (const q of providerQueries) {
1018
+ this.globalContextQueries.push({
1019
+ ...q,
1020
+ operationName: `[Global] ${q.operationName}`
1021
+ });
1022
+ }
1023
+ }
1024
+ }
1025
+ }
1026
+ if (this.globalContextQueries.length > 0) {
1027
+ this.log(`Found ${this.globalContextQueries.length} global context queries from _app`);
1028
+ }
1029
+ return;
1030
+ } catch {
1031
+ }
1032
+ }
1033
+ }
1034
+ /**
1035
+ * Load codegen mapping from __generated__ folders (optional)
1036
+ * 코드젠 폴더가 있으면 Document → Operation name 매핑 로드
1037
+ */
1038
+ async loadCodegenMapping() {
1039
+ const possibleDirs = [
1040
+ "__generated__",
1041
+ "src/__generated__",
1042
+ "src/__generated__/gql-graphql-gateway",
1043
+ "generated",
1044
+ "src/generated"
1045
+ ];
1046
+ for (const dir of possibleDirs) {
1047
+ const generatedPath = this.resolvePath(dir);
1048
+ try {
1049
+ const files = await fg(["**/*.ts", "**/*.tsx"], {
1050
+ cwd: generatedPath,
1051
+ absolute: true,
1052
+ onlyFiles: true
1053
+ });
1054
+ for (const file of files) {
1055
+ try {
1056
+ const sourceFile = this.project.addSourceFileAtPath(file);
1057
+ const varDecls = sourceFile.getVariableDeclarations();
1058
+ for (const varDecl of varDecls) {
1059
+ const name = varDecl.getName();
1060
+ if (name.endsWith("Document")) {
1061
+ const initializer = varDecl.getInitializer()?.getText() ?? "";
1062
+ const operationMatch = initializer.match(/(?:query|mutation|subscription)\s+(\w+)/);
1063
+ const typeMatch = initializer.match(/(query|mutation|subscription)\s+/);
1064
+ if (operationMatch) {
1065
+ this.codegenMap.set(name, {
1066
+ operationName: operationMatch[1],
1067
+ operationType: typeMatch ? typeMatch[1] : "query"
1068
+ });
1069
+ }
1070
+ }
1071
+ }
1072
+ const typeAliases = sourceFile.getTypeAliases();
1073
+ for (const typeAlias of typeAliases) {
1074
+ const name = typeAlias.getName();
1075
+ if ((name.endsWith("Query") || name.endsWith("Mutation") || name.endsWith("Subscription")) && !name.endsWith("Variables")) {
1076
+ const docName = name + "Document";
1077
+ if (!this.codegenMap.has(docName)) {
1078
+ const operationType = name.endsWith("Mutation") ? "mutation" : name.endsWith("Subscription") ? "subscription" : "query";
1079
+ this.codegenMap.set(docName, {
1080
+ operationName: name,
1081
+ operationType
1082
+ });
1083
+ }
1084
+ }
1085
+ }
1086
+ } catch {
1087
+ }
1088
+ }
1089
+ if (this.codegenMap.size > 0) {
1090
+ this.log(`Loaded ${this.codegenMap.size} codegen mappings from ${dir}`);
1091
+ return;
1092
+ }
1093
+ } catch {
1094
+ }
1095
+ }
1096
+ }
1097
+ /**
1098
+ * Resolve Document name to operation name using codegen map
1099
+ */
1100
+ resolveDocumentName(documentName) {
1101
+ const genericTypeNames = /* @__PURE__ */ new Set(["Query", "Mutation", "Subscription"]);
1102
+ if (genericTypeNames.has(documentName)) {
1103
+ return null;
1104
+ }
1105
+ const directResult = this.codegenMap.get(documentName);
1106
+ if (directResult !== void 0) {
1107
+ return directResult;
1108
+ }
1109
+ const withSuffix = documentName.endsWith("Document") ? documentName : documentName + "Document";
1110
+ const suffixResult = this.codegenMap.get(withSuffix);
1111
+ if (suffixResult !== void 0) {
1112
+ return suffixResult;
1113
+ }
1114
+ return null;
1115
+ }
1116
+ /**
1117
+ * Follow re-export in index file to find the actual component file
1118
+ */
1119
+ followReExport(indexFile, componentName, indexDir) {
1120
+ try {
1121
+ const exportDecls = indexFile.getExportDeclarations();
1122
+ let firstExportFile = null;
1123
+ for (const exportDecl of exportDecls) {
1124
+ const namedExports = exportDecl.getNamedExports();
1125
+ for (const named of namedExports) {
1126
+ const moduleSpec = exportDecl.getModuleSpecifierValue();
1127
+ if (!moduleSpec) continue;
1128
+ const resolvedPath = path.resolve(indexDir, moduleSpec);
1129
+ const possiblePaths = [
1130
+ `${resolvedPath}.tsx`,
1131
+ `${resolvedPath}.ts`,
1132
+ `${resolvedPath}/index.tsx`,
1133
+ `${resolvedPath}/index.ts`
1134
+ ];
1135
+ let file;
1136
+ for (const tryPath of possiblePaths) {
1137
+ try {
1138
+ file = this.project.addSourceFileAtPath(tryPath);
1139
+ if (file) break;
1140
+ } catch {
1141
+ }
1142
+ }
1143
+ if (!file) continue;
1144
+ if (!firstExportFile) {
1145
+ firstExportFile = file;
1146
+ }
1147
+ if (named.getName() === componentName || named.getAliasNode()?.getText() === componentName) {
1148
+ return file;
1149
+ }
1150
+ }
1151
+ }
1152
+ if (firstExportFile) {
1153
+ return firstExportFile;
1154
+ }
1155
+ for (const exportDecl of indexFile.getExportDeclarations()) {
1156
+ if (exportDecl.isNamespaceExport()) {
1157
+ const moduleSpec = exportDecl.getModuleSpecifierValue();
1158
+ if (moduleSpec) {
1159
+ const resolvedPath = path.resolve(indexDir, moduleSpec);
1160
+ const possiblePaths = [`${resolvedPath}.tsx`, `${resolvedPath}.ts`];
1161
+ for (const tryPath of possiblePaths) {
1162
+ try {
1163
+ const file = this.project.addSourceFileAtPath(tryPath);
1164
+ if (file) {
1165
+ const hasExport = file.getExportedDeclarations().has(componentName);
1166
+ if (hasExport) return file;
1167
+ }
1168
+ } catch {
1169
+ }
1170
+ }
1171
+ }
1172
+ }
1173
+ }
1174
+ } catch {
1175
+ }
1176
+ return null;
1177
+ }
1178
+ /**
1179
+ * Check if a name looks like a React component (PascalCase with common suffixes)
1180
+ */
1181
+ isComponentName(name) {
1182
+ if (!/^[A-Z]/.test(name)) return false;
1183
+ if (name.endsWith("Query") || name.endsWith("Mutation") || name.endsWith("Subscription") || name.endsWith("Fragment") || name.endsWith("Document") || name.endsWith("Variables") || name === "Query" || name === "Mutation" || name === "Subscription") {
1184
+ return false;
1185
+ }
1186
+ const typeDefinitions = /* @__PURE__ */ new Set([
1187
+ "NextPage",
1188
+ "NextPageContext",
1189
+ "NextApiRequest",
1190
+ "NextApiResponse",
1191
+ "GetServerSideProps",
1192
+ "GetStaticProps",
1193
+ "GetStaticPaths",
1194
+ "InferGetServerSidePropsType",
1195
+ "InferGetStaticPropsType",
1196
+ "FC",
1197
+ "FunctionComponent",
1198
+ "VFC",
1199
+ "Component",
1200
+ "PureComponent",
1201
+ "ReactNode",
1202
+ "ReactElement",
1203
+ "PropsWithChildren",
1204
+ "ComponentProps",
1205
+ "ComponentType",
1206
+ "ElementType",
1207
+ "RefObject",
1208
+ "MutableRefObject",
1209
+ "Dispatch",
1210
+ "SetStateAction",
1211
+ "ChangeEvent",
1212
+ "MouseEvent",
1213
+ "KeyboardEvent",
1214
+ "FormEvent",
1215
+ "SyntheticEvent"
1216
+ ]);
1217
+ if (typeDefinitions.has(name)) {
1218
+ return false;
1219
+ }
1220
+ const componentSuffixes = [
1221
+ "Container",
1222
+ "Page",
1223
+ "Screen",
1224
+ "View",
1225
+ "Form",
1226
+ "Modal",
1227
+ "Dialog",
1228
+ "Panel",
1229
+ "Root",
1230
+ "Provider",
1231
+ "Wrapper"
1232
+ ];
1233
+ if (componentSuffixes.some((suffix) => name.endsWith(suffix))) {
1234
+ return true;
1235
+ }
1236
+ if (/Page[A-Z]?\w*$/.test(name) || /Container[A-Z]?\w*$/.test(name)) {
1237
+ return true;
1238
+ }
1239
+ if (/^[A-Z][a-z]+[A-Z][a-z]+/.test(name)) {
1240
+ return true;
1241
+ }
1242
+ return false;
1243
+ }
1244
+ extractNavigation(sourceFile) {
1245
+ const result = {
1246
+ visible: true,
1247
+ currentNavItem: null
1248
+ };
1249
+ try {
1250
+ const navStyleAssignment = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression).find((node) => {
1251
+ try {
1252
+ return node.getName() === "globalNavigationStyle";
1253
+ } catch {
1254
+ return false;
1255
+ }
1256
+ });
1257
+ if (navStyleAssignment) {
1258
+ const parent = navStyleAssignment.getParent();
1259
+ if (Node.isBinaryExpression(parent)) {
1260
+ const right = parent.getRight();
1261
+ const visibleProp = right.getDescendantsOfKind(SyntaxKind.PropertyAssignment).find((prop) => {
1262
+ try {
1263
+ return prop.getName() === "visible";
1264
+ } catch {
1265
+ return false;
1266
+ }
1267
+ });
1268
+ if (visibleProp) {
1269
+ result.visible = visibleProp.getInitializer()?.getText() === "true";
1270
+ }
1271
+ const navItemProp = right.getDescendantsOfKind(SyntaxKind.PropertyAssignment).find((prop) => {
1272
+ try {
1273
+ return prop.getName() === "currentNavItem";
1274
+ } catch {
1275
+ return false;
1276
+ }
1277
+ });
1278
+ if (navItemProp) {
1279
+ const value = navItemProp.getInitializer()?.getText();
1280
+ result.currentNavItem = value && value !== "null" ? value.replace(/['"]/g, "") : null;
1281
+ }
1282
+ const miniProp = right.getDescendantsOfKind(SyntaxKind.PropertyAssignment).find((prop) => {
1283
+ try {
1284
+ return prop.getName() === "mini";
1285
+ } catch {
1286
+ return false;
1287
+ }
1288
+ });
1289
+ if (miniProp) {
1290
+ result.mini = miniProp.getInitializer()?.getText() === "true";
1291
+ }
1292
+ }
1293
+ }
1294
+ } catch {
1295
+ }
1296
+ return result;
1297
+ }
1298
+ /**
1299
+ * Extract multi-step flow information (wizard, stepper, onboarding)
1300
+ */
1301
+ extractSteps(sourceFile) {
1302
+ const steps = [];
1303
+ const useStateCalls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => call.getExpression().getText() === "useState");
1304
+ for (const call of useStateCalls) {
1305
+ const parent = call.getParent();
1306
+ if (!parent) continue;
1307
+ const parentText = parent.getText();
1308
+ const stepMatch = parentText.match(
1309
+ /\[\s*(step|currentStep|activeStep|page|currentPage|phase|stage)\s*,/i
1310
+ );
1311
+ if (stepMatch) {
1312
+ const stepVarName = stepMatch[1];
1313
+ const switchStatements = sourceFile.getDescendantsOfKind(SyntaxKind.SwitchStatement);
1314
+ for (const switchStmt of switchStatements) {
1315
+ const expression = switchStmt.getExpression().getText();
1316
+ if (expression.includes(stepVarName)) {
1317
+ const caseBlocks = switchStmt.getClauses();
1318
+ caseBlocks.forEach((clause, idx) => {
1319
+ if (clause.isKind(SyntaxKind.CaseClause)) {
1320
+ const caseExpr = clause.getExpression()?.getText() || String(idx);
1321
+ const jsxElements2 = clause.getDescendantsOfKind(SyntaxKind.JsxOpeningElement);
1322
+ const componentName = jsxElements2.length > 0 ? jsxElements2[0].getTagNameNode().getText() : void 0;
1323
+ steps.push({
1324
+ id: caseExpr.replace(/['"]/g, ""),
1325
+ name: `Step ${caseExpr.replace(/['"]/g, "")}`,
1326
+ component: componentName
1327
+ });
1328
+ }
1329
+ });
1330
+ }
1331
+ }
1332
+ const arrayLiterals = sourceFile.getDescendantsOfKind(SyntaxKind.ArrayLiteralExpression);
1333
+ for (const arr of arrayLiterals) {
1334
+ const parentVar = arr.getParent();
1335
+ if (parentVar && parentVar.getText().match(/steps|pages|screens|views|components/i)) {
1336
+ const elements = arr.getElements();
1337
+ elements.forEach((el, idx) => {
1338
+ const elText = el.getText();
1339
+ if (elText.startsWith("{")) {
1340
+ const nameMatch = elText.match(/(?:name|label|title)\s*:\s*['"]([^'"]+)['"]/);
1341
+ const compMatch = elText.match(/(?:component|content)\s*:\s*<?\s*(\w+)/);
1342
+ steps.push({
1343
+ id: idx + 1,
1344
+ name: nameMatch ? nameMatch[1] : `Step ${idx + 1}`,
1345
+ component: compMatch ? compMatch[1] : void 0
1346
+ });
1347
+ } else if (/^[A-Z]/.test(elText)) {
1348
+ steps.push({
1349
+ id: idx + 1,
1350
+ name: elText,
1351
+ component: elText
1352
+ });
1353
+ }
1354
+ });
1355
+ }
1356
+ }
1357
+ }
1358
+ }
1359
+ const jsxElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement);
1360
+ for (const jsx of jsxElements) {
1361
+ const tagName = jsx.getTagNameNode().getText();
1362
+ if (tagName.match(/Stepper|Wizard|Steps|TabPanel|FormStep/i)) {
1363
+ const parent = jsx.getParent();
1364
+ if (parent && parent.isKind(SyntaxKind.JsxElement)) {
1365
+ const children = parent.getJsxChildren();
1366
+ children.forEach((child, idx) => {
1367
+ if (child.isKind(SyntaxKind.JsxElement) || child.isKind(SyntaxKind.JsxSelfClosingElement)) {
1368
+ const childTag = child.isKind(SyntaxKind.JsxElement) ? child.getOpeningElement().getTagNameNode().getText() : child.getTagNameNode().getText();
1369
+ const attrs = child.isKind(SyntaxKind.JsxElement) ? child.getOpeningElement().getAttributes() : child.getAttributes();
1370
+ let stepName = childTag;
1371
+ for (const attr of attrs) {
1372
+ if (attr.isKind(SyntaxKind.JsxAttribute)) {
1373
+ const name = attr.getNameNode().getText();
1374
+ if (name === "label" || name === "title" || name === "name") {
1375
+ const value = attr.getInitializer()?.getText();
1376
+ if (value) {
1377
+ stepName = value.replace(/['"{}]/g, "");
1378
+ break;
1379
+ }
1380
+ }
1381
+ }
1382
+ }
1383
+ steps.push({
1384
+ id: idx + 1,
1385
+ name: stepName,
1386
+ component: childTag
1387
+ });
1388
+ }
1389
+ });
1390
+ }
1391
+ }
1392
+ }
1393
+ const conditionalExprs = sourceFile.getDescendantsOfKind(SyntaxKind.ConditionalExpression);
1394
+ for (const cond of conditionalExprs) {
1395
+ const condition = cond.getCondition().getText();
1396
+ if (condition.match(/step\s*===?\s*\d+|currentStep|activeStep/i)) {
1397
+ const whenTrue = cond.getWhenTrue();
1398
+ const stepNumMatch = condition.match(/===?\s*(\d+)/);
1399
+ if (stepNumMatch && steps.length === 0) {
1400
+ const trueJsx = whenTrue.getDescendantsOfKind(SyntaxKind.JsxOpeningElement);
1401
+ if (trueJsx.length > 0) {
1402
+ steps.push({
1403
+ id: parseInt(stepNumMatch[1]),
1404
+ component: trueJsx[0].getTagNameNode().getText()
1405
+ });
1406
+ }
1407
+ }
1408
+ }
1409
+ }
1410
+ return steps;
1411
+ }
1412
+ extractLinkedPages(sourceFile) {
1413
+ const linkedPages = [];
1414
+ const routerCalls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => {
1415
+ const text = call.getExpression().getText();
1416
+ return text.includes("router.push") || text.includes("router.replace") || text.includes("Link");
1417
+ });
1418
+ for (const call of routerCalls) {
1419
+ const args = call.getArguments();
1420
+ if (args.length > 0) {
1421
+ const pathArg = args[0].getText();
1422
+ const pathMatch = pathArg.match(/['"`]([^'"`]+)['"`]/);
1423
+ if (pathMatch && !linkedPages.includes(pathMatch[1])) {
1424
+ linkedPages.push(pathMatch[1]);
1425
+ }
1426
+ }
1427
+ }
1428
+ const linkElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement).filter((node) => node.getTagNameNode().getText() === "Link");
1429
+ for (const link of linkElements) {
1430
+ try {
1431
+ const attributes = link.getAttributes();
1432
+ for (const attr of attributes) {
1433
+ if (attr.isKind(SyntaxKind.JsxAttribute)) {
1434
+ const nameNode = attr.getNameNode();
1435
+ const name = nameNode.getText();
1436
+ if (name === "href") {
1437
+ const value = attr.getInitializer()?.getText();
1438
+ if (value) {
1439
+ const pathMatch = value.match(/['"`]([^'"`]+)['"`]/);
1440
+ if (pathMatch && !linkedPages.includes(pathMatch[1])) {
1441
+ linkedPages.push(pathMatch[1]);
1442
+ }
1443
+ }
1444
+ }
1445
+ }
1446
+ }
1447
+ } catch {
1448
+ }
1449
+ }
1450
+ return linkedPages;
1451
+ }
1452
+ };
1453
+ var GraphQLAnalyzer = class extends BaseAnalyzer {
1454
+ project;
1455
+ constructor(config) {
1456
+ super(config);
1457
+ this.project = new Project({
1458
+ tsConfigFilePath: this.resolvePath("tsconfig.json"),
1459
+ skipAddingFilesFromTsConfig: true
1460
+ });
1461
+ }
1462
+ getName() {
1463
+ return "GraphQLAnalyzer";
1464
+ }
1465
+ async analyze() {
1466
+ this.log("Starting GraphQL analysis...");
1467
+ const operations = [];
1468
+ const graphqlOperations = await this.analyzeGraphQLFiles();
1469
+ operations.push(...graphqlOperations);
1470
+ const inlineOperations = await this.analyzeInlineGraphQL();
1471
+ operations.push(...inlineOperations);
1472
+ await this.findOperationUsage(operations);
1473
+ this.log(`Found ${operations.length} GraphQL operations`);
1474
+ return { graphqlOperations: operations };
1475
+ }
1476
+ async analyzeGraphQLFiles() {
1477
+ const graphqlFiles = await fg(["**/*.graphql"], {
1478
+ cwd: this.basePath,
1479
+ ignore: ["**/node_modules/**", "**/.next/**"],
1480
+ absolute: true
1481
+ });
1482
+ const results = await parallelMapSafe(graphqlFiles, async (filePath) => {
1483
+ const content = await fs2.readFile(filePath, "utf-8");
1484
+ const document = parse(content);
1485
+ return this.extractOperationsFromDocument(document, path.relative(this.basePath, filePath));
1486
+ });
1487
+ return results.flat();
1488
+ }
1489
+ async analyzeInlineGraphQL() {
1490
+ const operations = [];
1491
+ const tsFiles = await fg(["**/*.ts", "**/*.tsx"], {
1492
+ cwd: this.basePath,
1493
+ ignore: [
1494
+ "**/node_modules/**",
1495
+ "**/.next/**",
1496
+ "**/__tests__/**",
1497
+ "**/*.test.*",
1498
+ "**/*.spec.*"
1499
+ ],
1500
+ absolute: true
1501
+ });
1502
+ for (const filePath of tsFiles) {
1503
+ try {
1504
+ const sourceFile = this.project.addSourceFileAtPath(filePath);
1505
+ const relativePath = path.relative(this.basePath, filePath);
1506
+ const hasGqlImport = sourceFile.getImportDeclarations().some((imp) => {
1507
+ const spec = imp.getModuleSpecifierValue();
1508
+ const namedImports = imp.getNamedImports().map((n) => n.getName());
1509
+ const defaultImport = imp.getDefaultImport()?.getText();
1510
+ return (namedImports.includes("gql") || namedImports.includes("graphql") || defaultImport === "gql") && (spec.includes("graphql") || spec.includes("apollo") || spec.includes("gql") || spec.includes("__generated__"));
1511
+ });
1512
+ const taggedTemplates = sourceFile.getDescendantsOfKind(
1513
+ SyntaxKind.TaggedTemplateExpression
1514
+ );
1515
+ for (const template of taggedTemplates) {
1516
+ const tag = template.getTag().getText();
1517
+ if (tag === "gql" || tag === "graphql") {
1518
+ try {
1519
+ const templateLiteral = template.getTemplate();
1520
+ let content = "";
1521
+ if (templateLiteral.isKind(SyntaxKind.NoSubstitutionTemplateLiteral)) {
1522
+ content = templateLiteral.getLiteralValue();
1523
+ } else if (templateLiteral.isKind(SyntaxKind.TemplateExpression)) {
1524
+ const fullText = templateLiteral.getText();
1525
+ content = fullText.slice(1, -1).replace(/\$\{[^}]*\}/g, "");
1526
+ }
1527
+ if (content && content.trim()) {
1528
+ try {
1529
+ const document = parse(content);
1530
+ const fileOperations = this.extractOperationsFromDocument(document, relativePath);
1531
+ operations.push(...fileOperations);
1532
+ } catch {
1533
+ }
1534
+ }
1535
+ } catch {
1536
+ }
1537
+ }
1538
+ }
1539
+ const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
1540
+ for (const call of callExpressions) {
1541
+ try {
1542
+ const expression = call.getExpression();
1543
+ const expressionText = expression.getText();
1544
+ if (expressionText === "gql" || expressionText === "graphql") {
1545
+ const args = call.getArguments();
1546
+ if (args.length > 0) {
1547
+ const firstArg = args[0];
1548
+ let content = "";
1549
+ if (firstArg.isKind(SyntaxKind.NoSubstitutionTemplateLiteral)) {
1550
+ content = firstArg.getLiteralValue();
1551
+ } else if (firstArg.isKind(SyntaxKind.TemplateExpression)) {
1552
+ const fullText = firstArg.getText();
1553
+ content = fullText.slice(1, -1).replace(/\$\{[^}]*\}/g, "");
1554
+ } else {
1555
+ const argText = firstArg.getText();
1556
+ if (argText.includes("`")) {
1557
+ const match = argText.match(/\/\*\s*GraphQL\s*\*\/\s*`([^`]*)`/);
1558
+ if (match) {
1559
+ content = match[1];
1560
+ } else {
1561
+ const simpleMatch = argText.match(/`([^`]*)`/);
1562
+ if (simpleMatch) {
1563
+ content = simpleMatch[1];
1564
+ }
1565
+ }
1566
+ }
1567
+ }
1568
+ if (content && content.trim()) {
1569
+ try {
1570
+ const document = parse(content);
1571
+ const fileOperations = this.extractOperationsFromDocument(
1572
+ document,
1573
+ relativePath
1574
+ );
1575
+ operations.push(...fileOperations);
1576
+ } catch {
1577
+ }
1578
+ }
1579
+ }
1580
+ }
1581
+ } catch {
1582
+ }
1583
+ }
1584
+ if (hasGqlImport) {
1585
+ const variableDeclarations = sourceFile.getVariableDeclarations();
1586
+ for (const varDecl of variableDeclarations) {
1587
+ const name = varDecl.getName();
1588
+ const isGraphQLLike = name.includes("QUERY") || name.includes("MUTATION") || name.includes("FRAGMENT") || name.includes("Query") || name.includes("Mutation") || name.includes("Subscription") || // SCREAMING_CASE constants ending in related words
1589
+ /^[A-Z_]+_(QUERY|MUTATION|FRAGMENT|SUBSCRIPTION)$/.test(name) || // PascalCase Query suffix
1590
+ /Query$|Mutation$|Fragment$|Subscription$/.test(name);
1591
+ if (isGraphQLLike) {
1592
+ const initializer = varDecl.getInitializer();
1593
+ if (initializer && initializer.isKind(SyntaxKind.CallExpression)) {
1594
+ }
1595
+ }
1596
+ }
1597
+ }
1598
+ } catch (error) {
1599
+ this.warn(`Failed to analyze ${filePath}: ${error.message}`);
1600
+ }
1601
+ }
1602
+ return operations;
1603
+ }
1604
+ extractOperationsFromDocument(document, filePath) {
1605
+ const operations = [];
1606
+ for (const definition of document.definitions) {
1607
+ const operation = this.extractOperation(definition, filePath);
1608
+ if (operation) {
1609
+ operations.push(operation);
1610
+ }
1611
+ }
1612
+ return operations;
1613
+ }
1614
+ extractOperation(definition, filePath) {
1615
+ if (definition.kind === "OperationDefinition") {
1616
+ const name = definition.name?.value || "anonymous";
1617
+ const type = definition.operation;
1618
+ const variables = this.extractVariables(definition);
1619
+ const fragments = this.extractFragmentReferences(definition);
1620
+ const fields = this.extractFields(definition);
1621
+ return {
1622
+ name,
1623
+ type,
1624
+ filePath,
1625
+ usedIn: [],
1626
+ variables,
1627
+ returnType: this.inferReturnType(definition),
1628
+ fragments,
1629
+ fields
1630
+ };
1631
+ }
1632
+ if (definition.kind === "FragmentDefinition") {
1633
+ return {
1634
+ name: definition.name.value,
1635
+ type: "fragment",
1636
+ filePath,
1637
+ usedIn: [],
1638
+ variables: [],
1639
+ returnType: definition.typeCondition.name.value,
1640
+ fragments: this.extractFragmentReferences(definition),
1641
+ fields: this.extractFields(definition)
1642
+ };
1643
+ }
1644
+ return null;
1645
+ }
1646
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1647
+ extractFields(definition) {
1648
+ const fields = [];
1649
+ const extractFromSelectionSet = (selectionSet, depth = 0) => {
1650
+ if (!selectionSet || !selectionSet.selections || depth > 5) return [];
1651
+ const result = [];
1652
+ for (const selection of selectionSet.selections) {
1653
+ if (selection.kind === "Field") {
1654
+ const field = {
1655
+ name: selection.name.value
1656
+ };
1657
+ if (selection.arguments && selection.arguments.length > 0) {
1658
+ const args = selection.arguments.map((arg) => arg.name.value).join(", ");
1659
+ field.type = `(${args})`;
1660
+ }
1661
+ if (selection.selectionSet) {
1662
+ field.fields = extractFromSelectionSet(selection.selectionSet, depth + 1);
1663
+ }
1664
+ result.push(field);
1665
+ } else if (selection.kind === "FragmentSpread") {
1666
+ result.push({ name: `...${selection.name.value}`, type: "fragment" });
1667
+ } else if (selection.kind === "InlineFragment") {
1668
+ if (selection.selectionSet) {
1669
+ const typeName = selection.typeCondition?.name?.value || "inline";
1670
+ result.push({
1671
+ name: `... on ${typeName}`,
1672
+ type: "inline-fragment",
1673
+ fields: extractFromSelectionSet(selection.selectionSet, depth + 1)
1674
+ });
1675
+ }
1676
+ }
1677
+ }
1678
+ return result;
1679
+ };
1680
+ if (definition.selectionSet) {
1681
+ return extractFromSelectionSet(definition.selectionSet);
1682
+ }
1683
+ return fields;
1684
+ }
1685
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1686
+ extractVariables(definition) {
1687
+ const variables = [];
1688
+ if (definition.variableDefinitions) {
1689
+ for (const varDef of definition.variableDefinitions) {
1690
+ const name = varDef.variable.name.value;
1691
+ const type = this.typeNodeToString(varDef.type);
1692
+ const required = varDef.type.kind === "NonNullType";
1693
+ variables.push({ name, type, required });
1694
+ }
1695
+ }
1696
+ return variables;
1697
+ }
1698
+ typeNodeToString(typeNode) {
1699
+ if (typeNode.kind === "NonNullType") {
1700
+ return `${this.typeNodeToString(typeNode.type)}!`;
1701
+ }
1702
+ if (typeNode.kind === "ListType") {
1703
+ return `[${this.typeNodeToString(typeNode.type)}]`;
1704
+ }
1705
+ if (typeNode.kind === "NamedType") {
1706
+ return typeNode.name.value;
1707
+ }
1708
+ return "unknown";
1709
+ }
1710
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1711
+ extractFragmentReferences(definition) {
1712
+ const fragments = [];
1713
+ const visit = (node) => {
1714
+ if (!node) return;
1715
+ if (node.kind === "FragmentSpread") {
1716
+ fragments.push(node.name.value);
1717
+ }
1718
+ if (node.selectionSet) {
1719
+ for (const selection of node.selectionSet.selections) {
1720
+ visit(selection);
1721
+ }
1722
+ }
1723
+ };
1724
+ visit(definition);
1725
+ return fragments;
1726
+ }
1727
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1728
+ inferReturnType(definition) {
1729
+ if (definition.selectionSet && definition.selectionSet.selections.length > 0) {
1730
+ const firstSelection = definition.selectionSet.selections[0];
1731
+ if (firstSelection.kind === "Field") {
1732
+ return firstSelection.name.value;
1733
+ }
1734
+ }
1735
+ return "unknown";
1736
+ }
1737
+ async findOperationUsage(operations) {
1738
+ const tsFiles = await fg(["**/*.ts", "**/*.tsx"], {
1739
+ cwd: this.basePath,
1740
+ ignore: ["**/node_modules/**", "**/.next/**"],
1741
+ absolute: true
1742
+ });
1743
+ const operationNames = /* @__PURE__ */ new Map();
1744
+ for (const op of operations) {
1745
+ operationNames.set(op.name, op);
1746
+ }
1747
+ for (const filePath of tsFiles) {
1748
+ try {
1749
+ const content = await fs2.readFile(filePath, "utf-8");
1750
+ const relativePath = path.relative(this.basePath, filePath);
1751
+ for (const [name, operation] of operationNames) {
1752
+ if (content.includes(`useQuery<${name}`) || content.includes(`useMutation<${name}`) || content.includes(`useLazyQuery<${name}`) || content.includes(`${name}Query`) || content.includes(`${name}Mutation`) || content.includes(`${name}Variables`)) {
1753
+ if (!operation.usedIn.includes(relativePath)) {
1754
+ operation.usedIn.push(relativePath);
1755
+ }
1756
+ }
1757
+ }
1758
+ } catch {
1759
+ }
1760
+ }
1761
+ }
1762
+ };
1763
+ var DataFlowAnalyzer = class extends BaseAnalyzer {
1764
+ project;
1765
+ componentCache = /* @__PURE__ */ new Map();
1766
+ constructor(config) {
1767
+ super(config);
1768
+ this.project = new Project({
1769
+ tsConfigFilePath: this.resolvePath("tsconfig.json"),
1770
+ skipAddingFilesFromTsConfig: true
1771
+ });
1772
+ }
1773
+ getName() {
1774
+ return "DataFlowAnalyzer";
1775
+ }
1776
+ async analyze() {
1777
+ this.log("Starting data flow analysis...");
1778
+ const components = await this.analyzeComponents();
1779
+ const dataFlows = await this.analyzeDataFlows(components);
1780
+ this.log(`Analyzed ${components.length} components and ${dataFlows.length} data flows`);
1781
+ return { components, dataFlows };
1782
+ }
1783
+ async analyzeComponents() {
1784
+ const components = [];
1785
+ const configuredDirs = [
1786
+ this.getSetting("featuresDir", ""),
1787
+ this.getSetting("componentsDir", ""),
1788
+ this.getSetting("pagesDir", "")
1789
+ ].filter(Boolean);
1790
+ const commonDirs = [
1791
+ "src/features",
1792
+ "src/components",
1793
+ "src/common/components",
1794
+ "src/common",
1795
+ "src/pages",
1796
+ "src/app",
1797
+ "src/modules",
1798
+ "src/views",
1799
+ "src/screens",
1800
+ "components",
1801
+ "pages",
1802
+ "app"
1803
+ ];
1804
+ const dirs = [.../* @__PURE__ */ new Set([...configuredDirs, ...commonDirs])];
1805
+ for (const dir of dirs) {
1806
+ const files = await fg(["**/*.tsx"], {
1807
+ cwd: this.resolvePath(dir),
1808
+ ignore: ["**/*.test.*", "**/*.spec.*", "**/*.stories.*"],
1809
+ absolute: true
1810
+ });
1811
+ for (const filePath of files) {
1812
+ try {
1813
+ const sourceFile = this.project.addSourceFileAtPath(filePath);
1814
+ const relativePath = path.relative(this.basePath, filePath);
1815
+ const componentInfos = this.analyzeComponentFile(sourceFile, relativePath);
1816
+ components.push(...componentInfos);
1817
+ } catch (error) {
1818
+ this.warn(`Failed to analyze ${filePath}: ${error.message}`);
1819
+ }
1820
+ }
1821
+ }
1822
+ this.buildDependencyGraph(components);
1823
+ return components;
1824
+ }
1825
+ analyzeComponentFile(sourceFile, filePath) {
1826
+ const components = [];
1827
+ const functionDeclarations = sourceFile.getFunctions();
1828
+ for (const func of functionDeclarations) {
1829
+ const name = func.getName();
1830
+ if (name && this.isComponentName(name)) {
1831
+ const info = this.extractComponentInfo(func, name, filePath);
1832
+ components.push(info);
1833
+ this.componentCache.set(name, info);
1834
+ }
1835
+ }
1836
+ const variableDeclarations = sourceFile.getVariableDeclarations();
1837
+ for (const varDecl of variableDeclarations) {
1838
+ const name = varDecl.getName();
1839
+ if (this.isComponentName(name)) {
1840
+ const initializer = varDecl.getInitializer();
1841
+ if (initializer && (initializer.isKind(SyntaxKind.ArrowFunction) || initializer.isKind(SyntaxKind.FunctionExpression))) {
1842
+ const info = this.extractComponentInfo(initializer, name, filePath);
1843
+ components.push(info);
1844
+ this.componentCache.set(name, info);
1845
+ }
1846
+ }
1847
+ }
1848
+ const hookFunctions = sourceFile.getFunctions().filter((f) => {
1849
+ const name = f.getName();
1850
+ return name && name.startsWith("use");
1851
+ });
1852
+ for (const hook of hookFunctions) {
1853
+ const name = hook.getName() ?? "";
1854
+ const info = this.extractHookInfo(hook, name, filePath);
1855
+ components.push(info);
1856
+ this.componentCache.set(name, info);
1857
+ }
1858
+ return components;
1859
+ }
1860
+ isComponentName(name) {
1861
+ return /^[A-Z][a-zA-Z0-9]*$/.test(name);
1862
+ }
1863
+ extractComponentInfo(node, name, filePath) {
1864
+ const sourceFile = node.getSourceFile();
1865
+ let type = "presentational";
1866
+ const pagePathPatterns = ["/pages/", "/app/", "/routes/", "/views/", "/screens/"];
1867
+ if (pagePathPatterns.some((pattern) => filePath.includes(pattern))) {
1868
+ type = "page";
1869
+ } else if (name.includes("Container") || name.includes("Provider")) {
1870
+ type = "container";
1871
+ } else if (
1872
+ // Layout detection - various naming patterns
1873
+ name.includes("Layout") || name.includes("Shell") || name.includes("Wrapper") || name.includes("Frame") || name.includes("Scaffold") || // Path-based layout detection
1874
+ filePath.includes("/layouts/") || filePath.includes("/layout/")
1875
+ ) {
1876
+ type = "layout";
1877
+ }
1878
+ const props = this.extractProps(node);
1879
+ const hooks = this.extractHooksUsed(node);
1880
+ const dependencies = this.extractDependencies(sourceFile);
1881
+ const stateManagement = this.extractStateManagement(node);
1882
+ return {
1883
+ name,
1884
+ filePath,
1885
+ type,
1886
+ props,
1887
+ dependencies,
1888
+ dependents: [],
1889
+ // Will be filled later
1890
+ hooks,
1891
+ stateManagement
1892
+ };
1893
+ }
1894
+ extractHookInfo(node, name, filePath) {
1895
+ const sourceFile = node.getSourceFile();
1896
+ const props = this.extractProps(node);
1897
+ const hooks = this.extractHooksUsed(node);
1898
+ const dependencies = this.extractDependencies(sourceFile);
1899
+ const stateManagement = this.extractStateManagement(node);
1900
+ return {
1901
+ name,
1902
+ filePath,
1903
+ type: "hook",
1904
+ props,
1905
+ dependencies,
1906
+ dependents: [],
1907
+ hooks,
1908
+ stateManagement
1909
+ };
1910
+ }
1911
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1912
+ extractProps(node) {
1913
+ const props = [];
1914
+ const parameters = node.getParameters?.() || [];
1915
+ if (parameters.length > 0) {
1916
+ const propsParam = parameters[0];
1917
+ const typeNode = propsParam.getTypeNode?.();
1918
+ if (typeNode) {
1919
+ const members = typeNode.getDescendantsOfKind?.(SyntaxKind.PropertySignature) || [];
1920
+ for (const member of members) {
1921
+ props.push({
1922
+ name: member.getName(),
1923
+ type: member.getType().getText(),
1924
+ required: !member.hasQuestionToken()
1925
+ });
1926
+ }
1927
+ }
1928
+ }
1929
+ return props;
1930
+ }
1931
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1932
+ extractHooksUsed(node) {
1933
+ const hooks = [];
1934
+ const callExpressions = node.getDescendantsOfKind?.(SyntaxKind.CallExpression) || [];
1935
+ for (const call of callExpressions) {
1936
+ try {
1937
+ const callName = call.getExpression().getText();
1938
+ if (callName.startsWith("use")) {
1939
+ if (callName === "useQuery" || callName === "useMutation" || callName === "useLazyQuery") {
1940
+ const operationInfo = this.extractOperationName(call, callName);
1941
+ if (!hooks.includes(operationInfo)) {
1942
+ hooks.push(operationInfo);
1943
+ }
1944
+ } else if (callName === "useContext") {
1945
+ const contextInfo = this.extractContextName(call);
1946
+ if (!hooks.includes(contextInfo)) {
1947
+ hooks.push(contextInfo);
1948
+ }
1949
+ } else if (!hooks.includes(callName)) {
1950
+ hooks.push(callName);
1951
+ }
1952
+ }
1953
+ } catch {
1954
+ }
1955
+ }
1956
+ return hooks;
1957
+ }
1958
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1959
+ extractOperationName(call, hookType) {
1960
+ try {
1961
+ const args = call.getArguments?.() || [];
1962
+ if (args.length > 0) {
1963
+ const firstArg = args[0].getText();
1964
+ const cleanName = firstArg.replace(/^(GET_|FETCH_|CREATE_|UPDATE_|DELETE_)/, "").replace(/_QUERY$|_MUTATION$/, "").replace(/Document$/, "").replace(/Query$|Mutation$/, "");
1965
+ const icon = hookType === "useMutation" ? "\u270F\uFE0F" : "\u{1F4E1}";
1966
+ const type = hookType === "useMutation" ? "Mutation" : "Query";
1967
+ return `${icon} ${type}: ${cleanName}`;
1968
+ }
1969
+ } catch {
1970
+ }
1971
+ return hookType;
1972
+ }
1973
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1974
+ extractContextName(call) {
1975
+ try {
1976
+ const args = call.getArguments?.() || [];
1977
+ if (args.length > 0) {
1978
+ const contextName = args[0].getText().replace(/Context$/, "").replace(/^Session|^Token|^Apollo/, (m) => m);
1979
+ return `\u{1F504} Context: ${contextName}`;
1980
+ }
1981
+ } catch {
1982
+ }
1983
+ return "useContext";
1984
+ }
1985
+ extractDependencies(sourceFile) {
1986
+ const dependencies = [];
1987
+ const importDeclarations = sourceFile.getImportDeclarations();
1988
+ for (const importDecl of importDeclarations) {
1989
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
1990
+ if (moduleSpecifier.startsWith(".") || moduleSpecifier.startsWith("@/")) {
1991
+ const namedImports = importDecl.getNamedImports();
1992
+ for (const namedImport of namedImports) {
1993
+ const name = namedImport.getName();
1994
+ if (this.isComponentName(name) || name.startsWith("use")) {
1995
+ dependencies.push(name);
1996
+ }
1997
+ }
1998
+ const defaultImport = importDecl.getDefaultImport();
1999
+ if (defaultImport) {
2000
+ const name = defaultImport.getText();
2001
+ if (this.isComponentName(name)) {
2002
+ dependencies.push(name);
2003
+ }
2004
+ }
2005
+ }
2006
+ }
2007
+ return dependencies;
2008
+ }
2009
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2010
+ extractStateManagement(node) {
2011
+ const statePatterns = [];
2012
+ const nodeText = node.getText?.() || "";
2013
+ if (nodeText.includes("useState")) statePatterns.push("useState");
2014
+ if (nodeText.includes("useReducer")) statePatterns.push("useReducer");
2015
+ if (nodeText.includes("useContext")) statePatterns.push("useContext");
2016
+ if (nodeText.includes("useQuery")) statePatterns.push("Apollo Query");
2017
+ if (nodeText.includes("useMutation")) statePatterns.push("Apollo Mutation");
2018
+ if (nodeText.includes("useRecoil")) statePatterns.push("Recoil");
2019
+ if (nodeText.includes("useSelector") || nodeText.includes("useDispatch")) {
2020
+ statePatterns.push("Redux");
2021
+ }
2022
+ return statePatterns;
2023
+ }
2024
+ buildDependencyGraph(components) {
2025
+ const componentMap = /* @__PURE__ */ new Map();
2026
+ for (const comp of components) {
2027
+ componentMap.set(comp.name, comp);
2028
+ }
2029
+ for (const comp of components) {
2030
+ for (const dep of comp.dependencies) {
2031
+ const depComponent = componentMap.get(dep);
2032
+ if (depComponent && !depComponent.dependents.includes(comp.name)) {
2033
+ depComponent.dependents.push(comp.name);
2034
+ }
2035
+ }
2036
+ }
2037
+ }
2038
+ async analyzeDataFlows(components) {
2039
+ const dataFlows = [];
2040
+ let flowId = 1;
2041
+ const contextFlows = this.analyzeContextFlows(components);
2042
+ dataFlows.push(...contextFlows.map((flow) => ({ ...flow, id: `flow-${flowId++}` })));
2043
+ const apolloFlows = this.analyzeApolloFlows(components);
2044
+ dataFlows.push(...apolloFlows.map((flow) => ({ ...flow, id: `flow-${flowId++}` })));
2045
+ const propDrillingFlows = this.analyzePropDrilling(components);
2046
+ dataFlows.push(...propDrillingFlows.map((flow) => ({ ...flow, id: `flow-${flowId++}` })));
2047
+ return dataFlows;
2048
+ }
2049
+ analyzeContextFlows(components) {
2050
+ const flows = [];
2051
+ const providers = components.filter(
2052
+ (c) => c.name.includes("Provider") || c.name.includes("Context")
2053
+ );
2054
+ const consumers = components.filter((c) => c.hooks.some((h) => h.includes("Context")));
2055
+ for (const provider of providers) {
2056
+ const contextName = provider.name.replace("Provider", "").replace("Context", "");
2057
+ for (const consumer of consumers) {
2058
+ const contextHook = consumer.hooks.find(
2059
+ (h) => h.includes("Context") && h.includes(contextName)
2060
+ );
2061
+ if (contextHook || consumer.hooks.some((h) => h.includes(contextName))) {
2062
+ flows.push({
2063
+ name: `\u{1F504} ${contextName} Context`,
2064
+ description: `Data flows from ${provider.name} to ${consumer.name} via Context`,
2065
+ source: { type: "context", name: provider.name },
2066
+ target: { type: "component", name: consumer.name },
2067
+ via: [],
2068
+ operations: [contextHook || `useContext(${contextName})`]
2069
+ });
2070
+ }
2071
+ }
2072
+ }
2073
+ return flows;
2074
+ }
2075
+ analyzeApolloFlows(components) {
2076
+ const flows = [];
2077
+ for (const comp of components) {
2078
+ const queryHooks = comp.hooks.filter(
2079
+ (h) => h.includes("Query:") || h === "useQuery" || h === "useLazyQuery"
2080
+ );
2081
+ for (const hook of queryHooks) {
2082
+ const operationName = hook.includes(":") ? hook.split(":")[1].trim() : comp.name;
2083
+ flows.push({
2084
+ name: `\u{1F4E1} ${operationName}`,
2085
+ description: `${comp.name} fetches ${operationName} via Apollo`,
2086
+ source: { type: "api", name: `GraphQL: ${operationName}` },
2087
+ target: { type: "component", name: comp.name },
2088
+ via: [{ type: "cache", name: "Apollo Cache" }],
2089
+ operations: [hook]
2090
+ });
2091
+ }
2092
+ const mutationHooks = comp.hooks.filter(
2093
+ (h) => h.includes("Mutation:") || h === "useMutation"
2094
+ );
2095
+ for (const hook of mutationHooks) {
2096
+ const operationName = hook.includes(":") ? hook.split(":")[1].trim() : comp.name;
2097
+ flows.push({
2098
+ name: `\u270F\uFE0F ${operationName}`,
2099
+ description: `${comp.name} mutates ${operationName} via Apollo`,
2100
+ source: { type: "component", name: comp.name },
2101
+ target: { type: "api", name: `GraphQL: ${operationName}` },
2102
+ via: [],
2103
+ operations: [hook]
2104
+ });
2105
+ }
2106
+ }
2107
+ return flows;
2108
+ }
2109
+ analyzePropDrilling(components) {
2110
+ const flows = [];
2111
+ for (const comp of components) {
2112
+ if (comp.props.length > 5 && comp.dependents.length > 0) {
2113
+ flows.push({
2114
+ name: `Prop Drilling through ${comp.name}`,
2115
+ description: `${comp.name} passes ${comp.props.length} props to children`,
2116
+ source: { type: "component", name: comp.name },
2117
+ target: { type: "component", name: comp.dependents[0] },
2118
+ via: [],
2119
+ operations: ["props"]
2120
+ });
2121
+ }
2122
+ }
2123
+ return flows;
2124
+ }
2125
+ };
2126
+ var RestApiAnalyzer = class extends BaseAnalyzer {
2127
+ project;
2128
+ apiCallCounter = 0;
2129
+ constructor(config) {
2130
+ super(config);
2131
+ this.project = new Project({
2132
+ tsConfigFilePath: this.resolvePath("tsconfig.json"),
2133
+ skipAddingFilesFromTsConfig: true
2134
+ });
2135
+ }
2136
+ getName() {
2137
+ return "RestApiAnalyzer";
2138
+ }
2139
+ async analyze() {
2140
+ this.log("Starting REST API analysis...");
2141
+ const tsFiles = await fg(["**/*.ts", "**/*.tsx"], {
2142
+ cwd: this.basePath,
2143
+ ignore: [
2144
+ "**/node_modules/**",
2145
+ "**/.next/**",
2146
+ "**/__tests__/**",
2147
+ "**/*.test.*",
2148
+ "**/*.spec.*",
2149
+ "**/*.stories.*",
2150
+ "**/__generated__/**",
2151
+ "**/dist/**",
2152
+ "**/build/**"
2153
+ ],
2154
+ absolute: true
2155
+ });
2156
+ const sourceFiles = /* @__PURE__ */ new Map();
2157
+ for (const filePath of tsFiles) {
2158
+ try {
2159
+ sourceFiles.set(filePath, this.project.addSourceFileAtPath(filePath));
2160
+ } catch {
2161
+ }
2162
+ }
2163
+ const results = await parallelMapSafe(
2164
+ Array.from(sourceFiles.entries()),
2165
+ async ([filePath, sourceFile]) => {
2166
+ const relativePath = path.relative(this.basePath, filePath);
2167
+ const calls = [];
2168
+ calls.push(...this.findFetchCalls(sourceFile, relativePath));
2169
+ calls.push(...this.findAxiosCalls(sourceFile, relativePath));
2170
+ calls.push(...this.findSwrCalls(sourceFile, relativePath));
2171
+ return calls;
2172
+ },
2173
+ 8
2174
+ );
2175
+ const apiCalls = results.flat();
2176
+ this.log(`Found ${apiCalls.length} REST API calls`);
2177
+ return { apiCalls };
2178
+ }
2179
+ /**
2180
+ * Find fetch() calls
2181
+ */
2182
+ findFetchCalls(sourceFile, filePath) {
2183
+ const calls = [];
2184
+ const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
2185
+ for (const call of callExpressions) {
2186
+ try {
2187
+ const expression = call.getExpression();
2188
+ const expressionText = expression.getText();
2189
+ if (expressionText === "fetch" || expressionText === "window.fetch") {
2190
+ const apiCall = this.extractFetchCall(call, filePath);
2191
+ if (apiCall) {
2192
+ calls.push(apiCall);
2193
+ }
2194
+ }
2195
+ } catch {
2196
+ }
2197
+ }
2198
+ return calls;
2199
+ }
2200
+ /**
2201
+ * Find axios calls (axios.get, axios.post, etc.)
2202
+ */
2203
+ findAxiosCalls(sourceFile, filePath) {
2204
+ const calls = [];
2205
+ const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
2206
+ for (const call of callExpressions) {
2207
+ try {
2208
+ const expression = call.getExpression();
2209
+ const expressionText = expression.getText();
2210
+ const axiosMatch = expressionText.match(/^axios\.(get|post|put|delete|patch)$/i);
2211
+ if (axiosMatch) {
2212
+ const apiCall = this.extractAxiosCall(call, filePath, axiosMatch[1].toUpperCase());
2213
+ if (apiCall) {
2214
+ calls.push(apiCall);
2215
+ }
2216
+ }
2217
+ if (expressionText === "axios") {
2218
+ const apiCall = this.extractAxiosDirectCall(call, filePath);
2219
+ if (apiCall) {
2220
+ calls.push(apiCall);
2221
+ }
2222
+ }
2223
+ } catch {
2224
+ }
2225
+ }
2226
+ return calls;
2227
+ }
2228
+ /**
2229
+ * Find useSWR calls
2230
+ */
2231
+ findSwrCalls(sourceFile, filePath) {
2232
+ const calls = [];
2233
+ const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
2234
+ for (const call of callExpressions) {
2235
+ try {
2236
+ const expression = call.getExpression();
2237
+ const expressionText = expression.getText();
2238
+ if (expressionText === "useSWR" || expressionText === "useSWRImmutable") {
2239
+ const apiCall = this.extractSwrCall(call, filePath);
2240
+ if (apiCall) {
2241
+ calls.push(apiCall);
2242
+ }
2243
+ }
2244
+ } catch {
2245
+ }
2246
+ }
2247
+ return calls;
2248
+ }
2249
+ /**
2250
+ * Extract API call info from fetch()
2251
+ */
2252
+ extractFetchCall(call, filePath) {
2253
+ const args = call.getArguments();
2254
+ if (args.length === 0) return null;
2255
+ const urlArg = args[0].getText();
2256
+ const urlInfo = this.extractUrlFromArg(urlArg);
2257
+ if (!urlInfo.url) return null;
2258
+ if (!urlInfo.isPlaceholder && !this.isApiUrl(urlInfo.url)) return null;
2259
+ let method = "GET";
2260
+ let requiresAuth = false;
2261
+ if (args.length > 1) {
2262
+ const optionsText = args[1].getText();
2263
+ const methodMatch = optionsText.match(/method:\s*["'](\w+)["']/i);
2264
+ if (methodMatch) {
2265
+ method = this.normalizeMethod(methodMatch[1]);
2266
+ }
2267
+ requiresAuth = optionsText.includes("credentials") || optionsText.includes("Authorization") || optionsText.includes("withCredentials");
2268
+ }
2269
+ const containingFunction = this.getContainingFunctionName(call);
2270
+ const line = call.getStartLineNumber();
2271
+ return {
2272
+ id: `api-${++this.apiCallCounter}`,
2273
+ method,
2274
+ url: urlInfo.url,
2275
+ callType: "fetch",
2276
+ filePath,
2277
+ line,
2278
+ containingFunction,
2279
+ usedIn: [],
2280
+ requiresAuth,
2281
+ category: this.categorizeApi(urlInfo.url)
2282
+ };
2283
+ }
2284
+ /**
2285
+ * Extract API call info from axios.method()
2286
+ */
2287
+ extractAxiosCall(call, filePath, method) {
2288
+ const args = call.getArguments();
2289
+ if (args.length === 0) return null;
2290
+ const urlArg = args[0].getText();
2291
+ const urlInfo = this.extractUrlFromArg(urlArg);
2292
+ if (!urlInfo.url) return null;
2293
+ let requiresAuth = false;
2294
+ if (args.length > 1) {
2295
+ const optionsText = args[args.length - 1].getText();
2296
+ requiresAuth = optionsText.includes("withCredentials") || optionsText.includes("Authorization");
2297
+ }
2298
+ const containingFunction = this.getContainingFunctionName(call);
2299
+ const line = call.getStartLineNumber();
2300
+ return {
2301
+ id: `api-${++this.apiCallCounter}`,
2302
+ method: this.normalizeMethod(method),
2303
+ url: urlInfo.url,
2304
+ callType: "axios",
2305
+ filePath,
2306
+ line,
2307
+ containingFunction,
2308
+ usedIn: [],
2309
+ requiresAuth,
2310
+ category: this.categorizeApi(urlInfo.url)
2311
+ };
2312
+ }
2313
+ /**
2314
+ * Extract API call info from axios() direct call
2315
+ */
2316
+ extractAxiosDirectCall(call, filePath) {
2317
+ const args = call.getArguments();
2318
+ if (args.length === 0) return null;
2319
+ const configText = args[0].getText();
2320
+ const urlMatch = configText.match(/url:\s*["'`]([^"'`]+)["'`]/);
2321
+ const methodMatch = configText.match(/method:\s*["'](\w+)["']/i);
2322
+ if (!urlMatch) return null;
2323
+ const url = urlMatch[1];
2324
+ const method = methodMatch ? this.normalizeMethod(methodMatch[1]) : "GET";
2325
+ const requiresAuth = configText.includes("withCredentials") || configText.includes("Authorization");
2326
+ const containingFunction = this.getContainingFunctionName(call);
2327
+ const line = call.getStartLineNumber();
2328
+ return {
2329
+ id: `api-${++this.apiCallCounter}`,
2330
+ method,
2331
+ url,
2332
+ callType: "axios",
2333
+ filePath,
2334
+ line,
2335
+ containingFunction,
2336
+ usedIn: [],
2337
+ requiresAuth,
2338
+ category: this.categorizeApi(url)
2339
+ };
2340
+ }
2341
+ /**
2342
+ * Extract API call info from useSWR()
2343
+ */
2344
+ extractSwrCall(call, filePath) {
2345
+ const args = call.getArguments();
2346
+ if (args.length === 0) return null;
2347
+ const keyArg = args[0].getText();
2348
+ let url = null;
2349
+ if (keyArg.startsWith('"') || keyArg.startsWith("'") || keyArg.startsWith("`")) {
2350
+ url = this.cleanStringLiteral(keyArg);
2351
+ } else if (keyArg.includes("?") && keyArg.includes(":")) {
2352
+ let match = keyArg.match(/\?\s*["'`]([^"'`]+)["'`]/);
2353
+ if (match) {
2354
+ url = match[1];
2355
+ } else {
2356
+ match = keyArg.match(/:\s*["'`]([^"'`]+)["'`]/);
2357
+ if (match) {
2358
+ url = match[1];
2359
+ }
2360
+ }
2361
+ if (!url) {
2362
+ match = keyArg.match(/\?\s*`([^`]+)`/);
2363
+ if (match) {
2364
+ url = match[1].replace(/\$\{[^}]+\}/g, ":param");
2365
+ }
2366
+ }
2367
+ } else {
2368
+ const urlInfo = this.extractUrlFromArg(keyArg);
2369
+ if (urlInfo.url && !keyArg.includes("null") && !keyArg.includes("undefined")) {
2370
+ url = urlInfo.url;
2371
+ }
2372
+ }
2373
+ if (!url) return null;
2374
+ const containingFunction = this.getContainingFunctionName(call);
2375
+ const line = call.getStartLineNumber();
2376
+ return {
2377
+ id: `api-${++this.apiCallCounter}`,
2378
+ method: "GET",
2379
+ // SWR is typically for GET requests
2380
+ url,
2381
+ callType: "useSWR",
2382
+ filePath,
2383
+ line,
2384
+ containingFunction,
2385
+ usedIn: [],
2386
+ requiresAuth: false,
2387
+ category: this.categorizeApi(url)
2388
+ };
2389
+ }
2390
+ /**
2391
+ * Get the name of the containing function/component
2392
+ */
2393
+ getContainingFunctionName(node) {
2394
+ let current = node;
2395
+ while (current) {
2396
+ if (Node.isFunctionDeclaration(current)) {
2397
+ return current.getName() || "anonymous";
2398
+ }
2399
+ if (Node.isVariableDeclaration(current)) {
2400
+ return current.getName();
2401
+ }
2402
+ if (Node.isMethodDeclaration(current)) {
2403
+ return current.getName();
2404
+ }
2405
+ if (Node.isArrowFunction(current)) {
2406
+ const parent = current.getParent();
2407
+ if (parent && Node.isVariableDeclaration(parent)) {
2408
+ return parent.getName();
2409
+ }
2410
+ }
2411
+ current = current.getParent();
2412
+ }
2413
+ return "unknown";
2414
+ }
2415
+ /**
2416
+ * Extract URL from argument (handles literals, variables, function calls)
2417
+ */
2418
+ extractUrlFromArg(argText) {
2419
+ if (/^["'`]/.test(argText)) {
2420
+ const url = this.cleanStringLiteral(argText);
2421
+ return { url, isPlaceholder: false };
2422
+ }
2423
+ const funcCallMatch = argText.match(/^(\w+)\s*\(\s*["'`]([^"'`]+)["'`]/);
2424
+ if (funcCallMatch) {
2425
+ return { url: `[${funcCallMatch[1]}] ${funcCallMatch[2]}`, isPlaceholder: true };
2426
+ }
2427
+ const funcTemplateMatch = argText.match(/^(\w+)\s*\(\s*`([^`]+)`/);
2428
+ if (funcTemplateMatch) {
2429
+ const path5 = funcTemplateMatch[2].replace(/\$\{[^}]+\}/g, ":param");
2430
+ return { url: `[${funcTemplateMatch[1]}] ${path5}`, isPlaceholder: true };
2431
+ }
2432
+ if (/^\w+(\.\w+)*$/.test(argText)) {
2433
+ return { url: `[${argText}]`, isPlaceholder: true };
2434
+ }
2435
+ if (argText.includes(".")) {
2436
+ return { url: `[${argText}]`, isPlaceholder: true };
2437
+ }
2438
+ return { url: null, isPlaceholder: false };
2439
+ }
2440
+ /**
2441
+ * Clean string literal (remove quotes)
2442
+ */
2443
+ cleanStringLiteral(value) {
2444
+ const cleaned = value.replace(/^["'`]|["'`]$/g, "").trim();
2445
+ if (cleaned.includes("${")) {
2446
+ const parameterized = cleaned.replace(/\$\{[^}]+\}/g, ":param");
2447
+ return parameterized;
2448
+ }
2449
+ return cleaned || null;
2450
+ }
2451
+ /**
2452
+ * Check if URL looks like an API endpoint
2453
+ */
2454
+ isApiUrl(url) {
2455
+ if (url.startsWith("data:") || url.startsWith("blob:")) {
2456
+ return false;
2457
+ }
2458
+ if (/\.(css|js|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|html)$/i.test(url)) {
2459
+ return false;
2460
+ }
2461
+ return url.startsWith("/") || url.startsWith("http") || url.includes("/api/") || url.includes(".json") || // Common external API patterns
2462
+ url.includes("api.") || url.includes("github.io") || // GitHub Pages hosted APIs
2463
+ url.includes("hsforms.com") || // HubSpot
2464
+ url.includes("hubspot") || // HubSpot
2465
+ url.includes("amazonaws.com") || // AWS S3
2466
+ url.includes("s3.") || // AWS S3 alternative
2467
+ url.includes("googleapis.com") || // Google APIs
2468
+ url.includes("stripe.com") || // Stripe
2469
+ url.includes("graph.facebook.com") || // Facebook
2470
+ url.includes("api.twitter.com") || // Twitter
2471
+ url.includes("slack.com") || // Slack
2472
+ url.includes("discord.com") || // Discord
2473
+ url.includes("sendgrid.com") || // SendGrid
2474
+ url.includes("twilio.com") || // Twilio
2475
+ url.includes("firebase") || // Firebase
2476
+ url.includes("supabase") || // Supabase
2477
+ url.includes("auth0.com") || // Auth0
2478
+ url.includes("okta.com") || // Okta
2479
+ url.includes("cloudflare.com") || // Cloudflare
2480
+ url.includes("vercel.com") || // Vercel
2481
+ url.includes("netlify.com");
2482
+ }
2483
+ /**
2484
+ * Categorize API by URL pattern
2485
+ */
2486
+ categorizeApi(url) {
2487
+ if (url.includes("hsforms.com") || url.includes("hubspot")) return "HubSpot";
2488
+ if (url.includes("amazonaws.com") || url.includes("s3.")) return "AWS S3";
2489
+ if (url.includes("googleapis.com")) return "Google API";
2490
+ if (url.includes("stripe.com")) return "Stripe";
2491
+ if (url.includes("graph.facebook.com")) return "Facebook";
2492
+ if (url.includes("api.twitter.com")) return "Twitter";
2493
+ if (url.includes("slack.com")) return "Slack";
2494
+ if (url.includes("discord.com")) return "Discord";
2495
+ if (url.includes("sendgrid.com")) return "SendGrid";
2496
+ if (url.includes("twilio.com")) return "Twilio";
2497
+ if (url.includes("firebase")) return "Firebase";
2498
+ if (url.includes("supabase")) return "Supabase";
2499
+ if (url.includes("auth0.com")) return "Auth0";
2500
+ if (url.includes("okta.com")) return "Okta";
2501
+ if (url.includes("github.io")) return "GitHub Pages API";
2502
+ if (url.startsWith("/api/")) return "Internal API";
2503
+ if (url.startsWith("/")) return "Internal Route";
2504
+ if (url.startsWith("[")) return "Dynamic URL";
2505
+ return void 0;
2506
+ }
2507
+ /**
2508
+ * Normalize HTTP method
2509
+ */
2510
+ normalizeMethod(method) {
2511
+ const upper = method.toUpperCase();
2512
+ if (["GET", "POST", "PUT", "DELETE", "PATCH"].includes(upper)) {
2513
+ return upper;
2514
+ }
2515
+ return "unknown";
2516
+ }
2517
+ };
2518
+
2519
+ export { BaseAnalyzer, DataFlowAnalyzer, GraphQLAnalyzer, PagesAnalyzer, RestApiAnalyzer };