|
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 |
|
} |