@angular/core
Version:
Angular - the core framework
1,065 lines (1,059 loc) • 100 kB
JavaScript
'use strict';
/**
* @license Angular v19.2.10
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var schematics = require('@angular-devkit/schematics');
var index = require('./index-BF06LaCS.js');
var fs = require('fs');
var p = require('path');
var ts = require('typescript');
var compiler_host = require('./compiler_host-BmQrIxJT.js');
var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.js');
var ng_decorators = require('./ng_decorators-DznZ5jMl.js');
var nodes = require('./nodes-B16H9JUd.js');
var imports = require('./imports-CIX-JgAN.js');
var checker = require('./checker-CGGdizaF.js');
require('os');
require('@angular-devkit/core');
require('module');
require('url');
function createProgram({ rootNames, options, host, oldProgram, }) {
return new index.NgtscProgram(rootNames, options, host, oldProgram);
}
/** Checks whether a node is referring to a specific import specifier. */
function isReferenceToImport(typeChecker, node, importSpecifier) {
// If this function is called on an identifier (should be most cases), we can quickly rule out
// non-matches by comparing the identifier's string and the local name of the import specifier
// which saves us some calls to the type checker.
if (ts.isIdentifier(node) && node.text !== importSpecifier.name.text) {
return false;
}
const nodeSymbol = typeChecker.getTypeAtLocation(node).getSymbol();
const importSymbol = typeChecker.getTypeAtLocation(importSpecifier).getSymbol();
return (!!(nodeSymbol?.declarations?.[0] && importSymbol?.declarations?.[0]) &&
nodeSymbol.declarations[0] === importSymbol.declarations[0]);
}
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/** Utility class used to track a one-to-many relationship where all the items are unique. */
class UniqueItemTracker {
_nodes = new Map();
track(key, item) {
const set = this._nodes.get(key);
if (set) {
set.add(item);
}
else {
this._nodes.set(key, new Set([item]));
}
}
get(key) {
return this._nodes.get(key);
}
getEntries() {
return this._nodes.entries();
}
isEmpty() {
return this._nodes.size === 0;
}
}
/** Resolves references to nodes. */
class ReferenceResolver {
_program;
_host;
_rootFileNames;
_basePath;
_excludedFiles;
_languageService;
/**
* If set, allows the language service to *only* read a specific file.
* Used to speed up single-file lookups.
*/
_tempOnlyFile = null;
constructor(_program, _host, _rootFileNames, _basePath, _excludedFiles) {
this._program = _program;
this._host = _host;
this._rootFileNames = _rootFileNames;
this._basePath = _basePath;
this._excludedFiles = _excludedFiles;
}
/** Finds all references to a node within the entire project. */
findReferencesInProject(node) {
const languageService = this._getLanguageService();
const fileName = node.getSourceFile().fileName;
const start = node.getStart();
let referencedSymbols;
// The language service can throw if it fails to read a file.
// Silently continue since we're making the lookup on a best effort basis.
try {
referencedSymbols = languageService.findReferences(fileName, start) || [];
}
catch (e) {
console.error('Failed reference lookup for node ' + node.getText(), e.message);
referencedSymbols = [];
}
const results = new Map();
for (const symbol of referencedSymbols) {
for (const ref of symbol.references) {
if (!ref.isDefinition || symbol.definition.kind === ts.ScriptElementKind.alias) {
if (!results.has(ref.fileName)) {
results.set(ref.fileName, []);
}
results
.get(ref.fileName)
.push([ref.textSpan.start, ref.textSpan.start + ref.textSpan.length]);
}
}
}
return results;
}
/** Finds all references to a node within a single file. */
findSameFileReferences(node, fileName) {
// Even though we're only passing in a single file into `getDocumentHighlights`, the language
// service ends up traversing the entire project. Prevent it from reading any files aside from
// the one we're interested in by intercepting it at the compiler host level.
// This is an order of magnitude faster on a large project.
this._tempOnlyFile = fileName;
const nodeStart = node.getStart();
const results = [];
let highlights;
// The language service can throw if it fails to read a file.
// Silently continue since we're making the lookup on a best effort basis.
try {
highlights = this._getLanguageService().getDocumentHighlights(fileName, nodeStart, [
fileName,
]);
}
catch (e) {
console.error('Failed reference lookup for node ' + node.getText(), e.message);
}
if (highlights) {
for (const file of highlights) {
// We are pretty much guaranteed to only have one match from the current file since it is
// the only one being passed in `getDocumentHighlight`, but we check here just in case.
if (file.fileName === fileName) {
for (const { textSpan: { start, length }, kind, } of file.highlightSpans) {
if (kind !== ts.HighlightSpanKind.none) {
results.push([start, start + length]);
}
}
}
}
}
// Restore full project access to the language service.
this._tempOnlyFile = null;
return results;
}
/** Used by the language service */
_readFile(path) {
if ((this._tempOnlyFile !== null && path !== this._tempOnlyFile) ||
this._excludedFiles?.test(path)) {
return '';
}
return this._host.readFile(path);
}
/** Gets a language service that can be used to perform lookups. */
_getLanguageService() {
if (!this._languageService) {
const rootFileNames = this._rootFileNames.slice();
this._program
.getTsProgram()
.getSourceFiles()
.forEach(({ fileName }) => {
if (!this._excludedFiles?.test(fileName) && !rootFileNames.includes(fileName)) {
rootFileNames.push(fileName);
}
});
this._languageService = ts.createLanguageService({
getCompilationSettings: () => this._program.getTsProgram().getCompilerOptions(),
getScriptFileNames: () => rootFileNames,
// The files won't change so we can return the same version.
getScriptVersion: () => '0',
getScriptSnapshot: (path) => {
const content = this._readFile(path);
return content ? ts.ScriptSnapshot.fromString(content) : undefined;
},
getCurrentDirectory: () => this._basePath,
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
readFile: (path) => this._readFile(path),
fileExists: (path) => this._host.fileExists(path),
}, ts.createDocumentRegistry(), ts.LanguageServiceMode.PartialSemantic);
}
return this._languageService;
}
}
/** Creates a NodeLookup object from a source file. */
function getNodeLookup(sourceFile) {
const lookup = new Map();
sourceFile.forEachChild(function walk(node) {
const nodesAtStart = lookup.get(node.getStart());
if (nodesAtStart) {
nodesAtStart.push(node);
}
else {
lookup.set(node.getStart(), [node]);
}
node.forEachChild(walk);
});
return lookup;
}
/**
* Converts node offsets to the nodes they correspond to.
* @param lookup Data structure used to look up nodes at particular positions.
* @param offsets Offsets of the nodes.
* @param results Set in which to store the results.
*/
function offsetsToNodes(lookup, offsets, results) {
for (const [start, end] of offsets) {
const match = lookup.get(start)?.find((node) => node.getEnd() === end);
if (match) {
results.add(match);
}
}
return results;
}
/**
* Finds the class declaration that is being referred to by a node.
* @param reference Node referring to a class declaration.
* @param typeChecker
*/
function findClassDeclaration(reference, typeChecker) {
return (typeChecker
.getTypeAtLocation(reference)
.getSymbol()
?.declarations?.find(ts.isClassDeclaration) || null);
}
/** Finds a property with a specific name in an object literal expression. */
function findLiteralProperty(literal, name) {
return literal.properties.find((prop) => prop.name && ts.isIdentifier(prop.name) && prop.name.text === name);
}
/** Gets a relative path between two files that can be used inside a TypeScript import. */
function getRelativeImportPath(fromFile, toFile) {
let path = p.relative(p.dirname(fromFile), toFile).replace(/\.ts$/, '');
// `relative` returns paths inside the same directory without `./`
if (!path.startsWith('.')) {
path = './' + path;
}
// Using the Node utilities can yield paths with forward slashes on Windows.
return compiler_host.normalizePath(path);
}
/** Function used to remap the generated `imports` for a component to known shorter aliases. */
function knownInternalAliasRemapper(imports) {
return imports.map((current) => current.moduleSpecifier === '@angular/common' && current.symbolName === 'NgForOf'
? { ...current, symbolName: 'NgFor' }
: current);
}
/**
* Gets the closest node that matches a predicate, including the node that the search started from.
* @param node Node from which to start the search.
* @param predicate Predicate that the result needs to pass.
*/
function closestOrSelf(node, predicate) {
return predicate(node) ? node : nodes.closestNode(node, predicate);
}
/**
* Checks whether a node is referring to a specific class declaration.
* @param node Node that is being checked.
* @param className Name of the class that the node might be referring to.
* @param moduleName Name of the Angular module that should contain the class.
* @param typeChecker
*/
function isClassReferenceInAngularModule(node, className, moduleName, typeChecker) {
const symbol = typeChecker.getTypeAtLocation(node).getSymbol();
const externalName = `@angular/${moduleName}`;
const internalName = `angular2/rc/packages/${moduleName}`;
return !!symbol?.declarations?.some((decl) => {
const closestClass = closestOrSelf(decl, ts.isClassDeclaration);
const closestClassFileName = closestClass?.getSourceFile().fileName;
if (!closestClass ||
!closestClassFileName ||
!closestClass.name ||
!ts.isIdentifier(closestClass.name) ||
(!closestClassFileName.includes(externalName) && !closestClassFileName.includes(internalName))) {
return false;
}
return typeof className === 'string'
? closestClass.name.text === className
: className.test(closestClass.name.text);
});
}
/**
* Finds the imports of testing libraries in a file.
*/
function getTestingImports(sourceFile) {
return {
testBed: imports.getImportSpecifier(sourceFile, '@angular/core/testing', 'TestBed'),
catalyst: imports.getImportSpecifier(sourceFile, /testing\/catalyst(\/(fake_)?async)?$/, 'setupModule'),
};
}
/**
* Determines if a node is a call to a testing API.
* @param typeChecker Type checker to use when resolving references.
* @param node Node to check.
* @param testBedImport Import of TestBed within the file.
* @param catalystImport Import of Catalyst within the file.
*/
function isTestCall(typeChecker, node, testBedImport, catalystImport) {
const isObjectLiteralCall = ts.isCallExpression(node) &&
node.arguments.length > 0 &&
// `arguments[0]` is the testing module config.
ts.isObjectLiteralExpression(node.arguments[0]);
const isTestBedCall = isObjectLiteralCall &&
testBedImport &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'configureTestingModule' &&
isReferenceToImport(typeChecker, node.expression.expression, testBedImport);
const isCatalystCall = isObjectLiteralCall &&
catalystImport &&
ts.isIdentifier(node.expression) &&
isReferenceToImport(typeChecker, node.expression, catalystImport);
return !!(isTestBedCall || isCatalystCall);
}
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* Converts all declarations in the specified files to standalone.
* @param sourceFiles Files that should be migrated.
* @param program
* @param printer
* @param fileImportRemapper Optional function that can be used to remap file-level imports.
* @param declarationImportRemapper Optional function that can be used to remap declaration-level
* imports.
*/
function toStandalone(sourceFiles, program, printer, fileImportRemapper, declarationImportRemapper) {
const templateTypeChecker = program.compiler.getTemplateTypeChecker();
const typeChecker = program.getTsProgram().getTypeChecker();
const modulesToMigrate = new Set();
const testObjectsToMigrate = new Set();
const declarations = new Set();
const tracker = new compiler_host.ChangeTracker(printer, fileImportRemapper);
for (const sourceFile of sourceFiles) {
const modules = findNgModuleClassesToMigrate(sourceFile, typeChecker);
const testObjects = findTestObjectsToMigrate(sourceFile, typeChecker);
for (const module of modules) {
const allModuleDeclarations = extractDeclarationsFromModule(module, templateTypeChecker);
const unbootstrappedDeclarations = filterNonBootstrappedDeclarations(allModuleDeclarations, module, templateTypeChecker, typeChecker);
if (unbootstrappedDeclarations.length > 0) {
modulesToMigrate.add(module);
unbootstrappedDeclarations.forEach((decl) => declarations.add(decl));
}
}
testObjects.forEach((obj) => testObjectsToMigrate.add(obj));
}
for (const declaration of declarations) {
convertNgModuleDeclarationToStandalone(declaration, declarations, tracker, templateTypeChecker, declarationImportRemapper);
}
for (const node of modulesToMigrate) {
migrateNgModuleClass(node, declarations, tracker, typeChecker, templateTypeChecker);
}
migrateTestDeclarations(testObjectsToMigrate, declarations, tracker, templateTypeChecker, typeChecker);
return tracker.recordChanges();
}
/**
* Converts a single declaration defined through an NgModule to standalone.
* @param decl Declaration being converted.
* @param tracker Tracker used to track the file changes.
* @param allDeclarations All the declarations that are being converted as a part of this migration.
* @param typeChecker
* @param importRemapper
*/
function convertNgModuleDeclarationToStandalone(decl, allDeclarations, tracker, typeChecker, importRemapper) {
const directiveMeta = typeChecker.getDirectiveMetadata(decl);
if (directiveMeta && directiveMeta.decorator && !directiveMeta.isStandalone) {
let decorator = markDecoratorAsStandalone(directiveMeta.decorator);
if (directiveMeta.isComponent) {
const importsToAdd = getComponentImportExpressions(decl, allDeclarations, tracker, typeChecker, importRemapper);
if (importsToAdd.length > 0) {
const hasTrailingComma = importsToAdd.length > 2 &&
!!extractMetadataLiteral(directiveMeta.decorator)?.properties.hasTrailingComma;
decorator = setPropertyOnAngularDecorator(decorator, 'imports', ts.factory.createArrayLiteralExpression(
// Create a multi-line array when it has a trailing comma.
ts.factory.createNodeArray(importsToAdd, hasTrailingComma), hasTrailingComma));
}
}
tracker.replaceNode(directiveMeta.decorator, decorator);
}
else {
const pipeMeta = typeChecker.getPipeMetadata(decl);
if (pipeMeta && pipeMeta.decorator && !pipeMeta.isStandalone) {
tracker.replaceNode(pipeMeta.decorator, markDecoratorAsStandalone(pipeMeta.decorator));
}
}
}
/**
* Gets the expressions that should be added to a component's
* `imports` array based on its template dependencies.
* @param decl Component class declaration.
* @param allDeclarations All the declarations that are being converted as a part of this migration.
* @param tracker
* @param typeChecker
* @param importRemapper
*/
function getComponentImportExpressions(decl, allDeclarations, tracker, typeChecker, importRemapper) {
const templateDependencies = findTemplateDependencies(decl, typeChecker);
const usedDependenciesInMigration = new Set(templateDependencies.filter((dep) => allDeclarations.has(dep.node)));
const seenImports = new Set();
const resolvedDependencies = [];
for (const dep of templateDependencies) {
const importLocation = findImportLocation(dep, decl, usedDependenciesInMigration.has(dep)
? checker.PotentialImportMode.ForceDirect
: checker.PotentialImportMode.Normal, typeChecker);
if (importLocation && !seenImports.has(importLocation.symbolName)) {
seenImports.add(importLocation.symbolName);
resolvedDependencies.push(importLocation);
}
}
return potentialImportsToExpressions(resolvedDependencies, decl.getSourceFile(), tracker, importRemapper);
}
/**
* Converts an array of potential imports to an array of expressions that can be
* added to the `imports` array.
* @param potentialImports Imports to be converted.
* @param component Component class to which the imports will be added.
* @param tracker
* @param importRemapper
*/
function potentialImportsToExpressions(potentialImports, toFile, tracker, importRemapper) {
const processedDependencies = importRemapper
? importRemapper(potentialImports)
: potentialImports;
return processedDependencies.map((importLocation) => {
if (importLocation.moduleSpecifier) {
return tracker.addImport(toFile, importLocation.symbolName, importLocation.moduleSpecifier);
}
const identifier = ts.factory.createIdentifier(importLocation.symbolName);
if (!importLocation.isForwardReference) {
return identifier;
}
const forwardRefExpression = tracker.addImport(toFile, 'forwardRef', '@angular/core');
const arrowFunction = ts.factory.createArrowFunction(undefined, undefined, [], undefined, undefined, identifier);
return ts.factory.createCallExpression(forwardRefExpression, undefined, [arrowFunction]);
});
}
/**
* Moves all of the declarations of a class decorated with `@NgModule` to its imports.
* @param node Class being migrated.
* @param allDeclarations All the declarations that are being converted as a part of this migration.
* @param tracker
* @param typeChecker
* @param templateTypeChecker
*/
function migrateNgModuleClass(node, allDeclarations, tracker, typeChecker, templateTypeChecker) {
const decorator = templateTypeChecker.getNgModuleMetadata(node)?.decorator;
const metadata = decorator ? extractMetadataLiteral(decorator) : null;
if (metadata) {
moveDeclarationsToImports(metadata, allDeclarations, typeChecker, templateTypeChecker, tracker);
}
}
/**
* Moves all the symbol references from the `declarations` array to the `imports`
* array of an `NgModule` class and removes the `declarations`.
* @param literal Object literal used to configure the module that should be migrated.
* @param allDeclarations All the declarations that are being converted as a part of this migration.
* @param typeChecker
* @param tracker
*/
function moveDeclarationsToImports(literal, allDeclarations, typeChecker, templateTypeChecker, tracker) {
const declarationsProp = findLiteralProperty(literal, 'declarations');
if (!declarationsProp) {
return;
}
const declarationsToPreserve = [];
const declarationsToCopy = [];
const properties = [];
const importsProp = findLiteralProperty(literal, 'imports');
const hasAnyArrayTrailingComma = literal.properties.some((prop) => ts.isPropertyAssignment(prop) &&
ts.isArrayLiteralExpression(prop.initializer) &&
prop.initializer.elements.hasTrailingComma);
// Separate the declarations that we want to keep and ones we need to copy into the `imports`.
if (ts.isPropertyAssignment(declarationsProp)) {
// If the declarations are an array, we can analyze it to
// find any classes from the current migration.
if (ts.isArrayLiteralExpression(declarationsProp.initializer)) {
for (const el of declarationsProp.initializer.elements) {
if (ts.isIdentifier(el)) {
const correspondingClass = findClassDeclaration(el, typeChecker);
if (!correspondingClass ||
// Check whether the declaration is either standalone already or is being converted
// in this migration. We need to check if it's standalone already, in order to correct
// some cases where the main app and the test files are being migrated in separate
// programs.
isStandaloneDeclaration(correspondingClass, allDeclarations, templateTypeChecker)) {
declarationsToCopy.push(el);
}
else {
declarationsToPreserve.push(el);
}
}
else {
declarationsToCopy.push(el);
}
}
}
else {
// Otherwise create a spread that will be copied into the `imports`.
declarationsToCopy.push(ts.factory.createSpreadElement(declarationsProp.initializer));
}
}
// If there are no `imports`, create them with the declarations we want to copy.
if (!importsProp && declarationsToCopy.length > 0) {
properties.push(ts.factory.createPropertyAssignment('imports', ts.factory.createArrayLiteralExpression(ts.factory.createNodeArray(declarationsToCopy, hasAnyArrayTrailingComma && declarationsToCopy.length > 2))));
}
for (const prop of literal.properties) {
if (!isNamedPropertyAssignment(prop)) {
properties.push(prop);
continue;
}
// If we have declarations to preserve, update the existing property, otherwise drop it.
if (prop === declarationsProp) {
if (declarationsToPreserve.length > 0) {
const hasTrailingComma = ts.isArrayLiteralExpression(prop.initializer)
? prop.initializer.elements.hasTrailingComma
: hasAnyArrayTrailingComma;
properties.push(ts.factory.updatePropertyAssignment(prop, prop.name, ts.factory.createArrayLiteralExpression(ts.factory.createNodeArray(declarationsToPreserve, hasTrailingComma && declarationsToPreserve.length > 2))));
}
continue;
}
// If we have an `imports` array and declarations
// that should be copied, we merge the two arrays.
if (prop === importsProp && declarationsToCopy.length > 0) {
let initializer;
if (ts.isArrayLiteralExpression(prop.initializer)) {
initializer = ts.factory.updateArrayLiteralExpression(prop.initializer, ts.factory.createNodeArray([...prop.initializer.elements, ...declarationsToCopy], prop.initializer.elements.hasTrailingComma));
}
else {
initializer = ts.factory.createArrayLiteralExpression(ts.factory.createNodeArray([ts.factory.createSpreadElement(prop.initializer), ...declarationsToCopy],
// Expect the declarations to be greater than 1 since
// we have the pre-existing initializer already.
hasAnyArrayTrailingComma && declarationsToCopy.length > 1));
}
properties.push(ts.factory.updatePropertyAssignment(prop, prop.name, initializer));
continue;
}
// Retain any remaining properties.
properties.push(prop);
}
tracker.replaceNode(literal, ts.factory.updateObjectLiteralExpression(literal, ts.factory.createNodeArray(properties, literal.properties.hasTrailingComma)), ts.EmitHint.Expression);
}
/** Sets a decorator node to be standalone. */
function markDecoratorAsStandalone(node) {
const metadata = extractMetadataLiteral(node);
if (metadata === null || !ts.isCallExpression(node.expression)) {
return node;
}
const standaloneProp = metadata.properties.find((prop) => {
return isNamedPropertyAssignment(prop) && prop.name.text === 'standalone';
});
// In v19 standalone is the default so don't do anything if there's no `standalone`
// property or it's initialized to anything other than `false`.
if (!standaloneProp || standaloneProp.initializer.kind !== ts.SyntaxKind.FalseKeyword) {
return node;
}
const newProperties = metadata.properties.filter((element) => element !== standaloneProp);
// Use `createDecorator` instead of `updateDecorator`, because
// the latter ends up duplicating the node's leading comment.
return ts.factory.createDecorator(ts.factory.createCallExpression(node.expression.expression, node.expression.typeArguments, [
ts.factory.createObjectLiteralExpression(ts.factory.createNodeArray(newProperties, metadata.properties.hasTrailingComma), newProperties.length > 1),
]));
}
/**
* Sets a property on an Angular decorator node. If the property
* already exists, its initializer will be replaced.
* @param node Decorator to which to add the property.
* @param name Name of the property to be added.
* @param initializer Initializer for the new property.
*/
function setPropertyOnAngularDecorator(node, name, initializer) {
// Invalid decorator.
if (!ts.isCallExpression(node.expression) || node.expression.arguments.length > 1) {
return node;
}
let literalProperties;
let hasTrailingComma = false;
if (node.expression.arguments.length === 0) {
literalProperties = [ts.factory.createPropertyAssignment(name, initializer)];
}
else if (ts.isObjectLiteralExpression(node.expression.arguments[0])) {
const literal = node.expression.arguments[0];
const existingProperty = findLiteralProperty(literal, name);
hasTrailingComma = literal.properties.hasTrailingComma;
if (existingProperty && ts.isPropertyAssignment(existingProperty)) {
literalProperties = literal.properties.slice();
literalProperties[literalProperties.indexOf(existingProperty)] =
ts.factory.updatePropertyAssignment(existingProperty, existingProperty.name, initializer);
}
else {
literalProperties = [
...literal.properties,
ts.factory.createPropertyAssignment(name, initializer),
];
}
}
else {
// Unsupported case (e.g. `@Component(SOME_CONST)`). Return the original node.
return node;
}
// Use `createDecorator` instead of `updateDecorator`, because
// the latter ends up duplicating the node's leading comment.
return ts.factory.createDecorator(ts.factory.createCallExpression(node.expression.expression, node.expression.typeArguments, [
ts.factory.createObjectLiteralExpression(ts.factory.createNodeArray(literalProperties, hasTrailingComma), literalProperties.length > 1),
]));
}
/** Checks if a node is a `PropertyAssignment` with a name. */
function isNamedPropertyAssignment(node) {
return ts.isPropertyAssignment(node) && node.name && ts.isIdentifier(node.name);
}
/**
* Finds the import from which to bring in a template dependency of a component.
* @param target Dependency that we're searching for.
* @param inContext Component in which the dependency is used.
* @param importMode Mode in which to resolve the import target.
* @param typeChecker
*/
function findImportLocation(target, inContext, importMode, typeChecker) {
const importLocations = typeChecker.getPotentialImportsFor(target, inContext, importMode);
let firstSameFileImport = null;
let firstModuleImport = null;
for (const location of importLocations) {
// Prefer a standalone import, if we can find one.
// Otherwise fall back to the first module-based import.
if (location.kind === checker.PotentialImportKind.Standalone) {
return location;
}
if (!location.moduleSpecifier && !firstSameFileImport) {
firstSameFileImport = location;
}
if (location.kind === checker.PotentialImportKind.NgModule &&
!firstModuleImport &&
// ɵ is used for some internal Angular modules that we want to skip over.
!location.symbolName.startsWith('ɵ')) {
firstModuleImport = location;
}
}
return firstSameFileImport || firstModuleImport || importLocations[0] || null;
}
/**
* Checks whether a node is an `NgModule` metadata element with at least one element.
* E.g. `declarations: [Foo]` or `declarations: SOME_VAR` would match this description,
* but not `declarations: []`.
*/
function hasNgModuleMetadataElements(node) {
return (ts.isPropertyAssignment(node) &&
(!ts.isArrayLiteralExpression(node.initializer) || node.initializer.elements.length > 0));
}
/** Finds all modules whose declarations can be migrated. */
function findNgModuleClassesToMigrate(sourceFile, typeChecker) {
const modules = [];
if (imports.getImportSpecifier(sourceFile, '@angular/core', 'NgModule')) {
sourceFile.forEachChild(function walk(node) {
if (ts.isClassDeclaration(node)) {
const decorator = ng_decorators.getAngularDecorators(typeChecker, ts.getDecorators(node) || []).find((current) => current.name === 'NgModule');
const metadata = decorator ? extractMetadataLiteral(decorator.node) : null;
if (metadata) {
const declarations = findLiteralProperty(metadata, 'declarations');
if (declarations != null && hasNgModuleMetadataElements(declarations)) {
modules.push(node);
}
}
}
node.forEachChild(walk);
});
}
return modules;
}
/** Finds all testing object literals that need to be migrated. */
function findTestObjectsToMigrate(sourceFile, typeChecker) {
const testObjects = [];
const { testBed, catalyst } = getTestingImports(sourceFile);
if (testBed || catalyst) {
sourceFile.forEachChild(function walk(node) {
if (isTestCall(typeChecker, node, testBed, catalyst)) {
const config = node.arguments[0];
const declarations = findLiteralProperty(config, 'declarations');
if (declarations &&
ts.isPropertyAssignment(declarations) &&
ts.isArrayLiteralExpression(declarations.initializer) &&
declarations.initializer.elements.length > 0) {
testObjects.push(config);
}
}
node.forEachChild(walk);
});
}
return testObjects;
}
/**
* Finds the classes corresponding to dependencies used in a component's template.
* @param decl Component in whose template we're looking for dependencies.
* @param typeChecker
*/
function findTemplateDependencies(decl, typeChecker) {
const results = [];
const usedDirectives = typeChecker.getUsedDirectives(decl);
const usedPipes = typeChecker.getUsedPipes(decl);
if (usedDirectives !== null) {
for (const dir of usedDirectives) {
if (ts.isClassDeclaration(dir.ref.node)) {
results.push(dir.ref);
}
}
}
if (usedPipes !== null) {
const potentialPipes = typeChecker.getPotentialPipes(decl);
for (const pipe of potentialPipes) {
if (ts.isClassDeclaration(pipe.ref.node) &&
usedPipes.some((current) => pipe.name === current)) {
results.push(pipe.ref);
}
}
}
return results;
}
/**
* Removes any declarations that are a part of a module's `bootstrap`
* array from an array of declarations.
* @param declarations Anaalyzed declarations of the module.
* @param ngModule Module whote declarations are being filtered.
* @param templateTypeChecker
* @param typeChecker
*/
function filterNonBootstrappedDeclarations(declarations, ngModule, templateTypeChecker, typeChecker) {
const metadata = templateTypeChecker.getNgModuleMetadata(ngModule);
const metaLiteral = metadata && metadata.decorator ? extractMetadataLiteral(metadata.decorator) : null;
const bootstrapProp = metaLiteral ? findLiteralProperty(metaLiteral, 'bootstrap') : null;
// If there's no `bootstrap`, we can't filter.
if (!bootstrapProp) {
return declarations;
}
// If we can't analyze the `bootstrap` property, we can't safely determine which
// declarations aren't bootstrapped so we assume that all of them are.
if (!ts.isPropertyAssignment(bootstrapProp) ||
!ts.isArrayLiteralExpression(bootstrapProp.initializer)) {
return [];
}
const bootstrappedClasses = new Set();
for (const el of bootstrapProp.initializer.elements) {
const referencedClass = ts.isIdentifier(el) ? findClassDeclaration(el, typeChecker) : null;
// If we can resolve an element to a class, we can filter it out,
// otherwise assume that the array isn't static.
if (referencedClass) {
bootstrappedClasses.add(referencedClass);
}
else {
return [];
}
}
return declarations.filter((ref) => !bootstrappedClasses.has(ref));
}
/**
* Extracts all classes that are referenced in a module's `declarations` array.
* @param ngModule Module whose declarations are being extraced.
* @param templateTypeChecker
*/
function extractDeclarationsFromModule(ngModule, templateTypeChecker) {
const metadata = templateTypeChecker.getNgModuleMetadata(ngModule);
return metadata
? metadata.declarations
.filter((decl) => ts.isClassDeclaration(decl.node))
.map((decl) => decl.node)
: [];
}
/**
* Migrates the `declarations` from a unit test file to standalone.
* @param testObjects Object literals used to configure the testing modules.
* @param declarationsOutsideOfTestFiles Non-testing declarations that are part of this migration.
* @param tracker
* @param templateTypeChecker
* @param typeChecker
*/
function migrateTestDeclarations(testObjects, declarationsOutsideOfTestFiles, tracker, templateTypeChecker, typeChecker) {
const { decorators, componentImports } = analyzeTestingModules(testObjects, typeChecker);
const allDeclarations = new Set(declarationsOutsideOfTestFiles);
for (const decorator of decorators) {
const closestClass = nodes.closestNode(decorator.node, ts.isClassDeclaration);
if (decorator.name === 'Pipe' || decorator.name === 'Directive') {
tracker.replaceNode(decorator.node, markDecoratorAsStandalone(decorator.node));
if (closestClass) {
allDeclarations.add(closestClass);
}
}
else if (decorator.name === 'Component') {
const newDecorator = markDecoratorAsStandalone(decorator.node);
const importsToAdd = componentImports.get(decorator.node);
if (closestClass) {
allDeclarations.add(closestClass);
}
if (importsToAdd && importsToAdd.size > 0) {
const hasTrailingComma = importsToAdd.size > 2 &&
!!extractMetadataLiteral(decorator.node)?.properties.hasTrailingComma;
const importsArray = ts.factory.createNodeArray(Array.from(importsToAdd), hasTrailingComma);
tracker.replaceNode(decorator.node, setPropertyOnAngularDecorator(newDecorator, 'imports', ts.factory.createArrayLiteralExpression(importsArray)));
}
else {
tracker.replaceNode(decorator.node, newDecorator);
}
}
}
for (const obj of testObjects) {
moveDeclarationsToImports(obj, allDeclarations, typeChecker, templateTypeChecker, tracker);
}
}
/**
* Analyzes a set of objects used to configure testing modules and returns the AST
* nodes that need to be migrated and the imports that should be added to the imports
* of any declared components.
* @param testObjects Object literals that should be analyzed.
*/
function analyzeTestingModules(testObjects, typeChecker) {
const seenDeclarations = new Set();
const decorators = [];
const componentImports = new Map();
for (const obj of testObjects) {
const declarations = extractDeclarationsFromTestObject(obj, typeChecker);
if (declarations.length === 0) {
continue;
}
const importsProp = findLiteralProperty(obj, 'imports');
const importElements = importsProp &&
hasNgModuleMetadataElements(importsProp) &&
ts.isArrayLiteralExpression(importsProp.initializer)
? importsProp.initializer.elements.filter((el) => {
// Filter out calls since they may be a `ModuleWithProviders`.
return (!ts.isCallExpression(el) &&
// Also filter out the animations modules since they throw errors if they're imported
// multiple times and it's common for apps to use the `NoopAnimationsModule` to
// disable animations in screenshot tests.
!isClassReferenceInAngularModule(el, /^BrowserAnimationsModule|NoopAnimationsModule$/, 'platform-browser/animations', typeChecker));
})
: null;
for (const decl of declarations) {
if (seenDeclarations.has(decl)) {
continue;
}
const [decorator] = ng_decorators.getAngularDecorators(typeChecker, ts.getDecorators(decl) || []);
if (decorator) {
seenDeclarations.add(decl);
decorators.push(decorator);
if (decorator.name === 'Component' && importElements) {
// We try to de-duplicate the imports being added to a component, because it may be
// declared in different testing modules with a different set of imports.
let imports = componentImports.get(decorator.node);
if (!imports) {
imports = new Set();
componentImports.set(decorator.node, imports);
}
importElements.forEach((imp) => imports.add(imp));
}
}
}
}
return { decorators, componentImports };
}
/**
* Finds the class declarations that are being referred
* to in the `declarations` of an object literal.
* @param obj Object literal that may contain the declarations.
* @param typeChecker
*/
function extractDeclarationsFromTestObject(obj, typeChecker) {
const results = [];
const declarations = findLiteralProperty(obj, 'declarations');
if (declarations &&
hasNgModuleMetadataElements(declarations) &&
ts.isArrayLiteralExpression(declarations.initializer)) {
for (const element of declarations.initializer.elements) {
const declaration = findClassDeclaration(element, typeChecker);
// Note that we only migrate classes that are in the same file as the testing module,
// because external fixture components are somewhat rare and handling them is going
// to involve a lot of assumptions that are likely to be incorrect.
if (declaration && declaration.getSourceFile().fileName === obj.getSourceFile().fileName) {
results.push(declaration);
}
}
}
return results;
}
/** Extracts the metadata object literal from an Angular decorator. */
function extractMetadataLiteral(decorator) {
// `arguments[0]` is the metadata object literal.
return ts.isCallExpression(decorator.expression) &&
decorator.expression.arguments.length === 1 &&
ts.isObjectLiteralExpression(decorator.expression.arguments[0])
? decorator.expression.arguments[0]
: null;
}
/**
* Checks whether a class is a standalone declaration.
* @param node Class being checked.
* @param declarationsInMigration Classes that are being converted to standalone in this migration.
* @param templateTypeChecker
*/
function isStandaloneDeclaration(node, declarationsInMigration, templateTypeChecker) {
if (declarationsInMigration.has(node)) {
return true;
}
const metadata = templateTypeChecker.getDirectiveMetadata(node) || templateTypeChecker.getPipeMetadata(node);
return metadata != null && metadata.isStandalone;
}
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
function pruneNgModules(program, host, basePath, rootFileNames, sourceFiles, printer, importRemapper, referenceLookupExcludedFiles, declarationImportRemapper) {
const filesToRemove = new Set();
const tracker = new compiler_host.ChangeTracker(printer, importRemapper);
const tsProgram = program.getTsProgram();
const typeChecker = tsProgram.getTypeChecker();
const templateTypeChecker = program.compiler.getTemplateTypeChecker();
const referenceResolver = new ReferenceResolver(program, host, rootFileNames, basePath, referenceLookupExcludedFiles);
const removalLocations = {
arrays: new UniqueItemTracker(),
imports: new UniqueItemTracker(),
exports: new UniqueItemTracker(),
unknown: new Set(),
};
const classesToRemove = new Set();
const barrelExports = new UniqueItemTracker();
const componentImportArrays = new UniqueItemTracker();
const testArrays = new UniqueItemTracker();
const nodesToRemove = new Set();
sourceFiles.forEach(function walk(node) {
if (ts.isClassDeclaration(node) && canRemoveClass(node, typeChecker)) {
collectChangeLocations(node, removalLocations, componentImportArrays, testArrays, templateTypeChecker, referenceResolver, program);
classesToRemove.add(node);
}
else if (ts.isExportDeclaration(node) &&
!node.exportClause &&
node.moduleSpecifier &&
ts.isStringLiteralLike(node.moduleSpecifier) &&
node.moduleSpecifier.text.startsWith('.')) {
const exportedSourceFile = typeChecker
.getSymbolAtLocation(node.moduleSpecifier)
?.valueDeclaration?.getSourceFile();
if (exportedSourceFile) {
barrelExports.track(exportedSourceFile, node);
}
}
node.forEachChild(walk);
});
replaceInComponentImportsArray(componentImportArrays, classesToRemove, tracker, typeChecker, templateTypeChecker, declarationImportRemapper);
replaceInTestImportsArray(testArrays, removalLocations, classesToRemove, tracker, typeChecker, templateTypeChecker, declarationImportRemapper);
// We collect all the places where we need to remove references first before generating the
// removal instructions since we may have to remove multiple references from one node.
removeArrayReferences(removalLocations.arrays, tracker);
removeImportReferences(removalLocations.imports, tracker);
removeExportReferences(removalLocations.exports, tracker);
addRemovalTodos(removalLocations.unknown, tracker);
// Collect all the nodes to be removed before determining which files to delete since we need
// to know it ahead of time when deleting barrel files that export other barrel files.
(function trackNodesToRemove(nodes) {
for (const node of nodes) {
const sourceFile = node.getSourceFile();
if (!filesToRemove.has(sourceFile) && canRemoveFile(sourceFile, nodes)) {
const barrelExportsForFile = barrelExports.get(sourceFile);
nodesToRemove.add(node);
filesToRemove.add(sourceFile);
barrelExportsForFile && trackNodesToRemove(barrelExportsForFile);
}
else {
nodesToRemove.add(node);
}
}
})(classesToRemove);
for (const node of nodesToRemove) {
const sourceFile = node.getSourceFile();
if (!filesToRemove.has(sourceFile) && canRemoveFile(sourceFile, nodesToRemove)) {
filesToRemove.add(sourceFile);
}
else {
tracker.removeNode(node);
}
}
return { pendingChanges: tracker.recordChanges(), filesToRemove };
}
/**
* Collects all the nodes that a module needs to be removed from.
* @param ngModule Module being removed.
* @param removalLocations Tracks the different places from which the class should be removed.
* @param componentImportArrays Set of `imports` arrays of components that need to be adjusted.
* @param testImportArrays Set of `imports` arrays of tests that need to be adjusted.
* @param referenceResolver
* @param program
*/
function collectChangeLocations(ngModule, removalLocations, componentImportArrays, testImportArrays, templateTypeChecker, referenceResolver, program) {
const refsByFile = referenceResolver.findReferencesInProject(ngModule.name);
const tsProgram = program.getTsProgram();
const typeChecker = tsProgram.getTypeChecker();
const nodes$1 = new Set();
for (const [fileName, refs] of refsByFile) {
const sourceFile = tsProgram.getSourceFile(fileName);
if (sourceFile) {
offsetsToNodes(getNodeLookup(sourceFile), refs, nodes$1);
}
}
for (const node of nodes$1) {
const closestArray = nodes.closestNode(node, ts.isArrayLiteralExpression);
if (closestArray) {
const closestAssignment = nodes.closestNode(closestArray, ts.isPropertyAssignment);
if (closestAssignment && isInImportsArray(closestAssignment, closestArray)) {
const closestCall = nodes.closestNode(closestAssignment, ts.isCallExpression);
if (closestCall) {
const closestDecorator = nodes.closestNode(closestCall, ts.isDecorator);
const closestClass = closestDecorator
? nodes.closestNode(closestDecorator, ts.isClassDeclaration)
: null;
const directiveMeta = closestClass
? templateTypeChecker.getDirectiveMetadata(closestClass)
: null;
// If the module was flagged as being removable, but it's still being used in a
// standalone component's `imports` array, it means that it was likely changed
// outside of the migration and deleting it now will be breaking. Track it
// separately so it can be handled properly.
if (directiveMeta && directiveMeta.isComponent && directiveMeta.isStandalone) {
componentImportArrays.track(closestArray, node);
continue;
}
// If the module is removable and used inside a test's `imports`,
// we track it separately so it can be replaced with its `exports`.
const { testBed, catalyst } = getTestingImports(node.getSourceFile());
if (isTestCall(typeChecker, closestCall, testBed, catalyst)) {
testImportArrays.track(closestArray, node);
continue;
}
}
}
removalLocations.arrays.track(closestArray, node);
continue;
}
const closestImport = nodes.closestNode(node, ts.isNamedImports);
if (closestImport) {
removalLocations.imports.track(closestImport, node);
continue;
}
const closestExport = nodes.closestNode(node, ts.isNamedExports);
if (closestExport) {
removalLocations.exports.track(closestExport, node);
continue;
}
removalLocations.unknown.add(node);
}
}
/**
* Replaces all the leftover modules in component `imports` arrays with their exports.
* @param componentImportArrays All the imports arrays and their nodes t