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:

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

  1. 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
  1. Handle Path Translations: Use TypeScript config for path translations:
new_path = usage.file.ts_config.translate_import_path(resolved_public_file)
  1. 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:

  1. Run your test suite to verify everything still works
  2. Review the generated import statements
  3. Check for any empty files that should be removed
  4. 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.

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()