@angular/core 19.0.0-next.3 → 19.0.0-next.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
  /**
3
- * @license Angular v19.0.0-next.3
3
+ * @license Angular v19.0.0-next.4
4
4
  * (c) 2010-2024 Google LLC. https://angular.io/
5
5
  * License: MIT
6
6
  */
@@ -11,11 +11,12 @@ Object.defineProperty(exports, '__esModule', { value: true });
11
11
  var schematics = require('@angular-devkit/schematics');
12
12
  require('os');
13
13
  var ts = require('typescript');
14
- var compiler_host = require('./compiler_host-bbb5d8fd.js');
14
+ var compiler_host = require('./compiler_host-ca7ba733.js');
15
15
  var p = require('path');
16
16
  var fs = require('fs');
17
17
  var project_tsconfig_paths = require('./project_tsconfig_paths-e9ccccbf.js');
18
- var nodes = require('./nodes-ddfa1613.js');
18
+ var nodes = require('./nodes-0e7d45ca.js');
19
+ var imports = require('./imports-4ac08251.js');
19
20
  require('module');
20
21
  require('url');
21
22
  require('@angular-devkit/core');
@@ -873,7 +874,7 @@ const MINIMUM_PARTIAL_LINKER_DEFER_SUPPORT_VERSION = '18.0.0';
873
874
  function compileDeclareClassMetadata(metadata) {
874
875
  const definitionMap = new compiler_host.DefinitionMap();
875
876
  definitionMap.set('minVersion', compiler_host.literal(MINIMUM_PARTIAL_LINKER_VERSION$5));
876
- definitionMap.set('version', compiler_host.literal('19.0.0-next.3'));
877
+ definitionMap.set('version', compiler_host.literal('19.0.0-next.4'));
877
878
  definitionMap.set('ngImport', compiler_host.importExpr(compiler_host.Identifiers.core));
878
879
  definitionMap.set('type', metadata.type);
879
880
  definitionMap.set('decorators', metadata.decorators);
@@ -891,7 +892,7 @@ function compileComponentDeclareClassMetadata(metadata, dependencies) {
891
892
  callbackReturnDefinitionMap.set('ctorParameters', metadata.ctorParameters ?? compiler_host.literal(null));
892
893
  callbackReturnDefinitionMap.set('propDecorators', metadata.propDecorators ?? compiler_host.literal(null));
893
894
  definitionMap.set('minVersion', compiler_host.literal(MINIMUM_PARTIAL_LINKER_DEFER_SUPPORT_VERSION));
894
- definitionMap.set('version', compiler_host.literal('19.0.0-next.3'));
895
+ definitionMap.set('version', compiler_host.literal('19.0.0-next.4'));
895
896
  definitionMap.set('ngImport', compiler_host.importExpr(compiler_host.Identifiers.core));
896
897
  definitionMap.set('type', metadata.type);
897
898
  definitionMap.set('resolveDeferredDeps', compileComponentMetadataAsyncResolver(dependencies));
@@ -986,7 +987,7 @@ function createDirectiveDefinitionMap(meta) {
986
987
  const definitionMap = new compiler_host.DefinitionMap();
987
988
  const minVersion = getMinimumVersionForPartialOutput(meta);
988
989
  definitionMap.set('minVersion', compiler_host.literal(minVersion));
989
- definitionMap.set('version', compiler_host.literal('19.0.0-next.3'));
990
+ definitionMap.set('version', compiler_host.literal('19.0.0-next.4'));
990
991
  // e.g. `type: MyDirective`
991
992
  definitionMap.set('type', meta.type.value);
992
993
  if (meta.isStandalone) {
@@ -1405,7 +1406,7 @@ const MINIMUM_PARTIAL_LINKER_VERSION$4 = '12.0.0';
1405
1406
  function compileDeclareFactoryFunction(meta) {
1406
1407
  const definitionMap = new compiler_host.DefinitionMap();
1407
1408
  definitionMap.set('minVersion', compiler_host.literal(MINIMUM_PARTIAL_LINKER_VERSION$4));
1408
- definitionMap.set('version', compiler_host.literal('19.0.0-next.3'));
1409
+ definitionMap.set('version', compiler_host.literal('19.0.0-next.4'));
1409
1410
  definitionMap.set('ngImport', compiler_host.importExpr(compiler_host.Identifiers.core));
1410
1411
  definitionMap.set('type', meta.type.value);
1411
1412
  definitionMap.set('deps', compileDependencies(meta.deps));
@@ -1440,7 +1441,7 @@ function compileDeclareInjectableFromMetadata(meta) {
1440
1441
  function createInjectableDefinitionMap(meta) {
1441
1442
  const definitionMap = new compiler_host.DefinitionMap();
1442
1443
  definitionMap.set('minVersion', compiler_host.literal(MINIMUM_PARTIAL_LINKER_VERSION$3));
1443
- definitionMap.set('version', compiler_host.literal('19.0.0-next.3'));
1444
+ definitionMap.set('version', compiler_host.literal('19.0.0-next.4'));
1444
1445
  definitionMap.set('ngImport', compiler_host.importExpr(compiler_host.Identifiers.core));
1445
1446
  definitionMap.set('type', meta.type.value);
1446
1447
  // Only generate providedIn property if it has a non-null value
@@ -1491,7 +1492,7 @@ function compileDeclareInjectorFromMetadata(meta) {
1491
1492
  function createInjectorDefinitionMap(meta) {
1492
1493
  const definitionMap = new compiler_host.DefinitionMap();
1493
1494
  definitionMap.set('minVersion', compiler_host.literal(MINIMUM_PARTIAL_LINKER_VERSION$2));
1494
- definitionMap.set('version', compiler_host.literal('19.0.0-next.3'));
1495
+ definitionMap.set('version', compiler_host.literal('19.0.0-next.4'));
1495
1496
  definitionMap.set('ngImport', compiler_host.importExpr(compiler_host.Identifiers.core));
1496
1497
  definitionMap.set('type', meta.type.value);
1497
1498
  definitionMap.set('providers', meta.providers);
@@ -1524,7 +1525,7 @@ function createNgModuleDefinitionMap(meta) {
1524
1525
  throw new Error('Invalid path! Local compilation mode should not get into the partial compilation path');
1525
1526
  }
1526
1527
  definitionMap.set('minVersion', compiler_host.literal(MINIMUM_PARTIAL_LINKER_VERSION$1));
1527
- definitionMap.set('version', compiler_host.literal('19.0.0-next.3'));
1528
+ definitionMap.set('version', compiler_host.literal('19.0.0-next.4'));
1528
1529
  definitionMap.set('ngImport', compiler_host.importExpr(compiler_host.Identifiers.core));
1529
1530
  definitionMap.set('type', meta.type.value);
1530
1531
  // We only generate the keys in the metadata if the arrays contain values.
@@ -1575,7 +1576,7 @@ function compileDeclarePipeFromMetadata(meta) {
1575
1576
  function createPipeDefinitionMap(meta) {
1576
1577
  const definitionMap = new compiler_host.DefinitionMap();
1577
1578
  definitionMap.set('minVersion', compiler_host.literal(MINIMUM_PARTIAL_LINKER_VERSION));
1578
- definitionMap.set('version', compiler_host.literal('19.0.0-next.3'));
1579
+ definitionMap.set('version', compiler_host.literal('19.0.0-next.4'));
1579
1580
  definitionMap.set('ngImport', compiler_host.importExpr(compiler_host.Identifiers.core));
1580
1581
  // e.g. `type: MyPipe`
1581
1582
  definitionMap.set('type', meta.type.value);
@@ -11535,7 +11536,7 @@ class PipeDecoratorHandler {
11535
11536
  * @description
11536
11537
  * Entry point for all public APIs of the compiler-cli package.
11537
11538
  */
11538
- new compiler_host.Version('19.0.0-next.3');
11539
+ new compiler_host.Version('19.0.0-next.4');
11539
11540
 
11540
11541
  /**
11541
11542
  * Whether a given decorator should be treated as an Angular decorator.
@@ -13744,7 +13745,7 @@ class DocsExtractor {
13744
13745
  *
13745
13746
  * @param sourceFile The file from which to extract documentable entries.
13746
13747
  */
13747
- extractAll(sourceFile, rootDir) {
13748
+ extractAll(sourceFile, rootDir, privateModules) {
13748
13749
  const entries = [];
13749
13750
  const symbols = new Map();
13750
13751
  const exportedDeclarations = this.getExportedDeclarations(sourceFile);
@@ -13768,8 +13769,7 @@ class DocsExtractor {
13768
13769
  */
13769
13770
  const importedSymbols = getImportedSymbols(realSourceFile);
13770
13771
  importedSymbols.forEach((moduleName, symbolName) => {
13771
- // TODO: we probably want to filter out symbols from private modules (like core/primitives)
13772
- if (symbolName.startsWith('ɵ')) {
13772
+ if (symbolName.startsWith('ɵ') || privateModules.has(moduleName)) {
13773
13773
  return;
13774
13774
  }
13775
13775
  if (symbols.has(symbolName) && symbols.get(symbolName) !== moduleName) {
@@ -18893,7 +18893,7 @@ var semver$1 = /*@__PURE__*/getDefaultExportFromCjs(semver);
18893
18893
  * @param minVersion Minimum required version for the feature.
18894
18894
  */
18895
18895
  function coreVersionSupportsFeature(coreVersion, minVersion) {
18896
- // A version of `19.0.0-next.3` usually means that core is at head so it supports
18896
+ // A version of `19.0.0-next.4` usually means that core is at head so it supports
18897
18897
  // all features. Use string interpolation prevent the placeholder from being replaced
18898
18898
  // with the current version during build time.
18899
18899
  if (coreVersion === `0.0.0-${'PLACEHOLDER'}`) {
@@ -19364,7 +19364,7 @@ class NgCompiler {
19364
19364
  *
19365
19365
  * @returns A map of symbols with their associated module, eg: ApplicationRef => @angular/core
19366
19366
  */
19367
- getApiDocumentation(entryPoint) {
19367
+ getApiDocumentation(entryPoint, privateModules) {
19368
19368
  const compilation = this.ensureAnalyzed();
19369
19369
  const checker = this.inputProgram.getTypeChecker();
19370
19370
  const docsExtractor = new DocsExtractor(checker, compilation.metaReader);
@@ -19379,7 +19379,7 @@ class NgCompiler {
19379
19379
  // TODO: Technically the current directory is not the root dir.
19380
19380
  // Should probably be derived from the config.
19381
19381
  const rootDir = this.inputProgram.getCurrentDirectory();
19382
- return docsExtractor.extractAll(entryPointSourceFile, rootDir);
19382
+ return docsExtractor.extractAll(entryPointSourceFile, rootDir, privateModules);
19383
19383
  }
19384
19384
  /**
19385
19385
  * Collect i18n messages into the `Xi18nContext`.
@@ -20462,8 +20462,8 @@ class NgtscProgram {
20462
20462
  * @param entryPoint Path to the entry point for the package for which API
20463
20463
  * docs should be extracted.
20464
20464
  */
20465
- getApiDocumentation(entryPoint) {
20466
- return this.compiler.getApiDocumentation(entryPoint);
20465
+ getApiDocumentation(entryPoint, privateModules) {
20466
+ return this.compiler.getApiDocumentation(entryPoint, privateModules);
20467
20467
  }
20468
20468
  getEmittedSourceFiles() {
20469
20469
  throw new Error('Method not implemented.');
@@ -20743,6 +20743,20 @@ function isClassReferenceInAngularModule(node, className, moduleName, typeChecke
20743
20743
  });
20744
20744
  }
20745
20745
 
20746
+ /** Checks whether a node is referring to a specific import specifier. */
20747
+ function isReferenceToImport(typeChecker, node, importSpecifier) {
20748
+ // If this function is called on an identifier (should be most cases), we can quickly rule out
20749
+ // non-matches by comparing the identifier's string and the local name of the import specifier
20750
+ // which saves us some calls to the type checker.
20751
+ if (ts__default["default"].isIdentifier(node) && node.text !== importSpecifier.name.text) {
20752
+ return false;
20753
+ }
20754
+ const nodeSymbol = typeChecker.getTypeAtLocation(node).getSymbol();
20755
+ const importSymbol = typeChecker.getTypeAtLocation(importSpecifier).getSymbol();
20756
+ return (!!(nodeSymbol?.declarations?.[0] && importSymbol?.declarations?.[0]) &&
20757
+ nodeSymbol.declarations[0] === importSymbol.declarations[0]);
20758
+ }
20759
+
20746
20760
  /*!
20747
20761
  * @license
20748
20762
  * Copyright Google LLC All Rights Reserved.
@@ -20750,906 +20764,994 @@ function isClassReferenceInAngularModule(node, className, moduleName, typeChecke
20750
20764
  * Use of this source code is governed by an MIT-style license that can be
20751
20765
  * found in the LICENSE file at https://angular.io/license
20752
20766
  */
20753
- function pruneNgModules(program, host, basePath, rootFileNames, sourceFiles, printer, importRemapper, referenceLookupExcludedFiles) {
20754
- const filesToRemove = new Set();
20755
- const tracker = new compiler_host.ChangeTracker(printer, importRemapper);
20756
- const tsProgram = program.getTsProgram();
20757
- const typeChecker = tsProgram.getTypeChecker();
20758
- const referenceResolver = new ReferenceResolver(program, host, rootFileNames, basePath, referenceLookupExcludedFiles);
20759
- const removalLocations = {
20760
- arrays: new UniqueItemTracker(),
20761
- imports: new UniqueItemTracker(),
20762
- exports: new UniqueItemTracker(),
20763
- unknown: new Set(),
20764
- };
20765
- const classesToRemove = new Set();
20766
- const barrelExports = new UniqueItemTracker();
20767
- const nodesToRemove = new Set();
20768
- sourceFiles.forEach(function walk(node) {
20769
- if (ts__default["default"].isClassDeclaration(node) && canRemoveClass(node, typeChecker)) {
20770
- collectRemovalLocations(node, removalLocations, referenceResolver, program);
20771
- classesToRemove.add(node);
20772
- }
20773
- else if (ts__default["default"].isExportDeclaration(node) &&
20774
- !node.exportClause &&
20775
- node.moduleSpecifier &&
20776
- ts__default["default"].isStringLiteralLike(node.moduleSpecifier) &&
20777
- node.moduleSpecifier.text.startsWith('.')) {
20778
- const exportedSourceFile = typeChecker
20779
- .getSymbolAtLocation(node.moduleSpecifier)
20780
- ?.valueDeclaration?.getSourceFile();
20781
- if (exportedSourceFile) {
20782
- barrelExports.track(exportedSourceFile, node);
20783
- }
20784
- }
20785
- node.forEachChild(walk);
20786
- });
20787
- // We collect all the places where we need to remove references first before generating the
20788
- // removal instructions since we may have to remove multiple references from one node.
20789
- removeArrayReferences(removalLocations.arrays, tracker);
20790
- removeImportReferences(removalLocations.imports, tracker);
20791
- removeExportReferences(removalLocations.exports, tracker);
20792
- addRemovalTodos(removalLocations.unknown, tracker);
20793
- // Collect all the nodes to be removed before determining which files to delete since we need
20794
- // to know it ahead of time when deleting barrel files that export other barrel files.
20795
- (function trackNodesToRemove(nodes) {
20796
- for (const node of nodes) {
20797
- const sourceFile = node.getSourceFile();
20798
- if (!filesToRemove.has(sourceFile) && canRemoveFile(sourceFile, nodes)) {
20799
- const barrelExportsForFile = barrelExports.get(sourceFile);
20800
- nodesToRemove.add(node);
20801
- filesToRemove.add(sourceFile);
20802
- barrelExportsForFile && trackNodesToRemove(barrelExportsForFile);
20803
- }
20804
- else {
20805
- nodesToRemove.add(node);
20767
+ /**
20768
+ * Converts all declarations in the specified files to standalone.
20769
+ * @param sourceFiles Files that should be migrated.
20770
+ * @param program
20771
+ * @param printer
20772
+ * @param fileImportRemapper Optional function that can be used to remap file-level imports.
20773
+ * @param componentImportRemapper Optional function that can be used to remap component-level
20774
+ * imports.
20775
+ */
20776
+ function toStandalone(sourceFiles, program, printer, fileImportRemapper, componentImportRemapper) {
20777
+ const templateTypeChecker = program.compiler.getTemplateTypeChecker();
20778
+ const typeChecker = program.getTsProgram().getTypeChecker();
20779
+ const modulesToMigrate = new Set();
20780
+ const testObjectsToMigrate = new Set();
20781
+ const declarations = new Set();
20782
+ const tracker = new compiler_host.ChangeTracker(printer, fileImportRemapper);
20783
+ for (const sourceFile of sourceFiles) {
20784
+ const modules = findNgModuleClassesToMigrate(sourceFile, typeChecker);
20785
+ const testObjects = findTestObjectsToMigrate(sourceFile, typeChecker);
20786
+ for (const module of modules) {
20787
+ const allModuleDeclarations = extractDeclarationsFromModule(module, templateTypeChecker);
20788
+ const unbootstrappedDeclarations = filterNonBootstrappedDeclarations(allModuleDeclarations, module, templateTypeChecker, typeChecker);
20789
+ if (unbootstrappedDeclarations.length > 0) {
20790
+ modulesToMigrate.add(module);
20791
+ unbootstrappedDeclarations.forEach((decl) => declarations.add(decl));
20806
20792
  }
20807
20793
  }
20808
- })(classesToRemove);
20809
- for (const node of nodesToRemove) {
20810
- const sourceFile = node.getSourceFile();
20811
- if (!filesToRemove.has(sourceFile) && canRemoveFile(sourceFile, nodesToRemove)) {
20812
- filesToRemove.add(sourceFile);
20813
- }
20814
- else {
20815
- tracker.removeNode(node);
20816
- }
20794
+ testObjects.forEach((obj) => testObjectsToMigrate.add(obj));
20817
20795
  }
20818
- return { pendingChanges: tracker.recordChanges(), filesToRemove };
20796
+ for (const declaration of declarations) {
20797
+ convertNgModuleDeclarationToStandalone(declaration, declarations, tracker, templateTypeChecker, componentImportRemapper);
20798
+ }
20799
+ for (const node of modulesToMigrate) {
20800
+ migrateNgModuleClass(node, declarations, tracker, typeChecker, templateTypeChecker);
20801
+ }
20802
+ migrateTestDeclarations(testObjectsToMigrate, declarations, tracker, templateTypeChecker, typeChecker);
20803
+ return tracker.recordChanges();
20819
20804
  }
20820
20805
  /**
20821
- * Collects all the nodes that a module needs to be removed from.
20822
- * @param ngModule Module being removed.
20823
- * @param removalLocations
20824
- * @param referenceResolver
20825
- * @param program
20806
+ * Converts a single declaration defined through an NgModule to standalone.
20807
+ * @param decl Declaration being converted.
20808
+ * @param tracker Tracker used to track the file changes.
20809
+ * @param allDeclarations All the declarations that are being converted as a part of this migration.
20810
+ * @param typeChecker
20811
+ * @param importRemapper
20826
20812
  */
20827
- function collectRemovalLocations(ngModule, removalLocations, referenceResolver, program) {
20828
- const refsByFile = referenceResolver.findReferencesInProject(ngModule.name);
20829
- const tsProgram = program.getTsProgram();
20830
- const nodes$1 = new Set();
20831
- for (const [fileName, refs] of refsByFile) {
20832
- const sourceFile = tsProgram.getSourceFile(fileName);
20833
- if (sourceFile) {
20834
- offsetsToNodes(getNodeLookup(sourceFile), refs, nodes$1);
20813
+ function convertNgModuleDeclarationToStandalone(decl, allDeclarations, tracker, typeChecker, importRemapper) {
20814
+ const directiveMeta = typeChecker.getDirectiveMetadata(decl);
20815
+ if (directiveMeta && directiveMeta.decorator && !directiveMeta.isStandalone) {
20816
+ let decorator = addStandaloneToDecorator(directiveMeta.decorator);
20817
+ if (directiveMeta.isComponent) {
20818
+ const importsToAdd = getComponentImportExpressions(decl, allDeclarations, tracker, typeChecker, importRemapper);
20819
+ if (importsToAdd.length > 0) {
20820
+ const hasTrailingComma = importsToAdd.length > 2 &&
20821
+ !!extractMetadataLiteral(directiveMeta.decorator)?.properties.hasTrailingComma;
20822
+ decorator = addPropertyToAngularDecorator(decorator, ts__default["default"].factory.createPropertyAssignment('imports', ts__default["default"].factory.createArrayLiteralExpression(
20823
+ // Create a multi-line array when it has a trailing comma.
20824
+ ts__default["default"].factory.createNodeArray(importsToAdd, hasTrailingComma), hasTrailingComma)));
20825
+ }
20835
20826
  }
20827
+ tracker.replaceNode(directiveMeta.decorator, decorator);
20836
20828
  }
20837
- for (const node of nodes$1) {
20838
- const closestArray = nodes.closestNode(node, ts__default["default"].isArrayLiteralExpression);
20839
- if (closestArray) {
20840
- removalLocations.arrays.track(closestArray, node);
20841
- continue;
20842
- }
20843
- const closestImport = nodes.closestNode(node, ts__default["default"].isNamedImports);
20844
- if (closestImport) {
20845
- removalLocations.imports.track(closestImport, node);
20846
- continue;
20847
- }
20848
- const closestExport = nodes.closestNode(node, ts__default["default"].isNamedExports);
20849
- if (closestExport) {
20850
- removalLocations.exports.track(closestExport, node);
20851
- continue;
20829
+ else {
20830
+ const pipeMeta = typeChecker.getPipeMetadata(decl);
20831
+ if (pipeMeta && pipeMeta.decorator && !pipeMeta.isStandalone) {
20832
+ tracker.replaceNode(pipeMeta.decorator, addStandaloneToDecorator(pipeMeta.decorator));
20852
20833
  }
20853
- removalLocations.unknown.add(node);
20854
20834
  }
20855
20835
  }
20856
20836
  /**
20857
- * Removes all tracked array references.
20858
- * @param locations Locations from which to remove the references.
20859
- * @param tracker Tracker in which to register the changes.
20837
+ * Gets the expressions that should be added to a component's
20838
+ * `imports` array based on its template dependencies.
20839
+ * @param decl Component class declaration.
20840
+ * @param allDeclarations All the declarations that are being converted as a part of this migration.
20841
+ * @param tracker
20842
+ * @param typeChecker
20843
+ * @param importRemapper
20860
20844
  */
20861
- function removeArrayReferences(locations, tracker) {
20862
- for (const [array, toRemove] of locations.getEntries()) {
20863
- const newElements = filterRemovedElements(array.elements, toRemove);
20864
- tracker.replaceNode(array, ts__default["default"].factory.updateArrayLiteralExpression(array, ts__default["default"].factory.createNodeArray(newElements, array.elements.hasTrailingComma)));
20845
+ function getComponentImportExpressions(decl, allDeclarations, tracker, typeChecker, importRemapper) {
20846
+ const templateDependencies = findTemplateDependencies(decl, typeChecker);
20847
+ const usedDependenciesInMigration = new Set(templateDependencies.filter((dep) => allDeclarations.has(dep.node)));
20848
+ const seenImports = new Set();
20849
+ const resolvedDependencies = [];
20850
+ for (const dep of templateDependencies) {
20851
+ const importLocation = findImportLocation(dep, decl, usedDependenciesInMigration.has(dep)
20852
+ ? compiler_host.PotentialImportMode.ForceDirect
20853
+ : compiler_host.PotentialImportMode.Normal, typeChecker);
20854
+ if (importLocation && !seenImports.has(importLocation.symbolName)) {
20855
+ seenImports.add(importLocation.symbolName);
20856
+ resolvedDependencies.push(importLocation);
20857
+ }
20865
20858
  }
20859
+ return potentialImportsToExpressions(resolvedDependencies, decl, tracker, importRemapper);
20866
20860
  }
20867
20861
  /**
20868
- * Removes all tracked import references.
20869
- * @param locations Locations from which to remove the references.
20870
- * @param tracker Tracker in which to register the changes.
20862
+ * Converts an array of potential imports to an array of expressions that can be
20863
+ * added to the `imports` array.
20864
+ * @param potentialImports Imports to be converted.
20865
+ * @param component Component class to which the imports will be added.
20866
+ * @param tracker
20867
+ * @param importRemapper
20871
20868
  */
20872
- function removeImportReferences(locations, tracker) {
20873
- for (const [namedImports, toRemove] of locations.getEntries()) {
20874
- const newElements = filterRemovedElements(namedImports.elements, toRemove);
20875
- // If no imports are left, we can try to drop the entire import.
20876
- if (newElements.length === 0) {
20877
- const importClause = nodes.closestNode(namedImports, ts__default["default"].isImportClause);
20878
- // If the import clause has a name we can only drop then named imports.
20879
- // e.g. `import Foo, {ModuleToRemove} from './foo';` becomes `import Foo from './foo';`.
20880
- if (importClause && importClause.name) {
20881
- tracker.replaceNode(importClause, ts__default["default"].factory.updateImportClause(importClause, importClause.isTypeOnly, importClause.name, undefined));
20882
- }
20883
- else {
20884
- // Otherwise we can drop the entire declaration.
20885
- const declaration = nodes.closestNode(namedImports, ts__default["default"].isImportDeclaration);
20886
- if (declaration) {
20887
- tracker.removeNode(declaration);
20888
- }
20889
- }
20869
+ function potentialImportsToExpressions(potentialImports, component, tracker, importRemapper) {
20870
+ const processedDependencies = importRemapper
20871
+ ? importRemapper(potentialImports, component)
20872
+ : potentialImports;
20873
+ return processedDependencies.map((importLocation) => {
20874
+ if (importLocation.moduleSpecifier) {
20875
+ return tracker.addImport(component.getSourceFile(), importLocation.symbolName, importLocation.moduleSpecifier);
20890
20876
  }
20891
- else {
20892
- // Otherwise we just drop the imported symbols and keep the declaration intact.
20893
- tracker.replaceNode(namedImports, ts__default["default"].factory.updateNamedImports(namedImports, newElements));
20877
+ const identifier = ts__default["default"].factory.createIdentifier(importLocation.symbolName);
20878
+ if (!importLocation.isForwardReference) {
20879
+ return identifier;
20894
20880
  }
20895
- }
20881
+ const forwardRefExpression = tracker.addImport(component.getSourceFile(), 'forwardRef', '@angular/core');
20882
+ const arrowFunction = ts__default["default"].factory.createArrowFunction(undefined, undefined, [], undefined, undefined, identifier);
20883
+ return ts__default["default"].factory.createCallExpression(forwardRefExpression, undefined, [arrowFunction]);
20884
+ });
20896
20885
  }
20897
20886
  /**
20898
- * Removes all tracked export references.
20899
- * @param locations Locations from which to remove the references.
20900
- * @param tracker Tracker in which to register the changes.
20887
+ * Moves all of the declarations of a class decorated with `@NgModule` to its imports.
20888
+ * @param node Class being migrated.
20889
+ * @param allDeclarations All the declarations that are being converted as a part of this migration.
20890
+ * @param tracker
20891
+ * @param typeChecker
20892
+ * @param templateTypeChecker
20901
20893
  */
20902
- function removeExportReferences(locations, tracker) {
20903
- for (const [namedExports, toRemove] of locations.getEntries()) {
20904
- const newElements = filterRemovedElements(namedExports.elements, toRemove);
20905
- // If no exports are left, we can drop the entire declaration.
20906
- if (newElements.length === 0) {
20907
- const declaration = nodes.closestNode(namedExports, ts__default["default"].isExportDeclaration);
20908
- if (declaration) {
20909
- tracker.removeNode(declaration);
20910
- }
20911
- }
20912
- else {
20913
- // Otherwise we just drop the exported symbols and keep the declaration intact.
20914
- tracker.replaceNode(namedExports, ts__default["default"].factory.updateNamedExports(namedExports, newElements));
20915
- }
20894
+ function migrateNgModuleClass(node, allDeclarations, tracker, typeChecker, templateTypeChecker) {
20895
+ const decorator = templateTypeChecker.getNgModuleMetadata(node)?.decorator;
20896
+ const metadata = decorator ? extractMetadataLiteral(decorator) : null;
20897
+ if (metadata) {
20898
+ moveDeclarationsToImports(metadata, allDeclarations, typeChecker, templateTypeChecker, tracker);
20916
20899
  }
20917
20900
  }
20918
20901
  /**
20919
- * Determines whether an `@NgModule` class is safe to remove. A module is safe to remove if:
20920
- * 1. It has no `declarations`.
20921
- * 2. It has no `providers`.
20922
- * 3. It has no `bootstrap` components.
20923
- * 4. It has no `ModuleWithProviders` in its `imports`.
20924
- * 5. It has no class members. Empty construstors are ignored.
20925
- * @param node Class that is being checked.
20902
+ * Moves all the symbol references from the `declarations` array to the `imports`
20903
+ * array of an `NgModule` class and removes the `declarations`.
20904
+ * @param literal Object literal used to configure the module that should be migrated.
20905
+ * @param allDeclarations All the declarations that are being converted as a part of this migration.
20926
20906
  * @param typeChecker
20907
+ * @param tracker
20927
20908
  */
20928
- function canRemoveClass(node, typeChecker) {
20929
- const decorator = findNgModuleDecorator(node, typeChecker)?.node;
20930
- // We can't remove a declaration if it's not a valid `NgModule`.
20931
- if (!decorator || !ts__default["default"].isCallExpression(decorator.expression)) {
20932
- return false;
20933
- }
20934
- // Unsupported case, e.g. `@NgModule(SOME_VALUE)`.
20935
- if (decorator.expression.arguments.length > 0 &&
20936
- !ts__default["default"].isObjectLiteralExpression(decorator.expression.arguments[0])) {
20937
- return false;
20938
- }
20939
- // We can't remove modules that have class members. We make an exception for an
20940
- // empty constructor which may have been generated by a tool and forgotten.
20941
- if (node.members.length > 0 && node.members.some((member) => !isEmptyConstructor(member))) {
20942
- return false;
20943
- }
20944
- // An empty `NgModule` call can be removed.
20945
- if (decorator.expression.arguments.length === 0) {
20946
- return true;
20909
+ function moveDeclarationsToImports(literal, allDeclarations, typeChecker, templateTypeChecker, tracker) {
20910
+ const declarationsProp = findLiteralProperty(literal, 'declarations');
20911
+ if (!declarationsProp) {
20912
+ return;
20947
20913
  }
20948
- const literal = decorator.expression.arguments[0];
20949
- const imports = findLiteralProperty(literal, 'imports');
20950
- if (imports && isNonEmptyNgModuleProperty(imports)) {
20951
- // We can't remove the class if at least one import isn't identifier, because it may be a
20952
- // `ModuleWithProviders` which is the equivalent of having something in the `providers` array.
20953
- for (const dep of imports.initializer.elements) {
20954
- if (!ts__default["default"].isIdentifier(dep)) {
20955
- return false;
20956
- }
20957
- const depDeclaration = findClassDeclaration(dep, typeChecker);
20958
- const depNgModule = depDeclaration
20959
- ? findNgModuleDecorator(depDeclaration, typeChecker)
20960
- : null;
20961
- // If any of the dependencies of the class is an `NgModule` that can't be removed, the class
20962
- // itself can't be removed either, because it may be part of a transitive dependency chain.
20963
- if (depDeclaration !== null &&
20964
- depNgModule !== null &&
20965
- !canRemoveClass(depDeclaration, typeChecker)) {
20966
- return false;
20914
+ const declarationsToPreserve = [];
20915
+ const declarationsToCopy = [];
20916
+ const properties = [];
20917
+ const importsProp = findLiteralProperty(literal, 'imports');
20918
+ const hasAnyArrayTrailingComma = literal.properties.some((prop) => ts__default["default"].isPropertyAssignment(prop) &&
20919
+ ts__default["default"].isArrayLiteralExpression(prop.initializer) &&
20920
+ prop.initializer.elements.hasTrailingComma);
20921
+ // Separate the declarations that we want to keep and ones we need to copy into the `imports`.
20922
+ if (ts__default["default"].isPropertyAssignment(declarationsProp)) {
20923
+ // If the declarations are an array, we can analyze it to
20924
+ // find any classes from the current migration.
20925
+ if (ts__default["default"].isArrayLiteralExpression(declarationsProp.initializer)) {
20926
+ for (const el of declarationsProp.initializer.elements) {
20927
+ if (ts__default["default"].isIdentifier(el)) {
20928
+ const correspondingClass = findClassDeclaration(el, typeChecker);
20929
+ if (!correspondingClass ||
20930
+ // Check whether the declaration is either standalone already or is being converted
20931
+ // in this migration. We need to check if it's standalone already, in order to correct
20932
+ // some cases where the main app and the test files are being migrated in separate
20933
+ // programs.
20934
+ isStandaloneDeclaration(correspondingClass, allDeclarations, templateTypeChecker)) {
20935
+ declarationsToCopy.push(el);
20936
+ }
20937
+ else {
20938
+ declarationsToPreserve.push(el);
20939
+ }
20940
+ }
20941
+ else {
20942
+ declarationsToCopy.push(el);
20943
+ }
20967
20944
  }
20968
20945
  }
20946
+ else {
20947
+ // Otherwise create a spread that will be copied into the `imports`.
20948
+ declarationsToCopy.push(ts__default["default"].factory.createSpreadElement(declarationsProp.initializer));
20949
+ }
20950
+ }
20951
+ // If there are no `imports`, create them with the declarations we want to copy.
20952
+ if (!importsProp && declarationsToCopy.length > 0) {
20953
+ properties.push(ts__default["default"].factory.createPropertyAssignment('imports', ts__default["default"].factory.createArrayLiteralExpression(ts__default["default"].factory.createNodeArray(declarationsToCopy, hasAnyArrayTrailingComma && declarationsToCopy.length > 2))));
20969
20954
  }
20970
- // We can't remove classes that have any `declarations`, `providers` or `bootstrap` elements.
20971
- // Also err on the side of caution and don't remove modules where any of the aforementioned
20972
- // properties aren't initialized to an array literal.
20973
20955
  for (const prop of literal.properties) {
20974
- if (isNonEmptyNgModuleProperty(prop) &&
20975
- (prop.name.text === 'declarations' ||
20976
- prop.name.text === 'providers' ||
20977
- prop.name.text === 'bootstrap')) {
20978
- return false;
20956
+ if (!isNamedPropertyAssignment(prop)) {
20957
+ properties.push(prop);
20958
+ continue;
20959
+ }
20960
+ // If we have declarations to preserve, update the existing property, otherwise drop it.
20961
+ if (prop === declarationsProp) {
20962
+ if (declarationsToPreserve.length > 0) {
20963
+ const hasTrailingComma = ts__default["default"].isArrayLiteralExpression(prop.initializer)
20964
+ ? prop.initializer.elements.hasTrailingComma
20965
+ : hasAnyArrayTrailingComma;
20966
+ properties.push(ts__default["default"].factory.updatePropertyAssignment(prop, prop.name, ts__default["default"].factory.createArrayLiteralExpression(ts__default["default"].factory.createNodeArray(declarationsToPreserve, hasTrailingComma && declarationsToPreserve.length > 2))));
20967
+ }
20968
+ continue;
20969
+ }
20970
+ // If we have an `imports` array and declarations
20971
+ // that should be copied, we merge the two arrays.
20972
+ if (prop === importsProp && declarationsToCopy.length > 0) {
20973
+ let initializer;
20974
+ if (ts__default["default"].isArrayLiteralExpression(prop.initializer)) {
20975
+ initializer = ts__default["default"].factory.updateArrayLiteralExpression(prop.initializer, ts__default["default"].factory.createNodeArray([...prop.initializer.elements, ...declarationsToCopy], prop.initializer.elements.hasTrailingComma));
20976
+ }
20977
+ else {
20978
+ initializer = ts__default["default"].factory.createArrayLiteralExpression(ts__default["default"].factory.createNodeArray([ts__default["default"].factory.createSpreadElement(prop.initializer), ...declarationsToCopy],
20979
+ // Expect the declarations to be greater than 1 since
20980
+ // we have the pre-existing initializer already.
20981
+ hasAnyArrayTrailingComma && declarationsToCopy.length > 1));
20982
+ }
20983
+ properties.push(ts__default["default"].factory.updatePropertyAssignment(prop, prop.name, initializer));
20984
+ continue;
20979
20985
  }
20986
+ // Retain any remaining properties.
20987
+ properties.push(prop);
20980
20988
  }
20981
- return true;
20989
+ tracker.replaceNode(literal, ts__default["default"].factory.updateObjectLiteralExpression(literal, ts__default["default"].factory.createNodeArray(properties, literal.properties.hasTrailingComma)), ts__default["default"].EmitHint.Expression);
20990
+ }
20991
+ /** Adds `standalone: true` to a decorator node. */
20992
+ function addStandaloneToDecorator(node) {
20993
+ return addPropertyToAngularDecorator(node, ts__default["default"].factory.createPropertyAssignment('standalone', ts__default["default"].factory.createToken(ts__default["default"].SyntaxKind.TrueKeyword)));
20982
20994
  }
20983
20995
  /**
20984
- * Checks whether a node is a non-empty property from an NgModule's metadata. This is defined as a
20985
- * property assignment with a static name, initialized to an array literal with more than one
20986
- * element.
20987
- * @param node Node to be checked.
20996
+ * Adds a property to an Angular decorator node.
20997
+ * @param node Decorator to which to add the property.
20998
+ * @param property Property to add.
20988
20999
  */
20989
- function isNonEmptyNgModuleProperty(node) {
20990
- return (ts__default["default"].isPropertyAssignment(node) &&
20991
- ts__default["default"].isIdentifier(node.name) &&
20992
- ts__default["default"].isArrayLiteralExpression(node.initializer) &&
20993
- node.initializer.elements.length > 0);
21000
+ function addPropertyToAngularDecorator(node, property) {
21001
+ // Invalid decorator.
21002
+ if (!ts__default["default"].isCallExpression(node.expression) || node.expression.arguments.length > 1) {
21003
+ return node;
21004
+ }
21005
+ let literalProperties;
21006
+ let hasTrailingComma = false;
21007
+ if (node.expression.arguments.length === 0) {
21008
+ literalProperties = [property];
21009
+ }
21010
+ else if (ts__default["default"].isObjectLiteralExpression(node.expression.arguments[0])) {
21011
+ hasTrailingComma = node.expression.arguments[0].properties.hasTrailingComma;
21012
+ literalProperties = [...node.expression.arguments[0].properties, property];
21013
+ }
21014
+ else {
21015
+ // Unsupported case (e.g. `@Component(SOME_CONST)`). Return the original node.
21016
+ return node;
21017
+ }
21018
+ // Use `createDecorator` instead of `updateDecorator`, because
21019
+ // the latter ends up duplicating the node's leading comment.
21020
+ return ts__default["default"].factory.createDecorator(ts__default["default"].factory.createCallExpression(node.expression.expression, node.expression.typeArguments, [
21021
+ ts__default["default"].factory.createObjectLiteralExpression(ts__default["default"].factory.createNodeArray(literalProperties, hasTrailingComma), literalProperties.length > 1),
21022
+ ]));
21023
+ }
21024
+ /** Checks if a node is a `PropertyAssignment` with a name. */
21025
+ function isNamedPropertyAssignment(node) {
21026
+ return ts__default["default"].isPropertyAssignment(node) && node.name && ts__default["default"].isIdentifier(node.name);
20994
21027
  }
20995
21028
  /**
20996
- * Determines if a file is safe to delete. A file is safe to delete if all it contains are
20997
- * import statements, class declarations that are about to be deleted and non-exported code.
20998
- * @param sourceFile File that is being checked.
20999
- * @param nodesToBeRemoved Nodes that are being removed as a part of the migration.
21029
+ * Finds the import from which to bring in a template dependency of a component.
21030
+ * @param target Dependency that we're searching for.
21031
+ * @param inComponent Component in which the dependency is used.
21032
+ * @param importMode Mode in which to resolve the import target.
21033
+ * @param typeChecker
21000
21034
  */
21001
- function canRemoveFile(sourceFile, nodesToBeRemoved) {
21002
- for (const node of sourceFile.statements) {
21003
- if (ts__default["default"].isImportDeclaration(node) || nodesToBeRemoved.has(node)) {
21004
- continue;
21035
+ function findImportLocation(target, inComponent, importMode, typeChecker) {
21036
+ const importLocations = typeChecker.getPotentialImportsFor(target, inComponent, importMode);
21037
+ let firstSameFileImport = null;
21038
+ let firstModuleImport = null;
21039
+ for (const location of importLocations) {
21040
+ // Prefer a standalone import, if we can find one.
21041
+ // Otherwise fall back to the first module-based import.
21042
+ if (location.kind === compiler_host.PotentialImportKind.Standalone) {
21043
+ return location;
21005
21044
  }
21006
- if (ts__default["default"].isExportDeclaration(node) ||
21007
- (ts__default["default"].canHaveModifiers(node) &&
21008
- ts__default["default"].getModifiers(node)?.some((m) => m.kind === ts__default["default"].SyntaxKind.ExportKeyword))) {
21009
- return false;
21045
+ if (!location.moduleSpecifier && !firstSameFileImport) {
21046
+ firstSameFileImport = location;
21047
+ }
21048
+ if (location.kind === compiler_host.PotentialImportKind.NgModule &&
21049
+ !firstModuleImport &&
21050
+ // ɵ is used for some internal Angular modules that we want to skip over.
21051
+ !location.symbolName.startsWith('ɵ')) {
21052
+ firstModuleImport = location;
21010
21053
  }
21011
21054
  }
21012
- return true;
21055
+ return firstSameFileImport || firstModuleImport || importLocations[0] || null;
21013
21056
  }
21014
21057
  /**
21015
- * Gets whether an AST node contains another AST node.
21016
- * @param parent Parent node that may contain the child.
21017
- * @param child Child node that is being checked.
21058
+ * Checks whether a node is an `NgModule` metadata element with at least one element.
21059
+ * E.g. `declarations: [Foo]` or `declarations: SOME_VAR` would match this description,
21060
+ * but not `declarations: []`.
21018
21061
  */
21019
- function contains(parent, child) {
21020
- return (parent === child ||
21021
- (parent.getSourceFile().fileName === child.getSourceFile().fileName &&
21022
- child.getStart() >= parent.getStart() &&
21023
- child.getStart() <= parent.getEnd()));
21062
+ function hasNgModuleMetadataElements(node) {
21063
+ return (ts__default["default"].isPropertyAssignment(node) &&
21064
+ (!ts__default["default"].isArrayLiteralExpression(node.initializer) || node.initializer.elements.length > 0));
21024
21065
  }
21025
- /**
21026
- * Removes AST nodes from a node array.
21027
- * @param elements Array from which to remove the nodes.
21028
- * @param toRemove Nodes that should be removed.
21029
- */
21030
- function filterRemovedElements(elements, toRemove) {
21031
- return elements.filter((el) => {
21032
- for (const node of toRemove) {
21033
- // Check that the element contains the node, despite knowing with relative certainty that it
21034
- // does, because this allows us to unwrap some nodes. E.g. if we have `[((toRemove))]`, we
21035
- // want to remove the entire parenthesized expression, rather than just `toRemove`.
21036
- if (contains(el, node)) {
21037
- return false;
21066
+ /** Finds all modules whose declarations can be migrated. */
21067
+ function findNgModuleClassesToMigrate(sourceFile, typeChecker) {
21068
+ const modules = [];
21069
+ if (imports.getImportSpecifier(sourceFile, '@angular/core', 'NgModule')) {
21070
+ sourceFile.forEachChild(function walk(node) {
21071
+ if (ts__default["default"].isClassDeclaration(node)) {
21072
+ const decorator = nodes.getAngularDecorators(typeChecker, ts__default["default"].getDecorators(node) || []).find((current) => current.name === 'NgModule');
21073
+ const metadata = decorator ? extractMetadataLiteral(decorator.node) : null;
21074
+ if (metadata) {
21075
+ const declarations = findLiteralProperty(metadata, 'declarations');
21076
+ if (declarations != null && hasNgModuleMetadataElements(declarations)) {
21077
+ modules.push(node);
21078
+ }
21079
+ }
21038
21080
  }
21039
- }
21040
- return true;
21041
- });
21081
+ node.forEachChild(walk);
21082
+ });
21083
+ }
21084
+ return modules;
21042
21085
  }
21043
- /** Returns whether a node as an empty constructor. */
21044
- function isEmptyConstructor(node) {
21045
- return (ts__default["default"].isConstructorDeclaration(node) &&
21046
- node.parameters.length === 0 &&
21047
- (node.body == null || node.body.statements.length === 0));
21086
+ /** Finds all testing object literals that need to be migrated. */
21087
+ function findTestObjectsToMigrate(sourceFile, typeChecker) {
21088
+ const testObjects = [];
21089
+ const testBedImport = imports.getImportSpecifier(sourceFile, '@angular/core/testing', 'TestBed');
21090
+ const catalystImport = imports.getImportSpecifier(sourceFile, /testing\/catalyst$/, 'setupModule');
21091
+ if (testBedImport || catalystImport) {
21092
+ sourceFile.forEachChild(function walk(node) {
21093
+ const isObjectLiteralCall = ts__default["default"].isCallExpression(node) &&
21094
+ node.arguments.length > 0 &&
21095
+ // `arguments[0]` is the testing module config.
21096
+ ts__default["default"].isObjectLiteralExpression(node.arguments[0]);
21097
+ const config = isObjectLiteralCall ? node.arguments[0] : null;
21098
+ const isTestBedCall = isObjectLiteralCall &&
21099
+ testBedImport &&
21100
+ ts__default["default"].isPropertyAccessExpression(node.expression) &&
21101
+ node.expression.name.text === 'configureTestingModule' &&
21102
+ isReferenceToImport(typeChecker, node.expression.expression, testBedImport);
21103
+ const isCatalystCall = isObjectLiteralCall &&
21104
+ catalystImport &&
21105
+ ts__default["default"].isIdentifier(node.expression) &&
21106
+ isReferenceToImport(typeChecker, node.expression, catalystImport);
21107
+ if ((isTestBedCall || isCatalystCall) && config) {
21108
+ const declarations = findLiteralProperty(config, 'declarations');
21109
+ if (declarations &&
21110
+ ts__default["default"].isPropertyAssignment(declarations) &&
21111
+ ts__default["default"].isArrayLiteralExpression(declarations.initializer) &&
21112
+ declarations.initializer.elements.length > 0) {
21113
+ testObjects.push(config);
21114
+ }
21115
+ }
21116
+ node.forEachChild(walk);
21117
+ });
21118
+ }
21119
+ return testObjects;
21048
21120
  }
21049
21121
  /**
21050
- * Adds TODO comments to nodes that couldn't be removed manually.
21051
- * @param nodes Nodes to which to add the TODO.
21052
- * @param tracker Tracker in which to register the changes.
21122
+ * Finds the classes corresponding to dependencies used in a component's template.
21123
+ * @param decl Component in whose template we're looking for dependencies.
21124
+ * @param typeChecker
21053
21125
  */
21054
- function addRemovalTodos(nodes, tracker) {
21055
- for (const node of nodes) {
21056
- // Note: the comment is inserted using string manipulation, instead of going through the AST,
21057
- // because this way we preserve more of the app's original formatting.
21058
- // Note: in theory this can duplicate comments if the module pruning runs multiple times on
21059
- // the same node. In practice it is unlikely, because the second time the node won't be picked
21060
- // up by the language service as a reference, because the class won't exist anymore.
21061
- tracker.insertText(node.getSourceFile(), node.getFullStart(), ` /* TODO(standalone-migration): clean up removed NgModule reference manually. */ `);
21126
+ function findTemplateDependencies(decl, typeChecker) {
21127
+ const results = [];
21128
+ const usedDirectives = typeChecker.getUsedDirectives(decl);
21129
+ const usedPipes = typeChecker.getUsedPipes(decl);
21130
+ if (usedDirectives !== null) {
21131
+ for (const dir of usedDirectives) {
21132
+ if (ts__default["default"].isClassDeclaration(dir.ref.node)) {
21133
+ results.push(dir.ref);
21134
+ }
21135
+ }
21062
21136
  }
21063
- }
21064
- /** Finds the `NgModule` decorator in a class, if it exists. */
21065
- function findNgModuleDecorator(node, typeChecker) {
21066
- const decorators = nodes.getAngularDecorators(typeChecker, ts__default["default"].getDecorators(node) || []);
21067
- return decorators.find((decorator) => decorator.name === 'NgModule') || null;
21068
- }
21069
-
21070
- /** Checks whether a node is referring to a specific import specifier. */
21071
- function isReferenceToImport(typeChecker, node, importSpecifier) {
21072
- // If this function is called on an identifier (should be most cases), we can quickly rule out
21073
- // non-matches by comparing the identifier's string and the local name of the import specifier
21074
- // which saves us some calls to the type checker.
21075
- if (ts__default["default"].isIdentifier(node) && node.text !== importSpecifier.name.text) {
21076
- return false;
21137
+ if (usedPipes !== null) {
21138
+ const potentialPipes = typeChecker.getPotentialPipes(decl);
21139
+ for (const pipe of potentialPipes) {
21140
+ if (ts__default["default"].isClassDeclaration(pipe.ref.node) &&
21141
+ usedPipes.some((current) => pipe.name === current)) {
21142
+ results.push(pipe.ref);
21143
+ }
21144
+ }
21077
21145
  }
21078
- const nodeSymbol = typeChecker.getTypeAtLocation(node).getSymbol();
21079
- const importSymbol = typeChecker.getTypeAtLocation(importSpecifier).getSymbol();
21080
- return (!!(nodeSymbol?.declarations?.[0] && importSymbol?.declarations?.[0]) &&
21081
- nodeSymbol.declarations[0] === importSymbol.declarations[0]);
21146
+ return results;
21082
21147
  }
21083
-
21084
- /*!
21085
- * @license
21086
- * Copyright Google LLC All Rights Reserved.
21087
- *
21088
- * Use of this source code is governed by an MIT-style license that can be
21089
- * found in the LICENSE file at https://angular.io/license
21090
- */
21091
21148
  /**
21092
- * Converts all declarations in the specified files to standalone.
21093
- * @param sourceFiles Files that should be migrated.
21094
- * @param program
21095
- * @param printer
21096
- * @param fileImportRemapper Optional function that can be used to remap file-level imports.
21097
- * @param componentImportRemapper Optional function that can be used to remap component-level
21098
- * imports.
21149
+ * Removes any declarations that are a part of a module's `bootstrap`
21150
+ * array from an array of declarations.
21151
+ * @param declarations Anaalyzed declarations of the module.
21152
+ * @param ngModule Module whote declarations are being filtered.
21153
+ * @param templateTypeChecker
21154
+ * @param typeChecker
21099
21155
  */
21100
- function toStandalone(sourceFiles, program, printer, fileImportRemapper, componentImportRemapper) {
21101
- const templateTypeChecker = program.compiler.getTemplateTypeChecker();
21102
- const typeChecker = program.getTsProgram().getTypeChecker();
21103
- const modulesToMigrate = new Set();
21104
- const testObjectsToMigrate = new Set();
21105
- const declarations = new Set();
21106
- const tracker = new compiler_host.ChangeTracker(printer, fileImportRemapper);
21107
- for (const sourceFile of sourceFiles) {
21108
- const modules = findNgModuleClassesToMigrate(sourceFile, typeChecker);
21109
- const testObjects = findTestObjectsToMigrate(sourceFile, typeChecker);
21110
- for (const module of modules) {
21111
- const allModuleDeclarations = extractDeclarationsFromModule(module, templateTypeChecker);
21112
- const unbootstrappedDeclarations = filterNonBootstrappedDeclarations(allModuleDeclarations, module, templateTypeChecker, typeChecker);
21113
- if (unbootstrappedDeclarations.length > 0) {
21114
- modulesToMigrate.add(module);
21115
- unbootstrappedDeclarations.forEach((decl) => declarations.add(decl));
21116
- }
21117
- }
21118
- testObjects.forEach((obj) => testObjectsToMigrate.add(obj));
21156
+ function filterNonBootstrappedDeclarations(declarations, ngModule, templateTypeChecker, typeChecker) {
21157
+ const metadata = templateTypeChecker.getNgModuleMetadata(ngModule);
21158
+ const metaLiteral = metadata && metadata.decorator ? extractMetadataLiteral(metadata.decorator) : null;
21159
+ const bootstrapProp = metaLiteral ? findLiteralProperty(metaLiteral, 'bootstrap') : null;
21160
+ // If there's no `bootstrap`, we can't filter.
21161
+ if (!bootstrapProp) {
21162
+ return declarations;
21119
21163
  }
21120
- for (const declaration of declarations) {
21121
- convertNgModuleDeclarationToStandalone(declaration, declarations, tracker, templateTypeChecker, componentImportRemapper);
21164
+ // If we can't analyze the `bootstrap` property, we can't safely determine which
21165
+ // declarations aren't bootstrapped so we assume that all of them are.
21166
+ if (!ts__default["default"].isPropertyAssignment(bootstrapProp) ||
21167
+ !ts__default["default"].isArrayLiteralExpression(bootstrapProp.initializer)) {
21168
+ return [];
21122
21169
  }
21123
- for (const node of modulesToMigrate) {
21124
- migrateNgModuleClass(node, declarations, tracker, typeChecker, templateTypeChecker);
21170
+ const bootstrappedClasses = new Set();
21171
+ for (const el of bootstrapProp.initializer.elements) {
21172
+ const referencedClass = ts__default["default"].isIdentifier(el) ? findClassDeclaration(el, typeChecker) : null;
21173
+ // If we can resolve an element to a class, we can filter it out,
21174
+ // otherwise assume that the array isn't static.
21175
+ if (referencedClass) {
21176
+ bootstrappedClasses.add(referencedClass);
21177
+ }
21178
+ else {
21179
+ return [];
21180
+ }
21125
21181
  }
21126
- migrateTestDeclarations(testObjectsToMigrate, declarations, tracker, templateTypeChecker, typeChecker);
21127
- return tracker.recordChanges();
21182
+ return declarations.filter((ref) => !bootstrappedClasses.has(ref));
21128
21183
  }
21129
21184
  /**
21130
- * Converts a single declaration defined through an NgModule to standalone.
21131
- * @param decl Declaration being converted.
21132
- * @param tracker Tracker used to track the file changes.
21133
- * @param allDeclarations All the declarations that are being converted as a part of this migration.
21185
+ * Extracts all classes that are referenced in a module's `declarations` array.
21186
+ * @param ngModule Module whose declarations are being extraced.
21187
+ * @param templateTypeChecker
21188
+ */
21189
+ function extractDeclarationsFromModule(ngModule, templateTypeChecker) {
21190
+ const metadata = templateTypeChecker.getNgModuleMetadata(ngModule);
21191
+ return metadata
21192
+ ? metadata.declarations
21193
+ .filter((decl) => ts__default["default"].isClassDeclaration(decl.node))
21194
+ .map((decl) => decl.node)
21195
+ : [];
21196
+ }
21197
+ /**
21198
+ * Migrates the `declarations` from a unit test file to standalone.
21199
+ * @param testObjects Object literals used to configure the testing modules.
21200
+ * @param declarationsOutsideOfTestFiles Non-testing declarations that are part of this migration.
21201
+ * @param tracker
21202
+ * @param templateTypeChecker
21134
21203
  * @param typeChecker
21135
- * @param importRemapper
21136
21204
  */
21137
- function convertNgModuleDeclarationToStandalone(decl, allDeclarations, tracker, typeChecker, importRemapper) {
21138
- const directiveMeta = typeChecker.getDirectiveMetadata(decl);
21139
- if (directiveMeta && directiveMeta.decorator && !directiveMeta.isStandalone) {
21140
- let decorator = addStandaloneToDecorator(directiveMeta.decorator);
21141
- if (directiveMeta.isComponent) {
21142
- const importsToAdd = getComponentImportExpressions(decl, allDeclarations, tracker, typeChecker, importRemapper);
21143
- if (importsToAdd.length > 0) {
21144
- const hasTrailingComma = importsToAdd.length > 2 &&
21145
- !!extractMetadataLiteral(directiveMeta.decorator)?.properties.hasTrailingComma;
21146
- decorator = addPropertyToAngularDecorator(decorator, ts__default["default"].factory.createPropertyAssignment('imports', ts__default["default"].factory.createArrayLiteralExpression(
21147
- // Create a multi-line array when it has a trailing comma.
21148
- ts__default["default"].factory.createNodeArray(importsToAdd, hasTrailingComma), hasTrailingComma)));
21205
+ function migrateTestDeclarations(testObjects, declarationsOutsideOfTestFiles, tracker, templateTypeChecker, typeChecker) {
21206
+ const { decorators, componentImports } = analyzeTestingModules(testObjects, typeChecker);
21207
+ const allDeclarations = new Set(declarationsOutsideOfTestFiles);
21208
+ for (const decorator of decorators) {
21209
+ const closestClass = nodes.closestNode(decorator.node, ts__default["default"].isClassDeclaration);
21210
+ if (decorator.name === 'Pipe' || decorator.name === 'Directive') {
21211
+ tracker.replaceNode(decorator.node, addStandaloneToDecorator(decorator.node));
21212
+ if (closestClass) {
21213
+ allDeclarations.add(closestClass);
21149
21214
  }
21150
21215
  }
21151
- tracker.replaceNode(directiveMeta.decorator, decorator);
21152
- }
21153
- else {
21154
- const pipeMeta = typeChecker.getPipeMetadata(decl);
21155
- if (pipeMeta && pipeMeta.decorator && !pipeMeta.isStandalone) {
21156
- tracker.replaceNode(pipeMeta.decorator, addStandaloneToDecorator(pipeMeta.decorator));
21216
+ else if (decorator.name === 'Component') {
21217
+ const newDecorator = addStandaloneToDecorator(decorator.node);
21218
+ const importsToAdd = componentImports.get(decorator.node);
21219
+ if (closestClass) {
21220
+ allDeclarations.add(closestClass);
21221
+ }
21222
+ if (importsToAdd && importsToAdd.size > 0) {
21223
+ const hasTrailingComma = importsToAdd.size > 2 &&
21224
+ !!extractMetadataLiteral(decorator.node)?.properties.hasTrailingComma;
21225
+ const importsArray = ts__default["default"].factory.createNodeArray(Array.from(importsToAdd), hasTrailingComma);
21226
+ tracker.replaceNode(decorator.node, addPropertyToAngularDecorator(newDecorator, ts__default["default"].factory.createPropertyAssignment('imports', ts__default["default"].factory.createArrayLiteralExpression(importsArray))));
21227
+ }
21228
+ else {
21229
+ tracker.replaceNode(decorator.node, newDecorator);
21230
+ }
21157
21231
  }
21158
21232
  }
21233
+ for (const obj of testObjects) {
21234
+ moveDeclarationsToImports(obj, allDeclarations, typeChecker, templateTypeChecker, tracker);
21235
+ }
21159
21236
  }
21160
21237
  /**
21161
- * Gets the expressions that should be added to a component's
21162
- * `imports` array based on its template dependencies.
21163
- * @param decl Component class declaration.
21164
- * @param allDeclarations All the declarations that are being converted as a part of this migration.
21165
- * @param tracker
21166
- * @param typeChecker
21167
- * @param importRemapper
21238
+ * Analyzes a set of objects used to configure testing modules and returns the AST
21239
+ * nodes that need to be migrated and the imports that should be added to the imports
21240
+ * of any declared components.
21241
+ * @param testObjects Object literals that should be analyzed.
21168
21242
  */
21169
- function getComponentImportExpressions(decl, allDeclarations, tracker, typeChecker, importRemapper) {
21170
- const templateDependencies = findTemplateDependencies(decl, typeChecker);
21171
- const usedDependenciesInMigration = new Set(templateDependencies.filter((dep) => allDeclarations.has(dep.node)));
21172
- const imports = [];
21173
- const seenImports = new Set();
21174
- const resolvedDependencies = [];
21175
- for (const dep of templateDependencies) {
21176
- const importLocation = findImportLocation(dep, decl, usedDependenciesInMigration.has(dep)
21177
- ? compiler_host.PotentialImportMode.ForceDirect
21178
- : compiler_host.PotentialImportMode.Normal, typeChecker);
21179
- if (importLocation && !seenImports.has(importLocation.symbolName)) {
21180
- seenImports.add(importLocation.symbolName);
21181
- resolvedDependencies.push(importLocation);
21182
- }
21183
- }
21184
- const processedDependencies = importRemapper
21185
- ? importRemapper(resolvedDependencies, decl)
21186
- : resolvedDependencies;
21187
- for (const importLocation of processedDependencies) {
21188
- if (importLocation.moduleSpecifier) {
21189
- const identifier = tracker.addImport(decl.getSourceFile(), importLocation.symbolName, importLocation.moduleSpecifier);
21190
- imports.push(identifier);
21243
+ function analyzeTestingModules(testObjects, typeChecker) {
21244
+ const seenDeclarations = new Set();
21245
+ const decorators = [];
21246
+ const componentImports = new Map();
21247
+ for (const obj of testObjects) {
21248
+ const declarations = extractDeclarationsFromTestObject(obj, typeChecker);
21249
+ if (declarations.length === 0) {
21250
+ continue;
21191
21251
  }
21192
- else {
21193
- const identifier = ts__default["default"].factory.createIdentifier(importLocation.symbolName);
21194
- if (importLocation.isForwardReference) {
21195
- const forwardRefExpression = tracker.addImport(decl.getSourceFile(), 'forwardRef', '@angular/core');
21196
- const arrowFunction = ts__default["default"].factory.createArrowFunction(undefined, undefined, [], undefined, undefined, identifier);
21197
- imports.push(ts__default["default"].factory.createCallExpression(forwardRefExpression, undefined, [arrowFunction]));
21252
+ const importsProp = findLiteralProperty(obj, 'imports');
21253
+ const importElements = importsProp &&
21254
+ hasNgModuleMetadataElements(importsProp) &&
21255
+ ts__default["default"].isArrayLiteralExpression(importsProp.initializer)
21256
+ ? importsProp.initializer.elements.filter((el) => {
21257
+ // Filter out calls since they may be a `ModuleWithProviders`.
21258
+ return (!ts__default["default"].isCallExpression(el) &&
21259
+ // Also filter out the animations modules since they throw errors if they're imported
21260
+ // multiple times and it's common for apps to use the `NoopAnimationsModule` to
21261
+ // disable animations in screenshot tests.
21262
+ !isClassReferenceInAngularModule(el, /^BrowserAnimationsModule|NoopAnimationsModule$/, 'platform-browser/animations', typeChecker));
21263
+ })
21264
+ : null;
21265
+ for (const decl of declarations) {
21266
+ if (seenDeclarations.has(decl)) {
21267
+ continue;
21198
21268
  }
21199
- else {
21200
- imports.push(identifier);
21269
+ const [decorator] = nodes.getAngularDecorators(typeChecker, ts__default["default"].getDecorators(decl) || []);
21270
+ if (decorator) {
21271
+ seenDeclarations.add(decl);
21272
+ decorators.push(decorator);
21273
+ if (decorator.name === 'Component' && importElements) {
21274
+ // We try to de-duplicate the imports being added to a component, because it may be
21275
+ // declared in different testing modules with a different set of imports.
21276
+ let imports = componentImports.get(decorator.node);
21277
+ if (!imports) {
21278
+ imports = new Set();
21279
+ componentImports.set(decorator.node, imports);
21280
+ }
21281
+ importElements.forEach((imp) => imports.add(imp));
21282
+ }
21201
21283
  }
21202
21284
  }
21203
21285
  }
21204
- return imports;
21286
+ return { decorators, componentImports };
21205
21287
  }
21206
21288
  /**
21207
- * Moves all of the declarations of a class decorated with `@NgModule` to its imports.
21208
- * @param node Class being migrated.
21209
- * @param allDeclarations All the declarations that are being converted as a part of this migration.
21210
- * @param tracker
21289
+ * Finds the class declarations that are being referred
21290
+ * to in the `declarations` of an object literal.
21291
+ * @param obj Object literal that may contain the declarations.
21211
21292
  * @param typeChecker
21212
- * @param templateTypeChecker
21213
21293
  */
21214
- function migrateNgModuleClass(node, allDeclarations, tracker, typeChecker, templateTypeChecker) {
21215
- const decorator = templateTypeChecker.getNgModuleMetadata(node)?.decorator;
21216
- const metadata = decorator ? extractMetadataLiteral(decorator) : null;
21217
- if (metadata) {
21218
- moveDeclarationsToImports(metadata, allDeclarations, typeChecker, templateTypeChecker, tracker);
21294
+ function extractDeclarationsFromTestObject(obj, typeChecker) {
21295
+ const results = [];
21296
+ const declarations = findLiteralProperty(obj, 'declarations');
21297
+ if (declarations &&
21298
+ hasNgModuleMetadataElements(declarations) &&
21299
+ ts__default["default"].isArrayLiteralExpression(declarations.initializer)) {
21300
+ for (const element of declarations.initializer.elements) {
21301
+ const declaration = findClassDeclaration(element, typeChecker);
21302
+ // Note that we only migrate classes that are in the same file as the testing module,
21303
+ // because external fixture components are somewhat rare and handling them is going
21304
+ // to involve a lot of assumptions that are likely to be incorrect.
21305
+ if (declaration && declaration.getSourceFile().fileName === obj.getSourceFile().fileName) {
21306
+ results.push(declaration);
21307
+ }
21308
+ }
21219
21309
  }
21310
+ return results;
21311
+ }
21312
+ /** Extracts the metadata object literal from an Angular decorator. */
21313
+ function extractMetadataLiteral(decorator) {
21314
+ // `arguments[0]` is the metadata object literal.
21315
+ return ts__default["default"].isCallExpression(decorator.expression) &&
21316
+ decorator.expression.arguments.length === 1 &&
21317
+ ts__default["default"].isObjectLiteralExpression(decorator.expression.arguments[0])
21318
+ ? decorator.expression.arguments[0]
21319
+ : null;
21220
21320
  }
21221
21321
  /**
21222
- * Moves all the symbol references from the `declarations` array to the `imports`
21223
- * array of an `NgModule` class and removes the `declarations`.
21224
- * @param literal Object literal used to configure the module that should be migrated.
21225
- * @param allDeclarations All the declarations that are being converted as a part of this migration.
21226
- * @param typeChecker
21227
- * @param tracker
21322
+ * Checks whether a class is a standalone declaration.
21323
+ * @param node Class being checked.
21324
+ * @param declarationsInMigration Classes that are being converted to standalone in this migration.
21325
+ * @param templateTypeChecker
21228
21326
  */
21229
- function moveDeclarationsToImports(literal, allDeclarations, typeChecker, templateTypeChecker, tracker) {
21230
- const declarationsProp = findLiteralProperty(literal, 'declarations');
21231
- if (!declarationsProp) {
21232
- return;
21233
- }
21234
- const declarationsToPreserve = [];
21235
- const declarationsToCopy = [];
21236
- const properties = [];
21237
- const importsProp = findLiteralProperty(literal, 'imports');
21238
- const hasAnyArrayTrailingComma = literal.properties.some((prop) => ts__default["default"].isPropertyAssignment(prop) &&
21239
- ts__default["default"].isArrayLiteralExpression(prop.initializer) &&
21240
- prop.initializer.elements.hasTrailingComma);
21241
- // Separate the declarations that we want to keep and ones we need to copy into the `imports`.
21242
- if (ts__default["default"].isPropertyAssignment(declarationsProp)) {
21243
- // If the declarations are an array, we can analyze it to
21244
- // find any classes from the current migration.
21245
- if (ts__default["default"].isArrayLiteralExpression(declarationsProp.initializer)) {
21246
- for (const el of declarationsProp.initializer.elements) {
21247
- if (ts__default["default"].isIdentifier(el)) {
21248
- const correspondingClass = findClassDeclaration(el, typeChecker);
21249
- if (!correspondingClass ||
21250
- // Check whether the declaration is either standalone already or is being converted
21251
- // in this migration. We need to check if it's standalone already, in order to correct
21252
- // some cases where the main app and the test files are being migrated in separate
21253
- // programs.
21254
- isStandaloneDeclaration(correspondingClass, allDeclarations, templateTypeChecker)) {
21255
- declarationsToCopy.push(el);
21256
- }
21257
- else {
21258
- declarationsToPreserve.push(el);
21259
- }
21260
- }
21261
- else {
21262
- declarationsToCopy.push(el);
21263
- }
21264
- }
21265
- }
21266
- else {
21267
- // Otherwise create a spread that will be copied into the `imports`.
21268
- declarationsToCopy.push(ts__default["default"].factory.createSpreadElement(declarationsProp.initializer));
21269
- }
21270
- }
21271
- // If there are no `imports`, create them with the declarations we want to copy.
21272
- if (!importsProp && declarationsToCopy.length > 0) {
21273
- properties.push(ts__default["default"].factory.createPropertyAssignment('imports', ts__default["default"].factory.createArrayLiteralExpression(ts__default["default"].factory.createNodeArray(declarationsToCopy, hasAnyArrayTrailingComma && declarationsToCopy.length > 2))));
21327
+ function isStandaloneDeclaration(node, declarationsInMigration, templateTypeChecker) {
21328
+ if (declarationsInMigration.has(node)) {
21329
+ return true;
21274
21330
  }
21275
- for (const prop of literal.properties) {
21276
- if (!isNamedPropertyAssignment(prop)) {
21277
- properties.push(prop);
21278
- continue;
21331
+ const metadata = templateTypeChecker.getDirectiveMetadata(node) || templateTypeChecker.getPipeMetadata(node);
21332
+ return metadata != null && metadata.isStandalone;
21333
+ }
21334
+
21335
+ /*!
21336
+ * @license
21337
+ * Copyright Google LLC All Rights Reserved.
21338
+ *
21339
+ * Use of this source code is governed by an MIT-style license that can be
21340
+ * found in the LICENSE file at https://angular.io/license
21341
+ */
21342
+ function pruneNgModules(program, host, basePath, rootFileNames, sourceFiles, printer, importRemapper, referenceLookupExcludedFiles, componentImportRemapper) {
21343
+ const filesToRemove = new Set();
21344
+ const tracker = new compiler_host.ChangeTracker(printer, importRemapper);
21345
+ const tsProgram = program.getTsProgram();
21346
+ const typeChecker = tsProgram.getTypeChecker();
21347
+ const templateTypeChecker = program.compiler.getTemplateTypeChecker();
21348
+ const referenceResolver = new ReferenceResolver(program, host, rootFileNames, basePath, referenceLookupExcludedFiles);
21349
+ const removalLocations = {
21350
+ arrays: new UniqueItemTracker(),
21351
+ imports: new UniqueItemTracker(),
21352
+ exports: new UniqueItemTracker(),
21353
+ unknown: new Set(),
21354
+ };
21355
+ const classesToRemove = new Set();
21356
+ const barrelExports = new UniqueItemTracker();
21357
+ const componentImportArrays = new UniqueItemTracker();
21358
+ const nodesToRemove = new Set();
21359
+ sourceFiles.forEach(function walk(node) {
21360
+ if (ts__default["default"].isClassDeclaration(node) && canRemoveClass(node, typeChecker)) {
21361
+ collectChangeLocations(node, removalLocations, componentImportArrays, templateTypeChecker, referenceResolver, program);
21362
+ classesToRemove.add(node);
21279
21363
  }
21280
- // If we have declarations to preserve, update the existing property, otherwise drop it.
21281
- if (prop === declarationsProp) {
21282
- if (declarationsToPreserve.length > 0) {
21283
- const hasTrailingComma = ts__default["default"].isArrayLiteralExpression(prop.initializer)
21284
- ? prop.initializer.elements.hasTrailingComma
21285
- : hasAnyArrayTrailingComma;
21286
- properties.push(ts__default["default"].factory.updatePropertyAssignment(prop, prop.name, ts__default["default"].factory.createArrayLiteralExpression(ts__default["default"].factory.createNodeArray(declarationsToPreserve, hasTrailingComma && declarationsToPreserve.length > 2))));
21364
+ else if (ts__default["default"].isExportDeclaration(node) &&
21365
+ !node.exportClause &&
21366
+ node.moduleSpecifier &&
21367
+ ts__default["default"].isStringLiteralLike(node.moduleSpecifier) &&
21368
+ node.moduleSpecifier.text.startsWith('.')) {
21369
+ const exportedSourceFile = typeChecker
21370
+ .getSymbolAtLocation(node.moduleSpecifier)
21371
+ ?.valueDeclaration?.getSourceFile();
21372
+ if (exportedSourceFile) {
21373
+ barrelExports.track(exportedSourceFile, node);
21287
21374
  }
21288
- continue;
21289
21375
  }
21290
- // If we have an `imports` array and declarations
21291
- // that should be copied, we merge the two arrays.
21292
- if (prop === importsProp && declarationsToCopy.length > 0) {
21293
- let initializer;
21294
- if (ts__default["default"].isArrayLiteralExpression(prop.initializer)) {
21295
- initializer = ts__default["default"].factory.updateArrayLiteralExpression(prop.initializer, ts__default["default"].factory.createNodeArray([...prop.initializer.elements, ...declarationsToCopy], prop.initializer.elements.hasTrailingComma));
21376
+ node.forEachChild(walk);
21377
+ });
21378
+ replaceInImportsArray(componentImportArrays, classesToRemove, tracker, typeChecker, templateTypeChecker, componentImportRemapper);
21379
+ // We collect all the places where we need to remove references first before generating the
21380
+ // removal instructions since we may have to remove multiple references from one node.
21381
+ removeArrayReferences(removalLocations.arrays, tracker);
21382
+ removeImportReferences(removalLocations.imports, tracker);
21383
+ removeExportReferences(removalLocations.exports, tracker);
21384
+ addRemovalTodos(removalLocations.unknown, tracker);
21385
+ // Collect all the nodes to be removed before determining which files to delete since we need
21386
+ // to know it ahead of time when deleting barrel files that export other barrel files.
21387
+ (function trackNodesToRemove(nodes) {
21388
+ for (const node of nodes) {
21389
+ const sourceFile = node.getSourceFile();
21390
+ if (!filesToRemove.has(sourceFile) && canRemoveFile(sourceFile, nodes)) {
21391
+ const barrelExportsForFile = barrelExports.get(sourceFile);
21392
+ nodesToRemove.add(node);
21393
+ filesToRemove.add(sourceFile);
21394
+ barrelExportsForFile && trackNodesToRemove(barrelExportsForFile);
21296
21395
  }
21297
21396
  else {
21298
- initializer = ts__default["default"].factory.createArrayLiteralExpression(ts__default["default"].factory.createNodeArray([ts__default["default"].factory.createSpreadElement(prop.initializer), ...declarationsToCopy],
21299
- // Expect the declarations to be greater than 1 since
21300
- // we have the pre-existing initializer already.
21301
- hasAnyArrayTrailingComma && declarationsToCopy.length > 1));
21397
+ nodesToRemove.add(node);
21302
21398
  }
21303
- properties.push(ts__default["default"].factory.updatePropertyAssignment(prop, prop.name, initializer));
21304
- continue;
21305
21399
  }
21306
- // Retain any remaining properties.
21307
- properties.push(prop);
21400
+ })(classesToRemove);
21401
+ for (const node of nodesToRemove) {
21402
+ const sourceFile = node.getSourceFile();
21403
+ if (!filesToRemove.has(sourceFile) && canRemoveFile(sourceFile, nodesToRemove)) {
21404
+ filesToRemove.add(sourceFile);
21405
+ }
21406
+ else {
21407
+ tracker.removeNode(node);
21408
+ }
21308
21409
  }
21309
- tracker.replaceNode(literal, ts__default["default"].factory.updateObjectLiteralExpression(literal, ts__default["default"].factory.createNodeArray(properties, literal.properties.hasTrailingComma)), ts__default["default"].EmitHint.Expression);
21310
- }
21311
- /** Adds `standalone: true` to a decorator node. */
21312
- function addStandaloneToDecorator(node) {
21313
- return addPropertyToAngularDecorator(node, ts__default["default"].factory.createPropertyAssignment('standalone', ts__default["default"].factory.createToken(ts__default["default"].SyntaxKind.TrueKeyword)));
21410
+ return { pendingChanges: tracker.recordChanges(), filesToRemove };
21314
21411
  }
21315
21412
  /**
21316
- * Adds a property to an Angular decorator node.
21317
- * @param node Decorator to which to add the property.
21318
- * @param property Property to add.
21413
+ * Collects all the nodes that a module needs to be removed from.
21414
+ * @param ngModule Module being removed.
21415
+ * @param removalLocations Tracks the different places from which the class should be removed.
21416
+ * @param componentImportArrays Set of `imports` arrays of components that need to be adjusted.
21417
+ * @param referenceResolver
21418
+ * @param program
21319
21419
  */
21320
- function addPropertyToAngularDecorator(node, property) {
21321
- // Invalid decorator.
21322
- if (!ts__default["default"].isCallExpression(node.expression) || node.expression.arguments.length > 1) {
21323
- return node;
21324
- }
21325
- let literalProperties;
21326
- let hasTrailingComma = false;
21327
- if (node.expression.arguments.length === 0) {
21328
- literalProperties = [property];
21329
- }
21330
- else if (ts__default["default"].isObjectLiteralExpression(node.expression.arguments[0])) {
21331
- hasTrailingComma = node.expression.arguments[0].properties.hasTrailingComma;
21332
- literalProperties = [...node.expression.arguments[0].properties, property];
21420
+ function collectChangeLocations(ngModule, removalLocations, componentImportArrays, templateTypeChecker, referenceResolver, program) {
21421
+ const refsByFile = referenceResolver.findReferencesInProject(ngModule.name);
21422
+ const tsProgram = program.getTsProgram();
21423
+ const nodes$1 = new Set();
21424
+ for (const [fileName, refs] of refsByFile) {
21425
+ const sourceFile = tsProgram.getSourceFile(fileName);
21426
+ if (sourceFile) {
21427
+ offsetsToNodes(getNodeLookup(sourceFile), refs, nodes$1);
21428
+ }
21333
21429
  }
21334
- else {
21335
- // Unsupported case (e.g. `@Component(SOME_CONST)`). Return the original node.
21336
- return node;
21430
+ for (const node of nodes$1) {
21431
+ const closestArray = nodes.closestNode(node, ts__default["default"].isArrayLiteralExpression);
21432
+ if (closestArray) {
21433
+ const closestAssignment = nodes.closestNode(closestArray, ts__default["default"].isPropertyAssignment);
21434
+ // If the module was flagged as being removable, but it's still being used in a standalone
21435
+ // component's `imports` array, it means that it was likely changed outside of the migration
21436
+ // and deleting it now will be breaking. Track it separately so it can be handled properly.
21437
+ if (closestAssignment && isInImportsArray(closestAssignment, closestArray)) {
21438
+ const closestDecorator = nodes.closestNode(closestAssignment, ts__default["default"].isDecorator);
21439
+ const closestClass = closestDecorator
21440
+ ? nodes.closestNode(closestDecorator, ts__default["default"].isClassDeclaration)
21441
+ : null;
21442
+ const directiveMeta = closestClass
21443
+ ? templateTypeChecker.getDirectiveMetadata(closestClass)
21444
+ : null;
21445
+ if (directiveMeta && directiveMeta.isComponent && directiveMeta.isStandalone) {
21446
+ componentImportArrays.track(closestArray, node);
21447
+ continue;
21448
+ }
21449
+ }
21450
+ removalLocations.arrays.track(closestArray, node);
21451
+ continue;
21452
+ }
21453
+ const closestImport = nodes.closestNode(node, ts__default["default"].isNamedImports);
21454
+ if (closestImport) {
21455
+ removalLocations.imports.track(closestImport, node);
21456
+ continue;
21457
+ }
21458
+ const closestExport = nodes.closestNode(node, ts__default["default"].isNamedExports);
21459
+ if (closestExport) {
21460
+ removalLocations.exports.track(closestExport, node);
21461
+ continue;
21462
+ }
21463
+ removalLocations.unknown.add(node);
21337
21464
  }
21338
- // Use `createDecorator` instead of `updateDecorator`, because
21339
- // the latter ends up duplicating the node's leading comment.
21340
- return ts__default["default"].factory.createDecorator(ts__default["default"].factory.createCallExpression(node.expression.expression, node.expression.typeArguments, [
21341
- ts__default["default"].factory.createObjectLiteralExpression(ts__default["default"].factory.createNodeArray(literalProperties, hasTrailingComma), literalProperties.length > 1),
21342
- ]));
21343
- }
21344
- /** Checks if a node is a `PropertyAssignment` with a name. */
21345
- function isNamedPropertyAssignment(node) {
21346
- return ts__default["default"].isPropertyAssignment(node) && node.name && ts__default["default"].isIdentifier(node.name);
21347
21465
  }
21348
21466
  /**
21349
- * Finds the import from which to bring in a template dependency of a component.
21350
- * @param target Dependency that we're searching for.
21351
- * @param inComponent Component in which the dependency is used.
21352
- * @param importMode Mode in which to resolve the import target.
21467
+ * Replaces all the leftover modules in imports arrays with their exports.
21468
+ * @param componentImportArrays All the imports arrays and their nodes that represent NgModules.
21469
+ * @param classesToRemove Set of classes that were marked for removal.
21470
+ * @param tracker
21353
21471
  * @param typeChecker
21472
+ * @param templateTypeChecker
21473
+ * @param importRemapper
21354
21474
  */
21355
- function findImportLocation(target, inComponent, importMode, typeChecker) {
21356
- const importLocations = typeChecker.getPotentialImportsFor(target, inComponent, importMode);
21357
- let firstSameFileImport = null;
21358
- let firstModuleImport = null;
21359
- for (const location of importLocations) {
21360
- // Prefer a standalone import, if we can find one.
21361
- // Otherwise fall back to the first module-based import.
21362
- if (location.kind === compiler_host.PotentialImportKind.Standalone) {
21363
- return location;
21364
- }
21365
- if (!location.moduleSpecifier && !firstSameFileImport) {
21366
- firstSameFileImport = location;
21475
+ function replaceInImportsArray(componentImportArrays, classesToRemove, tracker, typeChecker, templateTypeChecker, importRemapper) {
21476
+ for (const [array, toReplace] of componentImportArrays.getEntries()) {
21477
+ const closestClass = nodes.closestNode(array, ts__default["default"].isClassDeclaration);
21478
+ if (!closestClass) {
21479
+ continue;
21367
21480
  }
21368
- if (location.kind === compiler_host.PotentialImportKind.NgModule &&
21369
- !firstModuleImport &&
21370
- // ɵ is used for some internal Angular modules that we want to skip over.
21371
- !location.symbolName.startsWith('ɵ')) {
21372
- firstModuleImport = location;
21481
+ const replacements = new UniqueItemTracker();
21482
+ const usedImports = new Set(findTemplateDependencies(closestClass, templateTypeChecker).map((ref) => ref.node));
21483
+ for (const node of toReplace) {
21484
+ const moduleDecl = findClassDeclaration(node, typeChecker);
21485
+ if (moduleDecl) {
21486
+ const moduleMeta = templateTypeChecker.getNgModuleMetadata(moduleDecl);
21487
+ if (moduleMeta) {
21488
+ moduleMeta.exports.forEach((exp) => {
21489
+ if (usedImports.has(exp.node)) {
21490
+ replacements.track(node, exp);
21491
+ }
21492
+ });
21493
+ }
21494
+ else {
21495
+ // It's unlikely not to have module metadata at this point, but just in
21496
+ // case unmark the class for removal to reduce the chance of breakages.
21497
+ classesToRemove.delete(moduleDecl);
21498
+ }
21499
+ }
21373
21500
  }
21501
+ replaceModulesInImportsArray(array, closestClass, replacements, tracker, templateTypeChecker, importRemapper);
21374
21502
  }
21375
- return firstSameFileImport || firstModuleImport || importLocations[0] || null;
21376
21503
  }
21377
21504
  /**
21378
- * Checks whether a node is an `NgModule` metadata element with at least one element.
21379
- * E.g. `declarations: [Foo]` or `declarations: SOME_VAR` would match this description,
21380
- * but not `declarations: []`.
21505
+ * Replaces any leftover modules in `imports` arrays with their exports that are used within a
21506
+ * component.
21507
+ * @param array Imports array which is being migrated.
21508
+ * @param componentClass Class that the imports array belongs to.
21509
+ * @param replacements Map of NgModule references to their exports.
21510
+ * @param tracker
21511
+ * @param templateTypeChecker
21512
+ * @param importRemapper
21381
21513
  */
21382
- function hasNgModuleMetadataElements(node) {
21383
- return (ts__default["default"].isPropertyAssignment(node) &&
21384
- (!ts__default["default"].isArrayLiteralExpression(node.initializer) || node.initializer.elements.length > 0));
21385
- }
21386
- /** Finds all modules whose declarations can be migrated. */
21387
- function findNgModuleClassesToMigrate(sourceFile, typeChecker) {
21388
- const modules = [];
21389
- if (nodes.getImportSpecifier(sourceFile, '@angular/core', 'NgModule')) {
21390
- sourceFile.forEachChild(function walk(node) {
21391
- if (ts__default["default"].isClassDeclaration(node)) {
21392
- const decorator = nodes.getAngularDecorators(typeChecker, ts__default["default"].getDecorators(node) || []).find((current) => current.name === 'NgModule');
21393
- const metadata = decorator ? extractMetadataLiteral(decorator.node) : null;
21394
- if (metadata) {
21395
- const declarations = findLiteralProperty(metadata, 'declarations');
21396
- if (declarations != null && hasNgModuleMetadataElements(declarations)) {
21397
- modules.push(node);
21398
- }
21399
- }
21514
+ function replaceModulesInImportsArray(array, componentClass, replacements, tracker, templateTypeChecker, importRemapper) {
21515
+ const newElements = [];
21516
+ for (const element of array.elements) {
21517
+ const replacementRefs = replacements.get(element);
21518
+ if (!replacementRefs) {
21519
+ newElements.push(element);
21520
+ continue;
21521
+ }
21522
+ const potentialImports = [];
21523
+ for (const ref of replacementRefs) {
21524
+ const importLocation = findImportLocation(ref, componentClass, compiler_host.PotentialImportMode.Normal, templateTypeChecker);
21525
+ if (importLocation) {
21526
+ potentialImports.push(importLocation);
21400
21527
  }
21401
- node.forEachChild(walk);
21402
- });
21528
+ }
21529
+ newElements.push(...potentialImportsToExpressions(potentialImports, componentClass, tracker, importRemapper));
21403
21530
  }
21404
- return modules;
21531
+ tracker.replaceNode(array, ts__default["default"].factory.updateArrayLiteralExpression(array, newElements));
21405
21532
  }
21406
- /** Finds all testing object literals that need to be migrated. */
21407
- function findTestObjectsToMigrate(sourceFile, typeChecker) {
21408
- const testObjects = [];
21409
- const testBedImport = nodes.getImportSpecifier(sourceFile, '@angular/core/testing', 'TestBed');
21410
- const catalystImport = nodes.getImportSpecifier(sourceFile, /testing\/catalyst$/, 'setupModule');
21411
- if (testBedImport || catalystImport) {
21412
- sourceFile.forEachChild(function walk(node) {
21413
- const isObjectLiteralCall = ts__default["default"].isCallExpression(node) &&
21414
- node.arguments.length > 0 &&
21415
- // `arguments[0]` is the testing module config.
21416
- ts__default["default"].isObjectLiteralExpression(node.arguments[0]);
21417
- const config = isObjectLiteralCall ? node.arguments[0] : null;
21418
- const isTestBedCall = isObjectLiteralCall &&
21419
- testBedImport &&
21420
- ts__default["default"].isPropertyAccessExpression(node.expression) &&
21421
- node.expression.name.text === 'configureTestingModule' &&
21422
- isReferenceToImport(typeChecker, node.expression.expression, testBedImport);
21423
- const isCatalystCall = isObjectLiteralCall &&
21424
- catalystImport &&
21425
- ts__default["default"].isIdentifier(node.expression) &&
21426
- isReferenceToImport(typeChecker, node.expression, catalystImport);
21427
- if ((isTestBedCall || isCatalystCall) && config) {
21428
- const declarations = findLiteralProperty(config, 'declarations');
21429
- if (declarations &&
21430
- ts__default["default"].isPropertyAssignment(declarations) &&
21431
- ts__default["default"].isArrayLiteralExpression(declarations.initializer) &&
21432
- declarations.initializer.elements.length > 0) {
21433
- testObjects.push(config);
21434
- }
21435
- }
21436
- node.forEachChild(walk);
21437
- });
21533
+ /**
21534
+ * Removes all tracked array references.
21535
+ * @param locations Locations from which to remove the references.
21536
+ * @param tracker Tracker in which to register the changes.
21537
+ */
21538
+ function removeArrayReferences(locations, tracker) {
21539
+ for (const [array, toRemove] of locations.getEntries()) {
21540
+ const newElements = filterRemovedElements(array.elements, toRemove);
21541
+ tracker.replaceNode(array, ts__default["default"].factory.updateArrayLiteralExpression(array, ts__default["default"].factory.createNodeArray(newElements, array.elements.hasTrailingComma)));
21438
21542
  }
21439
- return testObjects;
21440
21543
  }
21441
21544
  /**
21442
- * Finds the classes corresponding to dependencies used in a component's template.
21443
- * @param decl Component in whose template we're looking for dependencies.
21444
- * @param typeChecker
21545
+ * Removes all tracked import references.
21546
+ * @param locations Locations from which to remove the references.
21547
+ * @param tracker Tracker in which to register the changes.
21445
21548
  */
21446
- function findTemplateDependencies(decl, typeChecker) {
21447
- const results = [];
21448
- const usedDirectives = typeChecker.getUsedDirectives(decl);
21449
- const usedPipes = typeChecker.getUsedPipes(decl);
21450
- if (usedDirectives !== null) {
21451
- for (const dir of usedDirectives) {
21452
- if (ts__default["default"].isClassDeclaration(dir.ref.node)) {
21453
- results.push(dir.ref);
21549
+ function removeImportReferences(locations, tracker) {
21550
+ for (const [namedImports, toRemove] of locations.getEntries()) {
21551
+ const newElements = filterRemovedElements(namedImports.elements, toRemove);
21552
+ // If no imports are left, we can try to drop the entire import.
21553
+ if (newElements.length === 0) {
21554
+ const importClause = nodes.closestNode(namedImports, ts__default["default"].isImportClause);
21555
+ // If the import clause has a name we can only drop then named imports.
21556
+ // e.g. `import Foo, {ModuleToRemove} from './foo';` becomes `import Foo from './foo';`.
21557
+ if (importClause && importClause.name) {
21558
+ tracker.replaceNode(importClause, ts__default["default"].factory.updateImportClause(importClause, importClause.isTypeOnly, importClause.name, undefined));
21454
21559
  }
21455
- }
21456
- }
21457
- if (usedPipes !== null) {
21458
- const potentialPipes = typeChecker.getPotentialPipes(decl);
21459
- for (const pipe of potentialPipes) {
21460
- if (ts__default["default"].isClassDeclaration(pipe.ref.node) &&
21461
- usedPipes.some((current) => pipe.name === current)) {
21462
- results.push(pipe.ref);
21560
+ else {
21561
+ // Otherwise we can drop the entire declaration.
21562
+ const declaration = nodes.closestNode(namedImports, ts__default["default"].isImportDeclaration);
21563
+ if (declaration) {
21564
+ tracker.removeNode(declaration);
21565
+ }
21463
21566
  }
21464
21567
  }
21568
+ else {
21569
+ // Otherwise we just drop the imported symbols and keep the declaration intact.
21570
+ tracker.replaceNode(namedImports, ts__default["default"].factory.updateNamedImports(namedImports, newElements));
21571
+ }
21465
21572
  }
21466
- return results;
21467
21573
  }
21468
21574
  /**
21469
- * Removes any declarations that are a part of a module's `bootstrap`
21470
- * array from an array of declarations.
21471
- * @param declarations Anaalyzed declarations of the module.
21472
- * @param ngModule Module whote declarations are being filtered.
21473
- * @param templateTypeChecker
21474
- * @param typeChecker
21575
+ * Removes all tracked export references.
21576
+ * @param locations Locations from which to remove the references.
21577
+ * @param tracker Tracker in which to register the changes.
21475
21578
  */
21476
- function filterNonBootstrappedDeclarations(declarations, ngModule, templateTypeChecker, typeChecker) {
21477
- const metadata = templateTypeChecker.getNgModuleMetadata(ngModule);
21478
- const metaLiteral = metadata && metadata.decorator ? extractMetadataLiteral(metadata.decorator) : null;
21479
- const bootstrapProp = metaLiteral ? findLiteralProperty(metaLiteral, 'bootstrap') : null;
21480
- // If there's no `bootstrap`, we can't filter.
21481
- if (!bootstrapProp) {
21482
- return declarations;
21483
- }
21484
- // If we can't analyze the `bootstrap` property, we can't safely determine which
21485
- // declarations aren't bootstrapped so we assume that all of them are.
21486
- if (!ts__default["default"].isPropertyAssignment(bootstrapProp) ||
21487
- !ts__default["default"].isArrayLiteralExpression(bootstrapProp.initializer)) {
21488
- return [];
21489
- }
21490
- const bootstrappedClasses = new Set();
21491
- for (const el of bootstrapProp.initializer.elements) {
21492
- const referencedClass = ts__default["default"].isIdentifier(el) ? findClassDeclaration(el, typeChecker) : null;
21493
- // If we can resolve an element to a class, we can filter it out,
21494
- // otherwise assume that the array isn't static.
21495
- if (referencedClass) {
21496
- bootstrappedClasses.add(referencedClass);
21579
+ function removeExportReferences(locations, tracker) {
21580
+ for (const [namedExports, toRemove] of locations.getEntries()) {
21581
+ const newElements = filterRemovedElements(namedExports.elements, toRemove);
21582
+ // If no exports are left, we can drop the entire declaration.
21583
+ if (newElements.length === 0) {
21584
+ const declaration = nodes.closestNode(namedExports, ts__default["default"].isExportDeclaration);
21585
+ if (declaration) {
21586
+ tracker.removeNode(declaration);
21587
+ }
21497
21588
  }
21498
21589
  else {
21499
- return [];
21590
+ // Otherwise we just drop the exported symbols and keep the declaration intact.
21591
+ tracker.replaceNode(namedExports, ts__default["default"].factory.updateNamedExports(namedExports, newElements));
21500
21592
  }
21501
21593
  }
21502
- return declarations.filter((ref) => !bootstrappedClasses.has(ref));
21503
- }
21504
- /**
21505
- * Extracts all classes that are referenced in a module's `declarations` array.
21506
- * @param ngModule Module whose declarations are being extraced.
21507
- * @param templateTypeChecker
21508
- */
21509
- function extractDeclarationsFromModule(ngModule, templateTypeChecker) {
21510
- const metadata = templateTypeChecker.getNgModuleMetadata(ngModule);
21511
- return metadata
21512
- ? metadata.declarations
21513
- .filter((decl) => ts__default["default"].isClassDeclaration(decl.node))
21514
- .map((decl) => decl.node)
21515
- : [];
21516
21594
  }
21517
21595
  /**
21518
- * Migrates the `declarations` from a unit test file to standalone.
21519
- * @param testObjects Object literals used to configure the testing modules.
21520
- * @param declarationsOutsideOfTestFiles Non-testing declarations that are part of this migration.
21521
- * @param tracker
21522
- * @param templateTypeChecker
21596
+ * Determines whether an `@NgModule` class is safe to remove. A module is safe to remove if:
21597
+ * 1. It has no `declarations`.
21598
+ * 2. It has no `providers`.
21599
+ * 3. It has no `bootstrap` components.
21600
+ * 4. It has no `ModuleWithProviders` in its `imports`.
21601
+ * 5. It has no class members. Empty construstors are ignored.
21602
+ * @param node Class that is being checked.
21523
21603
  * @param typeChecker
21524
21604
  */
21525
- function migrateTestDeclarations(testObjects, declarationsOutsideOfTestFiles, tracker, templateTypeChecker, typeChecker) {
21526
- const { decorators, componentImports } = analyzeTestingModules(testObjects, typeChecker);
21527
- const allDeclarations = new Set(declarationsOutsideOfTestFiles);
21528
- for (const decorator of decorators) {
21529
- const closestClass = nodes.closestNode(decorator.node, ts__default["default"].isClassDeclaration);
21530
- if (decorator.name === 'Pipe' || decorator.name === 'Directive') {
21531
- tracker.replaceNode(decorator.node, addStandaloneToDecorator(decorator.node));
21532
- if (closestClass) {
21533
- allDeclarations.add(closestClass);
21534
- }
21535
- }
21536
- else if (decorator.name === 'Component') {
21537
- const newDecorator = addStandaloneToDecorator(decorator.node);
21538
- const importsToAdd = componentImports.get(decorator.node);
21539
- if (closestClass) {
21540
- allDeclarations.add(closestClass);
21541
- }
21542
- if (importsToAdd && importsToAdd.size > 0) {
21543
- const hasTrailingComma = importsToAdd.size > 2 &&
21544
- !!extractMetadataLiteral(decorator.node)?.properties.hasTrailingComma;
21545
- const importsArray = ts__default["default"].factory.createNodeArray(Array.from(importsToAdd), hasTrailingComma);
21546
- tracker.replaceNode(decorator.node, addPropertyToAngularDecorator(newDecorator, ts__default["default"].factory.createPropertyAssignment('imports', ts__default["default"].factory.createArrayLiteralExpression(importsArray))));
21605
+ function canRemoveClass(node, typeChecker) {
21606
+ const decorator = findNgModuleDecorator(node, typeChecker)?.node;
21607
+ // We can't remove a declaration if it's not a valid `NgModule`.
21608
+ if (!decorator || !ts__default["default"].isCallExpression(decorator.expression)) {
21609
+ return false;
21610
+ }
21611
+ // Unsupported case, e.g. `@NgModule(SOME_VALUE)`.
21612
+ if (decorator.expression.arguments.length > 0 &&
21613
+ !ts__default["default"].isObjectLiteralExpression(decorator.expression.arguments[0])) {
21614
+ return false;
21615
+ }
21616
+ // We can't remove modules that have class members. We make an exception for an
21617
+ // empty constructor which may have been generated by a tool and forgotten.
21618
+ if (node.members.length > 0 && node.members.some((member) => !isEmptyConstructor(member))) {
21619
+ return false;
21620
+ }
21621
+ // An empty `NgModule` call can be removed.
21622
+ if (decorator.expression.arguments.length === 0) {
21623
+ return true;
21624
+ }
21625
+ const literal = decorator.expression.arguments[0];
21626
+ const imports = findLiteralProperty(literal, 'imports');
21627
+ if (imports && isNonEmptyNgModuleProperty(imports)) {
21628
+ // We can't remove the class if at least one import isn't identifier, because it may be a
21629
+ // `ModuleWithProviders` which is the equivalent of having something in the `providers` array.
21630
+ for (const dep of imports.initializer.elements) {
21631
+ if (!ts__default["default"].isIdentifier(dep)) {
21632
+ return false;
21547
21633
  }
21548
- else {
21549
- tracker.replaceNode(decorator.node, newDecorator);
21634
+ const depDeclaration = findClassDeclaration(dep, typeChecker);
21635
+ const depNgModule = depDeclaration
21636
+ ? findNgModuleDecorator(depDeclaration, typeChecker)
21637
+ : null;
21638
+ // If any of the dependencies of the class is an `NgModule` that can't be removed, the class
21639
+ // itself can't be removed either, because it may be part of a transitive dependency chain.
21640
+ if (depDeclaration !== null &&
21641
+ depNgModule !== null &&
21642
+ !canRemoveClass(depDeclaration, typeChecker)) {
21643
+ return false;
21550
21644
  }
21551
21645
  }
21552
21646
  }
21553
- for (const obj of testObjects) {
21554
- moveDeclarationsToImports(obj, allDeclarations, typeChecker, templateTypeChecker, tracker);
21647
+ // We can't remove classes that have any `declarations`, `providers` or `bootstrap` elements.
21648
+ // Also err on the side of caution and don't remove modules where any of the aforementioned
21649
+ // properties aren't initialized to an array literal.
21650
+ for (const prop of literal.properties) {
21651
+ if (isNonEmptyNgModuleProperty(prop) &&
21652
+ (prop.name.text === 'declarations' ||
21653
+ prop.name.text === 'providers' ||
21654
+ prop.name.text === 'bootstrap')) {
21655
+ return false;
21656
+ }
21555
21657
  }
21658
+ return true;
21556
21659
  }
21557
21660
  /**
21558
- * Analyzes a set of objects used to configure testing modules and returns the AST
21559
- * nodes that need to be migrated and the imports that should be added to the imports
21560
- * of any declared components.
21561
- * @param testObjects Object literals that should be analyzed.
21661
+ * Checks whether a node is a non-empty property from an NgModule's metadata. This is defined as a
21662
+ * property assignment with a static name, initialized to an array literal with more than one
21663
+ * element.
21664
+ * @param node Node to be checked.
21562
21665
  */
21563
- function analyzeTestingModules(testObjects, typeChecker) {
21564
- const seenDeclarations = new Set();
21565
- const decorators = [];
21566
- const componentImports = new Map();
21567
- for (const obj of testObjects) {
21568
- const declarations = extractDeclarationsFromTestObject(obj, typeChecker);
21569
- if (declarations.length === 0) {
21666
+ function isNonEmptyNgModuleProperty(node) {
21667
+ return (ts__default["default"].isPropertyAssignment(node) &&
21668
+ ts__default["default"].isIdentifier(node.name) &&
21669
+ ts__default["default"].isArrayLiteralExpression(node.initializer) &&
21670
+ node.initializer.elements.length > 0);
21671
+ }
21672
+ /**
21673
+ * Determines if a file is safe to delete. A file is safe to delete if all it contains are
21674
+ * import statements, class declarations that are about to be deleted and non-exported code.
21675
+ * @param sourceFile File that is being checked.
21676
+ * @param nodesToBeRemoved Nodes that are being removed as a part of the migration.
21677
+ */
21678
+ function canRemoveFile(sourceFile, nodesToBeRemoved) {
21679
+ for (const node of sourceFile.statements) {
21680
+ if (ts__default["default"].isImportDeclaration(node) || nodesToBeRemoved.has(node)) {
21570
21681
  continue;
21571
21682
  }
21572
- const importsProp = findLiteralProperty(obj, 'imports');
21573
- const importElements = importsProp &&
21574
- hasNgModuleMetadataElements(importsProp) &&
21575
- ts__default["default"].isArrayLiteralExpression(importsProp.initializer)
21576
- ? importsProp.initializer.elements.filter((el) => {
21577
- // Filter out calls since they may be a `ModuleWithProviders`.
21578
- return (!ts__default["default"].isCallExpression(el) &&
21579
- // Also filter out the animations modules since they throw errors if they're imported
21580
- // multiple times and it's common for apps to use the `NoopAnimationsModule` to
21581
- // disable animations in screenshot tests.
21582
- !isClassReferenceInAngularModule(el, /^BrowserAnimationsModule|NoopAnimationsModule$/, 'platform-browser/animations', typeChecker));
21583
- })
21584
- : null;
21585
- for (const decl of declarations) {
21586
- if (seenDeclarations.has(decl)) {
21587
- continue;
21588
- }
21589
- const [decorator] = nodes.getAngularDecorators(typeChecker, ts__default["default"].getDecorators(decl) || []);
21590
- if (decorator) {
21591
- seenDeclarations.add(decl);
21592
- decorators.push(decorator);
21593
- if (decorator.name === 'Component' && importElements) {
21594
- // We try to de-duplicate the imports being added to a component, because it may be
21595
- // declared in different testing modules with a different set of imports.
21596
- let imports = componentImports.get(decorator.node);
21597
- if (!imports) {
21598
- imports = new Set();
21599
- componentImports.set(decorator.node, imports);
21600
- }
21601
- importElements.forEach((imp) => imports.add(imp));
21602
- }
21603
- }
21683
+ if (ts__default["default"].isExportDeclaration(node) ||
21684
+ (ts__default["default"].canHaveModifiers(node) &&
21685
+ ts__default["default"].getModifiers(node)?.some((m) => m.kind === ts__default["default"].SyntaxKind.ExportKeyword))) {
21686
+ return false;
21604
21687
  }
21605
21688
  }
21606
- return { decorators, componentImports };
21689
+ return true;
21607
21690
  }
21608
21691
  /**
21609
- * Finds the class declarations that are being referred
21610
- * to in the `declarations` of an object literal.
21611
- * @param obj Object literal that may contain the declarations.
21612
- * @param typeChecker
21692
+ * Gets whether an AST node contains another AST node.
21693
+ * @param parent Parent node that may contain the child.
21694
+ * @param child Child node that is being checked.
21613
21695
  */
21614
- function extractDeclarationsFromTestObject(obj, typeChecker) {
21615
- const results = [];
21616
- const declarations = findLiteralProperty(obj, 'declarations');
21617
- if (declarations &&
21618
- hasNgModuleMetadataElements(declarations) &&
21619
- ts__default["default"].isArrayLiteralExpression(declarations.initializer)) {
21620
- for (const element of declarations.initializer.elements) {
21621
- const declaration = findClassDeclaration(element, typeChecker);
21622
- // Note that we only migrate classes that are in the same file as the testing module,
21623
- // because external fixture components are somewhat rare and handling them is going
21624
- // to involve a lot of assumptions that are likely to be incorrect.
21625
- if (declaration && declaration.getSourceFile().fileName === obj.getSourceFile().fileName) {
21626
- results.push(declaration);
21696
+ function contains(parent, child) {
21697
+ return (parent === child ||
21698
+ (parent.getSourceFile().fileName === child.getSourceFile().fileName &&
21699
+ child.getStart() >= parent.getStart() &&
21700
+ child.getStart() <= parent.getEnd()));
21701
+ }
21702
+ /**
21703
+ * Removes AST nodes from a node array.
21704
+ * @param elements Array from which to remove the nodes.
21705
+ * @param toRemove Nodes that should be removed.
21706
+ */
21707
+ function filterRemovedElements(elements, toRemove) {
21708
+ return elements.filter((el) => {
21709
+ for (const node of toRemove) {
21710
+ // Check that the element contains the node, despite knowing with relative certainty that it
21711
+ // does, because this allows us to unwrap some nodes. E.g. if we have `[((toRemove))]`, we
21712
+ // want to remove the entire parenthesized expression, rather than just `toRemove`.
21713
+ if (contains(el, node)) {
21714
+ return false;
21627
21715
  }
21628
21716
  }
21629
- }
21630
- return results;
21717
+ return true;
21718
+ });
21631
21719
  }
21632
- /** Extracts the metadata object literal from an Angular decorator. */
21633
- function extractMetadataLiteral(decorator) {
21634
- // `arguments[0]` is the metadata object literal.
21635
- return ts__default["default"].isCallExpression(decorator.expression) &&
21636
- decorator.expression.arguments.length === 1 &&
21637
- ts__default["default"].isObjectLiteralExpression(decorator.expression.arguments[0])
21638
- ? decorator.expression.arguments[0]
21639
- : null;
21720
+ /** Returns whether a node as an empty constructor. */
21721
+ function isEmptyConstructor(node) {
21722
+ return (ts__default["default"].isConstructorDeclaration(node) &&
21723
+ node.parameters.length === 0 &&
21724
+ (node.body == null || node.body.statements.length === 0));
21640
21725
  }
21641
21726
  /**
21642
- * Checks whether a class is a standalone declaration.
21643
- * @param node Class being checked.
21644
- * @param declarationsInMigration Classes that are being converted to standalone in this migration.
21645
- * @param templateTypeChecker
21727
+ * Adds TODO comments to nodes that couldn't be removed manually.
21728
+ * @param nodes Nodes to which to add the TODO.
21729
+ * @param tracker Tracker in which to register the changes.
21646
21730
  */
21647
- function isStandaloneDeclaration(node, declarationsInMigration, templateTypeChecker) {
21648
- if (declarationsInMigration.has(node)) {
21649
- return true;
21731
+ function addRemovalTodos(nodes, tracker) {
21732
+ for (const node of nodes) {
21733
+ // Note: the comment is inserted using string manipulation, instead of going through the AST,
21734
+ // because this way we preserve more of the app's original formatting.
21735
+ // Note: in theory this can duplicate comments if the module pruning runs multiple times on
21736
+ // the same node. In practice it is unlikely, because the second time the node won't be picked
21737
+ // up by the language service as a reference, because the class won't exist anymore.
21738
+ tracker.insertText(node.getSourceFile(), node.getFullStart(), ` /* TODO(standalone-migration): clean up removed NgModule reference manually. */ `);
21650
21739
  }
21651
- const metadata = templateTypeChecker.getDirectiveMetadata(node) || templateTypeChecker.getPipeMetadata(node);
21652
- return metadata != null && metadata.isStandalone;
21740
+ }
21741
+ /** Finds the `NgModule` decorator in a class, if it exists. */
21742
+ function findNgModuleDecorator(node, typeChecker) {
21743
+ const decorators = nodes.getAngularDecorators(typeChecker, ts__default["default"].getDecorators(node) || []);
21744
+ return decorators.find((decorator) => decorator.name === 'NgModule') || null;
21745
+ }
21746
+ /**
21747
+ * Checks whether a node is used inside of an `imports` array.
21748
+ * @param closestAssignment The closest property assignment to the node.
21749
+ * @param closestArray The closest array to the node.
21750
+ */
21751
+ function isInImportsArray(closestAssignment, closestArray) {
21752
+ return (closestAssignment.initializer === closestArray &&
21753
+ (ts__default["default"].isIdentifier(closestAssignment.name) || ts__default["default"].isStringLiteralLike(closestAssignment.name)) &&
21754
+ closestAssignment.name.text === 'imports');
21653
21755
  }
21654
21756
 
21655
21757
  /*!
@@ -22297,7 +22399,7 @@ function standaloneMigration(tree, tsconfigPath, basePath, pathToMigrate, schema
22297
22399
  let pendingChanges;
22298
22400
  let filesToRemove = null;
22299
22401
  if (schematicOptions.mode === MigrationMode.pruneModules) {
22300
- const result = pruneNgModules(program, host, basePath, rootNames, sourceFiles, printer, undefined, referenceLookupExcludedFiles);
22402
+ const result = pruneNgModules(program, host, basePath, rootNames, sourceFiles, printer, undefined, referenceLookupExcludedFiles, knownInternalAliasRemapper);
22301
22403
  pendingChanges = result.pendingChanges;
22302
22404
  filesToRemove = result.filesToRemove;
22303
22405
  }