fix-default-stubs.mjs 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. #!/usr/bin/env node
  2. /**
  3. * Finds all stub files with `export default {} as any` and rewrites them
  4. * with proper named exports based on what the source code actually imports.
  5. */
  6. import { execSync } from 'child_process';
  7. import { readFileSync, writeFileSync, existsSync } from 'fs';
  8. import { dirname, join, relative, resolve } from 'path';
  9. const ROOT = '/Users/konghayao/code/ai/claude-code';
  10. // Step 1: Find all stub files with only `export default {} as any`
  11. const stubFiles = new Set();
  12. const allTsFiles = execSync('find src -name "*.ts" -o -name "*.tsx"', {
  13. encoding: 'utf-8', cwd: ROOT
  14. }).trim().split('\n');
  15. for (const f of allTsFiles) {
  16. const fullPath = join(ROOT, f);
  17. const content = readFileSync(fullPath, 'utf-8').trim();
  18. if (content === 'export default {} as any') {
  19. stubFiles.add(f); // relative path like src/types/message.ts
  20. }
  21. }
  22. console.log(`Found ${stubFiles.size} stub files with 'export default {} as any'`);
  23. // Step 2: Scan all source files for imports from these stub modules
  24. // Map: stub file path -> { types: Set<string>, values: Set<string> }
  25. const stubNeeds = new Map();
  26. for (const sf of stubFiles) {
  27. stubNeeds.set(sf, { types: new Set(), values: new Set() });
  28. }
  29. // Helper: resolve an import path from a source file to a stub file
  30. function resolveImport(srcFile, importPath) {
  31. // Handle src/ prefix imports
  32. if (importPath.startsWith('src/')) {
  33. const resolved = importPath.replace(/\.js$/, '.ts');
  34. if (stubFiles.has(resolved)) return resolved;
  35. return null;
  36. }
  37. // Handle relative imports
  38. if (importPath.startsWith('.')) {
  39. const srcDir = dirname(srcFile);
  40. const resolved = join(srcDir, importPath).replace(/\.js$/, '.ts');
  41. if (stubFiles.has(resolved)) return resolved;
  42. // Try .tsx
  43. const resolvedTsx = join(srcDir, importPath).replace(/\.js$/, '.tsx');
  44. if (stubFiles.has(resolvedTsx)) return resolvedTsx;
  45. return null;
  46. }
  47. return null;
  48. }
  49. for (const srcFile of allTsFiles) {
  50. if (stubFiles.has(srcFile)) continue; // skip stub files themselves
  51. const fullPath = join(ROOT, srcFile);
  52. const content = readFileSync(fullPath, 'utf-8');
  53. // Match: import type { A, B } from 'path'
  54. const typeImportRegex = /import\s+type\s+\{([^}]+)\}\s+from\s+['"](.+?)['"]/g;
  55. let match;
  56. while ((match = typeImportRegex.exec(content)) !== null) {
  57. const members = match[1].split(',').map(s => {
  58. const parts = s.trim().split(/\s+as\s+/);
  59. return parts[0].trim();
  60. }).filter(Boolean);
  61. const resolved = resolveImport(srcFile, match[2]);
  62. if (resolved && stubNeeds.has(resolved)) {
  63. for (const m of members) stubNeeds.get(resolved).types.add(m);
  64. }
  65. }
  66. // Match: import { A, B } from 'path' (NOT import type)
  67. const valueImportRegex = /import\s+(?!type\s)\{([^}]+)\}\s+from\s+['"](.+?)['"]/g;
  68. while ((match = valueImportRegex.exec(content)) !== null) {
  69. const rawMembers = match[1];
  70. const members = rawMembers.split(',').map(s => {
  71. // Handle `type Foo` inline type imports
  72. const trimmed = s.trim();
  73. if (trimmed.startsWith('type ')) {
  74. return { name: trimmed.replace(/^type\s+/, '').split(/\s+as\s+/)[0].trim(), isType: true };
  75. }
  76. return { name: trimmed.split(/\s+as\s+/)[0].trim(), isType: false };
  77. }).filter(m => m.name);
  78. const resolved = resolveImport(srcFile, match[2]);
  79. if (resolved && stubNeeds.has(resolved)) {
  80. for (const m of members) {
  81. if (m.isType) {
  82. stubNeeds.get(resolved).types.add(m.name);
  83. } else {
  84. stubNeeds.get(resolved).values.add(m.name);
  85. }
  86. }
  87. }
  88. }
  89. // Match: import Default from 'path'
  90. const defaultImportRegex = /import\s+(?!type\s)(\w+)\s+from\s+['"](.+?)['"]/g;
  91. while ((match = defaultImportRegex.exec(content)) !== null) {
  92. const name = match[1];
  93. if (name === 'type') continue;
  94. const resolved = resolveImport(srcFile, match[2]);
  95. if (resolved && stubNeeds.has(resolved)) {
  96. stubNeeds.get(resolved).values.add('__default__:' + name);
  97. }
  98. }
  99. }
  100. // Step 3: Rewrite stub files
  101. let updated = 0;
  102. for (const [stubFile, needs] of stubNeeds) {
  103. const fullPath = join(ROOT, stubFile);
  104. const lines = ['// Auto-generated stub — replace with real implementation'];
  105. let hasDefault = false;
  106. // Add type exports
  107. for (const t of needs.types) {
  108. // Don't add as type if also in values
  109. if (!needs.values.has(t)) {
  110. lines.push(`export type ${t} = any;`);
  111. }
  112. }
  113. // Add value exports (as const with any type)
  114. for (const v of needs.values) {
  115. if (v.startsWith('__default__:')) {
  116. hasDefault = true;
  117. continue;
  118. }
  119. // Check if it's likely a type (starts with uppercase and not a known function pattern)
  120. // But since it's imported without `type`, treat as value to be safe
  121. lines.push(`export const ${v}: any = (() => {}) as any;`);
  122. }
  123. // Add default export if needed
  124. if (hasDefault) {
  125. lines.push(`export default {} as any;`);
  126. }
  127. if (needs.types.size === 0 && needs.values.size === 0) {
  128. lines.push('export {};');
  129. }
  130. writeFileSync(fullPath, lines.join('\n') + '\n');
  131. updated++;
  132. }
  133. console.log(`Updated ${updated} stub files`);
  134. // Print summary
  135. for (const [stubFile, needs] of stubNeeds) {
  136. if (needs.types.size > 0 || needs.values.size > 0) {
  137. console.log(` ${stubFile}: ${needs.types.size} types, ${needs.values.size} values`);
  138. }
  139. }