Files
bigcapital/packages/webapp/scripts/codemod-containers-exports.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

163 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @ts-check
'use strict';
/**
* Codemod: Convert `export default` to named exports in src/containers.
*
* Rules:
* 1. `export default function X` → `export function X`
* 2. `function X` + `export default compose(...)(X)` (inner name === basename)
* → rename X to XInner, emit `export const X = compose(...)(XInner)`
* 3. `function XRoot` + `export default compose(...)(XRoot)` (inner name !== basename)
* → emit `export const Basename = compose(...)(XRoot)` (no rename)
*
* Outputs: scripts/export-manifest.json (path → exportName)
*/
const { Project, Node, SyntaxKind } = require('ts-morph');
const path = require('path');
const fs = require('fs');
const ROOT = path.join(__dirname, '..');
const CONTAINERS_DIR = path.join(ROOT, 'src', 'containers');
const MANIFEST_PATH = path.join(__dirname, 'export-manifest.json');
// ─── 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;
}
/** Replace all occurrences of `oldName` identifier-only in source text using AST positions. */
function renameInFile(sourceFile, oldName, newName) {
const identifiers = sourceFile
.getDescendantsOfKind(SyntaxKind.Identifier)
.filter((id) => {
if (id.getText() !== oldName) return false;
// Skip property access RHS: `foo.oldName` — we only want standalone refs
const parent = id.getParent();
if (Node.isPropertyAccessExpression(parent) && parent.getNameNode() === id) return false;
// Skip property assignment keys: `{ oldName: value }` (though unlikely for component names)
if (Node.isPropertyAssignment(parent) && parent.getNameNode() === id) return false;
// Skip import/export specifiers that are the "original" name in an alias
// (we do want to rename them if they refer to the local declaration)
return true;
});
// Replace back-to-front to preserve positions
for (const id of [...identifiers].reverse()) {
id.replaceWithText(newName);
}
}
// ─── Main ────────────────────────────────────────────────────────────────────
function processFile(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
if (!content.includes('export default')) return null;
const basename = path.basename(filePath, path.extname(filePath));
// One isolated project per file guarantees rename stays local
const project = new Project({
useInMemoryFileSystem: true,
skipAddingFilesFromTsConfig: true,
compilerOptions: { allowJs: true, jsx: 4 /* react-jsx */ },
});
const sourceFile = project.createSourceFile(filePath, content);
// ── Rule 1: export default function X ──────────────────────────────────────
const defaultFunc = sourceFile.getFunctions().find((f) => f.isDefaultExport());
if (defaultFunc) {
const name = defaultFunc.getName() || basename;
defaultFunc.setIsDefaultExport(false);
defaultFunc.setIsExported(true);
fs.writeFileSync(filePath, sourceFile.getFullText(), 'utf-8');
return name;
}
// ── Rule 2 / 3: export default <expression> ─────────────────────────────────
const exportAssignment = sourceFile.getExportAssignment((e) => !e.isExportEquals());
if (!exportAssignment) return null;
const expr = exportAssignment.getExpression();
// Simple re-export: export default X;
if (Node.isIdentifier(expr)) {
const innerName = expr.getText();
const exportName = basename;
if (innerName === exportName) {
// export default X → export { X } (it's already declared above, just remove the default)
exportAssignment.remove();
// Add `export {}` won't work if X isn't already named-exported, so just leave as-is and warn
console.warn(`SKIP (self re-export): ${filePath}`);
return null;
}
exportAssignment.replaceWithText(`export const ${exportName} = ${innerName};`);
fs.writeFileSync(filePath, sourceFile.getFullText(), 'utf-8');
return exportName;
}
// HOC-wrapped: export default compose(a, b)(X) or export default R.compose(a,b)(X)
if (Node.isCallExpression(expr)) {
const args = expr.getArguments();
if (args.length === 1 && Node.isIdentifier(args[0])) {
const innerName = args[0].getText();
const exportName = basename;
if (innerName === exportName) {
// Conflict: rename inner declaration to XInner
const newInnerName = innerName + 'Inner';
renameInFile(sourceFile, innerName, newInnerName);
// After rename the expr text has updated references
}
const updatedExprText = exportAssignment.getExpression().getText();
exportAssignment.replaceWithText(`export const ${exportName} = ${updatedExprText};`);
fs.writeFileSync(filePath, sourceFile.getFullText(), 'utf-8');
return exportName;
}
// Inline arrow / complex inner arg
console.warn(`WARN (complex inner): ${path.relative(ROOT, filePath)}`);
return null;
}
console.warn(`WARN (unhandled): ${path.relative(ROOT, filePath)}`);
return null;
}
// ─── Run ─────────────────────────────────────────────────────────────────────
const files = findFiles(CONTAINERS_DIR);
const manifest = {};
let changed = 0;
let skipped = 0;
let warnings = 0;
for (const filePath of files) {
try {
const exportName = processFile(filePath);
if (exportName) {
manifest[filePath] = exportName;
changed++;
} else {
skipped++;
}
} catch (err) {
console.error(`ERROR: ${path.relative(ROOT, filePath)}\n ${err.message}`);
warnings++;
}
}
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2), 'utf-8');
console.log(`\nDone. Changed: ${changed} Skipped: ${skipped} Errors: ${warnings}`);
console.log(`Manifest written to: ${MANIFEST_PATH}`);