libs/nx/src/migrations/utils/package-import-migration.ts
Describes a set of symbols that have to be imported from targetPackage from now on.
Properties |
| symbols | |
| Type |
{}
|
| targetPackage | |
| Type |
string
|
import {
BindingElement,
CallExpression,
ImportDeclaration,
ImportSpecifier,
Node,
ObjectBindingPattern,
SourceFile
} from 'ts-morph';
/**
* Describes a set of symbols that have to be imported from `targetPackage` from now on.
*/
export interface PackageMigration {
readonly symbols: readonly string[];
readonly targetPackage: string;
}
/**
* Moves matched imports from a static import declaration to their new package.
*
* Example — all imports match:
* import { TalyPageService } from '@allianz/taly-core';
* import { TalyPageService } from '@allianz/taly-core/building-blocks'; (new)
*
* Example — mixed imports:
* import { TalyPageService, TalyStateService } from '@allianz/taly-core';
* import { TalyStateService } from '@allianz/taly-core'; (unchanged)
* import { TalyPageService } from '@allianz/taly-core/building-blocks'; (new)
*/
export function updateStaticImportPackage(
migration: PackageMigration,
declaration: ImportDeclaration,
sourceFile: SourceFile
): void {
const symbolsToMigrate = new Set(migration.symbols);
const namedImports = declaration.getNamedImports();
const { matchingImports, otherImports } = separateMatchingImports(namedImports, symbolsToMigrate);
if (matchingImports.length === 0) {
return;
}
if (otherImports.length === 0) {
declaration.setModuleSpecifier(migration.targetPackage);
} else {
// Mixed imports: extract matched ones into a new declaration for the target package.
const matchedStructures = matchingImports.map((imp) => imp.getStructure());
matchingImports.forEach((imp) => imp.remove());
// Insert after current declaration to maintain order
const insertIndex = declaration.getChildIndex();
sourceFile.insertImportDeclaration(insertIndex + 1, {
moduleSpecifier: migration.targetPackage,
namedImports: matchedStructures
});
}
}
/**
* Applies all migrations to a dynamic `import()` expression, moving the matched
* bindings to their new packages.
*/
export function splitDynamicImport(
callExpression: CallExpression,
migrations: readonly PackageMigration[]
): void {
const variableDeclaration = callExpression.getParent()?.getParent();
if (!Node.isVariableDeclaration(variableDeclaration)) return;
const specifierNode = callExpression.getArguments()[0];
if (!Node.isStringLiteral(specifierNode)) return;
for (const migration of migrations) {
// Refresh binding pattern for each iteration since it may have been modified
const bindingName = variableDeclaration.getNameNode();
if (!Node.isObjectBindingPattern(bindingName)) continue;
updateDynamicImportPackage(migration, callExpression, bindingName);
}
}
/**
* Moves matched imports from a dynamic import to their new package.
*
* Example — all imports match:
* const { TalyPageService } = await import('@allianz/taly-core');
* const { TalyPageService } = await import('@allianz/taly-core/building-blocks'); (new)
*
* Example — mixed imports:
* const { TalyPageService, TalyStateService } = await import('@allianz/taly-core');
* const { TalyStateService } = await import('@allianz/taly-core'); (unchanged)
* const { TalyPageService } = await import('@allianz/taly-core/building-blocks'); (new)
*/
export function updateDynamicImportPackage(
migration: PackageMigration,
callExpression: CallExpression,
bindingPattern: ObjectBindingPattern
): void {
const symbolsToMigrate = new Set(migration.symbols);
const elements = bindingPattern.getElements();
const { matchingImports, otherImports } = separateMatchingImports(elements, symbolsToMigrate);
if (matchingImports.length === 0) return;
if (otherImports.length === 0) {
// All elements go to new package
callExpression.getArguments()[0].replaceWithText(`'${migration.targetPackage}'`);
} else {
// Mixed imports: extract matched ones into a new import() for the target package.
const remainingText = otherImports.map((element) => element.getText()).join(', ');
const matchedText = matchingImports.map((element) => element.getText()).join(', ');
bindingPattern.replaceWithText(`{ ${remainingText} }`);
const newStatement = `const { ${matchedText} } = await import('${migration.targetPackage}');`;
// Insert new statement after current statement
const variableStatement = callExpression.getFirstAncestor(Node.isVariableStatement);
const parent = variableStatement?.getParent();
if (parent && Node.isStatemented(parent)) {
parent.insertStatements(
variableStatement ? variableStatement.getChildIndex() + 1 : 0,
newStatement
);
}
}
}
/**
* Splits import specifiers or binding elements into two groups based on whether
* they appear in the migration's symbol set.
*
* Example:
* import { TalyPageService, TalyStateService } from '@allianz/taly-core';
* symbolsToMigrate = ['TalyPageService']
* matchingImports: [TalyPageService], otherImports: [TalyStateService]
*/
export function separateMatchingImports<T extends ImportSpecifier | BindingElement>(
items: readonly T[],
symbolsToMigrate: Set<string>
): { matchingImports: T[]; otherImports: T[] } {
return items.reduce(
(acc, item) => {
let importedSymbolName = item.getName();
// For aliased dynamic imports like `{ TalyPageService: MyService }`,
// getName() returns the local alias ('MyService'), not the imported symbol ('TalyPageService').
// We need the property name (the original symbol) to correctly match against symbolsToMigrate.
if (Node.isBindingElement(item)) {
const propertyNameNode = item.getPropertyNameNode();
if (propertyNameNode) {
importedSymbolName = propertyNameNode.getText();
}
}
if (symbolsToMigrate.has(importedSymbolName)) {
acc.matchingImports.push(item);
} else {
acc.otherImports.push(item);
}
return acc;
},
{ matchingImports: [] as T[], otherImports: [] as T[] }
);
}