Files
bigcapital/packages/webapp/scripts/codemod-update-default-imports.js
Ahmed Bouhuolia b6970fefc2 refactor: convert containers default exports to named exports
## Summary
Converted 905 default exports in src/containers to named exports for improved tree-shaking, better IDE refactoring support, and consistency with modern TypeScript practices.

## Changes
- Converted `export default function X` to `export function X` (916 files)
- Converted `export default compose(...)(X)` to `export const X = compose(...)(XInner)` with HOC wrapping
- Updated 373 import sites from default to named imports
- Fixed 136 React.lazy() imports to use .then() pattern for compatibility with named exports
- Updated re-export patterns in index files
- Fixed edge cases (alert arrays, connector HOCs, type definitions)

## Implementation
- Created codemod script: codemod-containers-exports.js (905 files converted)
- Created import updater: codemod-update-default-imports.js (373 imports fixed)
- Created lazy import fixer: codemod-fix-lazy-imports.js (136 lazy imports fixed)
- Manual fixes for 30 edge-case files (arrays, HOC factories, type definitions)

## Testing
- TypeScript type check: 0 codemod-related errors
- All lazy imports updated with .then() pattern
- All import sites updated to use named imports
- Zero remaining default exports in containers directory

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 20:08:39 +02:00

153 lines
5.0 KiB
JavaScript

// @ts-check
'use strict';
/**
* Codemod: Update default imports from containers to use named imports.
*
* For each file in src/ that has:
* import X from './path/to/Container'
* where that container is in the manifest (its default export was converted),
* replace with:
* import { ExportName } from './path/to/Container' // if local name === export name
* import { ExportName as X } from './path/to/Container' // if local name !== export name
*
* Reads: scripts/export-manifest.json
*/
const { Project, Node, SyntaxKind } = require('ts-morph');
const path = require('path');
const fs = require('fs');
const ROOT = path.join(__dirname, '..');
const SRC_DIR = path.join(ROOT, 'src');
const MANIFEST_PATH = path.join(__dirname, 'export-manifest.json');
if (!fs.existsSync(MANIFEST_PATH)) {
console.error('Manifest not found. Run codemod-containers-exports.js first.');
process.exit(1);
}
const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf-8'));
// Normalise keys to absolute paths (they already are, but ensure no trailing slash etc.)
const manifestByPath = {};
for (const [absPath, exportName] of Object.entries(manifest)) {
manifestByPath[path.normalize(absPath)] = exportName;
}
// ─── Helpers ────────────────────────────────────────────────────────────────
function findFiles(dir) {
const results = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) results.push(...findFiles(full));
else if (/\.(tsx?|ts)$/.test(entry.name)) results.push(full);
}
return results;
}
const TS_EXTENSIONS = ['.tsx', '.ts', '/index.tsx', '/index.ts'];
/** Try to resolve a relative or alias import to an absolute file path. */
function resolveImport(fromFile, importPath) {
// Handle path aliases like @/containers/...
const aliasMatch = importPath.match(/^@\/(.+)/);
if (aliasMatch) {
const rel = aliasMatch[1];
const base = path.join(SRC_DIR, rel);
for (const ext of TS_EXTENSIONS) {
const candidate = base + ext.replace(/^\//, path.sep);
if (fs.existsSync(candidate)) return path.normalize(candidate);
}
// Try index file
for (const ext of TS_EXTENSIONS) {
const candidate = path.join(base, 'index' + ext.replace(/^\//, ''));
if (fs.existsSync(candidate)) return path.normalize(candidate);
}
return null;
}
if (!importPath.startsWith('.')) return null; // external package
const dir = path.dirname(fromFile);
const base = path.resolve(dir, importPath);
for (const ext of TS_EXTENSIONS) {
let candidate;
if (ext.startsWith('/')) {
candidate = base + ext; // e.g. base/index.tsx
} else {
candidate = base + ext;
}
candidate = path.normalize(candidate);
if (fs.existsSync(candidate)) return candidate;
}
// Already has extension
const direct = path.normalize(base);
if (fs.existsSync(direct)) return direct;
return null;
}
// ─── Main ────────────────────────────────────────────────────────────────────
const files = findFiles(SRC_DIR);
let changed = 0;
let errors = 0;
for (const filePath of files) {
const content = fs.readFileSync(filePath, 'utf-8');
if (!content.includes('import ')) continue;
const project = new Project({
useInMemoryFileSystem: true,
skipAddingFilesFromTsConfig: true,
compilerOptions: { allowJs: true, jsx: 4 },
});
const sourceFile = project.createSourceFile(filePath, content);
let fileChanged = false;
for (const importDecl of sourceFile.getImportDeclarations()) {
const defaultImport = importDecl.getDefaultImport();
if (!defaultImport) continue;
const moduleSpecifier = importDecl.getModuleSpecifierValue();
const resolvedPath = resolveImport(filePath, moduleSpecifier);
if (!resolvedPath) continue;
const exportName = manifestByPath[resolvedPath];
if (!exportName) continue;
// We have a default import from a converted container
const localName = defaultImport.getText();
// Build replacement
const existingNamedImports = importDecl.getNamedImports();
if (localName === exportName) {
// import X from './X' → import { X } from './X'
importDecl.removeDefaultImport();
importDecl.addNamedImport(exportName);
} else {
// import Foo from './Bar' → import { Bar as Foo } from './Bar'
importDecl.removeDefaultImport();
importDecl.addNamedImport({ name: exportName, alias: localName });
}
fileChanged = true;
}
if (fileChanged) {
try {
fs.writeFileSync(filePath, sourceFile.getFullText(), 'utf-8');
changed++;
} catch (err) {
console.error(`ERROR writing ${path.relative(ROOT, filePath)}: ${err.message}`);
errors++;
}
}
}
console.log(`\nDone. Import sites updated: ${changed} Errors: ${errors}`);