
package.schematics.bundles.standalone-migration.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core Show documentation
Show all versions of core Show documentation
Angular - the core framework
'use strict';
/**
* @license Angular v19.0.5
* (c) 2010-2024 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var schematics = require('@angular-devkit/schematics');
require('os');
var ts = require('typescript');
var checker = require('./checker-eced36c5.js');
var program = require('./program-c49e652e.js');
var p = require('path');
var fs = require('fs');
var compiler_host = require('./compiler_host-82c877de.js');
var project_tsconfig_paths = require('./project_tsconfig_paths-e9ccccbf.js');
var nodes = require('./nodes-a9f0b985.js');
var imports = require('./imports-abe29092.js');
require('module');
require('url');
require('@angular-devkit/core');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var ts__default = /*#__PURE__*/_interopDefaultLegacy(ts);
/**
* @module
* @description
* Entry point for all public APIs of the compiler-cli package.
*/
new checker.Version('19.0.5');
function createProgram({ rootNames, options, host, oldProgram, }) {
return new program.NgtscProgram(rootNames, options, host, oldProgram);
}
var LogLevel;
(function (LogLevel) {
LogLevel[LogLevel["debug"] = 0] = "debug";
LogLevel[LogLevel["info"] = 1] = "info";
LogLevel[LogLevel["warn"] = 2] = "warn";
LogLevel[LogLevel["error"] = 3] = "error";
})(LogLevel || (LogLevel = {}));
checker.setFileSystem(new checker.NodeJSFileSystem());
/** 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__default["default"].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__default["default"].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__default["default"].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__default["default"].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__default["default"].ScriptSnapshot.fromString(content) : undefined;
},
getCurrentDirectory: () => this._basePath,
getDefaultLibFileName: (options) => ts__default["default"].getDefaultLibFilePath(options),
readFile: (path) => this._readFile(path),
fileExists: (path) => this._host.fileExists(path),
}, ts__default["default"].createDocumentRegistry(), ts__default["default"].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__default["default"].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__default["default"].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__default["default"].isClassDeclaration);
const closestClassFileName = closestClass?.getSourceFile().fileName;
if (!closestClass ||
!closestClassFileName ||
!closestClass.name ||
!ts__default["default"].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__default["default"].isCallExpression(node) &&
node.arguments.length > 0 &&
// `arguments[0]` is the testing module config.
ts__default["default"].isObjectLiteralExpression(node.arguments[0]);
const isTestBedCall = isObjectLiteralCall &&
testBedImport &&
ts__default["default"].isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'configureTestingModule' &&
isReferenceToImport(typeChecker, node.expression.expression, testBedImport);
const isCatalystCall = isObjectLiteralCall &&
catalystImport &&
ts__default["default"].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__default["default"].factory.createArrayLiteralExpression(
// Create a multi-line array when it has a trailing comma.
ts__default["default"].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__default["default"].factory.createIdentifier(importLocation.symbolName);
if (!importLocation.isForwardReference) {
return identifier;
}
const forwardRefExpression = tracker.addImport(toFile, 'forwardRef', '@angular/core');
const arrowFunction = ts__default["default"].factory.createArrowFunction(undefined, undefined, [], undefined, undefined, identifier);
return ts__default["default"].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__default["default"].isPropertyAssignment(prop) &&
ts__default["default"].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__default["default"].isPropertyAssignment(declarationsProp)) {
// If the declarations are an array, we can analyze it to
// find any classes from the current migration.
if (ts__default["default"].isArrayLiteralExpression(declarationsProp.initializer)) {
for (const el of declarationsProp.initializer.elements) {
if (ts__default["default"].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__default["default"].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__default["default"].factory.createPropertyAssignment('imports', ts__default["default"].factory.createArrayLiteralExpression(ts__default["default"].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__default["default"].isArrayLiteralExpression(prop.initializer)
? prop.initializer.elements.hasTrailingComma
: hasAnyArrayTrailingComma;
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))));
}
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__default["default"].isArrayLiteralExpression(prop.initializer)) {
initializer = ts__default["default"].factory.updateArrayLiteralExpression(prop.initializer, ts__default["default"].factory.createNodeArray([...prop.initializer.elements, ...declarationsToCopy], prop.initializer.elements.hasTrailingComma));
}
else {
initializer = ts__default["default"].factory.createArrayLiteralExpression(ts__default["default"].factory.createNodeArray([ts__default["default"].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__default["default"].factory.updatePropertyAssignment(prop, prop.name, initializer));
continue;
}
// Retain any remaining properties.
properties.push(prop);
}
tracker.replaceNode(literal, ts__default["default"].factory.updateObjectLiteralExpression(literal, ts__default["default"].factory.createNodeArray(properties, literal.properties.hasTrailingComma)), ts__default["default"].EmitHint.Expression);
}
/** Sets a decorator node to be standalone. */
function markDecoratorAsStandalone(node) {
const metadata = extractMetadataLiteral(node);
if (metadata === null || !ts__default["default"].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__default["default"].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__default["default"].factory.createDecorator(ts__default["default"].factory.createCallExpression(node.expression.expression, node.expression.typeArguments, [
ts__default["default"].factory.createObjectLiteralExpression(ts__default["default"].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__default["default"].isCallExpression(node.expression) || node.expression.arguments.length > 1) {
return node;
}
let literalProperties;
let hasTrailingComma = false;
if (node.expression.arguments.length === 0) {
literalProperties = [ts__default["default"].factory.createPropertyAssignment(name, initializer)];
}
else if (ts__default["default"].isObjectLiteralExpression(node.expression.arguments[0])) {
const literal = node.expression.arguments[0];
const existingProperty = findLiteralProperty(literal, name);
hasTrailingComma = literal.properties.hasTrailingComma;
if (existingProperty && ts__default["default"].isPropertyAssignment(existingProperty)) {
literalProperties = literal.properties.slice();
literalProperties[literalProperties.indexOf(existingProperty)] =
ts__default["default"].factory.updatePropertyAssignment(existingProperty, existingProperty.name, initializer);
}
else {
literalProperties = [
...literal.properties,
ts__default["default"].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__default["default"].factory.createDecorator(ts__default["default"].factory.createCallExpression(node.expression.expression, node.expression.typeArguments, [
ts__default["default"].factory.createObjectLiteralExpression(ts__default["default"].factory.createNodeArray(literalProperties, hasTrailingComma), literalProperties.length > 1),
]));
}
/** Checks if a node is a `PropertyAssignment` with a name. */
function isNamedPropertyAssignment(node) {
return ts__default["default"].isPropertyAssignment(node) && node.name && ts__default["default"].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__default["default"].isPropertyAssignment(node) &&
(!ts__default["default"].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__default["default"].isClassDeclaration(node)) {
const decorator = nodes.getAngularDecorators(typeChecker, ts__default["default"].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__default["default"].isPropertyAssignment(declarations) &&
ts__default["default"].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__default["default"].isClassDeclaration(dir.ref.node)) {
results.push(dir.ref);
}
}
}
if (usedPipes !== null) {
const potentialPipes = typeChecker.getPotentialPipes(decl);
for (const pipe of potentialPipes) {
if (ts__default["default"].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__default["default"].isPropertyAssignment(bootstrapProp) ||
!ts__default["default"].isArrayLiteralExpression(bootstrapProp.initializer)) {
return [];
}
const bootstrappedClasses = new Set();
for (const el of bootstrapProp.initializer.elements) {
const referencedClass = ts__default["default"].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__default["default"].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__default["default"].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__default["default"].factory.createNodeArray(Array.from(importsToAdd), hasTrailingComma);
tracker.replaceNode(decorator.node, setPropertyOnAngularDecorator(newDecorator, 'imports', ts__default["default"].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__default["default"].isArrayLiteralExpression(importsProp.initializer)
? importsProp.initializer.elements.filter((el) => {
// Filter out calls since they may be a `ModuleWithProviders`.
return (!ts__default["default"].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] = nodes.getAngularDecorators(typeChecker, ts__default["default"].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__default["default"].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__default["default"].isCallExpression(decorator.expression) &&
decorator.expression.arguments.length === 1 &&
ts__default["default"].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__default["default"].isClassDeclaration(node) && canRemoveClass(node, typeChecker)) {
collectChangeLocations(node, removalLocations, componentImportArrays, testArrays, templateTypeChecker, referenceResolver, program);
classesToRemove.add(node);
}
else if (ts__default["default"].isExportDeclaration(node) &&
!node.exportClause &&
node.moduleSpecifier &&
ts__default["default"].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__default["default"].isArrayLiteralExpression);
if (closestArray) {
const closestAssignment = nodes.closestNode(closestArray, ts__default["default"].isPropertyAssignment);
if (closestAssignment && isInImportsArray(closestAssignment, closestArray)) {
const closestCall = nodes.closestNode(closestAssignment, ts__default["default"].isCallExpression);
if (closestCall) {
const closestDecorator = nodes.closestNode(closestCall, ts__default["default"].isDecorator);
const closestClass = closestDecorator
? nodes.closestNode(closestDecorator, ts__default["default"].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__default["default"].isNamedImports);
if (closestImport) {
removalLocations.imports.track(closestImport, node);
continue;
}
const closestExport = nodes.closestNode(node, ts__default["default"].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 that represent NgModules.
* @param classesToRemove Set of classes that were marked for removal.
* @param tracker
* @param typeChecker
* @param templateTypeChecker
* @param importRemapper
*/
function replaceInComponentImportsArray(componentImportArrays, classesToRemove, tracker, typeChecker, templateTypeChecker, importRemapper) {
for (const [array, toReplace] of componentImportArrays.getEntries()) {
const closestClass = nodes.closestNode(array, ts__default["default"].isClassDeclaration);
if (!closestClass) {
continue;
}
const replacements = new UniqueItemTracker();
const usedImports = new Set(findTemplateDependencies(closestClass, templateTypeChecker).map((ref) => ref.node));
for (const node of toReplace) {
const moduleDecl = findClassDeclaration(node, typeChecker);
if (moduleDecl) {
const moduleMeta = templateTypeChecker.getNgModuleMetadata(moduleDecl);
if (moduleMeta) {
moduleMeta.exports.forEach((exp) => {
if (usedImports.has(exp.node)) {
replacements.track(node, exp);
}
});
}
else {
// It's unlikely not to have module metadata at this point, but just in
// case unmark the class for removal to reduce the chance of breakages.
classesToRemove.delete(moduleDecl);
}
}
}
replaceModulesInImportsArray(array, replacements, tracker, templateTypeChecker, importRemapper);
}
}
/**
* Replaces all the leftover modules in testing `imports` arrays with their exports.
* @param testImportArrays All test `imports` arrays and their nodes that represent modules.
* @param classesToRemove Classes marked for removal by the migration.
* @param tracker
* @param typeChecker
* @param templateTypeChecker
* @param importRemapper
*/
function replaceInTestImportsArray(testImportArrays, removalLocations, classesToRemove, tracker, typeChecker, templateTypeChecker, importRemapper) {
for (const [array, toReplace] of testImportArrays.getEntries()) {
const replacements = new UniqueItemTracker();
for (const node of toReplace) {
const moduleDecl = findClassDeclaration(node, typeChecker);
if (moduleDecl) {
const moduleMeta = templateTypeChecker.getNgModuleMetadata(moduleDecl);
if (moduleMeta) {
// Since we don't have access to the template type checker in tests,
// we copy over all the `exports` that aren't flagged for removal.
const exports = moduleMeta.exports.filter((exp) => !classesToRemove.has(exp.node));
if (exports.length > 0) {
exports.forEach((exp) => replacements.track(node, exp));
}
else {
removalLocations.arrays.track(array, node);
}
}
else {
// It's unlikely not to have module metadata at this point, but just in
// case unmark the class for removal to reduce the chance of breakages.
classesToRemove.delete(moduleDecl);
}
}
}
replaceModulesInImportsArray(array, replacements, tracker, templateTypeChecker, importRemapper);
}
}
/**
* Replaces any leftover modules in an `imports` arrays with a set of specified exports
* @param array Imports array which is being migrated.
* @param replacements Map of NgModule references to their exports.
* @param tracker
* @param templateTypeChecker
* @param importRemapper
*/
function replaceModulesInImportsArray(array, replacements, tracker, templateTypeChecker, importRemapper) {
if (replacements.isEmpty()) {
return;
}
const newElements = [];
const identifiers = new Set();
for (const element of array.elements) {
if (ts__default["default"].isIdentifier(element)) {
identifiers.add(element.text);
}
}
for (const element of array.elements) {
const replacementRefs = replacements.get(element);
if (!replacementRefs) {
newElements.push(element);
continue;
}
const potentialImports = [];
for (const ref of replacementRefs) {
const importLocation = findImportLocation(ref, array, checker.PotentialImportMode.Normal, templateTypeChecker);
if (importLocation) {
potentialImports.push(importLocation);
}
}
potentialImportsToExpressions(potentialImports, array.getSourceFile(), tracker, importRemapper).forEach((expr) => {
if (!ts__default["default"].isIdentifier(expr) || !identifiers.has(expr.text)) {
newElements.push(expr);
}
});
}
tracker.replaceNode(array, ts__default["default"].factory.updateArrayLiteralExpression(array, newElements));
}
/**
* Removes all tracked array references.
* @param locations Locations from which to remove the references.
* @param tracker Tracker in which to register the changes.
*/
function removeArrayReferences(locations, tracker) {
for (const [array, toRemove] of locations.getEntries()) {
const newElements = filterRemovedElements(array.elements, toRemove);
tracker.replaceNode(array, ts__default["default"].factory.updateArrayLiteralExpression(array, ts__default["default"].factory.createNodeArray(newElements, array.elements.hasTrailingComma)));
}
}
/**
* Removes all tracked import references.
* @param locations Locations from which to remove the references.
* @param tracker Tracker in which to register the changes.
*/
function removeImportReferences(locations, tracker) {
for (const [namedImports, toRemove] of locations.getEntries()) {
const newElements = filterRemovedElements(namedImports.elements, toRemove);
// If no imports are left, we can try to drop the entire import.
if (newElements.length === 0) {
const importClause = nodes.closestNode(namedImports, ts__default["default"].isImportClause);
// If the import clause has a name we can only drop then named imports.
// e.g. `import Foo, {ModuleToRemove} from './foo';` becomes `import Foo from './foo';`.
if (importClause && importClause.name) {
tracker.replaceNode(importClause, ts__default["default"].factory.updateImportClause(importClause, importClause.isTypeOnly, importClause.name, undefined));
}
else {
// Otherwise we can drop the entire declaration.
const declaration = nodes.closestNode(namedImports, ts__default["default"].isImportDeclaration);
if (declaration) {
tracker.removeNode(declaration);
}
}
}
else {
// Otherwise we just drop the imported symbols and keep the declaration intact.
tracker.replaceNode(namedImports, ts__default["default"].factory.updateNamedImports(namedImports, newElements));
}
}
}
/**
* Removes all tracked export references.
* @param locations Locations from which to remove the references.
* @param tracker Tracker in which to register the changes.
*/
function removeExportReferences(locations, tracker) {
for (const [namedExports, toRemove] of locations.getEntries()) {
const newElements = filterRemovedElements(namedExports.elements, toRemove);
// If no exports are left, we can drop the entire declaration.
if (newElements.length === 0) {
const declaration = nodes.closestNode(namedExports, ts__default["default"].isExportDeclaration);
if (declaration) {
tracker.removeNode(declaration);
}
}
else {
// Otherwise we just drop the exported symbols and keep the declaration intact.
tracker.replaceNode(namedExports, ts__default["default"].factory.updateNamedExports(namedExports, newElements));
}
}
}
/**
* Determines whether an `@NgModule` class is safe to remove. A module is safe to remove if:
* 1. It has no `declarations`.
* 2. It has no `providers`.
* 3. It has no `bootstrap` components.
* 4. It has no `ModuleWithProviders` in its `imports`.
* 5. It has no class members. Empty construstors are ignored.
* @param node Class that is being checked.
* @param typeChecker
*/
function canRemoveClass(node, typeChecker) {
const decorator = findNgModuleDecorator(node, typeChecker)?.node;
// We can't remove a declaration if it's not a valid `NgModule`.
if (!decorator || !ts__default["default"].isCallExpression(decorator.expression)) {
return false;
}
// Unsupported case, e.g. `@NgModule(SOME_VALUE)`.
if (decorator.expression.arguments.length > 0 &&
!ts__default["default"].isObjectLiteralExpression(decorator.expression.arguments[0])) {
return false;
}
// We can't remove modules that have class members. We make an exception for an
// empty constructor which may have been generated by a tool and forgotten.
if (node.members.length > 0 && node.members.some((member) => !isEmptyConstructor(member))) {
return false;
}
// An empty `NgModule` call can be removed.
if (decorator.expression.arguments.length === 0) {
return true;
}
const literal = decorator.expression.arguments[0];
const imports = findLiteralProperty(literal, 'imports');
if (imports && isNonEmptyNgModuleProperty(imports)) {
// We can't remove the class if at least one import isn't identifier, because it may be a
// `ModuleWithProviders` which is the equivalent of having something in the `providers` array.
for (const dep of imports.initializer.elements) {
if (!ts__default["default"].isIdentifier(dep)) {
return false;
}
const depDeclaration = findClassDeclaration(dep, typeChecker);
const depNgModule = depDeclaration
? findNgModuleDecorator(depDeclaration, typeChecker)
: null;
// If any of the dependencies of the class is an `NgModule` that can't be removed, the class
// itself can't be removed either, because it may be part of a transitive dependency chain.
if (depDeclaration !== null &&
depNgModule !== null &&
!canRemoveClass(depDeclaration, typeChecker)) {
return false;
}
}
}
// We can't remove classes that have any `declarations`, `providers` or `bootstrap` elements.
// Also err on the side of caution and don't remove modules where any of the aforementioned
// properties aren't initialized to an array literal.
for (const prop of literal.properties) {
if (isNonEmptyNgModuleProperty(prop) &&
(prop.name.text === 'declarations' ||
prop.name.text === 'providers' ||
prop.name.text === 'bootstrap')) {
return false;
}
}
return true;
}
/**
* Checks whether a node is a non-empty property from an NgModule's metadata. This is defined as a
* property assignment with a static name, initialized to an array literal with more than one
* element.
* @param node Node to be checked.
*/
function isNonEmptyNgModuleProperty(node) {
return (ts__default["default"].isPropertyAssignment(node) &&
ts__default["default"].isIdentifier(node.name) &&
ts__default["default"].isArrayLiteralExpression(node.initializer) &&
node.initializer.elements.length > 0);
}
/**
* Determines if a file is safe to delete. A file is safe to delete if all it contains are
* import statements, class declarations that are about to be deleted and non-exported code.
* @param sourceFile File that is being checked.
* @param nodesToBeRemoved Nodes that are being removed as a part of the migration.
*/
function canRemoveFile(sourceFile, nodesToBeRemoved) {
for (const node of sourceFile.statements) {
if (ts__default["default"].isImportDeclaration(node) || nodesToBeRemoved.has(node)) {
continue;
}
if (ts__default["default"].isExportDeclaration(node) ||
(ts__default["default"].canHaveModifiers(node) &&
ts__default["default"].getModifiers(node)?.some((m) => m.kind === ts__default["default"].SyntaxKind.ExportKeyword))) {
return false;
}
}
return true;
}
/**
* Gets whether an AST node contains another AST node.
* @param parent Parent node that may contain the child.
* @param child Child node that is being checked.
*/
function contains(parent, child) {
return (parent === child ||
(parent.getSourceFile().fileName === child.getSourceFile().fileName &&
child.getStart() >= parent.getStart() &&
child.getStart() <= parent.getEnd()));
}
/**
* Removes AST nodes from a node array.
* @param elements Array from which to remove the nodes.
* @param toRemove Nodes that should be removed.
*/
function filterRemovedElements(elements, toRemove) {
return elements.filter((el) => {
for (const node of toRemove) {
// Check that the element contains the node, despite knowing with relative certainty that it
// does, because this allows us to unwrap some nodes. E.g. if we have `[((toRemove))]`, we
// want to remove the entire parenthesized expression, rather than just `toRemove`.
if (contains(el, node)) {
return false;
}
}
return true;
});
}
/** Returns whether a node as an empty constructor. */
function isEmptyConstructor(node) {
return (ts__default["default"].isConstructorDeclaration(node) &&
node.parameters.length === 0 &&
(node.body == null || node.body.statements.length === 0));
}
/**
* Adds TODO comments to nodes that couldn't be removed manually.
* @param nodes Nodes to which to add the TODO.
* @param tracker Tracker in which to register the changes.
*/
function addRemovalTodos(nodes, tracker) {
for (const node of nodes) {
// Note: the comment is inserted using string manipulation, instead of going through the AST,
// because this way we preserve more of the app's original formatting.
// Note: in theory this can duplicate comments if the module pruning runs multiple times on
// the same node. In practice it is unlikely, because the second time the node won't be picked
// up by the language service as a reference, because the class won't exist anymore.
tracker.insertText(node.getSourceFile(), node.getFullStart(), ` /* TODO(standalone-migration): clean up removed NgModule reference manually. */ `);
}
}
/** Finds the `NgModule` decorator in a class, if it exists. */
function findNgModuleDecorator(node, typeChecker) {
const decorators = nodes.getAngularDecorators(typeChecker, ts__default["default"].getDecorators(node) || []);
return decorators.find((decorator) => decorator.name === 'NgModule') || null;
}
/**
* Checks whether a node is used inside of an `imports` array.
* @param closestAssignment The closest property assignment to the node.
* @param closestArray The closest array to the node.
*/
function isInImportsArray(closestAssignment, closestArray) {
return (closestAssignment.initializer === closestArray &&
(ts__default["default"].isIdentifier(closestAssignment.name) || ts__default["default"].isStringLiteralLike(closestAssignment.name)) &&
closestAssignment.name.text === 'imports');
}
/*!
* @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 toStandaloneBootstrap(program, host, basePath, rootFileNames, sourceFiles, printer, importRemapper, referenceLookupExcludedFiles, declarationImportRemapper) {
const tracker = new compiler_host.ChangeTracker(printer, importRemapper);
const typeChecker = program.getTsProgram().getTypeChecker();
const templateTypeChecker = program.compiler.getTemplateTypeChecker();
const referenceResolver = new ReferenceResolver(program, host, rootFileNames, basePath, referenceLookupExcludedFiles);
const bootstrapCalls = [];
const testObjects = new Set();
const allDeclarations = new Set();
// `bootstrapApplication` doesn't include Protractor support by default
// anymore so we have to opt the app in, if we detect it being used.
const additionalProviders = hasImport(program, rootFileNames, 'protractor')
? new Map([['provideProtractorTestingSupport', '@angular/platform-browser']])
: null;
for (const sourceFile of sourceFiles) {
sourceFile.forEachChild(function walk(node) {
if (ts__default["default"].isCallExpression(node) &&
ts__default["default"].isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'bootstrapModule' &&
isClassReferenceInAngularModule(node.expression, 'PlatformRef', 'core', typeChecker)) {
const call = analyzeBootstrapCall(node, typeChecker, templateTypeChecker);
if (call) {
bootstrapCalls.push(call);
}
}
node.forEachChild(walk);
});
findTestObjectsToMigrate(sourceFile, typeChecker).forEach((obj) => testObjects.add(obj));
}
for (const call of bootstrapCalls) {
call.declarations.forEach((decl) => allDeclarations.add(decl));
migrateBootstrapCall(call, tracker, additionalProviders, referenceResolver, typeChecker, printer);
}
// The previous migrations explicitly skip over bootstrapped
// declarations so we have to migrate them now.
for (const declaration of allDeclarations) {
convertNgModuleDeclarationToStandalone(declaration, allDeclarations, tracker, templateTypeChecker, declarationImportRemapper);
}
migrateTestDeclarations(testObjects, allDeclarations, tracker, templateTypeChecker, typeChecker);
return tracker.recordChanges();
}
/**
* Extracts all of the information from a `bootstrapModule` call
* necessary to convert it to `bootstrapApplication`.
* @param call Call to be analyzed.
* @param typeChecker
* @param templateTypeChecker
*/
function analyzeBootstrapCall(call, typeChecker, templateTypeChecker) {
if (call.arguments.length === 0 || !ts__default["default"].isIdentifier(call.arguments[0])) {
return null;
}
const declaration = findClassDeclaration(call.arguments[0], typeChecker);
if (!declaration) {
return null;
}
const decorator = nodes.getAngularDecorators(typeChecker, ts__default["default"].getDecorators(declaration) || []).find((decorator) => decorator.name === 'NgModule');
if (!decorator ||
decorator.node.expression.arguments.length === 0 ||
!ts__default["default"].isObjectLiteralExpression(decorator.node.expression.arguments[0])) {
return null;
}
const metadata = decorator.node.expression.arguments[0];
const bootstrapProp = findLiteralProperty(metadata, 'bootstrap');
if (!bootstrapProp ||
!ts__default["default"].isPropertyAssignment(bootstrapProp) ||
!ts__default["default"].isArrayLiteralExpression(bootstrapProp.initializer) ||
bootstrapProp.initializer.elements.length === 0 ||
!ts__default["default"].isIdentifier(bootstrapProp.initializer.elements[0])) {
return null;
}
const component = findClassDeclaration(bootstrapProp.initializer.elements[0], typeChecker);
if (component && component.name && ts__default["default"].isIdentifier(component.name)) {
return {
module: declaration,
metadata,
component: component,
call,
declarations: extractDeclarationsFromModule(declaration, templateTypeChecker),
};
}
return null;
}
/**
* Converts a `bootstrapModule` call to `bootstrapApplication`.
* @param analysis Analysis result of the call.
* @param tracker Tracker in which to register the changes.
* @param additionalFeatures Additional providers, apart from the auto-detected ones, that should
* be added to the bootstrap call.
* @param referenceResolver
* @param typeChecker
* @param printer
*/
function migrateBootstrapCall(analysis, tracker, additionalProviders, referenceResolver, typeChecker, printer) {
const sourceFile = analysis.call.getSourceFile();
const moduleSourceFile = analysis.metadata.getSourceFile();
const providers = findLiteralProperty(analysis.metadata, 'providers');
const imports = findLiteralProperty(analysis.metadata, 'imports');
const nodesToCopy = new Set();
const providersInNewCall = [];
const moduleImportsInNewCall = [];
let nodeLookup = null;
// Comment out the metadata so that it'll be removed when we run the module pruning afterwards.
// If the pruning is left for some reason, the user will still have an actionable TODO.
tracker.insertText(moduleSourceFile, analysis.metadata.getStart(), '/* TODO(standalone-migration): clean up removed NgModule class manually. \n');
tracker.insertText(moduleSourceFile, analysis.metadata.getEnd(), ' */');
if (providers && ts__default["default"].isPropertyAssignment(providers)) {
nodeLookup = nodeLookup || getNodeLookup(moduleSourceFile);
if (ts__default["default"].isArrayLiteralExpression(providers.initializer)) {
providersInNewCall.push(...providers.initializer.elements);
}
else {
providersInNewCall.push(ts__default["default"].factory.createSpreadElement(providers.initializer));
}
addNodesToCopy(sourceFile, providers, nodeLookup, tracker, nodesToCopy, referenceResolver);
}
if (imports && ts__default["default"].isPropertyAssignment(imports)) {
nodeLookup = nodeLookup || getNodeLookup(moduleSourceFile);
migrateImportsForBootstrapCall(sourceFile, imports, nodeLookup, moduleImportsInNewCall, providersInNewCall, tracker, nodesToCopy, referenceResolver, typeChecker);
}
if (additionalProviders) {
additionalProviders.forEach((moduleSpecifier, name) => {
providersInNewCall.push(ts__default["default"].factory.createCallExpression(tracker.addImport(sourceFile, name, moduleSpecifier), undefined, undefined));
});
}
if (nodesToCopy.size > 0) {
let text = '\n\n';
nodesToCopy.forEach((node) => {
const transformedNode = remapDynamicImports(sourceFile.fileName, node);
// Use `getText` to try an preserve the original formatting. This only works if the node
// hasn't been transformed. If it has, we have to fall back to the printer.
if (transformedNode === node) {
text += transformedNode.getText() + '\n';
}
else {
text += printer.printNode(ts__default["default"].EmitHint.Unspecified, transformedNode, node.getSourceFile());
}
});
text += '\n';
tracker.insertText(sourceFile, getLastImportEnd(sourceFile), text);
}
replaceBootstrapCallExpression(analysis, providersInNewCall, moduleImportsInNewCall, tracker);
}
/**
* Replaces a `bootstrapModule` call with `bootstrapApplication`.
* @param analysis Analysis result of the `bootstrapModule` call.
* @param providers Providers that should be added to the new call.
* @param modules Modules that are being imported into the new call.
* @param tracker Object keeping track of the changes to the different files.
*/
function replaceBootstrapCallExpression(analysis, providers, modules, tracker) {
const sourceFile = analysis.call.getSourceFile();
const componentPath = getRelativeImportPath(sourceFile.fileName, analysis.component.getSourceFile().fileName);
const args = [tracker.addImport(sourceFile, analysis.component.name.text, componentPath)];
const bootstrapExpression = tracker.addImport(sourceFile, 'bootstrapApplication', '@angular/platform-browser');
if (providers.length > 0 || modules.length > 0) {
const combinedProviders = [];
if (modules.length > 0) {
const importProvidersExpression = tracker.addImport(sourceFile, 'importProvidersFrom', '@angular/core');
combinedProviders.push(ts__default["default"].factory.createCallExpression(importProvidersExpression, [], modules));
}
// Push the providers after `importProvidersFrom` call for better readability.
combinedProviders.push(...providers);
const providersArray = ts__default["default"].factory.createNodeArray(combinedProviders, analysis.metadata.properties.hasTrailingComma && combinedProviders.length > 2);
const initializer = remapDynamicImports(sourceFile.fileName, ts__default["default"].factory.createArrayLiteralExpression(providersArray, combinedProviders.length > 1));
args.push(ts__default["default"].factory.createObjectLiteralExpression([ts__default["default"].factory.createPropertyAssignment('providers', initializer)], true));
}
tracker.replaceNode(analysis.call, ts__default["default"].factory.createCallExpression(bootstrapExpression, [], args),
// Note: it's important to pass in the source file that the nodes originated from!
// Otherwise TS won't print out literals inside of the providers that we're copying
// over from the module file.
undefined, analysis.metadata.getSourceFile());
}
/**
* Processes the `imports` of an NgModule so that they can be used in the `bootstrapApplication`
* call inside of a different file.
* @param sourceFile File to which the imports will be moved.
* @param imports Node declaring the imports.
* @param nodeLookup Map used to look up nodes based on their positions in a file.
* @param importsForNewCall Array keeping track of the imports that are being added to the new call.
* @param providersInNewCall Array keeping track of the providers in the new call.
* @param tracker Tracker in which changes to files are being stored.
* @param nodesToCopy Nodes that should be copied to the new file.
* @param referenceResolver
* @param typeChecker
*/
function migrateImportsForBootstrapCall(sourceFile, imports, nodeLookup, importsForNewCall, providersInNewCall, tracker, nodesToCopy, referenceResolver, typeChecker) {
if (!ts__default["default"].isArrayLiteralExpression(imports.initializer)) {
importsForNewCall.push(imports.initializer);
return;
}
for (const element of imports.initializer.elements) {
// If the reference is to a `RouterModule.forRoot` call, we can try to migrate it.
if (ts__default["default"].isCallExpression(element) &&
ts__default["default"].isPropertyAccessExpression(element.expression) &&
element.arguments.length > 0 &&
element.expression.name.text === 'forRoot' &&
isClassReferenceInAngularModule(element.expression.expression, 'RouterModule', 'router', typeChecker)) {
const options = element.arguments[1];
const features = options ? getRouterModuleForRootFeatures(sourceFile, options, tracker) : [];
// If the features come back as null, it means that the router
// has a configuration that can't be migrated automatically.
if (features !== null) {
providersInNewCall.push(ts__default["default"].factory.createCallExpression(tracker.addImport(sourceFile, 'provideRouter', '@angular/router'), [], [element.arguments[0], ...features]));
addNodesToCopy(sourceFile, element.arguments[0], nodeLookup, tracker, nodesToCopy, referenceResolver);
if (options) {
addNodesToCopy(sourceFile, options, nodeLookup, tracker, nodesToCopy, referenceResolver);
}
continue;
}
}
if (ts__default["default"].isIdentifier(element)) {
// `BrowserAnimationsModule` can be replaced with `provideAnimations`.
const animationsModule = 'platform-browser/animations';
const animationsImport = `@angular/${animationsModule}`;
if (isClassReferenceInAngularModule(element, 'BrowserAnimationsModule', animationsModule, typeChecker)) {
providersInNewCall.push(ts__default["default"].factory.createCallExpression(tracker.addImport(sourceFile, 'provideAnimations', animationsImport), [], []));
continue;
}
// `NoopAnimationsModule` can be replaced with `provideNoopAnimations`.
if (isClassReferenceInAngularModule(element, 'NoopAnimationsModule', animationsModule, typeChecker)) {
providersInNewCall.push(ts__default["default"].factory.createCallExpression(tracker.addImport(sourceFile, 'provideNoopAnimations', animationsImport), [], []));
continue;
}
// `HttpClientModule` can be replaced with `provideHttpClient()`.
const httpClientModule = 'common/http';
const httpClientImport = `@angular/${httpClientModule}`;
if (isClassReferenceInAngularModule(element, 'HttpClientModule', httpClientModule, typeChecker)) {
const callArgs = [
// we add `withInterceptorsFromDi()` to the call to ensure that class-based interceptors
// still work
ts__default["default"].factory.createCallExpression(tracker.addImport(sourceFile, 'withInterceptorsFromDi', httpClientImport), [], []),
];
providersInNewCall.push(ts__default["default"].factory.createCallExpression(tracker.addImport(sourceFile, 'provideHttpClient', httpClientImport), [], callArgs));
continue;
}
}
const target =
// If it's a call, it'll likely be a `ModuleWithProviders`
// expression so the target is going to be call's expression.
ts__default["default"].isCallExpression(element) && ts__default["default"].isPropertyAccessExpression(element.expression)
? element.expression.expression
: element;
const classDeclaration = findClassDeclaration(target, typeChecker);
const decorators = classDeclaration
? nodes.getAngularDecorators(typeChecker, ts__default["default"].getDecorators(classDeclaration) || [])
: undefined;
if (!decorators ||
decorators.length === 0 ||
decorators.every(({ name }) => name !== 'Directive' && name !== 'Component' && name !== 'Pipe')) {
importsForNewCall.push(element);
addNodesToCopy(sourceFile, element, nodeLookup, tracker, nodesToCopy, referenceResolver);
}
}
}
/**
* Generates the call expressions that can be used to replace the options
* object that is passed into a `RouterModule.forRoot` call.
* @param sourceFile File that the `forRoot` call is coming from.
* @param options Node that is passed as the second argument to the `forRoot` call.
* @param tracker Tracker in which to track imports that need to be inserted.
* @returns Null if the options can't be migrated, otherwise an array of call expressions.
*/
function getRouterModuleForRootFeatures(sourceFile, options, tracker) {
// Options that aren't a static object literal can't be migrated.
if (!ts__default["default"].isObjectLiteralExpression(options)) {
return null;
}
const featureExpressions = [];
const configOptions = [];
const inMemoryScrollingOptions = [];
const features = new UniqueItemTracker();
for (const prop of options.properties) {
// We can't migrate options that we can't easily analyze.
if (!ts__default["default"].isPropertyAssignment(prop) ||
(!ts__default["default"].isIdentifier(prop.name) && !ts__default["default"].isStringLiteralLike(prop.name))) {
return null;
}
switch (prop.name.text) {
// `preloadingStrategy` maps to the `withPreloading` function.
case 'preloadingStrategy':
features.track('withPreloading', prop.initializer);
break;
// `enableTracing: true` maps to the `withDebugTracing` feature.
case 'enableTracing':
if (prop.initializer.kind === ts__default["default"].SyntaxKind.TrueKeyword) {
features.track('withDebugTracing', null);
}
break;
// `initialNavigation: 'enabled'` and `initialNavigation: 'enabledBlocking'` map to the
// `withEnabledBlockingInitialNavigation` feature, while `initialNavigation: 'disabled'` maps
// to the `withDisabledInitialNavigation` feature.
case 'initialNavigation':
if (!ts__default["default"].isStringLiteralLike(prop.initializer)) {
return null;
}
if (prop.initializer.text === 'enabledBlocking' || prop.initializer.text === 'enabled') {
features.track('withEnabledBlockingInitialNavigation', null);
}
else if (prop.initializer.text === 'disabled') {
features.track('withDisabledInitialNavigation', null);
}
break;
// `useHash: true` maps to the `withHashLocation` feature.
case 'useHash':
if (prop.initializer.kind === ts__default["default"].SyntaxKind.TrueKeyword) {
features.track('withHashLocation', null);
}
break;
// `errorHandler` maps to the `withNavigationErrorHandler` feature.
case 'errorHandler':
features.track('withNavigationErrorHandler', prop.initializer);
break;
// `anchorScrolling` and `scrollPositionRestoration` arguments have to be combined into an
// object literal that is passed into the `withInMemoryScrolling` feature.
case 'anchorScrolling':
case 'scrollPositionRestoration':
inMemoryScrollingOptions.push(prop);
break;
// All remaining properties can be passed through the `withRouterConfig` feature.
default:
configOptions.push(prop);
break;
}
}
if (inMemoryScrollingOptions.length > 0) {
features.track('withInMemoryScrolling', ts__default["default"].factory.createObjectLiteralExpression(inMemoryScrollingOptions));
}
if (configOptions.length > 0) {
features.track('withRouterConfig', ts__default["default"].factory.createObjectLiteralExpression(configOptions));
}
for (const [feature, featureArgs] of features.getEntries()) {
const callArgs = [];
featureArgs.forEach((arg) => {
if (arg !== null) {
callArgs.push(arg);
}
});
featureExpressions.push(ts__default["default"].factory.createCallExpression(tracker.addImport(sourceFile, feature, '@angular/router'), [], callArgs));
}
return featureExpressions;
}
/**
* Finds all the nodes that are referenced inside a root node and would need to be copied into a
* new file in order for the node to compile, and tracks them.
* @param targetFile File to which the nodes will be copied.
* @param rootNode Node within which to look for references.
* @param nodeLookup Map used to look up nodes based on their positions in a file.
* @param tracker Tracker in which changes to files are stored.
* @param nodesToCopy Set that keeps track of the nodes being copied.
* @param referenceResolver
*/
function addNodesToCopy(targetFile, rootNode, nodeLookup, tracker, nodesToCopy, referenceResolver) {
const refs = findAllSameFileReferences(rootNode, nodeLookup, referenceResolver);
for (const ref of refs) {
const importSpecifier = closestOrSelf(ref, ts__default["default"].isImportSpecifier);
const importDeclaration = importSpecifier
? nodes.closestNode(importSpecifier, ts__default["default"].isImportDeclaration)
: null;
// If the reference is in an import, we need to add an import to the main file.
if (importDeclaration &&
importSpecifier &&
ts__default["default"].isStringLiteralLike(importDeclaration.moduleSpecifier)) {
const moduleName = importDeclaration.moduleSpecifier.text.startsWith('.')
? remapRelativeImport(targetFile.fileName, importDeclaration.moduleSpecifier)
: importDeclaration.moduleSpecifier.text;
const symbolName = importSpecifier.propertyName
? importSpecifier.propertyName.text
: importSpecifier.name.text;
const alias = importSpecifier.propertyName ? importSpecifier.name.text : undefined;
tracker.addImport(targetFile, symbolName, moduleName, alias);
continue;
}
const variableDeclaration = closestOrSelf(ref, ts__default["default"].isVariableDeclaration);
const variableStatement = variableDeclaration
? nodes.closestNode(variableDeclaration, ts__default["default"].isVariableStatement)
: null;
// If the reference is a variable, we can attempt to import it or copy it over.
if (variableDeclaration && variableStatement && ts__default["default"].isIdentifier(variableDeclaration.name)) {
if (isExported(variableStatement)) {
tracker.addImport(targetFile, variableDeclaration.name.text, getRelativeImportPath(targetFile.fileName, ref.getSourceFile().fileName));
}
else {
nodesToCopy.add(variableStatement);
}
continue;
}
// Otherwise check if the reference is inside of an exportable declaration, e.g. a function.
// This code that is safe to copy over into the new file or import it, if it's exported.
const closestExportable = closestOrSelf(ref, isExportableDeclaration);
if (closestExportable) {
if (isExported(closestExportable) && closestExportable.name) {
tracker.addImport(targetFile, closestExportable.name.text, getRelativeImportPath(targetFile.fileName, ref.getSourceFile().fileName));
}
else {
nodesToCopy.add(closestExportable);
}
}
}
}
/**
* Finds all the nodes referenced within the root node in the same file.
* @param rootNode Node from which to start looking for references.
* @param nodeLookup Map used to look up nodes based on their positions in a file.
* @param referenceResolver
*/
function findAllSameFileReferences(rootNode, nodeLookup, referenceResolver) {
const results = new Set();
const traversedTopLevelNodes = new Set();
const excludeStart = rootNode.getStart();
const excludeEnd = rootNode.getEnd();
(function walk(node) {
if (!isReferenceIdentifier(node)) {
node.forEachChild(walk);
return;
}
const refs = referencesToNodeWithinSameFile(node, nodeLookup, excludeStart, excludeEnd, referenceResolver);
if (refs === null) {
return;
}
for (const ref of refs) {
if (results.has(ref)) {
continue;
}
results.add(ref);
const closestTopLevel = nodes.closestNode(ref, isTopLevelStatement);
// Avoid re-traversing the same top-level nodes since we know what the result will be.
if (!closestTopLevel || traversedTopLevelNodes.has(closestTopLevel)) {
continue;
}
// Keep searching, starting from the closest top-level node. We skip import declarations,
// because we already know about them and they may put the search into an infinite loop.
if (!ts__default["default"].isImportDeclaration(closestTopLevel) &&
isOutsideRange(excludeStart, excludeEnd, closestTopLevel.getStart(), closestTopLevel.getEnd())) {
traversedTopLevelNodes.add(closestTopLevel);
walk(closestTopLevel);
}
}
})(rootNode);
return results;
}
/**
* Finds all the nodes referring to a specific node within the same file.
* @param node Node whose references we're lookip for.
* @param nodeLookup Map used to look up nodes based on their positions in a file.
* @param excludeStart Start of a range that should be excluded from the results.
* @param excludeEnd End of a range that should be excluded from the results.
* @param referenceResolver
*/
function referencesToNodeWithinSameFile(node, nodeLookup, excludeStart, excludeEnd, referenceResolver) {
const offsets = referenceResolver
.findSameFileReferences(node, node.getSourceFile().fileName)
.filter(([start, end]) => isOutsideRange(excludeStart, excludeEnd, start, end));
if (offsets.length > 0) {
const nodes = offsetsToNodes(nodeLookup, offsets, new Set());
if (nodes.size > 0) {
return nodes;
}
}
return null;
}
/**
* Transforms a node so that any dynamic imports with relative file paths it contains are remapped
* as if they were specified in a different file. If no transformations have occurred, the original
* node will be returned.
* @param targetFileName File name to which to remap the imports.
* @param rootNode Node being transformed.
*/
function remapDynamicImports(targetFileName, rootNode) {
let hasChanged = false;
const transformer = (context) => {
return (sourceFile) => ts__default["default"].visitNode(sourceFile, function walk(node) {
if (ts__default["default"].isCallExpression(node) &&
node.expression.kind === ts__default["default"].SyntaxKind.ImportKeyword &&
node.arguments.length > 0 &&
ts__default["default"].isStringLiteralLike(node.arguments[0]) &&
node.arguments[0].text.startsWith('.')) {
hasChanged = true;
return context.factory.updateCallExpression(node, node.expression, node.typeArguments, [
context.factory.createStringLiteral(remapRelativeImport(targetFileName, node.arguments[0])),
...node.arguments.slice(1),
]);
}
return ts__default["default"].visitEachChild(node, walk, context);
});
};
const result = ts__default["default"].transform(rootNode, [transformer]).transformed[0];
return hasChanged ? result : rootNode;
}
/**
* Checks whether a node is a statement at the top level of a file.
* @param node Node to be checked.
*/
function isTopLevelStatement(node) {
return node.parent != null && ts__default["default"].isSourceFile(node.parent);
}
/**
* Asserts that a node is an identifier that might be referring to a symbol. This excludes
* identifiers of named nodes like property assignments.
* @param node Node to be checked.
*/
function isReferenceIdentifier(node) {
return (ts__default["default"].isIdentifier(node) &&
((!ts__default["default"].isPropertyAssignment(node.parent) && !ts__default["default"].isParameter(node.parent)) ||
node.parent.name !== node));
}
/**
* Checks whether a range is completely outside of another range.
* @param excludeStart Start of the exclusion range.
* @param excludeEnd End of the exclusion range.
* @param start Start of the range that is being checked.
* @param end End of the range that is being checked.
*/
function isOutsideRange(excludeStart, excludeEnd, start, end) {
return (start < excludeStart && end < excludeStart) || start > excludeEnd;
}
/**
* Remaps the specifier of a relative import from its original location to a new one.
* @param targetFileName Name of the file that the specifier will be moved to.
* @param specifier Specifier whose path is being remapped.
*/
function remapRelativeImport(targetFileName, specifier) {
return getRelativeImportPath(targetFileName, p.join(p.dirname(specifier.getSourceFile().fileName), specifier.text));
}
/**
* Whether a node is exported.
* @param node Node to be checked.
*/
function isExported(node) {
return ts__default["default"].canHaveModifiers(node) && node.modifiers
? node.modifiers.some((modifier) => modifier.kind === ts__default["default"].SyntaxKind.ExportKeyword)
: false;
}
/**
* Asserts that a node is an exportable declaration, which means that it can either be exported or
* it can be safely copied into another file.
* @param node Node to be checked.
*/
function isExportableDeclaration(node) {
return (ts__default["default"].isEnumDeclaration(node) ||
ts__default["default"].isClassDeclaration(node) ||
ts__default["default"].isFunctionDeclaration(node) ||
ts__default["default"].isInterfaceDeclaration(node) ||
ts__default["default"].isTypeAliasDeclaration(node));
}
/**
* Gets the index after the last import in a file. Can be used to insert new code into the file.
* @param sourceFile File in which to search for imports.
*/
function getLastImportEnd(sourceFile) {
let index = 0;
for (const statement of sourceFile.statements) {
if (ts__default["default"].isImportDeclaration(statement)) {
index = Math.max(index, statement.getEnd());
}
else {
break;
}
}
return index;
}
/** Checks if any of the program's files has an import of a specific module. */
function hasImport(program, rootFileNames, moduleName) {
const tsProgram = program.getTsProgram();
const deepImportStart = moduleName + '/';
for (const fileName of rootFileNames) {
const sourceFile = tsProgram.getSourceFile(fileName);
if (!sourceFile) {
continue;
}
for (const statement of sourceFile.statements) {
if (ts__default["default"].isImportDeclaration(statement) &&
ts__default["default"].isStringLiteralLike(statement.moduleSpecifier) &&
(statement.moduleSpecifier.text === moduleName ||
statement.moduleSpecifier.text.startsWith(deepImportStart))) {
return true;
}
}
}
return false;
}
var MigrationMode;
(function (MigrationMode) {
MigrationMode["toStandalone"] = "convert-to-standalone";
MigrationMode["pruneModules"] = "prune-ng-modules";
MigrationMode["standaloneBootstrap"] = "standalone-bootstrap";
})(MigrationMode || (MigrationMode = {}));
function migrate(options) {
return async (tree, context) => {
const { buildPaths, testPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(tree);
const basePath = process.cwd();
const allPaths = [...buildPaths, ...testPaths];
// TS and Schematic use paths in POSIX format even on Windows. This is needed as otherwise
// string matching such as `sourceFile.fileName.startsWith(pathToMigrate)` might not work.
const pathToMigrate = compiler_host.normalizePath(p.join(basePath, options.path));
let migratedFiles = 0;
if (!allPaths.length) {
throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the standalone migration.');
}
for (const tsconfigPath of allPaths) {
migratedFiles += standaloneMigration(tree, tsconfigPath, basePath, pathToMigrate, options);
}
if (migratedFiles === 0) {
throw new schematics.SchematicsException(`Could not find any files to migrate under the path ${pathToMigrate}. Cannot run the standalone migration.`);
}
context.logger.info('🎉 Automated migration step has finished! 🎉');
context.logger.info('IMPORTANT! Please verify manually that your application builds and behaves as expected.');
context.logger.info(`See https://angular.dev/reference/migrations/standalone for more information.`);
};
}
function standaloneMigration(tree, tsconfigPath, basePath, pathToMigrate, schematicOptions, oldProgram) {
if (schematicOptions.path.startsWith('..')) {
throw new schematics.SchematicsException('Cannot run standalone migration outside of the current project.');
}
const { host, options, rootNames } = compiler_host.createProgramOptions(tree, tsconfigPath, basePath, undefined, undefined, {
_enableTemplateTypeChecker: true, // Required for the template type checker to work.
compileNonExportedClasses: true, // We want to migrate non-exported classes too.
// Avoid checking libraries to speed up the migration.
skipLibCheck: true,
skipDefaultLibCheck: true,
});
const referenceLookupExcludedFiles = /node_modules|\.ngtypecheck\.ts/;
const program = createProgram({ rootNames, host, options, oldProgram });
const printer = ts__default["default"].createPrinter();
if (fs.existsSync(pathToMigrate) && !fs.statSync(pathToMigrate).isDirectory()) {
throw new schematics.SchematicsException(`Migration path ${pathToMigrate} has to be a directory. Cannot run the standalone migration.`);
}
const sourceFiles = program
.getTsProgram()
.getSourceFiles()
.filter((sourceFile) => sourceFile.fileName.startsWith(pathToMigrate) &&
compiler_host.canMigrateFile(basePath, sourceFile, program.getTsProgram()));
if (sourceFiles.length === 0) {
return 0;
}
let pendingChanges;
let filesToRemove = null;
if (schematicOptions.mode === MigrationMode.pruneModules) {
const result = pruneNgModules(program, host, basePath, rootNames, sourceFiles, printer, undefined, referenceLookupExcludedFiles, knownInternalAliasRemapper);
pendingChanges = result.pendingChanges;
filesToRemove = result.filesToRemove;
}
else if (schematicOptions.mode === MigrationMode.standaloneBootstrap) {
pendingChanges = toStandaloneBootstrap(program, host, basePath, rootNames, sourceFiles, printer, undefined, referenceLookupExcludedFiles, knownInternalAliasRemapper);
}
else {
// This shouldn't happen, but default to `MigrationMode.toStandalone` just in case.
pendingChanges = toStandalone(sourceFiles, program, printer, undefined, knownInternalAliasRemapper);
}
for (const [file, changes] of pendingChanges.entries()) {
// Don't attempt to edit a file if it's going to be deleted.
if (filesToRemove?.has(file)) {
continue;
}
const update = tree.beginUpdate(p.relative(basePath, file.fileName));
changes.forEach((change) => {
if (change.removeLength != null) {
update.remove(change.start, change.removeLength);
}
update.insertRight(change.start, change.text);
});
tree.commitUpdate(update);
}
if (filesToRemove) {
for (const file of filesToRemove) {
tree.delete(p.relative(basePath, file.fileName));
}
}
// Run the module pruning after the standalone bootstrap to automatically remove the root module.
// Note that we can't run the module pruning internally without propagating the changes to disk,
// because there may be conflicting AST node changes.
if (schematicOptions.mode === MigrationMode.standaloneBootstrap) {
return (standaloneMigration(tree, tsconfigPath, basePath, pathToMigrate, { ...schematicOptions, mode: MigrationMode.pruneModules }, program) + sourceFiles.length);
}
return sourceFiles.length;
}
exports.migrate = migrate;
© 2015 - 2025 Weber Informatics LLC | Privacy Policy