Skip to content

Instantly share code, notes, and snippets.

@arthurfiorette
Last active January 10, 2026 23:26
Show Gist options
  • Select an option

  • Save arthurfiorette/f7c840df6e3b28d8b48d3862ce2453b3 to your computer and use it in GitHub Desktop.

Select an option

Save arthurfiorette/f7c840df6e3b28d8b48d3862ce2453b3 to your computer and use it in GitHub Desktop.
JSCodeshift transform to convert namespace imports to named imports

Namespace Import Transformer

A JSCodeshift transform that converts namespace imports (import * as) to named imports for better tree-shaking and bundle size optimization.

Overview

This transformer uses a two-phase approach to handle complex codebases with multiple namespace imports and cross-references:

  1. Phase 1: Collects all transformation data for all namespaces before any AST modifications
  2. Phase 2: Applies all transformations based on the collected data

This approach prevents issues where modifying one namespace's AST would break the traversal of subsequent namespaces.

Basic Usage

For TypeScript files with JSX:

pnpm exec jscodeshift \
  -t scripts/transform-namespace-imports.ts \
  src \
  --parser=tsx \
  --extensions=tsx,ts

For TypeScript files without JSX (to avoid angle bracket syntax issues):

pnpm exec jscodeshift \
  -t scripts/transform-namespace-imports.ts \
  src \
  --parser=ts \
  --extensions=ts

With ignore patterns:

pnpm exec jscodeshift \
  -t scripts/transform-namespace-imports.ts \
  src \
  --parser=tsx \
  --extensions=tsx,ts \
  --ignore-pattern="**/node_modules/**" \
  --ignore-pattern="**/dist/**" \
  --ignore-pattern="**/*.test.tsx" \
  --ignore-pattern="**/*.test.ts"

Transformations

Basic namespace to named imports

Before:

import * as React from 'react'

const Component = React.forwardRef(() => <div />)

After:

import { forwardRef } from 'react'

const Component = forwardRef(() => <div />)

Type-only members

Before:

import * as React from 'react'

const Comp: React.FC<React.PropsWithChildren> = ({ children }) => <div>{children}</div>

After:

import { type FC, type PropsWithChildren } from 'react'

const Comp: FC<PropsWithChildren> = ({ children }) => <div>{children}</div>

Cross-namespace conflicts

When multiple namespaces export members with the same names, the transformer adds namespace prefixes:

Before:

import * as Checkbox from '@radix-ui/react-checkbox'
import * as RadioGroup from '@radix-ui/react-radio-group'

export function Example() {
  return (
    <>
      <Checkbox.Root><Checkbox.Indicator /></Checkbox.Root>
      <RadioGroup.Root><RadioGroup.Indicator /></RadioGroup.Root>
    </>
  )
}

After:

import { Indicator as CheckboxIndicator, Root as CheckboxRoot } from '@radix-ui/react-checkbox'
import { Indicator as RadioGroupIndicator, Root as RadioGroupRoot } from '@radix-ui/react-radio-group'

export function Example() {
  return (
    <>
      <CheckboxRoot><CheckboxIndicator /></CheckboxRoot>
      <RadioGroupRoot><RadioGroupIndicator /></RadioGroupRoot>
    </>
  )
}

Name collisions

When imported names conflict with local declarations, the transformer adds underscore prefixes:

Before:

import * as TooltipPrimitive from '@radix-ui/react-tooltip'

interface TooltipProps extends TooltipPrimitive.TooltipProps {
  message: string
}

After:

import { type TooltipProps as _TooltipProps } from '@radix-ui/react-tooltip'

interface TooltipProps extends _TooltipProps {
  message: string
}

Reserved keywords

When namespaces contain reserved keywords (like z.infer), the transformer preserves the namespace import for type-only usage:

Before:

import * as z from 'zod'

const schema = z.object({ name: z.string() })
type FormData = z.infer<typeof schema>

After:

import { object, string } from 'zod'
import type * as z from 'zod'

const schema = object({ name: string() })
type FormData = z.infer<typeof schema>

typeof expressions

Before:

import * as SheetPrimitive from '@radix-ui/react-dialog'

const Overlay: React.ElementRef<typeof SheetPrimitive.Overlay> = null

