UNPKG

@angular/core

Version:

Angular - the core framework

1,065 lines (1,059 loc) 100 kB
'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