libs/ngx-pfe/schematics/ng-update/utils/ng-module/remove-import.ts
import { FileSystem, getDecoratorMetadata, getMetadataField, WorkspacePath } from '@angular/cdk/schematics';
import { SyntaxKind } from 'typescript';

// We explicitely use the same type defined by the angular schematic function signature, to avoid typescript version mismatch
type SourceFile = Parameters<typeof getDecoratorMetadata>[0];
type Node = Parameters<typeof getMetadataField>[0];
/**
 * This is a rather basic implementation of a ngModule import removal.
 * It collects the necessary changes and returns them. If the NgModule or the import was not found an empty changes array is returned.
 *
 * It currently does not handle edge cases, like it being the only import. (Which simply leaves an empty array of imports)
 */
export function removeSymbolFromNgModuleImports(sourceFile: SourceFile, symbolName: string, fileSystem: FileSystem): RemovalChange[] {
  const filePath = fileSystem.resolve(sourceFile.getSourceFile().fileName);

  const changes: RemovalChange[] = [];
  const nodes = getDecoratorMetadata(sourceFile, 'NgModule', '@angular/core');
  const node = nodes[0];
  // Find the decorator declaration.
  if (!node) {
    return [];
  }

  // Get all the children property assignment of object literals.
  const matchingProperties = getMetadataField(node as Node, 'imports');

  // Get the last node of the array literal.
  if (!matchingProperties || !matchingProperties[0]) {
    return [];
  }
  const children = matchingProperties[0].getChildren();

  const importsChildren = children.find((value) => value.kind === SyntaxKind.ArrayLiteralExpression);

  if (!importsChildren) {
    return [];
  }

  const importTokens = importsChildren
    .getChildren()
    ?.find((value) => value.kind === SyntaxKind.SyntaxList)
    ?.getChildren();

  if (!importTokens) {
    return [];
  }

  const symbolIndex = importTokens.findIndex((value) => value.kind === SyntaxKind.Identifier && value.getText() === symbolName);
  const symbolIdentifier = importTokens[symbolIndex];

  const previousToken = importTokens[symbolIndex - 1];
  const previousTokenComma = previousToken !== undefined && previousToken.kind === SyntaxKind.CommaToken;

  const nextToken = importTokens[symbolIndex + 1];
  const nextTokenComma = nextToken !== undefined && nextToken.kind === SyntaxKind.CommaToken;

  if (previousTokenComma && nextTokenComma) {
    // Perfect! We're surrounded by commas, so it doesn't matter which one we remove...
    changes.push({
      filePath,
      index: nextToken.getStart(),
      width: nextToken.getWidth(),
    });
  } else if (!nextToken || (!nextTokenComma && previousTokenComma)) {
    // There is no comma behind us, but there is one in the front.
    // Make sure that this is the last entry in the array, then we can remove the comma with us, or leave it, or...
    if (!nextToken) {
      changes.push({
        filePath,
        index: previousToken.getStart(),
        width: previousToken.getWidth(),
      });
    } else {
      // There is a next token, but it is not a comma. This doesn't look like valid code in the first place...
      return [];
    }
  }

  if (symbolIdentifier) {
    changes.push({
      filePath,
      index: symbolIdentifier.getStart(),
      width: symbolIdentifier.getWidth(),
    });
  }

  return changes;
}

export interface RemovalChange {
  filePath: WorkspacePath;
  index: number;
  width: number;
}

results matching ""

    No results matching ""