override visitNode()

in src/material/schematics/ng-update/migrations/package-imports-v8/secondary-entry-points-migration.ts [44:153]


  override visitNode(declaration: ts.Node): void {
    // Only look at import declarations.
    if (
      !ts.isImportDeclaration(declaration) ||
      !ts.isStringLiteralLike(declaration.moduleSpecifier)
    ) {
      return;
    }

    const importLocation = declaration.moduleSpecifier.text;
    // If the import module is not @angular/material, skip the check.
    if (importLocation !== materialModuleSpecifier) {
      return;
    }

    // If no import clause is found, or nothing is named as a binding in the
    // import, add failure saying to import symbols in clause.
    if (!declaration.importClause || !declaration.importClause.namedBindings) {
      this.createFailureAtNode(declaration, NO_IMPORT_NAMED_SYMBOLS_FAILURE_STR);
      return;
    }

    // All named bindings in import clauses must be named symbols, otherwise add
    // failure saying to import symbols in clause.
    if (!ts.isNamedImports(declaration.importClause.namedBindings)) {
      this.createFailureAtNode(declaration, NO_IMPORT_NAMED_SYMBOLS_FAILURE_STR);
      return;
    }

    // If no symbols are in the named bindings then add failure saying to
    // import symbols in clause.
    if (!declaration.importClause.namedBindings.elements.length) {
      this.createFailureAtNode(declaration, NO_IMPORT_NAMED_SYMBOLS_FAILURE_STR);
      return;
    }

    // Whether the existing import declaration is using a single quote module specifier.
    const singleQuoteImport = declaration.moduleSpecifier.getText()[0] === `'`;

    // Map which consists of secondary entry-points and import specifiers which are used
    // within the current import declaration.
    const importMap = new Map<string, ts.ImportSpecifier[]>();

    // Determine the subpackage each symbol in the namedBinding comes from.
    for (const element of declaration.importClause.namedBindings.elements) {
      const elementName = element.propertyName ? element.propertyName : element.name;

      // Try to resolve the module name via the type checker, and if it fails, fall back to
      // resolving it from our list of symbol to entry point mappings. Using the type checker is
      // more accurate and doesn't require us to keep a list of symbols, but it won't work if
      // the symbols don't exist anymore (e.g. after we remove the top-level @angular/material).
      const moduleName =
        resolveModuleName(elementName, this.typeChecker) ||
        ENTRY_POINT_MAPPINGS[elementName.text] ||
        null;

      if (!moduleName) {
        this.createFailureAtNode(
          element,
          `"${element.getText()}" was not found in the Material library.`,
        );
        return;
      }

      // The module name where the symbol is defined e.g. card, dialog. The
      // first capture group is contains the module name.
      if (importMap.has(moduleName)) {
        importMap.get(moduleName)!.push(element);
      } else {
        importMap.set(moduleName, [element]);
      }
    }

    // Transforms the import declaration into multiple import declarations that import
    // the given symbols from the individual secondary entry-points. For example:
    // import {MatCardModule, MatCardTitle} from '@angular/material/card';
    // import {MatRadioModule} from '@angular/material/radio';
    const newImportStatements = Array.from(importMap.entries())
      .sort()
      .map(([name, elements]) => {
        const newImport = ts.createImportDeclaration(
          undefined,
          undefined,
          ts.createImportClause(undefined, ts.createNamedImports(elements)),
          createStringLiteral(`${materialModuleSpecifier}/${name}`, singleQuoteImport),
        );
        return this.printer.printNode(
          ts.EmitHint.Unspecified,
          newImport,
          declaration.getSourceFile(),
        );
      })
      .join('\n');

    // Without any import statements that were generated, we can assume that this was an empty
    // import declaration. We still want to add a failure in order to make developers aware that
    // importing from "@angular/material" is deprecated.
    if (!newImportStatements) {
      this.createFailureAtNode(declaration.moduleSpecifier, ONLY_SUBPACKAGE_FAILURE_STR);
      return;
    }

    const filePath = this.fileSystem.resolve(declaration.moduleSpecifier.getSourceFile().fileName);
    const recorder = this.fileSystem.edit(filePath);

    // Perform the replacement that switches the primary entry-point import to
    // the individual secondary entry-point imports.
    recorder.remove(declaration.getStart(), declaration.getWidth());
    recorder.insertRight(declaration.getStart(), newImportStatements);
  }