Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions examples/reexport_management/README.md
Original file line number Diff line number Diff line change
@@ -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!
Original file line number Diff line number Diff line change
@@ -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();
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { calculateSum, formatName, capitalize } from '../module_a/src/functions';
export { validateEmail } from '../module_c/src/shared/symbols/exports';

Original file line number Diff line number Diff line change
@@ -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)}!`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { calculateSum, formatName, capitalize } from "../../imports";
export {validateEmail} from "../../imports"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { validateEmail, generateId } from '../module_a/src/functions';
export { calculateAverage, multiply, createUserProfile } from '../module_b/src/functions';

Original file line number Diff line number Diff line change
@@ -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}`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { validateEmail, generateId } from '../../../imports'
export { calculateAverage, multiply, createUserProfile } from '../../../imports'
15 changes: 15 additions & 0 deletions examples/reexport_management/input_repo/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
9 changes: 9 additions & 0 deletions examples/reexport_management/input_repo/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"*": ["modules/*"]
}
},
"include": ["modules/**/*"]
}
138 changes: 138 additions & 0 deletions examples/reexport_management/run.py
Original file line number Diff line number Diff line change
@@ -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!")