After:

import { Overlay, type ElementRef } from '@radix-ui/react-dialog'

const Overlay: ElementRef<typeof Overlay> = null

Indexed access types

Before:

import * as PrimitiveSwitch from '@radix-ui/react-switch'

export function Switch(props: ComponentPropsWithoutRef<(typeof PrimitiveSwitch)['Root']>) {
  return <PrimitiveSwitch.Root {...props} />
}

After:

import { Root } from '@radix-ui/react-switch'

export function Switch(props: ComponentPropsWithoutRef<typeof Root>) {
  return <Root {...props} />
}

Alias declarations

Before:

import * as SheetPrimitive from '@radix-ui/react-dialog'

const SheetTrigger = SheetPrimitive.Trigger
export { SheetTrigger }

After:

import { Trigger as SheetTrigger } from '@radix-ui/react-dialog'

export { SheetTrigger }

Re-exports

Before:

import * as RadixAccordion from '@radix-ui/react-accordion'
export { RadixAccordion }

After:

export * as RadixAccordion from '@radix-ui/react-accordion'

Edge Cases Handled

  • Interface extends clauses: interface Props extends Namespace.BaseProps
  • Class implements clauses: class Foo implements Namespace.Interface
  • JSX member expressions: <Namespace.Component />
  • Type queries in generics: React.ElementRef<typeof Component>
  • Parenthesized types: (typeof Namespace)['Member']
  • Multiple namespaces with cross-references
  • Type-only vs runtime member detection

Parser Selection

Use --parser=tsx for files that may contain JSX syntax. Use --parser=ts for pure TypeScript files that use angle bracket type assertions (<Type>value), as these conflict with JSX syntax in the TSX parser.

Reserved Keywords

The transformer maintains a list of TypeScript reserved keywords that cannot be imported as identifiers: infer, type, interface, enum, namespace, module, declare, abstract, as, any, boolean, constructor, false, from, get, implements, keyof, never, null, number, object, readonly, require, set, string, symbol, true, undefined, unique, unknown, void

When these keywords are encountered, the namespace import is preserved as import type * as Namespace.

