Introduction
Tutorials
Building with Codegen
- At a Glance
- Parsing Codebases
- Reusable Codemods
- .codegen Directory
- @codegen.function
- Language Support
- Commit and Reset
- Git Operations
- Files & Directories
- Editables
- Symbols
- Classes
- Imports
- Exports
- Inheritable Behaviors
- Statements and Code Blocks
- Dependencies and Usages
- Function Calls
- Variable Assignments
- Local Variables
- Comments & Docstrings
- External Modules
- Type Annotations
- Moving Symbols
- Collections
- Call Graph
- React and JSX
- Visualization
- LLM Integration
- Reducing Conditions
Managing TypeScript Exports
Safely and systematically manage exports in your TypeScript codebase
Codegen provides powerful tools for managing and reorganizing exports in TypeScript codebases. This tutorial builds on the concepts covered in exports to show you how to automate common export management tasks and ensure your module boundaries stay clean and maintainable.
Common Export Management Tasks
Collecting and Processing Exports
When reorganizing exports, the first step is identifying which exports need to be processed:
processed_imports = set()
for file in codebase.files:
# Only process files under /src/shared
if '/src/shared' not in file.filepath:
continue
# 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)
# Skip if there are none
if not all_reexports:
continue
Moving Exports to Public Files
When centralizing exports in public-facing files:
# Replace "src/" with "src/shared/"
resolved_public_file = export.resolved_symbol.filepath.replace("src/", "src/shared/")
# 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):
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
Managing Different Export Types
Codegen can handle all types of exports automatically:
# A) Wildcard export, e.g. `export * from "..."`
if export.is_wildcard_export():
target_file.insert_before(f'export * from "{relative_path}"')
# B) Type export, e.g. `export type { Foo, Bar } from "..."`
elif export.is_type_export():
# Does this file already have a type export statement for the path?
statement = file.get_export_statement_for_path(relative_path, "TYPE")
if statement:
# Insert into existing statement
if export.is_aliased():
statement.insert(0, f"{export.resolved_symbol.name} as {export.name}")
else:
statement.insert(0, f"{export.name}")
else:
# Insert a new type export statement
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}"'
)
# C) Normal export, e.g. `export { Foo, Bar } from "..."`
else:
statement = file.get_export_statement_for_path(relative_path, "EXPORT")
if statement:
# Insert into existing statement
if export.is_aliased():
statement.insert(0, f"{export.resolved_symbol.name} as {export.name}")
else:
statement.insert(0, f"{export.name}")
else:
# Insert a brand-new normal export statement
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}"'
)
Updating Import References
After moving exports, you need to update all import references:
# Now update all import usages that refer to this export
for usage in export.symbol_usages():
if isinstance(usage, TSImport) and usage not in processed_imports:
processed_imports.add(usage)
# Translate the resolved_public_file to the usage file's TS config import path
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()
# Remove the old export from the original file
export.remove()
# If the file ends up with no exports, remove it entirely
if not file.export_statements and len(file.symbols) == 0:
file.remove()
Best Practices
- Check for Wildcards First: Always check for existing wildcard exports before adding new ones:
if target_file.has_export_statement_for_path(relative_path, "WILDCARD"):
has_wildcard = True
continue
- Handle Path Translations: Use TypeScript config for path translations:
new_path = usage.file.ts_config.translate_import_path(resolved_public_file)
- Clean Up Empty Files: Remove files that no longer contain exports or symbols:
if not file.export_statements and len(file.symbols) == 0:
file.remove()
Next Steps
After reorganizing your exports:
- Run your test suite to verify everything still works
- Review the generated import statements
- Check for any empty files that should be removed
- Verify that all export types (wildcard, type, named) are working as expected
Remember that managing exports is an iterative process. You may need to run the codemod multiple times as your codebase evolves.
Related tutorials
Complete Codemod
Here’s the complete codemod that you can copy and use directly:
processed_imports = set()
for file in codebase.files:
# Only process files under /src/shared
if '/src/shared' not in file.filepath:
continue
# 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)
# Skip if there are none
if not all_reexports:
continue
for export in all_reexports:
has_wildcard = False
# Replace "src/" with "src/shared/"
resolved_public_file = export.resolved_symbol.filepath.replace("src/", "src/shared/")
# 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):
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, e.g. `export * from "..."`
if export.is_wildcard_export():
target_file.insert_before(f'export * from "{relative_path}"')
# B) Type export, e.g. `export type { Foo, Bar } from "..."`
elif export.is_type_export():
# Does this file already have a type export statement for the path?
statement = file.get_export_statement_for_path(relative_path, "TYPE")
if statement:
# Insert into existing statement
if export.is_aliased():
statement.insert(0, f"{export.resolved_symbol.name} as {export.name}")
else:
statement.insert(0, f"{export.name}")
else:
# Insert a new type export statement
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}"'
)
# C) Normal export, e.g. `export { Foo, Bar } from "..."`
else:
statement = file.get_export_statement_for_path(relative_path, "EXPORT")
if statement:
# Insert into existing statement
if export.is_aliased():
statement.insert(0, f"{export.resolved_symbol.name} as {export.name}")
else:
statement.insert(0, f"{export.name}")
else:
# Insert a brand-new normal export statement
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}"'
)
# Now update all import usages that refer to this export
for usage in export.symbol_usages():
if isinstance(usage, TSImport) and usage not in processed_imports:
processed_imports.add(usage)
# Translate the resolved_public_file to the usage file's TS config import path
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()
# Remove the old export from the original file
export.remove()
# If the file ends up with no exports, remove it entirely
if not file.export_statements and len(file.symbols) == 0:
file.remove()
Was this page helpful?