File

libs/nx/src/migrations/utils/package-import-migration.ts

Description

Describes a set of symbols that have to be imported from targetPackage from now on.

Index

Properties

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[] }
  );
}

results matching ""

    No results matching ""