diff --git a/examples/reexport_management/README.md b/examples/reexport_management/README.md new file mode 100644 index 0000000..6d95b8b --- /dev/null +++ b/examples/reexport_management/README.md @@ -0,0 +1,109 @@ +# Transform Module Re-exports Organization + +This example demonstrates how to use Codegen to automatically analyze and reorganize TypeScript module re-exports through shared directories. The script makes this process simple by handling all the tedious manual updates automatically. + +> [!NOTE] +> This codemod helps maintain clean module boundaries and improves code organization by centralizing shared exports. + +## How the Migration Script Works + +The script automates the entire reorganization process in a few key steps: + +1. **Export Analysis** + ```python + for export_stmt in file.export_statements: + for export in export_stmt.exports: + if export.is_reexport() and not export.is_external_export: + all_reexports.append(export) + ``` + - Automatically identifies re-exports in shared directories + - Analyzes export patterns and dependencies + - Uses Codegen's intelligent code analysis engine + +2. **Shared File Management** + ```python + resolved_public_file = export.resolved_symbol.filepath.replace("src/", "src/shared/") + if not codebase.has_file(resolved_public_file): + target_file = codebase.create_file(resolved_public_file, sync=True) + ``` + - Creates or updates shared export files + - Maintains proper file structure + - Handles path resolution automatically + +3. **Import Updates** + ```python + # Updates imports to use new shared paths + new_path = usage.file.ts_config.translate_import_path(resolved_public_file) + new_import = f'import {{ {name} }} from "{new_path}"' + ``` + - Updates all import statements to use new paths + - Maintains proper TypeScript path resolution + - Handles different import types (normal, type) + +## Why This Makes Organization Easy + +1. **Zero Manual Updates** + - Codegen SDK handles all file creation and updates + - No tedious export management + +2. **Consistent Structure** + - Ensures all shared exports follow the same pattern + - Maintains clean module boundaries + +3. **Safe Transformations** + - Validates changes before applying them + - Preserves existing functionality + +## Common Re-export Patterns + +### Module to Shared Exports +```typescript +// Before: Direct module import +import { validateEmail } from '../module_a/src/functions'; + +// After: Import through shared +import { validateEmail } from '../module_a/src/shared'; +``` + +### Export Consolidation +```typescript +// Before: Multiple export files +export { foo } from './foo'; +export { bar } from './bar'; + +// After: Consolidated in shared +export * from '../functions'; +``` + +## Key Benefits to Note + +1. **Better Module Boundaries** + - Clear public API for each module + - Centralized shared functionality + +2. **Improved Maintainability** + - Easier to track dependencies + - Simplified import paths + +3. **Code Organization** + - Consistent export structure + - Reduced import complexity + + +The script will: +1. ๐ŸŽฏ Start the reexport organization +2. ๐Ÿ“ Analyze shared directories +3. ๐Ÿ”„ Process and update exports +4. โœจ Create shared export files +5. ๐Ÿงน Clean up redundant exports + +## Learn More + +- [TypeScript Modules](https://www.typescriptlang.org/docs/handbook/modules.html) +- [Export/Import Documentation](https://www.typescriptlang.org/docs/handbook/modules.html#export) +- [Codegen Documentation](https://docs.codegen.com) +- [Tutorial on Analyzing and Organizing Re-exports](https://docs.codegen.com/tutorials/managing-typescript-exports) +- [More on exports ](https://docs.codegen.com/building-with-codegen/exports) +## Contributing + +Feel free to submit issues and enhancement requests! \ No newline at end of file diff --git a/examples/reexport_management/input_repo/modules/module_a/src/functions.ts b/examples/reexport_management/input_repo/modules/module_a/src/functions.ts new file mode 100644 index 0000000..7e21f38 --- /dev/null +++ b/examples/reexport_management/input_repo/modules/module_a/src/functions.ts @@ -0,0 +1,20 @@ +export const calculateSum = (a: number, b: number): number => { + return a + b; +}; + +export const formatName = (firstName: string, lastName: string): string => { + return `${firstName} ${lastName}`; +}; + +export const generateId = (): string => { + return Math.random().toString(36).substring(7); +}; + +export const validateEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +}; + +export const capitalize = (str: string): string => { + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); +}; diff --git a/examples/reexport_management/input_repo/modules/module_a/src/shared/index.ts b/examples/reexport_management/input_repo/modules/module_a/src/shared/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/examples/reexport_management/input_repo/modules/module_b/imports.ts b/examples/reexport_management/input_repo/modules/module_b/imports.ts new file mode 100644 index 0000000..7f4dd6f --- /dev/null +++ b/examples/reexport_management/input_repo/modules/module_b/imports.ts @@ -0,0 +1,3 @@ +export { calculateSum, formatName, capitalize } from '../module_a/src/functions'; +export { validateEmail } from '../module_c/src/shared/symbols/exports'; + diff --git a/examples/reexport_management/input_repo/modules/module_b/src/functions.ts b/examples/reexport_management/input_repo/modules/module_b/src/functions.ts new file mode 100644 index 0000000..1158610 --- /dev/null +++ b/examples/reexport_management/input_repo/modules/module_b/src/functions.ts @@ -0,0 +1,24 @@ +import { calculateSum, formatName, capitalize, validateEmail } from './shared/exports'; + +export const calculateAverage = (numbers: number[]): number => { + const sum = numbers.reduce((acc, curr) => calculateSum(acc, curr), 0); + return sum / numbers.length; +}; + +export const createUserProfile = (firstName: string, lastName: string): string => { + const formattedName = formatName(firstName, lastName); + return `Profile: ${formattedName}`; +}; + +export const formatText = (text: string): string => { + return text.split(' ').map(capitalize).join(' '); +}; + +export const multiply = (a: number, b: number): number => { + return a * b; +}; + +export const generateGreeting = (name: string): string => { + const email = validateEmail(name); + return `Hello, ${capitalize(name)}!`; +}; diff --git a/examples/reexport_management/input_repo/modules/module_b/src/shared/exports.ts b/examples/reexport_management/input_repo/modules/module_b/src/shared/exports.ts new file mode 100644 index 0000000..87c1f3e --- /dev/null +++ b/examples/reexport_management/input_repo/modules/module_b/src/shared/exports.ts @@ -0,0 +1,2 @@ +export { calculateSum, formatName, capitalize } from "../../imports"; +export {validateEmail} from "../../imports" \ No newline at end of file diff --git a/examples/reexport_management/input_repo/modules/module_c/imports.ts b/examples/reexport_management/input_repo/modules/module_c/imports.ts new file mode 100644 index 0000000..cf00eb0 --- /dev/null +++ b/examples/reexport_management/input_repo/modules/module_c/imports.ts @@ -0,0 +1,3 @@ +export { validateEmail, generateId } from '../module_a/src/functions'; +export { calculateAverage, multiply, createUserProfile } from '../module_b/src/functions'; + diff --git a/examples/reexport_management/input_repo/modules/module_c/src/functions.ts b/examples/reexport_management/input_repo/modules/module_c/src/functions.ts new file mode 100644 index 0000000..37a0443 --- /dev/null +++ b/examples/reexport_management/input_repo/modules/module_c/src/functions.ts @@ -0,0 +1,39 @@ +import { validateEmail, generateId, calculateAverage, multiply, createUserProfile } from './shared/symbols/exports'; + +export const createUser = (email: string, firstName: string, lastName: string) => { + if (!validateEmail(email)) { + throw new Error('Invalid email'); + } + + return { + id: generateId(), + profile: createUserProfile(firstName, lastName), + email + }; +}; + +export const calculateMetrics = (values: number[]): { average: number; scaled: number[] } => { + const avg = calculateAverage(values); + const scaled = values.map(v => multiply(v, 2)); + return { average: avg, scaled }; +}; + +export const validateAndFormatUser = (email: string, firstName: string, lastName: string) => { + if (!validateEmail(email)) { + return { success: false, message: 'Invalid email' }; + } + + const profile = createUserProfile(firstName, lastName); + return { success: true, profile }; +}; + +export const processNumbers = (numbers: number[]): number => { + const { average } = calculateMetrics(numbers); + return multiply(average, 100); +}; + +export const generateReport = (userData: { email: string; name: string }): string => { + const isValidEmail = validateEmail(userData.email); + const id = generateId(); + return `Report ${id}: Email ${isValidEmail ? 'valid' : 'invalid'} - ${userData.name}`; +}; diff --git a/examples/reexport_management/input_repo/modules/module_c/src/shared/symbols/exports.ts b/examples/reexport_management/input_repo/modules/module_c/src/shared/symbols/exports.ts new file mode 100644 index 0000000..5b24025 --- /dev/null +++ b/examples/reexport_management/input_repo/modules/module_c/src/shared/symbols/exports.ts @@ -0,0 +1,2 @@ +export { validateEmail, generateId } from '../../../imports' +export { calculateAverage, multiply, createUserProfile } from '../../../imports' diff --git a/examples/reexport_management/input_repo/package.json b/examples/reexport_management/input_repo/package.json new file mode 100644 index 0000000..ce1ed32 --- /dev/null +++ b/examples/reexport_management/input_repo/package.json @@ -0,0 +1,15 @@ +{ + "name": "default-exports-test", + "version": "1.0.0", + "description": "Test codebase for converting default exports", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/examples/reexport_management/input_repo/tsconfig.json b/examples/reexport_management/input_repo/tsconfig.json new file mode 100644 index 0000000..6c9c6ed --- /dev/null +++ b/examples/reexport_management/input_repo/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "paths": { + "*": ["modules/*"] + } + }, + "include": ["modules/**/*"] +} \ No newline at end of file diff --git a/examples/reexport_management/run.py b/examples/reexport_management/run.py new file mode 100644 index 0000000..2d80234 --- /dev/null +++ b/examples/reexport_management/run.py @@ -0,0 +1,138 @@ +import codegen +from codegen import Codebase +from codegen.sdk.typescript.file import TSImport +from codegen.sdk.enums import ProgrammingLanguage + +processed_imports = set() +@codegen.function("reexport_management") +def run(codebase: Codebase): + print("๐Ÿš€ Starting reexport analysis...") + for file in codebase.files: + # Only process files under /src/shared + if "examples/analize_reexports" not in file.filepath or '/src/shared' not in file.filepath: + continue + + print(f"๐Ÿ“ Analyzing: {file.filepath}") + + # Gather all reexports that are not external exports + all_reexports = [] + for export_stmt in file.export_statements: + for export in export_stmt.exports: + if export.is_reexport() and not export.is_external_export: + all_reexports.append(export) + + if not all_reexports: + continue + + print(f"๐Ÿ“ฆ Found {len(all_reexports)} reexports to process") + + for export in all_reexports: + has_wildcard = False + + # Replace "src/" with "src/shared/" + resolved_public_file = export.resolved_symbol.filepath.replace("src/", "src/shared/") + print(f"๐Ÿ”„ Processing: {export.name} -> {resolved_public_file}") + + # Get relative path from the "public" file back to the original file + relative_path = codebase.get_relative_path( + from_file=resolved_public_file, + to_file=export.resolved_symbol.filepath + ) + + # Ensure the "public" file exists + if not codebase.has_file(resolved_public_file): + print(f"โœจ Creating new public file: {resolved_public_file}") + target_file = codebase.create_file(resolved_public_file, sync=True) + else: + target_file = codebase.get_file(resolved_public_file) + + # If target file already has a wildcard export for this relative path, skip + if target_file.has_export_statement_for_path(relative_path, "WILDCARD"): + has_wildcard = True + continue + + # Compare "public" path to the local file's export.filepath + if codebase._remove_extension(resolved_public_file) != codebase._remove_extension(export.filepath): + # A) Wildcard export + if export.is_wildcard_export(): + target_file.insert_before(f'export * from "{relative_path}"') + print(f"โญ Added wildcard export for {relative_path}") + + # B) Type export + elif export.is_type_export(): + statement = file.get_export_statement_for_path(relative_path, "TYPE") + if statement: + if export.is_aliased(): + statement.insert(0, f"{export.resolved_symbol.name} as {export.name}") + else: + statement.insert(0, f"{export.name}") + print(f"๐Ÿ“ Updated existing type export for {export.name}") + else: + if export.is_aliased(): + target_file.insert_before( + f'export type {{ {export.resolved_symbol.name} as {export.name} }} ' + f'from "{relative_path}"' + ) + else: + target_file.insert_before( + f'export type {{ {export.name} }} from "{relative_path}"' + ) + print(f"โœจ Added new type export for {export.name}") + + # C) Normal export + else: + statement = file.get_export_statement_for_path(relative_path, "EXPORT") + if statement: + if export.is_aliased(): + statement.insert(0, f"{export.resolved_symbol.name} as {export.name}") + else: + statement.insert(0, f"{export.name}") + print(f"๐Ÿ“ Updated existing export for {export.name}") + else: + if export.is_aliased(): + target_file.insert_before( + f'export {{ {export.resolved_symbol.name} as {export.name} }} ' + f'from "{relative_path}"' + ) + else: + target_file.insert_before( + f'export {{ {export.name} }} from "{relative_path}"' + ) + print(f"โœจ Added new export for {export.name}") + + # Update import usages + for usage in export.symbol_usages(): + if isinstance(usage, TSImport) and usage not in processed_imports: + processed_imports.add(usage) + + new_path = usage.file.ts_config.translate_import_path(resolved_public_file) + + if has_wildcard and export.name != export.resolved_symbol.name: + name = f"{export.resolved_symbol.name} as {export.name}" + else: + name = usage.name + + if usage.is_type_import(): + new_import = f'import type {{ {name} }} from "{new_path}"' + else: + new_import = f'import {{ {name} }} from "{new_path}"' + + usage.file.insert_before(new_import) + usage.remove() + print(f"๐Ÿ”„ Updated import in {usage.file.filepath}") + + # Remove old export + export.remove() + print(f"๐Ÿ—‘๏ธ Removed old export from {export.filepath}") + + # Clean up empty files + if not file.export_statements and len(file.symbols) == 0: + file.remove() + print(f"๐Ÿงน Removed empty file: {file.filepath}") + codebase.commit() + +if __name__ == "__main__": + print("๐ŸŽฏ Starting reexport organization...") + codebase = Codebase("./", programming_language=ProgrammingLanguage.TYPESCRIPT) + run(codebase) + print("โœ… Done! All reexports organized successfully!")