import type { API, FileInfo, Options } from 'jscodeshift'
export default function transformer(file: FileInfo, api: API, _options: Options) {
const j = api.jscodeshift
const root = j(file.source)
let hasModifications = false
const RESERVED_KEYWORDS = new Set([
'infer',
'type',
'interface',
'enum',
'namespace',
'module',
'declare',
'abstract',
'as',
'any',
'boolean',
'constructor',
'false',
'from',
'get',
'implements',
'keyof',
'never',
'null',
'number',
'object',
'readonly',
'require',
'set',
'string',
'symbol',
'true',
'undefined',
'unique',
'unknown',
'void',
])
// PHASE 1: Collect ALL namespace import data and usage info BEFORE any modifications
const namespaces: Map<
string,
{
path: any
importSource: string
runtimeMembers: Map<string, { alias: string | null; isAliasDeclaration: boolean }>
typeOnlyMembers: Set<string>
hasReservedKeywordUsage: boolean
isReExported: boolean
aliasDeclarations: any[]
}
> = new Map()
// Find all namespace imports
root
.find(j.ImportDeclaration)
.filter((path) => {
const specifiers = path.node.specifiers
return (
specifiers && specifiers.length === 1 && specifiers[0].type === 'ImportNamespaceSpecifier'
)
})
.forEach((path) => {
const namespaceSpecifier = path.node.specifiers?.[0]
if (namespaceSpecifier?.type !== 'ImportNamespaceSpecifier') return
const namespaceName = namespaceSpecifier.local?.name
if (!namespaceName) return
const importSource = path.node.source.value
if (typeof importSource !== 'string') return
namespaces.set(namespaceName, {
path,
importSource,
runtimeMembers: new Map(),
typeOnlyMembers: new Set(),
hasReservedKeywordUsage: false,
isReExported: false,
aliasDeclarations: [],
})
})
// Collect ALL usage data for ALL namespaces
namespaces.forEach((data, namespaceName) => {
// Check re-export
data.isReExported = root.find(j.ExportNamedDeclaration).some((exportPath) => {
const specifiers = exportPath.node.specifiers
return specifiers?.some(
(spec) => spec.type === 'ExportSpecifier' && spec.exported.name === namespaceName,
)
})
// Find alias declarations
root
.find(j.VariableDeclarator)
.filter((varPath) => {
const init = varPath.node.init
return (
init &&
init.type === 'MemberExpression' &&
init.object.type === 'Identifier' &&
init.object.name === namespaceName &&
init.property.type === 'Identifier'
)
})
.forEach((varPath) => {
const aliasName = varPath.node.id.type === 'Identifier' ? varPath.node.id.name : null
const memberName =
varPath.node.init?.type === 'MemberExpression' &&
varPath.node.init.property.type === 'Identifier'
? varPath.node.init.property.name
: null
if (aliasName && memberName) {
data.runtimeMembers.set(memberName, { alias: aliasName, isAliasDeclaration: true })
data.aliasDeclarations.push(varPath)
}
})
// Find Member Expressions (runtime)
root
.find(j.MemberExpression, {
object: { type: 'Identifier', name: namespaceName },
})
.forEach((memberPath) => {
const property = memberPath.node.property
if (property.type === 'Identifier') {
if (!data.runtimeMembers.has(property.name)) {
data.runtimeMembers.set(property.name, { alias: null, isAliasDeclaration: false })
}
}
})
// Find JSX Member Expressions (runtime)
root
.find(j.JSXMemberExpression, {
object: { type: 'JSXIdentifier', name: namespaceName },
})
.forEach((jsxPath) => {
const property = jsxPath.node.property
if (property.type === 'JSXIdentifier') {
if (!data.runtimeMembers.has(property.name)) {
data.runtimeMembers.set(property.name, { alias: null, isAliasDeclaration: false })
}
}
})
// Find typeof expressions and type references - manually traverse to find all nodes
function findTypeReferences(node: any, depth = 0): void {
if (!node || typeof node !== 'object') return
if (node.type === 'TSTypeQuery') {
const exprName = node.exprName
if (exprName?.type === 'TSQualifiedName') {
const left = exprName.left
const right = exprName.right
if (left?.type === 'Identifier' && left.name === namespaceName) {
if (right?.type === 'Identifier') {
if (!data.runtimeMembers.has(right.name)) {
data.runtimeMembers.set(right.name, { alias: null, isAliasDeclaration: false })
}
}
}
}
}
if (node.type === 'TSTypeReference') {
const typeName = node.typeName
if (typeName?.type === 'TSQualifiedName') {
const left = typeName.left
const right = typeName.right
if (left?.type === 'Identifier' && left.name === namespaceName) {
if (right?.type === 'Identifier') {
if (RESERVED_KEYWORDS.has(right.name)) {
data.hasReservedKeywordUsage = true
return
}
if (!data.runtimeMembers.has(right.name)) {
data.typeOnlyMembers.add(right.name)
}
}
}
}
}
// Handle interface extends and class implements
if (node.type === 'TSExpressionWithTypeArguments') {
const expression = node.expression
if (expression?.type === 'TSQualifiedName') {
const left = expression.left
const right = expression.right
if (left?.type === 'Identifier' && left.name === namespaceName) {
if (right?.type === 'Identifier') {
if (RESERVED_KEYWORDS.has(right.name)) {
data.hasReservedKeywordUsage = true
return
}
if (!data.runtimeMembers.has(right.name)) {
data.typeOnlyMembers.add(right.name)
}
}
}
}
}
// Handle indexed access types: (typeof Namespace)['Member']
if (node.type === 'TSIndexedAccessType') {
let objectType = node.objectType
const indexType = node.indexType
// Unwrap parenthesized types
if (objectType?.type === 'TSParenthesizedType') {
objectType = objectType.typeAnnotation
}
if (objectType?.type === 'TSTypeQuery') {
const exprName = objectType.exprName
if (exprName?.type === 'Identifier' && exprName.name === namespaceName) {
if (
indexType?.type === 'TSLiteralType' &&
indexType.literal?.type === 'StringLiteral'
) {
const memberName = indexType.literal.value
if (!data.runtimeMembers.has(memberName)) {
data.runtimeMembers.set(memberName, { alias: null, isAliasDeclaration: false })
}
}
}
}
}
// Recurse into all object properties
for (const key in node) {
if (Object.hasOwn(node, key) && key !== 'loc' && key !== 'range' && key !== 'tokens') {
const value = node[key]
if (Array.isArray(value)) {
value.forEach((item) => findTypeReferences(item, depth + 1))
} else if (typeof value === 'object') {
findTypeReferences(value, depth + 1)
}
}
}
}
findTypeReferences(root.find(j.Program).get().node)
})
// Detect cross-namespace collisions
const memberToNamespaces = new Map<string, Set<string>>()
namespaces.forEach((data, namespaceName) => {
const allMembers = new Set([
...Array.from(data.runtimeMembers.keys()),
...Array.from(data.typeOnlyMembers),
])
allMembers.forEach((memberName) => {
if (!memberToNamespaces.has(memberName)) {
memberToNamespaces.set(memberName, new Set())
}
memberToNamespaces.get(memberName)!.add(namespaceName)
})
})
// Find namespaces with cross-namespace collisions
const namespacesWithCrossConflicts = new Set<string>()
memberToNamespaces.forEach((namespacesSet, _memberName) => {
if (namespacesSet.size > 1) {
namespacesSet.forEach((ns) => namespacesWithCrossConflicts.add(ns))
}
})
// PHASE 2: Apply transformations
namespaces.forEach((data, namespaceName) => {
const {
path,
importSource,
runtimeMembers,
typeOnlyMembers,
hasReservedKeywordUsage,
isReExported,
aliasDeclarations,
} = data
// Handle re-exports
if (isReExported) {
const exportAllDeclaration = j.exportAllDeclaration(
j.literal(importSource),
j.identifier(namespaceName),
)
j(path).replaceWith(exportAllDeclaration)
root.find(j.ExportNamedDeclaration).forEach((exportPath) => {
if (!exportPath.node.specifiers) return
const remainingSpecifiers = exportPath.node.specifiers.filter(
(spec) => spec.type !== 'ExportSpecifier' || spec.exported.name !== namespaceName,
)
if (remainingSpecifiers.length === 0) {
j(exportPath).remove()
} else {
exportPath.node.specifiers = remainingSpecifiers
}
})
hasModifications = true
return
}
// Handle reserved keywords - use import type * as
if (hasReservedKeywordUsage) {
if (runtimeMembers.size === 0) {
const typeImport = j.importDeclaration(
[j.importNamespaceSpecifier(j.identifier(namespaceName))],
j.literal(importSource),
)
typeImport.importKind = 'type'
j(path).replaceWith(typeImport)
hasModifications = true
return
} else {
const hasCrossConflict = namespacesWithCrossConflicts.has(namespaceName)
const namespacePrefix = hasCrossConflict ? namespaceName : ''
const importSpecifiers = Array.from(runtimeMembers.keys())
.sort()
.map((memberName) => {
const { alias } = runtimeMembers.get(memberName)!
if (alias) {
return j.importSpecifier(j.identifier(memberName), j.identifier(alias))
} else if (hasCrossConflict) {
return j.importSpecifier(
j.identifier(memberName),
j.identifier(`${namespacePrefix}${memberName}`),
)
} else {
return j.importSpecifier(j.identifier(memberName))
}
})
const namedImport = j.importDeclaration(importSpecifiers, j.literal(importSource))
const typeImport = j.importDeclaration(
[j.importNamespaceSpecifier(j.identifier(namespaceName))],
j.literal(importSource),
)
typeImport.importKind = 'type'
j(path).replaceWith(namedImport)
j(path).insertAfter(typeImport)
// Remove aliases and replace runtime usages
aliasDeclarations.forEach((varPath) => {
const parentStatement = varPath.parent
if (parentStatement?.value.type === 'VariableDeclaration') {
if (parentStatement.value.declarations.length === 1) {
j(parentStatement).remove()
} else {
j(varPath).remove()
}
}
})
const memberRenames = new Map<string, string>()
Array.from(runtimeMembers.entries()).forEach(([memberName, { alias }]) => {
if (alias) {
memberRenames.set(memberName, alias)
} else if (hasCrossConflict) {
memberRenames.set(memberName, `${namespacePrefix}${memberName}`)
} else {
memberRenames.set(memberName, memberName)
}
})
root
.find(j.MemberExpression, {
object: { type: 'Identifier', name: namespaceName },
})
.forEach((memberPath) => {
const property = memberPath.node.property
if (property.type === 'Identifier' && runtimeMembers.has(property.name)) {
const finalName = memberRenames.get(property.name) || property.name
j(memberPath).replaceWith(j.identifier(finalName))
}
})
root
.find(j.JSXMemberExpression, {
object: { type: 'JSXIdentifier', name: namespaceName },
})
.forEach((jsxPath) => {
const property = jsxPath.node.property
if (property.type === 'JSXIdentifier' && runtimeMembers.has(property.name)) {
const finalName = memberRenames.get(property.name) || property.name
j(jsxPath).replaceWith(j.jsxIdentifier(finalName))
}
})
root.find(j.TSTypeQuery).forEach((typeQueryPath) => {
const exprName = typeQueryPath.node.exprName
if (exprName.type === 'TSQualifiedName') {
const left = exprName.left
const right = exprName.right
if (
left.type === 'Identifier' &&
left.name === namespaceName &&
right.type === 'Identifier' &&
runtimeMembers.has(right.name)
) {
const finalName = memberRenames.get(right.name) || right.name
typeQueryPath.node.exprName = j.identifier(finalName)
}
}
})
hasModifications = true
return
}
}
// No reserved keywords - standard transformation
if (runtimeMembers.size === 0 && typeOnlyMembers.size === 0) {
j(path).remove()
hasModifications = true
return
}
// Detect collisions
const existingNames = new Set<string>()
root.find(j.Identifier).forEach((idPath) => {
const parent = idPath.parent?.value
if (
parent &&
(parent.type === 'VariableDeclarator' ||
parent.type === 'FunctionDeclaration' ||
parent.type === 'ClassDeclaration' ||
parent.type === 'TSInterfaceDeclaration' ||
parent.type === 'TSTypeAliasDeclaration') &&
parent.id === idPath.node
) {
existingNames.add(idPath.node.name)
}
})
const importSpecifiers: any[] = []
const memberRenames = new Map<string, string>()
// Check if this namespace has cross-namespace conflicts
const hasCrossConflict = namespacesWithCrossConflicts.has(namespaceName)
const namespacePrefix = hasCrossConflict ? namespaceName : ''
// Runtime members
Array.from(runtimeMembers.entries())
.sort(([a], [b]) => a.localeCompare(b))
.forEach(([memberName, { alias, isAliasDeclaration }]) => {
let finalName: string
if (alias) {
finalName = alias
if (isAliasDeclaration) {
importSpecifiers.push(j.importSpecifier(j.identifier(memberName), j.identifier(alias)))
} else {
finalName = memberName
importSpecifiers.push(j.importSpecifier(j.identifier(memberName)))
}
} else if (hasCrossConflict) {
// Add namespace prefix for cross-namespace conflicts
finalName = `${namespacePrefix}${memberName}`
importSpecifiers.push(
j.importSpecifier(j.identifier(memberName), j.identifier(finalName)),
)
} else if (existingNames.has(memberName) && !isAliasDeclaration) {
finalName = `_${memberName}`
importSpecifiers.push(
j.importSpecifier(j.identifier(memberName), j.identifier(finalName)),
)
} else {
finalName = memberName
importSpecifiers.push(j.importSpecifier(j.identifier(memberName)))
}
memberRenames.set(memberName, finalName)
})
// Type-only members
Array.from(typeOnlyMembers)
.sort()
.forEach((memberName) => {
let finalName: string
if (hasCrossConflict) {
// Add namespace prefix for cross-namespace conflicts
finalName = `${namespacePrefix}${memberName}`
const spec = j.importSpecifier(j.identifier(memberName), j.identifier(finalName))
spec.importKind = 'type'
importSpecifiers.push(spec)
} else if (existingNames.has(memberName)) {
finalName = `_${memberName}`
const spec = j.importSpecifier(j.identifier(memberName), j.identifier(finalName))
spec.importKind = 'type'
importSpecifiers.push(spec)
} else {
finalName = memberName
const spec = j.importSpecifier(j.identifier(memberName))
spec.importKind = 'type'
importSpecifiers.push(spec)
}
memberRenames.set(memberName, finalName)
})
const namedImport = j.importDeclaration(importSpecifiers, j.literal(importSource))
j(path).replaceWith(namedImport)
// Remove aliases
aliasDeclarations.forEach((varPath) => {
const parentStatement = varPath.parent
if (parentStatement?.value.type === 'VariableDeclaration') {
if (parentStatement.value.declarations.length === 1) {
j(parentStatement).remove()
} else {
j(varPath).remove()
}
}
})
// Replace usages
root
.find(j.MemberExpression, {
object: { type: 'Identifier', name: namespaceName },
})
.forEach((memberPath) => {
const property = memberPath.node.property
if (property.type === 'Identifier') {
const finalName = memberRenames.get(property.name) || property.name
j(memberPath).replaceWith(j.identifier(finalName))
}
})
root
.find(j.JSXMemberExpression, {
object: { type: 'JSXIdentifier', name: namespaceName },
})
.forEach((jsxPath) => {
const property = jsxPath.node.property
if (property.type === 'JSXIdentifier') {
const finalName = memberRenames.get(property.name) || property.name
j(jsxPath).replaceWith(j.jsxIdentifier(finalName))
}
})
// Replace type references - manually traverse to replace all nodes
function replaceTypeReferences(node: any): void {
if (!node || typeof node !== 'object') return
if (node.type === 'TSTypeQuery') {
const exprName = node.exprName
if (exprName?.type === 'TSQualifiedName') {
const left = exprName.left
const right = exprName.right
if (left?.type === 'Identifier' && left.name === namespaceName) {
if (right?.type === 'Identifier') {
const finalName = memberRenames.get(right.name) || right.name
node.exprName = j.identifier(finalName)
}
}
}
}
if (node.type === 'TSTypeReference') {
const typeName = node.typeName
if (typeName?.type === 'TSQualifiedName') {
const left = typeName.left
const right = typeName.right
if (left?.type === 'Identifier' && left.name === namespaceName) {
if (right?.type === 'Identifier') {
if (!RESERVED_KEYWORDS.has(right.name)) {
const finalName = memberRenames.get(right.name) || right.name
node.typeName = j.identifier(finalName)
}
}
}
}
}
// Handle interface extends and class implements replacement
if (node.type === 'TSExpressionWithTypeArguments') {
const expression = node.expression
if (expression?.type === 'TSQualifiedName') {
const left = expression.left
const right = expression.right
if (left?.type === 'Identifier' && left.name === namespaceName) {
if (right?.type === 'Identifier') {
if (!RESERVED_KEYWORDS.has(right.name)) {
const finalName = memberRenames.get(right.name) || right.name
node.expression = j.identifier(finalName)
}
}
}
}
}
// Handle indexed access type replacement: (typeof Namespace)['Member'] -> typeof Member
if (node.type === 'TSIndexedAccessType') {
let objectType = node.objectType
const indexType = node.indexType
// Unwrap parenthesized types
if (objectType?.type === 'TSParenthesizedType') {
objectType = objectType.typeAnnotation
}
if (objectType?.type === 'TSTypeQuery') {
const exprName = objectType.exprName
if (exprName?.type === 'Identifier' && exprName.name === namespaceName) {
if (
indexType?.type === 'TSLiteralType' &&
indexType.literal?.type === 'StringLiteral'
) {
const memberName = indexType.literal.value
const finalName = memberRenames.get(memberName) || memberName
// Replace the entire indexed access with typeof Member
const newTypeQuery = j.tsTypeQuery(j.identifier(finalName))
Object.keys(node).forEach((key) => delete node[key])
Object.assign(node, newTypeQuery)
}
}
}
}
// Recurse into all object properties
for (const key in node) {
if (Object.hasOwn(node, key) && key !== 'loc' && key !== 'range' && key !== 'tokens') {
const value = node[key]
if (Array.isArray(value)) {
value.forEach((item) => replaceTypeReferences(item))
} else if (typeof value === 'object') {
replaceTypeReferences(value)
}
}
}
}
replaceTypeReferences(root.find(j.Program).get().node)
hasModifications = true
})
return hasModifications ? root.toSource({ quote: 'single' }) : null
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment