You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

@angular/core

Package Overview
Dependencies
Maintainers
2
Versions
1020
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@angular/core - npm Package Compare versions

Comparing version
20.3.15
to
20.3.16
+67
schematics/bundles/apply_import_manager-C-hIllzM.cjs
'use strict';
/**
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
require('os');
var project_tsconfig_paths = require('./project_tsconfig_paths-C5dXM8JP.cjs');
var project_paths = require('./project_paths-qw-vHpyb.cjs');
/**
* Applies import manager changes, and writes them as replacements the
* given result array.
*/
function applyImportManagerChanges(importManager, replacements, sourceFiles, info) {
const { newImports, updatedImports, deletedImports } = importManager.finalize();
const printer = ts.createPrinter({});
const pathToFile = new Map(sourceFiles.map((s) => [s.fileName, s]));
// Capture new imports
newImports.forEach((newImports, fileName) => {
newImports.forEach((newImport) => {
const printedImport = printer.printNode(ts.EmitHint.Unspecified, newImport, pathToFile.get(fileName));
replacements.push(new project_paths.Replacement(project_paths.projectFile(project_tsconfig_paths.absoluteFrom(fileName), info), new project_paths.TextUpdate({ position: 0, end: 0, toInsert: `${printedImport}\n` })));
});
});
// Capture updated imports
for (const [oldBindings, newBindings] of updatedImports.entries()) {
// The import will be generated as multi-line if it already is multi-line,
// or if the number of elements significantly increased and it previously
// consisted of very few specifiers.
const isMultiline = oldBindings.getText().includes('\n') ||
(newBindings.elements.length >= 6 && oldBindings.elements.length <= 3);
const hasSpaceBetweenBraces = oldBindings.getText().startsWith('{ ');
let formatFlags = ts.ListFormat.NamedImportsOrExportsElements |
ts.ListFormat.Indented |
ts.ListFormat.Braces |
ts.ListFormat.PreserveLines |
(isMultiline ? ts.ListFormat.MultiLine : ts.ListFormat.SingleLine);
if (hasSpaceBetweenBraces) {
formatFlags |= ts.ListFormat.SpaceBetweenBraces;
}
else {
formatFlags &= ~ts.ListFormat.SpaceBetweenBraces;
}
const printedBindings = printer.printList(formatFlags, newBindings.elements, oldBindings.getSourceFile());
replacements.push(new project_paths.Replacement(project_paths.projectFile(oldBindings.getSourceFile(), info), new project_paths.TextUpdate({
position: oldBindings.getStart(),
end: oldBindings.getEnd(),
// TS uses four spaces as indent. We migrate to two spaces as we
// assume this to be more common.
toInsert: printedBindings.replace(/^ {4}/gm, ' '),
})));
}
// Update removed imports
for (const removedImport of deletedImports) {
replacements.push(new project_paths.Replacement(project_paths.projectFile(removedImport.getSourceFile(), info), new project_paths.TextUpdate({
position: removedImport.getStart(),
end: removedImport.getEnd(),
toInsert: '',
})));
}
}
exports.applyImportManagerChanges = applyImportManagerChanges;
'use strict';
/**
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
var project_tsconfig_paths = require('./project_tsconfig_paths-C5dXM8JP.cjs');
require('os');
var p = require('path');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var p__namespace = /*#__PURE__*/_interopNamespaceDefault(p);
/** Tracks changes that have to be made for specific files. */
class ChangeTracker {
_printer;
_importRemapper;
_changes = new Map();
_importManager;
_quotesCache = new WeakMap();
constructor(_printer, _importRemapper) {
this._printer = _printer;
this._importRemapper = _importRemapper;
this._importManager = new project_tsconfig_paths.ImportManager({
shouldUseSingleQuotes: (file) => this._getQuoteKind(file) === 0 /* QuoteKind.SINGLE */,
});
}
/**
* Tracks the insertion of some text.
* @param sourceFile File in which the text is being inserted.
* @param start Index at which the text is insert.
* @param text Text to be inserted.
*/
insertText(sourceFile, index, text) {
this._trackChange(sourceFile, { start: index, text });
}
/**
* Replaces text within a file.
* @param sourceFile File in which to replace the text.
* @param start Index from which to replace the text.
* @param removeLength Length of the text being replaced.
* @param text Text to be inserted instead of the old one.
*/
replaceText(sourceFile, start, removeLength, text) {
this._trackChange(sourceFile, { start, removeLength, text });
}
/**
* Replaces the text of an AST node with a new one.
* @param oldNode Node to be replaced.
* @param newNode New node to be inserted.
* @param emitHint Hint when formatting the text of the new node.
* @param sourceFileWhenPrinting File to use when printing out the new node. This is important
* when copying nodes from one file to another, because TypeScript might not output literal nodes
* without it.
*/
replaceNode(oldNode, newNode, emitHint = ts.EmitHint.Unspecified, sourceFileWhenPrinting) {
const sourceFile = oldNode.getSourceFile();
this.replaceText(sourceFile, oldNode.getStart(), oldNode.getWidth(), this._printer.printNode(emitHint, newNode, sourceFileWhenPrinting || sourceFile));
}
/**
* Removes the text of an AST node from a file.
* @param node Node whose text should be removed.
* @param useFullOffsets Whether to remove the node using its full offset (e.g. `getFullStart`
* rather than `fullStart`). This has the advantage of removing any comments that may be tied
* to the node, but can lead to too much code being deleted.
*/
removeNode(node, useFullOffsets = false) {
this._trackChange(node.getSourceFile(), {
start: useFullOffsets ? node.getFullStart() : node.getStart(),
removeLength: useFullOffsets ? node.getFullWidth() : node.getWidth(),
text: '',
});
}
/**
* Adds an import to a file.
* @param sourceFile File to which to add the import.
* @param symbolName Symbol being imported.
* @param moduleName Module from which the symbol is imported.
* @param alias Alias to use for the import.
*/
addImport(sourceFile, symbolName, moduleName, alias) {
if (this._importRemapper) {
moduleName = this._importRemapper(moduleName, sourceFile.fileName);
}
// It's common for paths to be manipulated with Node's `path` utilties which
// can yield a path with back slashes. Normalize them since outputting such
// paths will also cause TS to escape the forward slashes.
moduleName = normalizePath(moduleName);
if (!this._changes.has(sourceFile)) {
this._changes.set(sourceFile, []);
}
return this._importManager.addImport({
requestedFile: sourceFile,
exportSymbolName: symbolName,
exportModuleSpecifier: moduleName,
unsafeAliasOverride: alias,
});
}
/**
* Removes an import from a file.
* @param sourceFile File from which to remove the import.
* @param symbolName Original name of the symbol to be removed. Used even if the import is aliased.
* @param moduleName Module from which the symbol is imported.
*/
removeImport(sourceFile, symbolName, moduleName) {
// It's common for paths to be manipulated with Node's `path` utilties which
// can yield a path with back slashes. Normalize them since outputting such
// paths will also cause TS to escape the forward slashes.
moduleName = normalizePath(moduleName);
if (!this._changes.has(sourceFile)) {
this._changes.set(sourceFile, []);
}
this._importManager.removeImport(sourceFile, symbolName, moduleName);
}
/**
* Gets the changes that should be applied to all the files in the migration.
* The changes are sorted in the order in which they should be applied.
*/
recordChanges() {
this._recordImports();
return this._changes;
}
/**
* Clear the tracked changes
*/
clearChanges() {
this._changes.clear();
}
/**
* Adds a change to a `ChangesByFile` map.
* @param file File that the change is associated with.
* @param change Change to be added.
*/
_trackChange(file, change) {
const changes = this._changes.get(file);
if (changes) {
// Insert the changes in reverse so that they're applied in reverse order.
// This ensures that the offsets of subsequent changes aren't affected by
// previous changes changing the file's text.
const insertIndex = changes.findIndex((current) => current.start <= change.start);
if (insertIndex === -1) {
changes.push(change);
}
else {
changes.splice(insertIndex, 0, change);
}
}
else {
this._changes.set(file, [change]);
}
}
/** Determines what kind of quotes to use for a specific file. */
_getQuoteKind(sourceFile) {
if (this._quotesCache.has(sourceFile)) {
return this._quotesCache.get(sourceFile);
}
let kind = 0 /* QuoteKind.SINGLE */;
for (const statement of sourceFile.statements) {
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
kind = statement.moduleSpecifier.getText()[0] === '"' ? 1 /* QuoteKind.DOUBLE */ : 0 /* QuoteKind.SINGLE */;
this._quotesCache.set(sourceFile, kind);
break;
}
}
return kind;
}
/** Records the pending import changes from the import manager. */
_recordImports() {
const { newImports, updatedImports, deletedImports } = this._importManager.finalize();
for (const [original, replacement] of updatedImports) {
this.replaceNode(original, replacement);
}
for (const node of deletedImports) {
this.removeNode(node);
}
for (const [sourceFile] of this._changes) {
const importsToAdd = newImports.get(sourceFile.fileName);
if (!importsToAdd) {
continue;
}
const importLines = [];
let lastImport = null;
for (const statement of sourceFile.statements) {
if (ts.isImportDeclaration(statement)) {
lastImport = statement;
}
}
for (const decl of importsToAdd) {
importLines.push(this._printer.printNode(ts.EmitHint.Unspecified, decl, sourceFile));
}
this.insertText(sourceFile, lastImport ? lastImport.getEnd() : 0, (lastImport ? '\n' : '') + importLines.join('\n'));
}
}
}
/** Normalizes a path to use posix separators. */
function normalizePath(path) {
return path.replace(/\\/g, '/');
}
function parseTsconfigFile(tsconfigPath, basePath) {
const { config } = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
const parseConfigHost = {
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
fileExists: ts.sys.fileExists,
readDirectory: ts.sys.readDirectory,
readFile: ts.sys.readFile,
};
// Throw if incorrect arguments are passed to this function. Passing relative base paths
// results in root directories not being resolved and in later type checking runtime errors.
// More details can be found here: https://github.com/microsoft/TypeScript/issues/37731.
if (!p__namespace.isAbsolute(basePath)) {
throw Error('Unexpected relative base path has been specified.');
}
return ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, {});
}
/**
* Creates a TypeScript program instance for a TypeScript project within
* the virtual file system tree.
* @param tree Virtual file system tree that contains the source files.
* @param tsconfigPath Virtual file system path that resolves to the TypeScript project.
* @param basePath Base path for the virtual file system tree.
* @param fakeFileRead Optional file reader function. Can be used to overwrite files in
* the TypeScript program, or to add in-memory files (e.g. to add global types).
* @param additionalFiles Additional file paths that should be added to the program.
*/
function createMigrationProgram(tree, tsconfigPath, basePath, fakeFileRead, additionalFiles) {
const { rootNames, options, host } = createProgramOptions(tree, tsconfigPath, basePath, fakeFileRead);
return ts.createProgram(rootNames, options, host);
}
/**
* Creates the options necessary to instantiate a TypeScript program.
* @param tree Virtual file system tree that contains the source files.
* @param tsconfigPath Virtual file system path that resolves to the TypeScript project.
* @param basePath Base path for the virtual file system tree.
* @param fakeFileRead Optional file reader function. Can be used to overwrite files in
* the TypeScript program, or to add in-memory files (e.g. to add global types).
* @param additionalFiles Additional file paths that should be added to the program.
* @param optionOverrides Overrides of the parsed compiler options.
*/
function createProgramOptions(tree, tsconfigPath, basePath, fakeFileRead, additionalFiles, optionOverrides) {
// Resolve the tsconfig path to an absolute path. This is needed as TypeScript otherwise
// is not able to resolve root directories in the given tsconfig. More details can be found
// in the following issue: https://github.com/microsoft/TypeScript/issues/37731.
tsconfigPath = p.resolve(basePath, tsconfigPath);
const parsed = parseTsconfigFile(tsconfigPath, p.dirname(tsconfigPath));
const options = optionOverrides ? { ...parsed.options, ...optionOverrides } : parsed.options;
const host = createMigrationCompilerHost(tree, options, basePath, fakeFileRead);
return { rootNames: parsed.fileNames.concat([]), options, host };
}
function createMigrationCompilerHost(tree, options, basePath, fakeRead) {
const host = ts.createCompilerHost(options, true);
const defaultReadFile = host.readFile;
// We need to overwrite the host "readFile" method, as we want the TypeScript
// program to be based on the file contents in the virtual file tree. Otherwise
// if we run multiple migrations we might have intersecting changes and
// source files.
host.readFile = (fileName) => {
const treeRelativePath = p.relative(basePath, fileName);
let result = fakeRead?.(treeRelativePath);
if (typeof result !== 'string') {
// If the relative path resolved to somewhere outside of the tree, fall back to
// TypeScript's default file reading function since the `tree` will throw an error.
result = treeRelativePath.startsWith('..')
? defaultReadFile.call(host, fileName)
: tree.read(treeRelativePath)?.toString();
}
// Strip BOM as otherwise TSC methods (Ex: getWidth) will return an offset,
// which breaks the CLI UpdateRecorder.
// See: https://github.com/angular/angular/pull/30719
return typeof result === 'string' ? result.replace(/^\uFEFF/, '') : undefined;
};
return host;
}
/**
* Checks whether a file can be migrate by our automated migrations.
* @param basePath Absolute path to the project.
* @param sourceFile File being checked.
* @param program Program that includes the source file.
*/
function canMigrateFile(basePath, sourceFile, program) {
// We shouldn't migrate .d.ts files, files from an external library or type checking files.
if (sourceFile.fileName.endsWith('.ngtypecheck.ts') ||
sourceFile.isDeclarationFile ||
program.isSourceFileFromExternalLibrary(sourceFile)) {
return false;
}
// Our migrations are set up to create a `Program` from the project's tsconfig and to migrate all
// the files within the program. This can include files that are outside of the Angular CLI
// project. We can't migrate files outside of the project, because our file system interactions
// go through the CLI's `Tree` which assumes that all files are within the project. See:
// https://github.com/angular/angular-cli/blob/0b0961c9c233a825b6e4bb59ab7f0790f9b14676/packages/angular_devkit/schematics/src/tree/host-tree.ts#L131
return !p.relative(basePath, sourceFile.fileName).startsWith('..');
}
exports.ChangeTracker = ChangeTracker;
exports.canMigrateFile = canMigrateFile;
exports.createMigrationProgram = createMigrationProgram;
exports.createProgramOptions = createProgramOptions;
exports.normalizePath = normalizePath;
'use strict';
/**
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
require('os');
var project_tsconfig_paths = require('./project_tsconfig_paths-C5dXM8JP.cjs');
var index = require('./index-CEqacA4Q.cjs');
require('path');
require('node:path');
var project_paths = require('./project_paths-qw-vHpyb.cjs');
function getMemberName(member) {
if (member.name === undefined) {
return null;
}
if (ts.isIdentifier(member.name) || ts.isStringLiteralLike(member.name)) {
return member.name.text;
}
if (ts.isPrivateIdentifier(member.name)) {
return `#${member.name.text}`;
}
return null;
}
/** Checks whether the given node can be an `@Input()` declaration node. */
function isInputContainerNode(node) {
return (((ts.isAccessor(node) && ts.isClassDeclaration(node.parent)) ||
ts.isPropertyDeclaration(node)) &&
getMemberName(node) !== null);
}
/**
* Detects `query(By.directive(T)).componentInstance` patterns and enhances
* them with information of `T`. This is important because `.componentInstance`
* is currently typed as `any` and may cause runtime test failures after input
* migrations then.
*
* The reference resolution pass leverages information from this pattern
* recognizer.
*/
class DebugElementComponentInstance {
checker;
cache = new WeakMap();
constructor(checker) {
this.checker = checker;
}
detect(node) {
if (this.cache.has(node)) {
return this.cache.get(node);
}
if (!ts.isPropertyAccessExpression(node)) {
return null;
}
// Check for `<>.componentInstance`.
if (!ts.isIdentifier(node.name) || node.name.text !== 'componentInstance') {
return null;
}
// Check for `<>.query(..).<>`.
if (!ts.isCallExpression(node.expression) ||
!ts.isPropertyAccessExpression(node.expression.expression) ||
!ts.isIdentifier(node.expression.expression.name) ||
node.expression.expression.name.text !== 'query') {
return null;
}
const queryCall = node.expression;
if (queryCall.arguments.length !== 1) {
return null;
}
const queryArg = queryCall.arguments[0];
let typeExpr;
if (ts.isCallExpression(queryArg) &&
queryArg.arguments.length === 1 &&
ts.isIdentifier(queryArg.arguments[0])) {
// Detect references, like: `query(By.directive(T))`.
typeExpr = queryArg.arguments[0];
}
else if (ts.isIdentifier(queryArg)) {
// Detect references, like: `harness.query(T)`.
typeExpr = queryArg;
}
else {
return null;
}
const symbol = this.checker.getSymbolAtLocation(typeExpr);
if (symbol?.valueDeclaration === undefined ||
!ts.isClassDeclaration(symbol?.valueDeclaration)) {
// Cache this as we use the expensive type checker.
this.cache.set(node, null);
return null;
}
const type = this.checker.getTypeAtLocation(symbol.valueDeclaration);
this.cache.set(node, type);
return type;
}
}
/**
* Recognizes `Partial<T>` instances in Catalyst tests. Those type queries
* are likely used for typing property initialization values for the given class `T`
* and we have a few scenarios:
*
* 1. The API does not unwrap signal inputs. In which case, the values are likely no
* longer assignable to an `InputSignal`.
* 2. The API does unwrap signal inputs, in which case we need to unwrap the `Partial`
* because the values are raw initial values, like they were before.
*
* We can enable this heuristic when we detect Catalyst as we know it supports unwrapping.
*/
class PartialDirectiveTypeInCatalystTests {
checker;
knownFields;
constructor(checker, knownFields) {
this.checker = checker;
this.knownFields = knownFields;
}
detect(node) {
// Detect `Partial<...>`
if (!ts.isTypeReferenceNode(node) ||
!ts.isIdentifier(node.typeName) ||
node.typeName.text !== 'Partial') {
return null;
}
// Ignore if the source file doesn't reference Catalyst.
if (!node.getSourceFile().text.includes('angular2/testing/catalyst')) {
return null;
}
// Extract T of `Partial<T>`.
const cmpTypeArg = node.typeArguments?.[0];
if (!cmpTypeArg ||
!ts.isTypeReferenceNode(cmpTypeArg) ||
!ts.isIdentifier(cmpTypeArg.typeName)) {
return null;
}
const cmpType = cmpTypeArg.typeName;
const symbol = this.checker.getSymbolAtLocation(cmpType);
// Note: Technically the class might be derived of an input-containing class,
// but this is out of scope for now. We can expand if we see it's a common case.
if (symbol?.valueDeclaration === undefined ||
!ts.isClassDeclaration(symbol.valueDeclaration) ||
!this.knownFields.shouldTrackClassReference(symbol.valueDeclaration)) {
return null;
}
return { referenceNode: node, targetClass: symbol.valueDeclaration };
}
}
/**
* Attempts to look up the given property access chain using
* the type checker.
*
* Notably this is not as safe as using the type checker directly to
* retrieve symbols of a given identifier, but in some cases this is
* a necessary approach to compensate e.g. for a lack of TCB information
* when processing Angular templates.
*
* The path is a list of properties to be accessed sequentially on the
* given type.
*/
function lookupPropertyAccess(checker, type, path, options = {}) {
let symbol = null;
for (const propName of path) {
// Note: We support assuming `NonNullable` for the pathl This is necessary
// in some situations as otherwise the lookups would fail to resolve the target
// symbol just because of e.g. a ternary. This is used in the signal input migration
// for host bindings.
type = options.ignoreNullability ? type.getNonNullableType() : type;
const propSymbol = type.getProperty(propName);
if (propSymbol === undefined) {
return null;
}
symbol = propSymbol;
type = checker.getTypeOfSymbol(propSymbol);
}
if (symbol === null) {
return null;
}
return { symbol, type };
}
/**
* AST visitor that iterates through a template and finds all
* input references.
*
* This resolution is important to be able to migrate references to inputs
* that will be migrated to signal inputs.
*/
class TemplateReferenceVisitor extends project_tsconfig_paths.RecursiveVisitor {
result = [];
/**
* Whether we are currently descending into HTML AST nodes
* where all bound attributes are considered potentially narrowing.
*
* Keeps track of all referenced inputs in such attribute expressions.
*/
templateAttributeReferencedFields = null;
expressionVisitor;
seenKnownFieldsCount = new Map();
constructor(typeChecker, templateTypeChecker, componentClass, knownFields, fieldNamesToConsiderForReferenceLookup) {
super();
this.expressionVisitor = new TemplateExpressionReferenceVisitor(typeChecker, templateTypeChecker, componentClass, knownFields, fieldNamesToConsiderForReferenceLookup);
}
checkExpressionForReferencedFields(activeNode, expressionNode) {
const referencedFields = this.expressionVisitor.checkTemplateExpression(activeNode, expressionNode);
// Add all references to the overall visitor result.
this.result.push(...referencedFields);
// Count usages of seen input references. We'll use this to make decisions
// based on whether inputs are potentially narrowed or not.
for (const input of referencedFields) {
this.seenKnownFieldsCount.set(input.targetField.key, (this.seenKnownFieldsCount.get(input.targetField.key) ?? 0) + 1);
}
return referencedFields;
}
descendAndCheckForNarrowedSimilarReferences(potentiallyNarrowedInputs, descend) {
const inputs = potentiallyNarrowedInputs.map((i) => ({
ref: i,
key: i.targetField.key,
pastCount: this.seenKnownFieldsCount.get(i.targetField.key) ?? 0,
}));
descend();
for (const input of inputs) {
// Input was referenced inside a narrowable spot, and is used in child nodes.
// This is a sign for the input to be narrowed. Mark it as such.
if ((this.seenKnownFieldsCount.get(input.key) ?? 0) > input.pastCount) {
input.ref.isLikelyNarrowed = true;
}
}
}
visitTemplate(template) {
// Note: We assume all bound expressions for templates may be subject
// to TCB narrowing. This is relevant for now until we support narrowing
// of signal calls in templates.
// TODO: Remove with: https://github.com/angular/angular/pull/55456.
this.templateAttributeReferencedFields = [];
project_tsconfig_paths.visitAll(this, template.attributes);
project_tsconfig_paths.visitAll(this, template.templateAttrs);
// If we are dealing with a microsyntax template, do not check
// inputs and outputs as those are already passed to the children.
// Template attributes may contain relevant expressions though.
if (template.tagName === 'ng-template') {
project_tsconfig_paths.visitAll(this, template.inputs);
project_tsconfig_paths.visitAll(this, template.outputs);
}
const referencedInputs = this.templateAttributeReferencedFields;
this.templateAttributeReferencedFields = null;
this.descendAndCheckForNarrowedSimilarReferences(referencedInputs, () => {
project_tsconfig_paths.visitAll(this, template.children);
project_tsconfig_paths.visitAll(this, template.references);
project_tsconfig_paths.visitAll(this, template.variables);
});
}
visitIfBlockBranch(block) {
if (block.expression) {
const referencedFields = this.checkExpressionForReferencedFields(block, block.expression);
this.descendAndCheckForNarrowedSimilarReferences(referencedFields, () => {
super.visitIfBlockBranch(block);
});
}
else {
super.visitIfBlockBranch(block);
}
}
visitForLoopBlock(block) {
this.checkExpressionForReferencedFields(block, block.expression);
this.checkExpressionForReferencedFields(block, block.trackBy);
super.visitForLoopBlock(block);
}
visitSwitchBlock(block) {
const referencedFields = this.checkExpressionForReferencedFields(block, block.expression);
this.descendAndCheckForNarrowedSimilarReferences(referencedFields, () => {
super.visitSwitchBlock(block);
});
}
visitSwitchBlockCase(block) {
if (block.expression) {
const referencedFields = this.checkExpressionForReferencedFields(block, block.expression);
this.descendAndCheckForNarrowedSimilarReferences(referencedFields, () => {
super.visitSwitchBlockCase(block);
});
}
else {
super.visitSwitchBlockCase(block);
}
}
visitDeferredBlock(deferred) {
if (deferred.triggers.when) {
this.checkExpressionForReferencedFields(deferred, deferred.triggers.when.value);
}
if (deferred.prefetchTriggers.when) {
this.checkExpressionForReferencedFields(deferred, deferred.prefetchTriggers.when.value);
}
super.visitDeferredBlock(deferred);
}
visitBoundText(text) {
this.checkExpressionForReferencedFields(text, text.value);
}
visitBoundEvent(attribute) {
this.checkExpressionForReferencedFields(attribute, attribute.handler);
}
visitBoundAttribute(attribute) {
const referencedFields = this.checkExpressionForReferencedFields(attribute, attribute.value);
// Attributes inside templates are potentially "narrowed" and hence we
// keep track of all referenced inputs to see if they actually are.
if (this.templateAttributeReferencedFields !== null) {
this.templateAttributeReferencedFields.push(...referencedFields);
}
}
}
/**
* Expression AST visitor that checks whether a given expression references
* a known `@Input()`.
*
* This resolution is important to be able to migrate references to inputs
* that will be migrated to signal inputs.
*/
class TemplateExpressionReferenceVisitor extends project_tsconfig_paths.RecursiveAstVisitor {
typeChecker;
templateTypeChecker;
componentClass;
knownFields;
fieldNamesToConsiderForReferenceLookup;
activeTmplAstNode = null;
detectedInputReferences = [];
isInsideObjectShorthandExpression = false;
insideConditionalExpressionsWithReads = [];
constructor(typeChecker, templateTypeChecker, componentClass, knownFields, fieldNamesToConsiderForReferenceLookup) {
super();
this.typeChecker = typeChecker;
this.templateTypeChecker = templateTypeChecker;
this.componentClass = componentClass;
this.knownFields = knownFields;
this.fieldNamesToConsiderForReferenceLookup = fieldNamesToConsiderForReferenceLookup;
}
/** Checks the given AST expression. */
checkTemplateExpression(activeNode, expressionNode) {
this.detectedInputReferences = [];
this.activeTmplAstNode = activeNode;
expressionNode.visit(this, []);
return this.detectedInputReferences;
}
visit(ast, context) {
super.visit(ast, [...context, ast]);
}
// Keep track when we are inside an object shorthand expression. This is
// necessary as we need to expand the shorthand to invoke a potential new signal.
// E.g. `{bla}` may be transformed to `{bla: bla()}`.
visitLiteralMap(ast, context) {
for (const [idx, key] of ast.keys.entries()) {
this.isInsideObjectShorthandExpression = !!key.isShorthandInitialized;
ast.values[idx].visit(this, context);
this.isInsideObjectShorthandExpression = false;
}
}
visitPropertyRead(ast, context) {
this._inspectPropertyAccess(ast, false, context);
super.visitPropertyRead(ast, context);
}
visitSafePropertyRead(ast, context) {
this._inspectPropertyAccess(ast, false, context);
super.visitPropertyRead(ast, context);
}
visitBinary(ast, context) {
if (ast.operation === '=' && ast.left instanceof project_tsconfig_paths.PropertyRead) {
this._inspectPropertyAccess(ast.left, true, [...context, ast, ast.left]);
}
else {
super.visitBinary(ast, context);
}
}
visitConditional(ast, context) {
this.visit(ast.condition, context);
this.insideConditionalExpressionsWithReads.push(ast.condition);
this.visit(ast.trueExp, context);
this.visit(ast.falseExp, context);
this.insideConditionalExpressionsWithReads.pop();
}
/**
* Inspects the property access and attempts to resolve whether they access
* a known field. If so, the result is captured.
*/
_inspectPropertyAccess(ast, isAssignment, astPath) {
if (this.fieldNamesToConsiderForReferenceLookup !== null &&
!this.fieldNamesToConsiderForReferenceLookup.has(ast.name)) {
return;
}
const isWrite = !!(isAssignment ||
(this.activeTmplAstNode && isTwoWayBindingNode(this.activeTmplAstNode)));
this._checkAccessViaTemplateTypeCheckBlock(ast, isWrite, astPath) ||
this._checkAccessViaOwningComponentClassType(ast, isWrite, astPath);
}
/**
* Checks whether the node refers to an input using the TCB information.
* Type check block may not exist for e.g. test components, so this can return `null`.
*/
_checkAccessViaTemplateTypeCheckBlock(ast, isWrite, astPath) {
// There might be no template type checker. E.g. if we check host bindings.
if (this.templateTypeChecker === null) {
return false;
}
const symbol = this.templateTypeChecker.getSymbolOfNode(ast, this.componentClass);
if (symbol?.kind !== project_tsconfig_paths.SymbolKind.Expression || symbol.tsSymbol === null) {
return false;
}
// Dangerous: Type checking symbol retrieval is a totally different `ts.Program`,
// than the one where we analyzed `knownInputs`.
// --> Find the input via its input id.
const targetInput = this.knownFields.attemptRetrieveDescriptorFromSymbol(symbol.tsSymbol);
if (targetInput === null) {
return false;
}
this.detectedInputReferences.push({
targetNode: targetInput.node,
targetField: targetInput,
read: ast,
readAstPath: astPath,
context: this.activeTmplAstNode,
isLikelyNarrowed: this._isPartOfNarrowingTernary(ast),
isObjectShorthandExpression: this.isInsideObjectShorthandExpression,
isWrite,
});
return true;
}
/**
* Simple resolution checking whether the given AST refers to a known input.
* This is a fallback for when there is no type checking information (e.g. in host bindings).
*
* It attempts to resolve references by traversing accesses of the "component class" type.
* e.g. `this.bla` is resolved via `CompType#bla` and further.
*/
_checkAccessViaOwningComponentClassType(ast, isWrite, astPath) {
// We might check host bindings, which can never point to template variables or local refs.
const expressionTemplateTarget = this.templateTypeChecker === null
? null
: this.templateTypeChecker.getExpressionTarget(ast, this.componentClass);
// Skip checking if:
// - the reference resolves to a template variable or local ref. No way to resolve without TCB.
// - the owning component does not have a name (should not happen technically).
if (expressionTemplateTarget !== null || this.componentClass.name === undefined) {
return;
}
const property = traverseReceiverAndLookupSymbol(ast, this.componentClass, this.typeChecker);
if (property === null) {
return;
}
const matchingTarget = this.knownFields.attemptRetrieveDescriptorFromSymbol(property);
if (matchingTarget === null) {
return;
}
this.detectedInputReferences.push({
targetNode: matchingTarget.node,
targetField: matchingTarget,
read: ast,
readAstPath: astPath,
context: this.activeTmplAstNode,
isLikelyNarrowed: this._isPartOfNarrowingTernary(ast),
isObjectShorthandExpression: this.isInsideObjectShorthandExpression,
isWrite,
});
}
_isPartOfNarrowingTernary(read) {
// Note: We do not safe check that the reads are fully matching 1:1. This is acceptable
// as worst case we just skip an input from being migrated. This is very unlikely too.
return this.insideConditionalExpressionsWithReads.some((r) => (r instanceof project_tsconfig_paths.PropertyRead || r instanceof project_tsconfig_paths.SafePropertyRead) && r.name === read.name);
}
}
/**
* Emulates an access to a given field using the TypeScript `ts.Type`
* of the given class. The resolved symbol of the access is returned.
*/
function traverseReceiverAndLookupSymbol(readOrWrite, componentClass, checker) {
const path = [readOrWrite.name];
let node = readOrWrite;
while (node.receiver instanceof project_tsconfig_paths.PropertyRead) {
node = node.receiver;
path.unshift(node.name);
}
if (!(node.receiver instanceof project_tsconfig_paths.ImplicitReceiver || node.receiver instanceof project_tsconfig_paths.ThisReceiver)) {
return null;
}
const classType = checker.getTypeAtLocation(componentClass.name);
return (lookupPropertyAccess(checker, classType, path, {
// Necessary to avoid breaking the resolution if there is
// some narrowing involved. E.g. `myClass ? myClass.input`.
ignoreNullability: true,
})?.symbol ?? null);
}
/** Whether the given node refers to a two-way binding AST node. */
function isTwoWayBindingNode(node) {
return ((node instanceof project_tsconfig_paths.BoundAttribute && node.type === project_tsconfig_paths.BindingType.TwoWay) ||
(node instanceof project_tsconfig_paths.BoundEvent && node.type === project_tsconfig_paths.ParsedEventType.TwoWay));
}
/** Possible types of references to known fields detected. */
exports.ReferenceKind = void 0;
(function (ReferenceKind) {
ReferenceKind[ReferenceKind["InTemplate"] = 0] = "InTemplate";
ReferenceKind[ReferenceKind["InHostBinding"] = 1] = "InHostBinding";
ReferenceKind[ReferenceKind["TsReference"] = 2] = "TsReference";
ReferenceKind[ReferenceKind["TsClassTypeReference"] = 3] = "TsClassTypeReference";
})(exports.ReferenceKind || (exports.ReferenceKind = {}));
/** Whether the given reference is a TypeScript reference. */
function isTsReference(ref) {
return ref.kind === exports.ReferenceKind.TsReference;
}
/** Whether the given reference is a template reference. */
function isTemplateReference(ref) {
return ref.kind === exports.ReferenceKind.InTemplate;
}
/** Whether the given reference is a host binding reference. */
function isHostBindingReference(ref) {
return ref.kind === exports.ReferenceKind.InHostBinding;
}
/**
* Whether the given reference is a TypeScript `ts.Type` reference
* to a class containing known fields.
*/
function isTsClassTypeReference(ref) {
return ref.kind === exports.ReferenceKind.TsClassTypeReference;
}
/**
* Checks host bindings of the given class and tracks all
* references to inputs within bindings.
*/
function identifyHostBindingReferences(node, programInfo, checker, reflector, result, knownFields, fieldNamesToConsiderForReferenceLookup) {
if (node.name === undefined) {
return;
}
const decorators = reflector.getDecoratorsOfDeclaration(node);
if (decorators === null) {
return;
}
const angularDecorators = project_tsconfig_paths.getAngularDecorators(decorators, ['Directive', 'Component'],
/* isAngularCore */ false);
if (angularDecorators.length === 0) {
return;
}
// Assume only one Angular decorator per class.
const ngDecorator = angularDecorators[0];
if (ngDecorator.args?.length !== 1) {
return;
}
const metadataNode = project_tsconfig_paths.unwrapExpression(ngDecorator.args[0]);
if (!ts.isObjectLiteralExpression(metadataNode)) {
return;
}
const metadata = project_tsconfig_paths.reflectObjectLiteral(metadataNode);
if (!metadata.has('host')) {
return;
}
let hostField = project_tsconfig_paths.unwrapExpression(metadata.get('host'));
// Special-case in case host bindings are shared via a variable.
// e.g. Material button shares host bindings as a constant in the same target.
if (ts.isIdentifier(hostField)) {
let symbol = checker.getSymbolAtLocation(hostField);
// Plain identifier references can point to alias symbols (e.g. imports).
if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) {
symbol = checker.getAliasedSymbol(symbol);
}
if (symbol !== undefined &&
symbol.valueDeclaration !== undefined &&
ts.isVariableDeclaration(symbol.valueDeclaration)) {
hostField = symbol?.valueDeclaration.initializer;
}
}
if (hostField === undefined || !ts.isObjectLiteralExpression(hostField)) {
return;
}
const hostMap = project_tsconfig_paths.reflectObjectLiteral(hostField);
const expressionResult = [];
const expressionVisitor = new TemplateExpressionReferenceVisitor(checker, null, node, knownFields, fieldNamesToConsiderForReferenceLookup);
for (const [rawName, expression] of hostMap.entries()) {
if (!ts.isStringLiteralLike(expression)) {
continue;
}
const isEventBinding = rawName.startsWith('(');
const isPropertyBinding = rawName.startsWith('[');
// Only migrate property or event bindings.
if (!isPropertyBinding && !isEventBinding) {
continue;
}
const parser = project_tsconfig_paths.makeBindingParser();
const sourceSpan = new project_tsconfig_paths.ParseSourceSpan(
// Fake source span to keep parsing offsets zero-based.
// We then later combine these with the expression TS node offsets.
new project_tsconfig_paths.ParseLocation({ content: '', url: '' }, 0, 0, 0), new project_tsconfig_paths.ParseLocation({ content: '', url: '' }, 0, 0, 0));
const name = rawName.substring(1, rawName.length - 1);
let parsed = undefined;
if (isEventBinding) {
const result = [];
parser.parseEvent(name.substring(1, name.length - 1), expression.text, false, sourceSpan, sourceSpan, [], result, sourceSpan);
parsed = result[0].handler;
}
else {
const result = [];
parser.parsePropertyBinding(name, expression.text, true,
/* isTwoWayBinding */ false, sourceSpan, 0, sourceSpan, [], result, sourceSpan);
parsed = result[0].expression;
}
if (parsed != null) {
expressionResult.push(...expressionVisitor.checkTemplateExpression(expression, parsed));
}
}
for (const ref of expressionResult) {
result.references.push({
kind: exports.ReferenceKind.InHostBinding,
from: {
read: ref.read,
readAstPath: ref.readAstPath,
isObjectShorthandExpression: ref.isObjectShorthandExpression,
isWrite: ref.isWrite,
file: project_paths.projectFile(ref.context.getSourceFile(), programInfo),
hostPropertyNode: ref.context,
},
target: ref.targetField,
});
}
}
/**
* Attempts to extract the `TemplateDefinition` for the given
* class, if possible.
*
* The definition can then be used with the Angular compiler to
* load/parse the given template.
*/
function attemptExtractTemplateDefinition(node, checker, reflector, resourceLoader) {
const classDecorators = reflector.getDecoratorsOfDeclaration(node);
const evaluator = new index.PartialEvaluator(reflector, checker, null);
const ngDecorators = classDecorators !== null
? project_tsconfig_paths.getAngularDecorators(classDecorators, ['Component'], /* isAngularCore */ false)
: [];
if (ngDecorators.length === 0 ||
ngDecorators[0].args === null ||
ngDecorators[0].args.length === 0 ||
!ts.isObjectLiteralExpression(ngDecorators[0].args[0])) {
return null;
}
const properties = project_tsconfig_paths.reflectObjectLiteral(ngDecorators[0].args[0]);
const templateProp = properties.get('template');
const templateUrlProp = properties.get('templateUrl');
const containingFile = node.getSourceFile().fileName;
// inline template.
if (templateProp !== undefined) {
const templateStr = evaluator.evaluate(templateProp);
if (typeof templateStr === 'string') {
return {
isInline: true,
expression: templateProp,
interpolationConfig: project_tsconfig_paths.DEFAULT_INTERPOLATION_CONFIG,
preserveWhitespaces: false,
resolvedTemplateUrl: containingFile,
templateUrl: containingFile,
};
}
}
try {
// external template.
if (templateUrlProp !== undefined) {
const templateUrl = evaluator.evaluate(templateUrlProp);
if (typeof templateUrl === 'string') {
return {
isInline: false,
interpolationConfig: project_tsconfig_paths.DEFAULT_INTERPOLATION_CONFIG,
preserveWhitespaces: false,
templateUrlExpression: templateUrlProp,
templateUrl,
resolvedTemplateUrl: resourceLoader.resolve(templateUrl, containingFile),
};
}
}
}
catch (e) {
console.error(`Could not parse external template: ${e}`);
}
return null;
}
/**
* Checks whether the given class has an Angular template, and resolves
* all of the references to inputs.
*/
function identifyTemplateReferences(programInfo, node, reflector, checker, evaluator, templateTypeChecker, resourceLoader, options, result, knownFields, fieldNamesToConsiderForReferenceLookup) {
const template = templateTypeChecker.getTemplate(node, project_tsconfig_paths.OptimizeFor.WholeProgram) ??
// If there is no template registered in the TCB or compiler, the template may
// be skipped due to an explicit `jit: true` setting. We try to detect this case
// and parse the template manually.
extractTemplateWithoutCompilerAnalysis(node, checker, reflector, resourceLoader, evaluator, options);
if (template !== null) {
const visitor = new TemplateReferenceVisitor(checker, templateTypeChecker, node, knownFields, fieldNamesToConsiderForReferenceLookup);
template.forEach((node) => node.visit(visitor));
for (const res of visitor.result) {
const templateFilePath = res.context.sourceSpan.start.file.url;
// Templates without an URL are non-mappable artifacts of e.g.
// string concatenated templates. See the `indirect` template
// source mapping concept in the compiler. We skip such references
// as those cannot be migrated, but print an error for now.
if (templateFilePath === '') {
// TODO: Incorporate a TODO potentially.
console.error(`Found reference to field ${res.targetField.key} that cannot be ` +
`migrated because the template cannot be parsed with source map information ` +
`(in file: ${node.getSourceFile().fileName}).`);
continue;
}
result.references.push({
kind: exports.ReferenceKind.InTemplate,
from: {
read: res.read,
readAstPath: res.readAstPath,
node: res.context,
isObjectShorthandExpression: res.isObjectShorthandExpression,
originatingTsFile: project_paths.projectFile(node.getSourceFile(), programInfo),
templateFile: project_paths.projectFile(project_tsconfig_paths.absoluteFrom(templateFilePath), programInfo),
isLikelyPartOfNarrowing: res.isLikelyNarrowed,
isWrite: res.isWrite,
},
target: res.targetField,
});
}
}
}
/**
* Attempts to extract a `@Component` template from the given class,
* without relying on the `NgCompiler` program analysis.
*
* This is useful for JIT components using `jit: true` which were not
* processed by the Angular compiler, but may still have templates that
* contain references to inputs that we can resolve via the fallback
* reference resolutions (that does not use the type check block).
*/
function extractTemplateWithoutCompilerAnalysis(node, checker, reflector, resourceLoader, evaluator, options) {
if (node.name === undefined) {
return null;
}
const tmplDef = attemptExtractTemplateDefinition(node, checker, reflector, resourceLoader);
if (tmplDef === null) {
return null;
}
return index.extractTemplate(node, tmplDef, evaluator, null, resourceLoader, {
enableBlockSyntax: true,
enableLetSyntax: true,
usePoisonedData: true,
enableI18nLegacyMessageIdFormat: options.enableI18nLegacyMessageIdFormat !== false,
i18nNormalizeLineEndingsInICUs: options.i18nNormalizeLineEndingsInICUs === true,
enableSelectorless: false,
}, project_tsconfig_paths.CompilationMode.FULL).nodes;
}
/** Gets the pattern and property name for a given binding element. */
function resolveBindingElement(node) {
const name = node.propertyName ?? node.name;
// If we are discovering a non-analyzable element in the path, abort.
if (!ts.isStringLiteralLike(name) && !ts.isIdentifier(name)) {
return null;
}
return {
pattern: node.parent,
propertyName: name.text,
};
}
/** Gets the declaration node of the given binding element. */
function getBindingElementDeclaration(node) {
while (true) {
if (ts.isBindingElement(node.parent.parent)) {
node = node.parent.parent;
}
else {
return node.parent.parent;
}
}
}
/**
* Expands the given reference to its containing expression, capturing
* the full context.
*
* E.g. `traverseAccess(ref<`bla`>)` may return `this.bla`
* or `traverseAccess(ref<`bla`>)` may return `this.someObj.a.b.c.bla`.
*
* This helper is useful as we will replace the full access with a temporary
* variable for narrowing. Replacing just the identifier is wrong.
*/
function traverseAccess(access) {
if (ts.isPropertyAccessExpression(access.parent) && access.parent.name === access) {
return access.parent;
}
else if (ts.isElementAccessExpression(access.parent) &&
access.parent.argumentExpression === access) {
return access.parent;
}
return access;
}
/**
* Unwraps the parent of the given node, if it's a
* parenthesized expression or `as` expression.
*/
function unwrapParent(node) {
if (ts.isParenthesizedExpression(node.parent)) {
return unwrapParent(node.parent);
}
else if (ts.isAsExpression(node.parent)) {
return unwrapParent(node.parent);
}
return node;
}
/**
* List of binary operators that indicate a write operation.
*
* Useful for figuring out whether an expression assigns to
* something or not.
*/
const writeBinaryOperators = [
ts.SyntaxKind.EqualsToken,
ts.SyntaxKind.BarBarEqualsToken,
ts.SyntaxKind.BarEqualsToken,
ts.SyntaxKind.AmpersandEqualsToken,
ts.SyntaxKind.AmpersandAmpersandEqualsToken,
ts.SyntaxKind.SlashEqualsToken,
ts.SyntaxKind.MinusEqualsToken,
ts.SyntaxKind.PlusEqualsToken,
ts.SyntaxKind.CaretEqualsToken,
ts.SyntaxKind.PercentEqualsToken,
ts.SyntaxKind.AsteriskEqualsToken,
ts.SyntaxKind.ExclamationEqualsToken,
];
/**
* Checks whether given TypeScript reference refers to an Angular input, and captures
* the reference if possible.
*
* @param fieldNamesToConsiderForReferenceLookup List of field names that should be
* respected when expensively looking up references to known fields.
* May be null if all identifiers should be inspected.
*/
function identifyPotentialTypeScriptReference(node, programInfo, checker, knownFields, result, fieldNamesToConsiderForReferenceLookup, advisors) {
// Skip all identifiers that never can point to a migrated field.
// TODO: Capture these assumptions and performance optimizations in the design doc.
if (fieldNamesToConsiderForReferenceLookup !== null &&
!fieldNamesToConsiderForReferenceLookup.has(node.text)) {
return;
}
let target = undefined;
try {
// Resolve binding elements to their declaration symbol.
// Commonly inputs are accessed via object expansion. e.g. `const {input} = this;`.
if (ts.isBindingElement(node.parent)) {
// Skip binding elements that are using spread.
if (node.parent.dotDotDotToken !== undefined) {
return;
}
const bindingInfo = resolveBindingElement(node.parent);
if (bindingInfo === null) {
// The declaration could not be resolved. Skip analyzing this.
return;
}
const bindingType = checker.getTypeAtLocation(bindingInfo.pattern);
const resolved = lookupPropertyAccess(checker, bindingType, [bindingInfo.propertyName]);
target = resolved?.symbol;
}
else {
target = checker.getSymbolAtLocation(node);
}
}
catch (e) {
console.error('Unexpected error while trying to resolve identifier reference:');
console.error(e);
// Gracefully skip analyzing. This can happen when e.g. a reference is named similar
// to an input, but is dependant on `.d.ts` that is not necessarily available (clutz dts).
return;
}
noTargetSymbolCheck: if (target === undefined) {
if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
const propAccessSymbol = checker.getSymbolAtLocation(node.parent.expression);
if (propAccessSymbol !== undefined &&
propAccessSymbol.valueDeclaration !== undefined &&
ts.isVariableDeclaration(propAccessSymbol.valueDeclaration) &&
propAccessSymbol.valueDeclaration.initializer !== undefined) {
target = advisors.debugElComponentInstanceTracker
.detect(propAccessSymbol.valueDeclaration.initializer)
?.getProperty(node.text);
// We found a target in the fallback path. Break out.
if (target !== undefined) {
break noTargetSymbolCheck;
}
}
}
return;
}
let targetInput = knownFields.attemptRetrieveDescriptorFromSymbol(target);
if (targetInput === null) {
return;
}
const access = unwrapParent(traverseAccess(node));
const accessParent = access.parent;
const isWriteReference = ts.isBinaryExpression(accessParent) &&
accessParent.left === access &&
writeBinaryOperators.includes(accessParent.operatorToken.kind);
// track accesses from source files to known fields.
result.references.push({
kind: exports.ReferenceKind.TsReference,
from: {
node,
file: project_paths.projectFile(node.getSourceFile(), programInfo),
isWrite: isWriteReference,
isPartOfElementBinding: ts.isBindingElement(node.parent),
},
target: targetInput,
});
}
/**
* Phase where we iterate through all source file references and
* detect references to known fields (e.g. commonly inputs).
*
* This is useful, for example in the signal input migration whe
* references need to be migrated to unwrap signals, given that
* their target properties is no longer holding a raw value, but
* instead an `InputSignal`.
*
* This phase detects references in all types of locations:
* - TS source files
* - Angular templates (inline or external)
* - Host binding expressions.
*/
function createFindAllSourceFileReferencesVisitor(programInfo, checker, reflector, resourceLoader, evaluator, templateTypeChecker, knownFields, fieldNamesToConsiderForReferenceLookup, result) {
const debugElComponentInstanceTracker = new DebugElementComponentInstance(checker);
const partialDirectiveCatalystTracker = new PartialDirectiveTypeInCatalystTests(checker, knownFields);
const perfCounters = {
template: 0,
hostBindings: 0,
tsReferences: 0,
tsTypes: 0,
};
// Schematic NodeJS execution may not have `global.performance` defined.
const currentTimeInMs = () => typeof global.performance !== 'undefined' ? global.performance.now() : Date.now();
const visitor = (node) => {
let lastTime = currentTimeInMs();
// Note: If there is no template type checker and resource loader, we aren't processing
// an Angular program, and can skip template detection.
if (ts.isClassDeclaration(node) && templateTypeChecker !== null && resourceLoader !== null) {
identifyTemplateReferences(programInfo, node, reflector, checker, evaluator, templateTypeChecker, resourceLoader, programInfo.userOptions, result, knownFields, fieldNamesToConsiderForReferenceLookup);
perfCounters.template += (currentTimeInMs() - lastTime) / 1000;
lastTime = currentTimeInMs();
identifyHostBindingReferences(node, programInfo, checker, reflector, result, knownFields, fieldNamesToConsiderForReferenceLookup);
perfCounters.hostBindings += (currentTimeInMs() - lastTime) / 1000;
lastTime = currentTimeInMs();
}
lastTime = currentTimeInMs();
// find references, but do not capture input declarations itself.
if (ts.isIdentifier(node) &&
!(isInputContainerNode(node.parent) && node.parent.name === node)) {
identifyPotentialTypeScriptReference(node, programInfo, checker, knownFields, result, fieldNamesToConsiderForReferenceLookup, {
debugElComponentInstanceTracker,
});
}
perfCounters.tsReferences += (currentTimeInMs() - lastTime) / 1000;
lastTime = currentTimeInMs();
// Detect `Partial<T>` references.
// Those are relevant to be tracked as they may be updated in Catalyst to
// unwrap signal inputs. Commonly people use `Partial` in Catalyst to type
// some "component initial values".
const partialDirectiveInCatalyst = partialDirectiveCatalystTracker.detect(node);
if (partialDirectiveInCatalyst !== null) {
result.references.push({
kind: exports.ReferenceKind.TsClassTypeReference,
from: {
file: project_paths.projectFile(partialDirectiveInCatalyst.referenceNode.getSourceFile(), programInfo),
node: partialDirectiveInCatalyst.referenceNode,
},
isPartialReference: true,
isPartOfCatalystFile: true,
target: partialDirectiveInCatalyst.targetClass,
});
}
perfCounters.tsTypes += (currentTimeInMs() - lastTime) / 1000;
};
return {
visitor,
debugPrintMetrics: () => {
console.info('Source file analysis performance', perfCounters);
},
};
}
exports.createFindAllSourceFileReferencesVisitor = createFindAllSourceFileReferencesVisitor;
exports.getBindingElementDeclaration = getBindingElementDeclaration;
exports.getMemberName = getMemberName;
exports.isHostBindingReference = isHostBindingReference;
exports.isInputContainerNode = isInputContainerNode;
exports.isTemplateReference = isTemplateReference;
exports.isTsClassTypeReference = isTsClassTypeReference;
exports.isTsReference = isTsReference;
exports.traverseAccess = traverseAccess;
exports.unwrapParent = unwrapParent;

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

'use strict';
/**
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var index = require('./index-CEqacA4Q.cjs');
var schematics = require('@angular-devkit/schematics');
var core = require('@angular-devkit/core');
var posixPath = require('node:path/posix');
var os = require('os');
var ts = require('typescript');
var project_tsconfig_paths = require('./project_tsconfig_paths-C5dXM8JP.cjs');
require('path');
var path = require('node:path');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var posixPath__namespace = /*#__PURE__*/_interopNamespaceDefault(posixPath);
var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
/// <reference types="node" />
class NgtscCompilerHost {
fs;
options;
constructor(fs, options = {}) {
this.fs = fs;
this.options = options;
}
getSourceFile(fileName, languageVersion) {
const text = this.readFile(fileName);
return text !== undefined
? ts.createSourceFile(fileName, text, languageVersion, true)
: undefined;
}
getDefaultLibFileName(options) {
return this.fs.join(this.getDefaultLibLocation(), ts.getDefaultLibFileName(options));
}
getDefaultLibLocation() {
return this.fs.getDefaultLibLocation();
}
writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles) {
const path = project_tsconfig_paths.absoluteFrom(fileName);
this.fs.ensureDir(this.fs.dirname(path));
this.fs.writeFile(path, data);
}
getCurrentDirectory() {
return this.fs.pwd();
}
getCanonicalFileName(fileName) {
return this.useCaseSensitiveFileNames() ? fileName : fileName.toLowerCase();
}
useCaseSensitiveFileNames() {
return this.fs.isCaseSensitive();
}
getNewLine() {
switch (this.options.newLine) {
case ts.NewLineKind.CarriageReturnLineFeed:
return '\r\n';
case ts.NewLineKind.LineFeed:
return '\n';
default:
return os__namespace.EOL;
}
}
fileExists(fileName) {
const absPath = this.fs.resolve(fileName);
return this.fs.exists(absPath) && this.fs.stat(absPath).isFile();
}
readFile(fileName) {
const absPath = this.fs.resolve(fileName);
if (!this.fileExists(absPath)) {
return undefined;
}
return this.fs.readFile(absPath);
}
realpath(path) {
return this.fs.realpath(this.fs.resolve(path));
}
}
// We use TypeScript's native `ts.matchFiles` utility for the virtual file systems
// and their TypeScript compiler host `readDirectory` implementation. TypeScript's
// function implements complex logic for matching files with respect to root
// directory, extensions, excludes, includes etc. The function is currently
// internal but we can use it as the API most likely will not change any time soon,
// nor does it seem like this is being made public any time soon.
// Related issue for tracking: https://github.com/microsoft/TypeScript/issues/13793.
/**
* Creates a {@link ts.CompilerHost#readDirectory} implementation function,
* that leverages the specified file system (that may be e.g. virtual).
*/
function createFileSystemTsReadDirectoryFn(fs) {
if (ts.matchFiles === undefined) {
throw Error('Unable to read directory in configured file system. This means that ' +
'TypeScript changed its file matching internals.\n\nPlease consider downgrading your ' +
'TypeScript version, and report an issue in the Angular framework repository.');
}
const matchFilesFn = ts.matchFiles.bind(ts);
return (rootDir, extensions, excludes, includes, depth) => {
const directoryExists = (p) => {
const resolvedPath = fs.resolve(p);
return fs.exists(resolvedPath) && fs.stat(resolvedPath).isDirectory();
};
return matchFilesFn(rootDir, extensions, excludes, includes, fs.isCaseSensitive(), fs.pwd(), depth, (p) => {
const resolvedPath = fs.resolve(p);
// TS also gracefully returns an empty file set.
if (!directoryExists(resolvedPath)) {
return { directories: [], files: [] };
}
const children = fs.readdir(resolvedPath);
const files = [];
const directories = [];
for (const child of children) {
if (fs.stat(fs.join(resolvedPath, child))?.isDirectory()) {
directories.push(child);
}
else {
files.push(child);
}
}
return { files, directories };
}, (p) => fs.resolve(p), (p) => directoryExists(p));
};
}
function calcProjectFileAndBasePath(project, host = project_tsconfig_paths.getFileSystem()) {
const absProject = host.resolve(project);
const projectIsDir = host.lstat(absProject).isDirectory();
const projectFile = projectIsDir ? host.join(absProject, 'tsconfig.json') : absProject;
const projectDir = projectIsDir ? absProject : host.dirname(absProject);
const basePath = host.resolve(projectDir);
return { projectFile, basePath };
}
function readConfiguration(project, existingOptions, host = project_tsconfig_paths.getFileSystem()) {
try {
const fs = project_tsconfig_paths.getFileSystem();
const readConfigFile = (configFile) => ts.readConfigFile(configFile, (file) => host.readFile(host.resolve(file)));
const readAngularCompilerOptions = (configFile, parentOptions = {}) => {
const { config, error } = readConfigFile(configFile);
if (error) {
// Errors are handled later on by 'parseJsonConfigFileContent'
return parentOptions;
}
// Note: In Google, `angularCompilerOptions` are stored in `bazelOptions`.
// This function typically doesn't run for actual Angular compilations, but
// tooling like Tsurge, or schematics may leverage this helper, so we account
// for this here.
const angularCompilerOptions = config.angularCompilerOptions ?? config.bazelOptions?.angularCompilerOptions;
// we are only interested into merging 'angularCompilerOptions' as
// other options like 'compilerOptions' are merged by TS
let existingNgCompilerOptions = { ...angularCompilerOptions, ...parentOptions };
if (!config.extends) {
return existingNgCompilerOptions;
}
const extendsPaths = typeof config.extends === 'string' ? [config.extends] : config.extends;
// Call readAngularCompilerOptions recursively to merge NG Compiler options
// Reverse the array so the overrides happen from right to left.
return [...extendsPaths].reverse().reduce((prevOptions, extendsPath) => {
const extendedConfigPath = getExtendedConfigPath(configFile, extendsPath, host, fs);
return extendedConfigPath === null
? prevOptions
: readAngularCompilerOptions(extendedConfigPath, prevOptions);
}, existingNgCompilerOptions);
};
const { projectFile, basePath } = calcProjectFileAndBasePath(project, host);
const configFileName = host.resolve(host.pwd(), projectFile);
const { config, error } = readConfigFile(projectFile);
if (error) {
return {
project,
errors: [error],
rootNames: [],
options: {},
emitFlags: index.EmitFlags.Default,
};
}
const existingCompilerOptions = {
genDir: basePath,
basePath,
...readAngularCompilerOptions(configFileName),
...existingOptions,
};
const parseConfigHost = createParseConfigHost(host, fs);
const { options, errors, fileNames: rootNames, projectReferences, } = ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, existingCompilerOptions, configFileName);
let emitFlags = index.EmitFlags.Default;
if (!(options['skipMetadataEmit'] || options['flatModuleOutFile'])) {
emitFlags |= index.EmitFlags.Metadata;
}
if (options['skipTemplateCodegen']) {
emitFlags = emitFlags & ~index.EmitFlags.Codegen;
}
return { project: projectFile, rootNames, projectReferences, options, errors, emitFlags };
}
catch (e) {
const errors = [
{
category: ts.DiagnosticCategory.Error,
messageText: e.stack ?? e.message,
file: undefined,
start: undefined,
length: undefined,
source: 'angular',
code: index.UNKNOWN_ERROR_CODE,
},
];
return { project: '', errors, rootNames: [], options: {}, emitFlags: index.EmitFlags.Default };
}
}
function createParseConfigHost(host, fs = project_tsconfig_paths.getFileSystem()) {
return {
fileExists: host.exists.bind(host),
readDirectory: createFileSystemTsReadDirectoryFn(fs),
readFile: host.readFile.bind(host),
useCaseSensitiveFileNames: fs.isCaseSensitive(),
};
}
function getExtendedConfigPath(configFile, extendsValue, host, fs) {
const result = getExtendedConfigPathWorker(configFile, extendsValue, host, fs);
if (result !== null) {
return result;
}
// Try to resolve the paths with a json extension append a json extension to the file in case if
// it is missing and the resolution failed. This is to replicate TypeScript behaviour, see:
// https://github.com/microsoft/TypeScript/blob/294a5a7d784a5a95a8048ee990400979a6bc3a1c/src/compiler/commandLineParser.ts#L2806
return getExtendedConfigPathWorker(configFile, `${extendsValue}.json`, host, fs);
}
function getExtendedConfigPathWorker(configFile, extendsValue, host, fs) {
if (extendsValue.startsWith('.') || fs.isRooted(extendsValue)) {
const extendedConfigPath = host.resolve(host.dirname(configFile), extendsValue);
if (host.exists(extendedConfigPath)) {
return extendedConfigPath;
}
}
else {
const parseConfigHost = createParseConfigHost(host, fs);
// Path isn't a rooted or relative path, resolve like a module.
const { resolvedModule } = ts.nodeModuleNameResolver(extendsValue, configFile, { moduleResolution: ts.ModuleResolutionKind.Node10, resolveJsonModule: true }, parseConfigHost);
if (resolvedModule) {
return project_tsconfig_paths.absoluteFrom(resolvedModule.resolvedFileName);
}
}
return null;
}
/**
* Angular compiler file system implementation that leverages an
* CLI schematic virtual file tree.
*/
class DevkitMigrationFilesystem {
tree;
constructor(tree) {
this.tree = tree;
}
extname(path) {
return core.extname(path);
}
isRoot(path) {
return path === core.normalize('/');
}
isRooted(path) {
return this.normalize(path).startsWith('/');
}
dirname(file) {
return this.normalize(core.dirname(file));
}
join(basePath, ...paths) {
return this.normalize(core.join(basePath, ...paths));
}
relative(from, to) {
return this.normalize(core.relative(from, to));
}
basename(filePath, extension) {
return posixPath__namespace.basename(filePath, extension);
}
normalize(path) {
return core.normalize(path);
}
resolve(...paths) {
const normalizedPaths = paths.map((p) => core.normalize(p));
// In dev-kit, the NodeJS working directory should never be
// considered, so `/` is the last resort over `cwd`.
return this.normalize(posixPath__namespace.resolve(core.normalize('/'), ...normalizedPaths));
}
pwd() {
return '/';
}
isCaseSensitive() {
return true;
}
exists(path) {
return statPath(this.tree, path) !== null;
}
readFile(path) {
return this.tree.readText(path);
}
readFileBuffer(path) {
const buffer = this.tree.read(path);
if (buffer === null) {
throw new Error(`File does not exist: ${path}`);
}
return buffer;
}
readdir(path) {
const dir = this.tree.getDir(path);
return [
...dir.subdirs,
...dir.subfiles,
];
}
lstat(path) {
const stat = statPath(this.tree, path);
if (stat === null) {
throw new Error(`File does not exist for "lstat": ${path}`);
}
return stat;
}
stat(path) {
const stat = statPath(this.tree, path);
if (stat === null) {
throw new Error(`File does not exist for "stat": ${path}`);
}
return stat;
}
realpath(filePath) {
return filePath;
}
getDefaultLibLocation() {
return 'node_modules/typescript/lib';
}
ensureDir(path) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#ensureDir is not supported.');
}
writeFile(path, data) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#writeFile is not supported.');
}
removeFile(path) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#removeFile is not supported.');
}
copyFile(from, to) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#copyFile is not supported.');
}
moveFile(from, to) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#moveFile is not supported.');
}
removeDeep(path) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#removeDeep is not supported.');
}
chdir(_path) {
throw new Error('FileSystem#chdir is not supported.');
}
symlink() {
throw new Error('FileSystem#symlink is not supported.');
}
}
/** Stats the given path in the virtual tree. */
function statPath(tree, path) {
let fileInfo = null;
let dirInfo = null;
try {
fileInfo = tree.get(path);
}
catch (e) {
if (e.constructor.name === 'PathIsDirectoryException') {
dirInfo = tree.getDir(path);
}
else {
throw e;
}
}
if (fileInfo !== null || dirInfo !== null) {
return {
isDirectory: () => dirInfo !== null,
isFile: () => fileInfo !== null,
isSymbolicLink: () => false,
};
}
return null;
}
/**
* Groups the given replacements per project relative
* file path.
*
* This allows for simple execution of the replacements
* against a given file. E.g. via {@link applyTextUpdates}.
*/
function groupReplacementsByFile(replacements) {
const result = new Map();
for (const { projectFile, update } of replacements) {
if (!result.has(projectFile.rootRelativePath)) {
result.set(projectFile.rootRelativePath, []);
}
result.get(projectFile.rootRelativePath).push(update);
}
return result;
}
/**
* Synchronously combines unit data for the given migration.
*
* Note: This helper is useful for testing and execution of
* Tsurge migrations in non-batchable environments. In general,
* prefer parallel execution of combining via e.g. Beam combiners.
*/
async function synchronouslyCombineUnitData(migration, unitDatas) {
if (unitDatas.length === 0) {
return null;
}
if (unitDatas.length === 1) {
return unitDatas[0];
}
let combined = unitDatas[0];
for (let i = 1; i < unitDatas.length; i++) {
const other = unitDatas[i];
combined = await migration.combine(combined, other);
}
return combined;
}
/** Whether we are executing inside Google */
function isGoogle3() {
return process.env['GOOGLE3_TSURGE'] === '1';
}
/**
* By default, Tsurge will always create an Angular compiler program
* for projects analyzed and migrated. This works perfectly fine in
* third-party where Tsurge migrations run in Angular CLI projects.
*
* In first party, when running against full Google3, creating an Angular
* program for e.g. plain `ts_library` targets is overly expensive and
* can result in out of memory issues for large TS targets. In 1P we can
* reliably distinguish between TS and Angular targets via the `angularCompilerOptions`.
*/
function google3UsePlainTsProgramIfNoKnownAngularOption() {
return process.env['GOOGLE3_TSURGE'] === '1';
}
/** Options that are good defaults for Tsurge migrations. */
const defaultMigrationTsOptions = {
// Avoid checking libraries to speed up migrations.
skipLibCheck: true,
skipDefaultLibCheck: true,
noEmit: true,
// Does not apply to g3 and externally is enforced when the app is built by the compiler.
disableTypeScriptVersionCheck: true,
};
/**
* Creates an instance of a TypeScript program for the given project.
*/
function createPlainTsProgram(tsHost, tsconfig, optionOverrides) {
const program = ts.createProgram({
rootNames: tsconfig.rootNames,
options: {
...tsconfig.options,
...defaultMigrationTsOptions,
...optionOverrides,
},
});
return {
ngCompiler: null,
program,
userOptions: tsconfig.options,
__programAbsoluteRootFileNames: tsconfig.rootNames,
host: tsHost,
};
}
/**
* Parses the configuration of the given TypeScript project and creates
* an instance of the Angular compiler for the project.
*/
function createNgtscProgram(tsHost, tsconfig, optionOverrides) {
const ngtscProgram = new index.NgtscProgram(tsconfig.rootNames, {
...tsconfig.options,
...defaultMigrationTsOptions,
...optionOverrides,
}, tsHost);
// Expose an easy way to debug-print ng semantic diagnostics.
if (process.env['DEBUG_NG_SEMANTIC_DIAGNOSTICS'] === '1') {
console.error(ts.formatDiagnosticsWithColorAndContext(ngtscProgram.getNgSemanticDiagnostics(), tsHost));
}
return {
ngCompiler: ngtscProgram.compiler,
program: ngtscProgram.getTsProgram(),
userOptions: tsconfig.options,
__programAbsoluteRootFileNames: tsconfig.rootNames,
host: tsHost,
};
}
/** Code of the error raised by TypeScript when a tsconfig doesn't match any files. */
const NO_INPUTS_ERROR_CODE = 18003;
/** Parses the given tsconfig file, supporting Angular compiler options. */
function parseTsconfigOrDie(absoluteTsconfigPath, fs) {
const tsconfig = readConfiguration(absoluteTsconfigPath, {}, fs);
// Skip the "No inputs found..." error since we don't want to interrupt the migration if a
// tsconfig doesn't match a file. This will result in an empty `Program` which is still valid.
const errors = tsconfig.errors.filter((diag) => diag.code !== NO_INPUTS_ERROR_CODE);
if (errors.length) {
throw new Error(`Tsconfig could not be parsed or is invalid:\n\n` + `${errors.map((e) => e.messageText)}`);
}
return tsconfig;
}
// Note: Try to keep mostly in sync with
// //depot/google3/javascript/angular2/tools/ngc_wrapped/tsc_plugin.ts
// TODO: Consider moving this logic into the 1P launcher.
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
function fileNameToModuleNameFactory(rootDirs, workspaceName) {
return (importedFilePath) => {
let relativePath = '';
for (const rootDir of rootDirs) {
const rel = path__namespace.posix.relative(rootDir, importedFilePath);
if (!rel.startsWith('.')) {
relativePath = rel;
break;
}
}
if (relativePath) {
return `${workspaceName}/${relativePath.replace(EXT, '')}`;
}
else {
return importedFilePath.replace(EXT, '');
}
};
}
/** Creates the base program info for the given tsconfig path. */
function createBaseProgramInfo(absoluteTsconfigPath, fs, optionOverrides = {}) {
// Make sure the FS becomes globally available. Some code paths
// of the Angular compiler, or tsconfig parsing aren't leveraging
// the specified file system.
project_tsconfig_paths.setFileSystem(fs);
const tsconfig = parseTsconfigOrDie(absoluteTsconfigPath, fs);
const tsHost = new NgtscCompilerHost(fs, tsconfig.options);
// When enabled, use a plain TS program if we are sure it's not
// an Angular project based on the `tsconfig.json`.
if (google3UsePlainTsProgramIfNoKnownAngularOption() &&
tsconfig.options['_useHostForImportGeneration'] === undefined) {
return createPlainTsProgram(tsHost, tsconfig, optionOverrides);
}
// The Angular program may try to emit references during analysis or migration.
// To replicate the Google3 import emission here, ensure the unified module resolution
// can be enabled by the compiler.
if (isGoogle3() && tsconfig.options.rootDirs) {
tsHost.fileNameToModuleName = fileNameToModuleNameFactory(tsconfig.options.rootDirs,
/* workspaceName*/ 'google3');
}
return createNgtscProgram(tsHost, tsconfig, optionOverrides);
}
/**
* Creates the {@link ProgramInfo} from the given base information.
*
* This function purely exists to support custom programs that are
* intended to be injected into Tsurge migrations. e.g. for language
* service refactorings.
*/
function getProgramInfoFromBaseInfo(baseInfo) {
const fullProgramSourceFiles = [...baseInfo.program.getSourceFiles()];
const sourceFiles = fullProgramSourceFiles.filter((f) => !f.isDeclarationFile &&
// Note `isShim` will work for the initial program, but for TCB programs, the shims are no longer annotated.
!project_tsconfig_paths.isShim(f) &&
!f.fileName.endsWith('.ngtypecheck.ts'));
// Sort it by length in reverse order (longest first). This speeds up lookups,
// since there's no need to keep going through the array once a match is found.
const sortedRootDirs = project_tsconfig_paths.getRootDirs(baseInfo.host, baseInfo.userOptions).sort((a, b) => b.length - a.length);
// TODO: Consider also following TS's logic here, finding the common source root.
// See: Program#getCommonSourceDirectory.
const primaryRoot = project_tsconfig_paths.absoluteFrom(baseInfo.userOptions.rootDir ?? sortedRootDirs.at(-1) ?? baseInfo.program.getCurrentDirectory());
return {
...baseInfo,
sourceFiles,
fullProgramSourceFiles,
sortedRootDirs,
projectRoot: primaryRoot,
};
}
/**
* @private
*
* Base class for the possible Tsurge migration variants.
*
* For example, this class exposes methods to conveniently create
* TypeScript programs, while also allowing migration authors to override.
*/
class TsurgeBaseMigration {
/**
* Creates the TypeScript program for a given compilation unit.
*
* By default:
* - In 3P: Ngtsc programs are being created.
* - In 1P: Ngtsc or TS programs are created based on the Blaze target.
*/
createProgram(tsconfigAbsPath, fs, optionsOverride) {
return getProgramInfoFromBaseInfo(createBaseProgramInfo(tsconfigAbsPath, fs, optionsOverride));
}
}
/**
* A simpler variant of a {@link TsurgeComplexMigration} that does not
* fan-out into multiple workers per compilation unit to compute
* the final migration replacements.
*
* This is faster and less resource intensive as workers and TS programs
* are only ever created once.
*
* This is commonly the case when migrations are refactored to eagerly
* compute replacements in the analyze stage, and then leverage the
* global unit data to filter replacements that turned out to be "invalid".
*/
class TsurgeFunnelMigration extends TsurgeBaseMigration {
}
/**
* Complex variant of a `Tsurge` migration.
*
* For example, every analyze worker may contribute to a list of TS
* references that are later combined. The migrate phase can then compute actual
* file updates for all individual compilation units, leveraging the global metadata
* to e.g. see if there are any references from other compilation units that may be
* problematic and prevent migration of a given file.
*/
class TsurgeComplexMigration extends TsurgeBaseMigration {
}
/*!
* @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
*/
exports.MigrationStage = void 0;
(function (MigrationStage) {
/** The migration is analyzing an entrypoint */
MigrationStage[MigrationStage["Analysis"] = 0] = "Analysis";
/** The migration is about to migrate an entrypoint */
MigrationStage[MigrationStage["Migrate"] = 1] = "Migrate";
})(exports.MigrationStage || (exports.MigrationStage = {}));
/** Runs a Tsurge within an Angular Devkit context. */
async function runMigrationInDevkit(config) {
const { buildPaths, testPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(config.tree);
if (!buildPaths.length && !testPaths.length) {
throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the migration.');
}
const tsconfigPaths = [...buildPaths, ...testPaths];
const fs = new DevkitMigrationFilesystem(config.tree);
project_tsconfig_paths.setFileSystem(fs);
const migration = config.getMigration(fs);
const unitResults = [];
const isFunnelMigration = migration instanceof TsurgeFunnelMigration;
const compilationUnitAssignments = new Map();
for (const tsconfigPath of tsconfigPaths) {
config.beforeProgramCreation?.(tsconfigPath, exports.MigrationStage.Analysis);
const info = migration.createProgram(tsconfigPath, fs);
modifyProgramInfoToEnsureNonOverlappingFiles(tsconfigPath, info, compilationUnitAssignments);
config.afterProgramCreation?.(info, fs, exports.MigrationStage.Analysis);
config.beforeUnitAnalysis?.(tsconfigPath);
unitResults.push(await migration.analyze(info));
}
config.afterAllAnalyzed?.();
const combined = await synchronouslyCombineUnitData(migration, unitResults);
if (combined === null) {
config.afterAnalysisFailure?.();
return;
}
const globalMeta = await migration.globalMeta(combined);
let replacements;
if (isFunnelMigration) {
replacements = (await migration.migrate(globalMeta)).replacements;
}
else {
replacements = [];
for (const tsconfigPath of tsconfigPaths) {
config.beforeProgramCreation?.(tsconfigPath, exports.MigrationStage.Migrate);
const info = migration.createProgram(tsconfigPath, fs);
modifyProgramInfoToEnsureNonOverlappingFiles(tsconfigPath, info, compilationUnitAssignments);
config.afterProgramCreation?.(info, fs, exports.MigrationStage.Migrate);
const result = await migration.migrate(globalMeta, info);
replacements.push(...result.replacements);
}
}
const replacementsPerFile = new Map();
const changesPerFile = groupReplacementsByFile(replacements);
for (const [file, changes] of changesPerFile) {
if (!replacementsPerFile.has(file)) {
replacementsPerFile.set(file, changes);
}
}
for (const [file, changes] of replacementsPerFile) {
const recorder = config.tree.beginUpdate(file);
for (const c of changes) {
recorder
.remove(c.data.position, c.data.end - c.data.position)
.insertRight(c.data.position, c.data.toInsert);
}
config.tree.commitUpdate(recorder);
}
config.whenDone?.(await migration.stats(globalMeta));
}
/**
* Special logic for devkit migrations. In the Angular CLI, or in 3P precisely,
* projects can have tsconfigs with overlapping source files. i.e. two tsconfigs
* like e.g. build or test include the same `ts.SourceFile` (`.ts`). Migrations
* should never have 2+ compilation units with overlapping source files as this
* can result in duplicated replacements or analysis— hence we only ever assign a
* source file to a compilation unit *once*.
*
* Note that this is fine as we expect Tsurge migrations to work together as
* isolated compilation units— so it shouldn't matter if worst case a `.ts`
* file ends up in the e.g. test program.
*/
function modifyProgramInfoToEnsureNonOverlappingFiles(tsconfigPath, info, compilationUnitAssignments) {
const sourceFiles = [];
for (const sf of info.sourceFiles) {
const assignment = compilationUnitAssignments.get(sf.fileName);
// File is already assigned to a different compilation unit.
if (assignment !== undefined && assignment !== tsconfigPath) {
continue;
}
compilationUnitAssignments.set(sf.fileName, tsconfigPath);
sourceFiles.push(sf);
}
info.sourceFiles = sourceFiles;
}
/** A text replacement for the given file. */
class Replacement {
projectFile;
update;
constructor(projectFile, update) {
this.projectFile = projectFile;
this.update = update;
}
}
/** An isolated text update that may be applied to a file. */
class TextUpdate {
data;
constructor(data) {
this.data = data;
}
}
/** Confirms that the given data `T` is serializable. */
function confirmAsSerializable(data) {
return data;
}
/**
* Gets a project file instance for the given file.
*
* Use this helper for dealing with project paths throughout your
* migration. The return type is serializable.
*
* See {@link ProjectFile}.
*/
function projectFile(file, { sortedRootDirs, projectRoot }) {
const fs = project_tsconfig_paths.getFileSystem();
const filePath = fs.resolve(typeof file === 'string' ? file : file.fileName);
// Sorted root directories are sorted longest to shortest. First match
// is the appropriate root directory for ID computation.
for (const rootDir of sortedRootDirs) {
if (!isWithinBasePath(fs, rootDir, filePath)) {
continue;
}
return {
id: fs.relative(rootDir, filePath),
rootRelativePath: fs.relative(projectRoot, filePath),
};
}
// E.g. project directory may be `src/`, but files may be looked up
// from `node_modules/`. This is fine, but in those cases, no root
// directory matches.
const rootRelativePath = fs.relative(projectRoot, filePath);
return {
id: rootRelativePath,
rootRelativePath: rootRelativePath,
};
}
/**
* Whether `path` is a descendant of the `base`?
* E.g. `a/b/c` is within `a/b` but not within `a/x`.
*/
function isWithinBasePath(fs, base, path) {
return project_tsconfig_paths.isLocalRelativePath(fs.relative(base, path));
}
exports.Replacement = Replacement;
exports.TextUpdate = TextUpdate;
exports.TsurgeComplexMigration = TsurgeComplexMigration;
exports.TsurgeFunnelMigration = TsurgeFunnelMigration;
exports.confirmAsSerializable = confirmAsSerializable;
exports.projectFile = projectFile;
exports.runMigrationInDevkit = runMigrationInDevkit;

Sorry, the diff of this file is too big to display

+1
-1
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

{
"name": "@angular/core",
"version": "20.3.15",
"version": "20.3.16",
"description": "Angular - the core framework",

@@ -49,3 +49,3 @@ "author": "angular",

"peerDependencies": {
"@angular/compiler": "20.3.15",
"@angular/compiler": "20.3.16",
"rxjs": "^6.5.3 || ^7.4.0",

@@ -52,0 +52,0 @@ "zone.js": "~0.15.0"

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -11,8 +11,8 @@ * License: MIT

require('node:path/posix');
var project_paths = require('./project_paths-Ck6Mza_h.cjs');
var project_tsconfig_paths = require('./project_tsconfig_paths-CgKwP2MX.cjs');
var project_paths = require('./project_paths-qw-vHpyb.cjs');
var project_tsconfig_paths = require('./project_tsconfig_paths-C5dXM8JP.cjs');
var ts = require('typescript');
require('os');
var apply_import_manager = require('./apply_import_manager-BKP1lF28.cjs');
require('./index-Cu2VLWJC.cjs');
var apply_import_manager = require('./apply_import_manager-C-hIllzM.cjs');
require('./index-CEqacA4Q.cjs');
require('path');

@@ -19,0 +19,0 @@ require('node:path');

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -11,10 +11,10 @@ * License: MIT

require('node:path/posix');
var project_paths = require('./project_paths-Ck6Mza_h.cjs');
var project_paths = require('./project_paths-qw-vHpyb.cjs');
var ts = require('typescript');
require('os');
var project_tsconfig_paths = require('./project_tsconfig_paths-CgKwP2MX.cjs');
var index = require('./index-Cu2VLWJC.cjs');
var project_tsconfig_paths = require('./project_tsconfig_paths-C5dXM8JP.cjs');
var index = require('./index-CEqacA4Q.cjs');
require('path');
require('node:path');
var apply_import_manager = require('./apply_import_manager-BKP1lF28.cjs');
var apply_import_manager = require('./apply_import_manager-C-hIllzM.cjs');
require('@angular-devkit/schematics');

@@ -21,0 +21,0 @@ require('fs');

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -9,10 +9,10 @@ * License: MIT

var project_tsconfig_paths = require('./project_tsconfig_paths-CgKwP2MX.cjs');
var project_tsconfig_paths = require('./project_tsconfig_paths-C5dXM8JP.cjs');
require('typescript');
require('os');
var apply_import_manager = require('./apply_import_manager-BKP1lF28.cjs');
require('./index-Cu2VLWJC.cjs');
var apply_import_manager = require('./apply_import_manager-C-hIllzM.cjs');
require('./index-CEqacA4Q.cjs');
require('path');
require('node:path');
var project_paths = require('./project_paths-Ck6Mza_h.cjs');
var project_paths = require('./project_paths-qw-vHpyb.cjs');
var imports = require('./imports-CIX-JgAN.cjs');

@@ -19,0 +19,0 @@ require('@angular-devkit/core');

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -5,0 +5,0 @@ * License: MIT

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -11,8 +11,8 @@ * License: MIT

require('os');
var project_tsconfig_paths = require('./project_tsconfig_paths-CgKwP2MX.cjs');
require('./index-Cu2VLWJC.cjs');
var project_tsconfig_paths = require('./project_tsconfig_paths-C5dXM8JP.cjs');
require('./index-CEqacA4Q.cjs');
require('path');
require('node:path');
var project_paths = require('./project_paths-Ck6Mza_h.cjs');
var apply_import_manager = require('./apply_import_manager-BKP1lF28.cjs');
var project_paths = require('./project_paths-qw-vHpyb.cjs');
var apply_import_manager = require('./apply_import_manager-C-hIllzM.cjs');
var imports = require('./imports-CIX-JgAN.cjs');

@@ -19,0 +19,0 @@ require('@angular-devkit/core');

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -11,3 +11,3 @@ * License: MIT

var p = require('path');
var compiler_host = require('./compiler_host-CNpC4-d2.cjs');
var compiler_host = require('./compiler_host-C7zi7NDP.cjs');
var ts = require('typescript');

@@ -18,3 +18,3 @@ var ng_decorators = require('./ng_decorators-B5HCqr20.cjs');

var leading_space = require('./leading_space-D9nQ8UQC.cjs');
var project_tsconfig_paths = require('./project_tsconfig_paths-CgKwP2MX.cjs');
var project_tsconfig_paths = require('./project_tsconfig_paths-C5dXM8JP.cjs');
require('os');

@@ -21,0 +21,0 @@ require('fs');

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -5,0 +5,0 @@ * License: MIT

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -5,0 +5,0 @@ * License: MIT

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -5,0 +5,0 @@ * License: MIT

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -11,9 +11,9 @@ * License: MIT

require('os');
var project_tsconfig_paths = require('./project_tsconfig_paths-CgKwP2MX.cjs');
var index$1 = require('./index-Cu2VLWJC.cjs');
var project_tsconfig_paths = require('./project_tsconfig_paths-C5dXM8JP.cjs');
var index$1 = require('./index-CEqacA4Q.cjs');
require('path');
require('node:path');
var project_paths = require('./project_paths-Ck6Mza_h.cjs');
var apply_import_manager = require('./apply_import_manager-BKP1lF28.cjs');
var index = require('./index-DxdWo-Gr.cjs');
var project_paths = require('./project_paths-qw-vHpyb.cjs');
var apply_import_manager = require('./apply_import_manager-C-hIllzM.cjs');
var index = require('./index-BdbAclL_.cjs');
require('@angular-devkit/core');

@@ -20,0 +20,0 @@ require('node:path/posix');

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -5,0 +5,0 @@ * License: MIT

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -12,4 +12,4 @@ * License: MIT

var p = require('path');
var compiler_host = require('./compiler_host-CNpC4-d2.cjs');
var project_tsconfig_paths = require('./project_tsconfig_paths-CgKwP2MX.cjs');
var compiler_host = require('./compiler_host-C7zi7NDP.cjs');
var project_tsconfig_paths = require('./project_tsconfig_paths-C5dXM8JP.cjs');
var ts = require('typescript');

@@ -16,0 +16,0 @@ var property_name = require('./property_name-BBwFuqMe.cjs');

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -11,7 +11,7 @@ * License: MIT

require('os');
require('./project_tsconfig_paths-CgKwP2MX.cjs');
require('./index-Cu2VLWJC.cjs');
require('./project_tsconfig_paths-C5dXM8JP.cjs');
require('./index-CEqacA4Q.cjs');
require('path');
require('node:path');
var project_paths = require('./project_paths-Ck6Mza_h.cjs');
var project_paths = require('./project_paths-qw-vHpyb.cjs');
var imports = require('./imports-CIX-JgAN.cjs');

@@ -18,0 +18,0 @@ var symbol = require('./symbol-VPWguRxr.cjs');

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -11,7 +11,7 @@ * License: MIT

require('os');
var project_tsconfig_paths = require('./project_tsconfig_paths-CgKwP2MX.cjs');
require('./index-Cu2VLWJC.cjs');
var project_tsconfig_paths = require('./project_tsconfig_paths-C5dXM8JP.cjs');
require('./index-CEqacA4Q.cjs');
require('path');
require('node:path');
var project_paths = require('./project_paths-Ck6Mza_h.cjs');
var project_paths = require('./project_paths-qw-vHpyb.cjs');
var ng_decorators = require('./ng_decorators-B5HCqr20.cjs');

@@ -18,0 +18,0 @@ var property_name = require('./property_name-BBwFuqMe.cjs');

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -9,14 +9,14 @@ * License: MIT

var project_tsconfig_paths = require('./project_tsconfig_paths-CgKwP2MX.cjs');
var project_tsconfig_paths = require('./project_tsconfig_paths-C5dXM8JP.cjs');
var ts = require('typescript');
require('os');
var index$1 = require('./index-Cu2VLWJC.cjs');
var index$1 = require('./index-CEqacA4Q.cjs');
require('path');
require('node:path');
var project_paths = require('./project_paths-Ck6Mza_h.cjs');
var apply_import_manager = require('./apply_import_manager-BKP1lF28.cjs');
var migrate_ts_type_references = require('./migrate_ts_type_references-Cyyq9Vr9.cjs');
var project_paths = require('./project_paths-qw-vHpyb.cjs');
var apply_import_manager = require('./apply_import_manager-C-hIllzM.cjs');
var migrate_ts_type_references = require('./migrate_ts_type_references-BJnim1Tm.cjs');
var assert = require('assert');
require('node:assert');
var index = require('./index-DxdWo-Gr.cjs');
var index = require('./index-BdbAclL_.cjs');
require('@angular-devkit/core');

@@ -23,0 +23,0 @@ require('node:path/posix');

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -13,3 +13,3 @@ * License: MIT

var outputMigration = require('./output-migration.cjs');
require('./project_tsconfig_paths-CgKwP2MX.cjs');
require('./project_tsconfig_paths-C5dXM8JP.cjs');
require('typescript');

@@ -22,10 +22,10 @@ require('os');

require('@angular-devkit/core');
require('./index-Cu2VLWJC.cjs');
require('./index-CEqacA4Q.cjs');
require('node:path');
require('./project_paths-Ck6Mza_h.cjs');
require('./project_paths-qw-vHpyb.cjs');
require('node:path/posix');
require('./apply_import_manager-BKP1lF28.cjs');
require('./migrate_ts_type_references-Cyyq9Vr9.cjs');
require('./apply_import_manager-C-hIllzM.cjs');
require('./migrate_ts_type_references-BJnim1Tm.cjs');
require('assert');
require('./index-DxdWo-Gr.cjs');
require('./index-BdbAclL_.cjs');
require('./leading_space-D9nQ8UQC.cjs');

@@ -32,0 +32,0 @@ require('node:assert');

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -5,0 +5,0 @@ * License: MIT

'use strict';
/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.io/

@@ -11,7 +11,7 @@ * License: MIT

require('os');
require('./project_tsconfig_paths-CgKwP2MX.cjs');
require('./index-Cu2VLWJC.cjs');
require('./project_tsconfig_paths-C5dXM8JP.cjs');
require('./index-CEqacA4Q.cjs');
require('path');
require('node:path');
var project_paths = require('./project_paths-Ck6Mza_h.cjs');
var project_paths = require('./project_paths-qw-vHpyb.cjs');
var imports = require('./imports-CIX-JgAN.cjs');

@@ -18,0 +18,0 @@ var symbol = require('./symbol-VPWguRxr.cjs');

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

/**
* @license Angular v20.3.15
* @license Angular v20.3.16
* (c) 2010-2025 Google LLC. https://angular.dev/

@@ -4,0 +4,0 @@ * License: MIT

'use strict';
/**
* @license Angular v20.3.15
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
require('os');
var project_tsconfig_paths = require('./project_tsconfig_paths-CgKwP2MX.cjs');
var project_paths = require('./project_paths-Ck6Mza_h.cjs');
/**
* Applies import manager changes, and writes them as replacements the
* given result array.
*/
function applyImportManagerChanges(importManager, replacements, sourceFiles, info) {
const { newImports, updatedImports, deletedImports } = importManager.finalize();
const printer = ts.createPrinter({});
const pathToFile = new Map(sourceFiles.map((s) => [s.fileName, s]));
// Capture new imports
newImports.forEach((newImports, fileName) => {
newImports.forEach((newImport) => {
const printedImport = printer.printNode(ts.EmitHint.Unspecified, newImport, pathToFile.get(fileName));
replacements.push(new project_paths.Replacement(project_paths.projectFile(project_tsconfig_paths.absoluteFrom(fileName), info), new project_paths.TextUpdate({ position: 0, end: 0, toInsert: `${printedImport}\n` })));
});
});
// Capture updated imports
for (const [oldBindings, newBindings] of updatedImports.entries()) {
// The import will be generated as multi-line if it already is multi-line,
// or if the number of elements significantly increased and it previously
// consisted of very few specifiers.
const isMultiline = oldBindings.getText().includes('\n') ||
(newBindings.elements.length >= 6 && oldBindings.elements.length <= 3);
const hasSpaceBetweenBraces = oldBindings.getText().startsWith('{ ');
let formatFlags = ts.ListFormat.NamedImportsOrExportsElements |
ts.ListFormat.Indented |
ts.ListFormat.Braces |
ts.ListFormat.PreserveLines |
(isMultiline ? ts.ListFormat.MultiLine : ts.ListFormat.SingleLine);
if (hasSpaceBetweenBraces) {
formatFlags |= ts.ListFormat.SpaceBetweenBraces;
}
else {
formatFlags &= ~ts.ListFormat.SpaceBetweenBraces;
}
const printedBindings = printer.printList(formatFlags, newBindings.elements, oldBindings.getSourceFile());
replacements.push(new project_paths.Replacement(project_paths.projectFile(oldBindings.getSourceFile(), info), new project_paths.TextUpdate({
position: oldBindings.getStart(),
end: oldBindings.getEnd(),
// TS uses four spaces as indent. We migrate to two spaces as we
// assume this to be more common.
toInsert: printedBindings.replace(/^ {4}/gm, ' '),
})));
}
// Update removed imports
for (const removedImport of deletedImports) {
replacements.push(new project_paths.Replacement(project_paths.projectFile(removedImport.getSourceFile(), info), new project_paths.TextUpdate({
position: removedImport.getStart(),
end: removedImport.getEnd(),
toInsert: '',
})));
}
}
exports.applyImportManagerChanges = applyImportManagerChanges;
'use strict';
/**
* @license Angular v20.3.15
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
var project_tsconfig_paths = require('./project_tsconfig_paths-CgKwP2MX.cjs');
require('os');
var p = require('path');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var p__namespace = /*#__PURE__*/_interopNamespaceDefault(p);
/** Tracks changes that have to be made for specific files. */
class ChangeTracker {
_printer;
_importRemapper;
_changes = new Map();
_importManager;
_quotesCache = new WeakMap();
constructor(_printer, _importRemapper) {
this._printer = _printer;
this._importRemapper = _importRemapper;
this._importManager = new project_tsconfig_paths.ImportManager({
shouldUseSingleQuotes: (file) => this._getQuoteKind(file) === 0 /* QuoteKind.SINGLE */,
});
}
/**
* Tracks the insertion of some text.
* @param sourceFile File in which the text is being inserted.
* @param start Index at which the text is insert.
* @param text Text to be inserted.
*/
insertText(sourceFile, index, text) {
this._trackChange(sourceFile, { start: index, text });
}
/**
* Replaces text within a file.
* @param sourceFile File in which to replace the text.
* @param start Index from which to replace the text.
* @param removeLength Length of the text being replaced.
* @param text Text to be inserted instead of the old one.
*/
replaceText(sourceFile, start, removeLength, text) {
this._trackChange(sourceFile, { start, removeLength, text });
}
/**
* Replaces the text of an AST node with a new one.
* @param oldNode Node to be replaced.
* @param newNode New node to be inserted.
* @param emitHint Hint when formatting the text of the new node.
* @param sourceFileWhenPrinting File to use when printing out the new node. This is important
* when copying nodes from one file to another, because TypeScript might not output literal nodes
* without it.
*/
replaceNode(oldNode, newNode, emitHint = ts.EmitHint.Unspecified, sourceFileWhenPrinting) {
const sourceFile = oldNode.getSourceFile();
this.replaceText(sourceFile, oldNode.getStart(), oldNode.getWidth(), this._printer.printNode(emitHint, newNode, sourceFileWhenPrinting || sourceFile));
}
/**
* Removes the text of an AST node from a file.
* @param node Node whose text should be removed.
* @param useFullOffsets Whether to remove the node using its full offset (e.g. `getFullStart`
* rather than `fullStart`). This has the advantage of removing any comments that may be tied
* to the node, but can lead to too much code being deleted.
*/
removeNode(node, useFullOffsets = false) {
this._trackChange(node.getSourceFile(), {
start: useFullOffsets ? node.getFullStart() : node.getStart(),
removeLength: useFullOffsets ? node.getFullWidth() : node.getWidth(),
text: '',
});
}
/**
* Adds an import to a file.
* @param sourceFile File to which to add the import.
* @param symbolName Symbol being imported.
* @param moduleName Module from which the symbol is imported.
* @param alias Alias to use for the import.
*/
addImport(sourceFile, symbolName, moduleName, alias) {
if (this._importRemapper) {
moduleName = this._importRemapper(moduleName, sourceFile.fileName);
}
// It's common for paths to be manipulated with Node's `path` utilties which
// can yield a path with back slashes. Normalize them since outputting such
// paths will also cause TS to escape the forward slashes.
moduleName = normalizePath(moduleName);
if (!this._changes.has(sourceFile)) {
this._changes.set(sourceFile, []);
}
return this._importManager.addImport({
requestedFile: sourceFile,
exportSymbolName: symbolName,
exportModuleSpecifier: moduleName,
unsafeAliasOverride: alias,
});
}
/**
* Removes an import from a file.
* @param sourceFile File from which to remove the import.
* @param symbolName Original name of the symbol to be removed. Used even if the import is aliased.
* @param moduleName Module from which the symbol is imported.
*/
removeImport(sourceFile, symbolName, moduleName) {
// It's common for paths to be manipulated with Node's `path` utilties which
// can yield a path with back slashes. Normalize them since outputting such
// paths will also cause TS to escape the forward slashes.
moduleName = normalizePath(moduleName);
if (!this._changes.has(sourceFile)) {
this._changes.set(sourceFile, []);
}
this._importManager.removeImport(sourceFile, symbolName, moduleName);
}
/**
* Gets the changes that should be applied to all the files in the migration.
* The changes are sorted in the order in which they should be applied.
*/
recordChanges() {
this._recordImports();
return this._changes;
}
/**
* Clear the tracked changes
*/
clearChanges() {
this._changes.clear();
}
/**
* Adds a change to a `ChangesByFile` map.
* @param file File that the change is associated with.
* @param change Change to be added.
*/
_trackChange(file, change) {
const changes = this._changes.get(file);
if (changes) {
// Insert the changes in reverse so that they're applied in reverse order.
// This ensures that the offsets of subsequent changes aren't affected by
// previous changes changing the file's text.
const insertIndex = changes.findIndex((current) => current.start <= change.start);
if (insertIndex === -1) {
changes.push(change);
}
else {
changes.splice(insertIndex, 0, change);
}
}
else {
this._changes.set(file, [change]);
}
}
/** Determines what kind of quotes to use for a specific file. */
_getQuoteKind(sourceFile) {
if (this._quotesCache.has(sourceFile)) {
return this._quotesCache.get(sourceFile);
}
let kind = 0 /* QuoteKind.SINGLE */;
for (const statement of sourceFile.statements) {
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
kind = statement.moduleSpecifier.getText()[0] === '"' ? 1 /* QuoteKind.DOUBLE */ : 0 /* QuoteKind.SINGLE */;
this._quotesCache.set(sourceFile, kind);
break;
}
}
return kind;
}
/** Records the pending import changes from the import manager. */
_recordImports() {
const { newImports, updatedImports, deletedImports } = this._importManager.finalize();
for (const [original, replacement] of updatedImports) {
this.replaceNode(original, replacement);
}
for (const node of deletedImports) {
this.removeNode(node);
}
for (const [sourceFile] of this._changes) {
const importsToAdd = newImports.get(sourceFile.fileName);
if (!importsToAdd) {
continue;
}
const importLines = [];
let lastImport = null;
for (const statement of sourceFile.statements) {
if (ts.isImportDeclaration(statement)) {
lastImport = statement;
}
}
for (const decl of importsToAdd) {
importLines.push(this._printer.printNode(ts.EmitHint.Unspecified, decl, sourceFile));
}
this.insertText(sourceFile, lastImport ? lastImport.getEnd() : 0, (lastImport ? '\n' : '') + importLines.join('\n'));
}
}
}
/** Normalizes a path to use posix separators. */
function normalizePath(path) {
return path.replace(/\\/g, '/');
}
function parseTsconfigFile(tsconfigPath, basePath) {
const { config } = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
const parseConfigHost = {
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
fileExists: ts.sys.fileExists,
readDirectory: ts.sys.readDirectory,
readFile: ts.sys.readFile,
};
// Throw if incorrect arguments are passed to this function. Passing relative base paths
// results in root directories not being resolved and in later type checking runtime errors.
// More details can be found here: https://github.com/microsoft/TypeScript/issues/37731.
if (!p__namespace.isAbsolute(basePath)) {
throw Error('Unexpected relative base path has been specified.');
}
return ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, {});
}
/**
* Creates a TypeScript program instance for a TypeScript project within
* the virtual file system tree.
* @param tree Virtual file system tree that contains the source files.
* @param tsconfigPath Virtual file system path that resolves to the TypeScript project.
* @param basePath Base path for the virtual file system tree.
* @param fakeFileRead Optional file reader function. Can be used to overwrite files in
* the TypeScript program, or to add in-memory files (e.g. to add global types).
* @param additionalFiles Additional file paths that should be added to the program.
*/
function createMigrationProgram(tree, tsconfigPath, basePath, fakeFileRead, additionalFiles) {
const { rootNames, options, host } = createProgramOptions(tree, tsconfigPath, basePath, fakeFileRead);
return ts.createProgram(rootNames, options, host);
}
/**
* Creates the options necessary to instantiate a TypeScript program.
* @param tree Virtual file system tree that contains the source files.
* @param tsconfigPath Virtual file system path that resolves to the TypeScript project.
* @param basePath Base path for the virtual file system tree.
* @param fakeFileRead Optional file reader function. Can be used to overwrite files in
* the TypeScript program, or to add in-memory files (e.g. to add global types).
* @param additionalFiles Additional file paths that should be added to the program.
* @param optionOverrides Overrides of the parsed compiler options.
*/
function createProgramOptions(tree, tsconfigPath, basePath, fakeFileRead, additionalFiles, optionOverrides) {
// Resolve the tsconfig path to an absolute path. This is needed as TypeScript otherwise
// is not able to resolve root directories in the given tsconfig. More details can be found
// in the following issue: https://github.com/microsoft/TypeScript/issues/37731.
tsconfigPath = p.resolve(basePath, tsconfigPath);
const parsed = parseTsconfigFile(tsconfigPath, p.dirname(tsconfigPath));
const options = optionOverrides ? { ...parsed.options, ...optionOverrides } : parsed.options;
const host = createMigrationCompilerHost(tree, options, basePath, fakeFileRead);
return { rootNames: parsed.fileNames.concat([]), options, host };
}
function createMigrationCompilerHost(tree, options, basePath, fakeRead) {
const host = ts.createCompilerHost(options, true);
const defaultReadFile = host.readFile;
// We need to overwrite the host "readFile" method, as we want the TypeScript
// program to be based on the file contents in the virtual file tree. Otherwise
// if we run multiple migrations we might have intersecting changes and
// source files.
host.readFile = (fileName) => {
const treeRelativePath = p.relative(basePath, fileName);
let result = fakeRead?.(treeRelativePath);
if (typeof result !== 'string') {
// If the relative path resolved to somewhere outside of the tree, fall back to
// TypeScript's default file reading function since the `tree` will throw an error.
result = treeRelativePath.startsWith('..')
? defaultReadFile.call(host, fileName)
: tree.read(treeRelativePath)?.toString();
}
// Strip BOM as otherwise TSC methods (Ex: getWidth) will return an offset,
// which breaks the CLI UpdateRecorder.
// See: https://github.com/angular/angular/pull/30719
return typeof result === 'string' ? result.replace(/^\uFEFF/, '') : undefined;
};
return host;
}
/**
* Checks whether a file can be migrate by our automated migrations.
* @param basePath Absolute path to the project.
* @param sourceFile File being checked.
* @param program Program that includes the source file.
*/
function canMigrateFile(basePath, sourceFile, program) {
// We shouldn't migrate .d.ts files, files from an external library or type checking files.
if (sourceFile.fileName.endsWith('.ngtypecheck.ts') ||
sourceFile.isDeclarationFile ||
program.isSourceFileFromExternalLibrary(sourceFile)) {
return false;
}
// Our migrations are set up to create a `Program` from the project's tsconfig and to migrate all
// the files within the program. This can include files that are outside of the Angular CLI
// project. We can't migrate files outside of the project, because our file system interactions
// go through the CLI's `Tree` which assumes that all files are within the project. See:
// https://github.com/angular/angular-cli/blob/0b0961c9c233a825b6e4bb59ab7f0790f9b14676/packages/angular_devkit/schematics/src/tree/host-tree.ts#L131
return !p.relative(basePath, sourceFile.fileName).startsWith('..');
}
exports.ChangeTracker = ChangeTracker;
exports.canMigrateFile = canMigrateFile;
exports.createMigrationProgram = createMigrationProgram;
exports.createProgramOptions = createProgramOptions;
exports.normalizePath = normalizePath;

Sorry, the diff of this file is too big to display

'use strict';
/**
* @license Angular v20.3.15
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
require('os');
var project_tsconfig_paths = require('./project_tsconfig_paths-CgKwP2MX.cjs');
var index = require('./index-Cu2VLWJC.cjs');
require('path');
require('node:path');
var project_paths = require('./project_paths-Ck6Mza_h.cjs');
function getMemberName(member) {
if (member.name === undefined) {
return null;
}
if (ts.isIdentifier(member.name) || ts.isStringLiteralLike(member.name)) {
return member.name.text;
}
if (ts.isPrivateIdentifier(member.name)) {
return `#${member.name.text}`;
}
return null;
}
/** Checks whether the given node can be an `@Input()` declaration node. */
function isInputContainerNode(node) {
return (((ts.isAccessor(node) && ts.isClassDeclaration(node.parent)) ||
ts.isPropertyDeclaration(node)) &&
getMemberName(node) !== null);
}
/**
* Detects `query(By.directive(T)).componentInstance` patterns and enhances
* them with information of `T`. This is important because `.componentInstance`
* is currently typed as `any` and may cause runtime test failures after input
* migrations then.
*
* The reference resolution pass leverages information from this pattern
* recognizer.
*/
class DebugElementComponentInstance {
checker;
cache = new WeakMap();
constructor(checker) {
this.checker = checker;
}
detect(node) {
if (this.cache.has(node)) {
return this.cache.get(node);
}
if (!ts.isPropertyAccessExpression(node)) {
return null;
}
// Check for `<>.componentInstance`.
if (!ts.isIdentifier(node.name) || node.name.text !== 'componentInstance') {
return null;
}
// Check for `<>.query(..).<>`.
if (!ts.isCallExpression(node.expression) ||
!ts.isPropertyAccessExpression(node.expression.expression) ||
!ts.isIdentifier(node.expression.expression.name) ||
node.expression.expression.name.text !== 'query') {
return null;
}
const queryCall = node.expression;
if (queryCall.arguments.length !== 1) {
return null;
}
const queryArg = queryCall.arguments[0];
let typeExpr;
if (ts.isCallExpression(queryArg) &&
queryArg.arguments.length === 1 &&
ts.isIdentifier(queryArg.arguments[0])) {
// Detect references, like: `query(By.directive(T))`.
typeExpr = queryArg.arguments[0];
}
else if (ts.isIdentifier(queryArg)) {
// Detect references, like: `harness.query(T)`.
typeExpr = queryArg;
}
else {
return null;
}
const symbol = this.checker.getSymbolAtLocation(typeExpr);
if (symbol?.valueDeclaration === undefined ||
!ts.isClassDeclaration(symbol?.valueDeclaration)) {
// Cache this as we use the expensive type checker.
this.cache.set(node, null);
return null;
}
const type = this.checker.getTypeAtLocation(symbol.valueDeclaration);
this.cache.set(node, type);
return type;
}
}
/**
* Recognizes `Partial<T>` instances in Catalyst tests. Those type queries
* are likely used for typing property initialization values for the given class `T`
* and we have a few scenarios:
*
* 1. The API does not unwrap signal inputs. In which case, the values are likely no
* longer assignable to an `InputSignal`.
* 2. The API does unwrap signal inputs, in which case we need to unwrap the `Partial`
* because the values are raw initial values, like they were before.
*
* We can enable this heuristic when we detect Catalyst as we know it supports unwrapping.
*/
class PartialDirectiveTypeInCatalystTests {
checker;
knownFields;
constructor(checker, knownFields) {
this.checker = checker;
this.knownFields = knownFields;
}
detect(node) {
// Detect `Partial<...>`
if (!ts.isTypeReferenceNode(node) ||
!ts.isIdentifier(node.typeName) ||
node.typeName.text !== 'Partial') {
return null;
}
// Ignore if the source file doesn't reference Catalyst.
if (!node.getSourceFile().text.includes('angular2/testing/catalyst')) {
return null;
}
// Extract T of `Partial<T>`.
const cmpTypeArg = node.typeArguments?.[0];
if (!cmpTypeArg ||
!ts.isTypeReferenceNode(cmpTypeArg) ||
!ts.isIdentifier(cmpTypeArg.typeName)) {
return null;
}
const cmpType = cmpTypeArg.typeName;
const symbol = this.checker.getSymbolAtLocation(cmpType);
// Note: Technically the class might be derived of an input-containing class,
// but this is out of scope for now. We can expand if we see it's a common case.
if (symbol?.valueDeclaration === undefined ||
!ts.isClassDeclaration(symbol.valueDeclaration) ||
!this.knownFields.shouldTrackClassReference(symbol.valueDeclaration)) {
return null;
}
return { referenceNode: node, targetClass: symbol.valueDeclaration };
}
}
/**
* Attempts to look up the given property access chain using
* the type checker.
*
* Notably this is not as safe as using the type checker directly to
* retrieve symbols of a given identifier, but in some cases this is
* a necessary approach to compensate e.g. for a lack of TCB information
* when processing Angular templates.
*
* The path is a list of properties to be accessed sequentially on the
* given type.
*/
function lookupPropertyAccess(checker, type, path, options = {}) {
let symbol = null;
for (const propName of path) {
// Note: We support assuming `NonNullable` for the pathl This is necessary
// in some situations as otherwise the lookups would fail to resolve the target
// symbol just because of e.g. a ternary. This is used in the signal input migration
// for host bindings.
type = options.ignoreNullability ? type.getNonNullableType() : type;
const propSymbol = type.getProperty(propName);
if (propSymbol === undefined) {
return null;
}
symbol = propSymbol;
type = checker.getTypeOfSymbol(propSymbol);
}
if (symbol === null) {
return null;
}
return { symbol, type };
}
/**
* AST visitor that iterates through a template and finds all
* input references.
*
* This resolution is important to be able to migrate references to inputs
* that will be migrated to signal inputs.
*/
class TemplateReferenceVisitor extends project_tsconfig_paths.RecursiveVisitor {
result = [];
/**
* Whether we are currently descending into HTML AST nodes
* where all bound attributes are considered potentially narrowing.
*
* Keeps track of all referenced inputs in such attribute expressions.
*/
templateAttributeReferencedFields = null;
expressionVisitor;
seenKnownFieldsCount = new Map();
constructor(typeChecker, templateTypeChecker, componentClass, knownFields, fieldNamesToConsiderForReferenceLookup) {
super();
this.expressionVisitor = new TemplateExpressionReferenceVisitor(typeChecker, templateTypeChecker, componentClass, knownFields, fieldNamesToConsiderForReferenceLookup);
}
checkExpressionForReferencedFields(activeNode, expressionNode) {
const referencedFields = this.expressionVisitor.checkTemplateExpression(activeNode, expressionNode);
// Add all references to the overall visitor result.
this.result.push(...referencedFields);
// Count usages of seen input references. We'll use this to make decisions
// based on whether inputs are potentially narrowed or not.
for (const input of referencedFields) {
this.seenKnownFieldsCount.set(input.targetField.key, (this.seenKnownFieldsCount.get(input.targetField.key) ?? 0) + 1);
}
return referencedFields;
}
descendAndCheckForNarrowedSimilarReferences(potentiallyNarrowedInputs, descend) {
const inputs = potentiallyNarrowedInputs.map((i) => ({
ref: i,
key: i.targetField.key,
pastCount: this.seenKnownFieldsCount.get(i.targetField.key) ?? 0,
}));
descend();
for (const input of inputs) {
// Input was referenced inside a narrowable spot, and is used in child nodes.
// This is a sign for the input to be narrowed. Mark it as such.
if ((this.seenKnownFieldsCount.get(input.key) ?? 0) > input.pastCount) {
input.ref.isLikelyNarrowed = true;
}
}
}
visitTemplate(template) {
// Note: We assume all bound expressions for templates may be subject
// to TCB narrowing. This is relevant for now until we support narrowing
// of signal calls in templates.
// TODO: Remove with: https://github.com/angular/angular/pull/55456.
this.templateAttributeReferencedFields = [];
project_tsconfig_paths.visitAll(this, template.attributes);
project_tsconfig_paths.visitAll(this, template.templateAttrs);
// If we are dealing with a microsyntax template, do not check
// inputs and outputs as those are already passed to the children.
// Template attributes may contain relevant expressions though.
if (template.tagName === 'ng-template') {
project_tsconfig_paths.visitAll(this, template.inputs);
project_tsconfig_paths.visitAll(this, template.outputs);
}
const referencedInputs = this.templateAttributeReferencedFields;
this.templateAttributeReferencedFields = null;
this.descendAndCheckForNarrowedSimilarReferences(referencedInputs, () => {
project_tsconfig_paths.visitAll(this, template.children);
project_tsconfig_paths.visitAll(this, template.references);
project_tsconfig_paths.visitAll(this, template.variables);
});
}
visitIfBlockBranch(block) {
if (block.expression) {
const referencedFields = this.checkExpressionForReferencedFields(block, block.expression);
this.descendAndCheckForNarrowedSimilarReferences(referencedFields, () => {
super.visitIfBlockBranch(block);
});
}
else {
super.visitIfBlockBranch(block);
}
}
visitForLoopBlock(block) {
this.checkExpressionForReferencedFields(block, block.expression);
this.checkExpressionForReferencedFields(block, block.trackBy);
super.visitForLoopBlock(block);
}
visitSwitchBlock(block) {
const referencedFields = this.checkExpressionForReferencedFields(block, block.expression);
this.descendAndCheckForNarrowedSimilarReferences(referencedFields, () => {
super.visitSwitchBlock(block);
});
}
visitSwitchBlockCase(block) {
if (block.expression) {
const referencedFields = this.checkExpressionForReferencedFields(block, block.expression);
this.descendAndCheckForNarrowedSimilarReferences(referencedFields, () => {
super.visitSwitchBlockCase(block);
});
}
else {
super.visitSwitchBlockCase(block);
}
}
visitDeferredBlock(deferred) {
if (deferred.triggers.when) {
this.checkExpressionForReferencedFields(deferred, deferred.triggers.when.value);
}
if (deferred.prefetchTriggers.when) {
this.checkExpressionForReferencedFields(deferred, deferred.prefetchTriggers.when.value);
}
super.visitDeferredBlock(deferred);
}
visitBoundText(text) {
this.checkExpressionForReferencedFields(text, text.value);
}
visitBoundEvent(attribute) {
this.checkExpressionForReferencedFields(attribute, attribute.handler);
}
visitBoundAttribute(attribute) {
const referencedFields = this.checkExpressionForReferencedFields(attribute, attribute.value);
// Attributes inside templates are potentially "narrowed" and hence we
// keep track of all referenced inputs to see if they actually are.
if (this.templateAttributeReferencedFields !== null) {
this.templateAttributeReferencedFields.push(...referencedFields);
}
}
}
/**
* Expression AST visitor that checks whether a given expression references
* a known `@Input()`.
*
* This resolution is important to be able to migrate references to inputs
* that will be migrated to signal inputs.
*/
class TemplateExpressionReferenceVisitor extends project_tsconfig_paths.RecursiveAstVisitor {
typeChecker;
templateTypeChecker;
componentClass;
knownFields;
fieldNamesToConsiderForReferenceLookup;
activeTmplAstNode = null;
detectedInputReferences = [];
isInsideObjectShorthandExpression = false;
insideConditionalExpressionsWithReads = [];
constructor(typeChecker, templateTypeChecker, componentClass, knownFields, fieldNamesToConsiderForReferenceLookup) {
super();
this.typeChecker = typeChecker;
this.templateTypeChecker = templateTypeChecker;
this.componentClass = componentClass;
this.knownFields = knownFields;
this.fieldNamesToConsiderForReferenceLookup = fieldNamesToConsiderForReferenceLookup;
}
/** Checks the given AST expression. */
checkTemplateExpression(activeNode, expressionNode) {
this.detectedInputReferences = [];
this.activeTmplAstNode = activeNode;
expressionNode.visit(this, []);
return this.detectedInputReferences;
}
visit(ast, context) {
super.visit(ast, [...context, ast]);
}
// Keep track when we are inside an object shorthand expression. This is
// necessary as we need to expand the shorthand to invoke a potential new signal.
// E.g. `{bla}` may be transformed to `{bla: bla()}`.
visitLiteralMap(ast, context) {
for (const [idx, key] of ast.keys.entries()) {
this.isInsideObjectShorthandExpression = !!key.isShorthandInitialized;
ast.values[idx].visit(this, context);
this.isInsideObjectShorthandExpression = false;
}
}
visitPropertyRead(ast, context) {
this._inspectPropertyAccess(ast, false, context);
super.visitPropertyRead(ast, context);
}
visitSafePropertyRead(ast, context) {
this._inspectPropertyAccess(ast, false, context);
super.visitPropertyRead(ast, context);
}
visitBinary(ast, context) {
if (ast.operation === '=' && ast.left instanceof project_tsconfig_paths.PropertyRead) {
this._inspectPropertyAccess(ast.left, true, [...context, ast, ast.left]);
}
else {
super.visitBinary(ast, context);
}
}
visitConditional(ast, context) {
this.visit(ast.condition, context);
this.insideConditionalExpressionsWithReads.push(ast.condition);
this.visit(ast.trueExp, context);
this.visit(ast.falseExp, context);
this.insideConditionalExpressionsWithReads.pop();
}
/**
* Inspects the property access and attempts to resolve whether they access
* a known field. If so, the result is captured.
*/
_inspectPropertyAccess(ast, isAssignment, astPath) {
if (this.fieldNamesToConsiderForReferenceLookup !== null &&
!this.fieldNamesToConsiderForReferenceLookup.has(ast.name)) {
return;
}
const isWrite = !!(isAssignment ||
(this.activeTmplAstNode && isTwoWayBindingNode(this.activeTmplAstNode)));
this._checkAccessViaTemplateTypeCheckBlock(ast, isWrite, astPath) ||
this._checkAccessViaOwningComponentClassType(ast, isWrite, astPath);
}
/**
* Checks whether the node refers to an input using the TCB information.
* Type check block may not exist for e.g. test components, so this can return `null`.
*/
_checkAccessViaTemplateTypeCheckBlock(ast, isWrite, astPath) {
// There might be no template type checker. E.g. if we check host bindings.
if (this.templateTypeChecker === null) {
return false;
}
const symbol = this.templateTypeChecker.getSymbolOfNode(ast, this.componentClass);
if (symbol?.kind !== project_tsconfig_paths.SymbolKind.Expression || symbol.tsSymbol === null) {
return false;
}
// Dangerous: Type checking symbol retrieval is a totally different `ts.Program`,
// than the one where we analyzed `knownInputs`.
// --> Find the input via its input id.
const targetInput = this.knownFields.attemptRetrieveDescriptorFromSymbol(symbol.tsSymbol);
if (targetInput === null) {
return false;
}
this.detectedInputReferences.push({
targetNode: targetInput.node,
targetField: targetInput,
read: ast,
readAstPath: astPath,
context: this.activeTmplAstNode,
isLikelyNarrowed: this._isPartOfNarrowingTernary(ast),
isObjectShorthandExpression: this.isInsideObjectShorthandExpression,
isWrite,
});
return true;
}
/**
* Simple resolution checking whether the given AST refers to a known input.
* This is a fallback for when there is no type checking information (e.g. in host bindings).
*
* It attempts to resolve references by traversing accesses of the "component class" type.
* e.g. `this.bla` is resolved via `CompType#bla` and further.
*/
_checkAccessViaOwningComponentClassType(ast, isWrite, astPath) {
// We might check host bindings, which can never point to template variables or local refs.
const expressionTemplateTarget = this.templateTypeChecker === null
? null
: this.templateTypeChecker.getExpressionTarget(ast, this.componentClass);
// Skip checking if:
// - the reference resolves to a template variable or local ref. No way to resolve without TCB.
// - the owning component does not have a name (should not happen technically).
if (expressionTemplateTarget !== null || this.componentClass.name === undefined) {
return;
}
const property = traverseReceiverAndLookupSymbol(ast, this.componentClass, this.typeChecker);
if (property === null) {
return;
}
const matchingTarget = this.knownFields.attemptRetrieveDescriptorFromSymbol(property);
if (matchingTarget === null) {
return;
}
this.detectedInputReferences.push({
targetNode: matchingTarget.node,
targetField: matchingTarget,
read: ast,
readAstPath: astPath,
context: this.activeTmplAstNode,
isLikelyNarrowed: this._isPartOfNarrowingTernary(ast),
isObjectShorthandExpression: this.isInsideObjectShorthandExpression,
isWrite,
});
}
_isPartOfNarrowingTernary(read) {
// Note: We do not safe check that the reads are fully matching 1:1. This is acceptable
// as worst case we just skip an input from being migrated. This is very unlikely too.
return this.insideConditionalExpressionsWithReads.some((r) => (r instanceof project_tsconfig_paths.PropertyRead || r instanceof project_tsconfig_paths.SafePropertyRead) && r.name === read.name);
}
}
/**
* Emulates an access to a given field using the TypeScript `ts.Type`
* of the given class. The resolved symbol of the access is returned.
*/
function traverseReceiverAndLookupSymbol(readOrWrite, componentClass, checker) {
const path = [readOrWrite.name];
let node = readOrWrite;
while (node.receiver instanceof project_tsconfig_paths.PropertyRead) {
node = node.receiver;
path.unshift(node.name);
}
if (!(node.receiver instanceof project_tsconfig_paths.ImplicitReceiver || node.receiver instanceof project_tsconfig_paths.ThisReceiver)) {
return null;
}
const classType = checker.getTypeAtLocation(componentClass.name);
return (lookupPropertyAccess(checker, classType, path, {
// Necessary to avoid breaking the resolution if there is
// some narrowing involved. E.g. `myClass ? myClass.input`.
ignoreNullability: true,
})?.symbol ?? null);
}
/** Whether the given node refers to a two-way binding AST node. */
function isTwoWayBindingNode(node) {
return ((node instanceof project_tsconfig_paths.BoundAttribute && node.type === project_tsconfig_paths.BindingType.TwoWay) ||
(node instanceof project_tsconfig_paths.BoundEvent && node.type === project_tsconfig_paths.ParsedEventType.TwoWay));
}
/** Possible types of references to known fields detected. */
exports.ReferenceKind = void 0;
(function (ReferenceKind) {
ReferenceKind[ReferenceKind["InTemplate"] = 0] = "InTemplate";
ReferenceKind[ReferenceKind["InHostBinding"] = 1] = "InHostBinding";
ReferenceKind[ReferenceKind["TsReference"] = 2] = "TsReference";
ReferenceKind[ReferenceKind["TsClassTypeReference"] = 3] = "TsClassTypeReference";
})(exports.ReferenceKind || (exports.ReferenceKind = {}));
/** Whether the given reference is a TypeScript reference. */
function isTsReference(ref) {
return ref.kind === exports.ReferenceKind.TsReference;
}
/** Whether the given reference is a template reference. */
function isTemplateReference(ref) {
return ref.kind === exports.ReferenceKind.InTemplate;
}
/** Whether the given reference is a host binding reference. */
function isHostBindingReference(ref) {
return ref.kind === exports.ReferenceKind.InHostBinding;
}
/**
* Whether the given reference is a TypeScript `ts.Type` reference
* to a class containing known fields.
*/
function isTsClassTypeReference(ref) {
return ref.kind === exports.ReferenceKind.TsClassTypeReference;
}
/**
* Checks host bindings of the given class and tracks all
* references to inputs within bindings.
*/
function identifyHostBindingReferences(node, programInfo, checker, reflector, result, knownFields, fieldNamesToConsiderForReferenceLookup) {
if (node.name === undefined) {
return;
}
const decorators = reflector.getDecoratorsOfDeclaration(node);
if (decorators === null) {
return;
}
const angularDecorators = project_tsconfig_paths.getAngularDecorators(decorators, ['Directive', 'Component'],
/* isAngularCore */ false);
if (angularDecorators.length === 0) {
return;
}
// Assume only one Angular decorator per class.
const ngDecorator = angularDecorators[0];
if (ngDecorator.args?.length !== 1) {
return;
}
const metadataNode = project_tsconfig_paths.unwrapExpression(ngDecorator.args[0]);
if (!ts.isObjectLiteralExpression(metadataNode)) {
return;
}
const metadata = project_tsconfig_paths.reflectObjectLiteral(metadataNode);
if (!metadata.has('host')) {
return;
}
let hostField = project_tsconfig_paths.unwrapExpression(metadata.get('host'));
// Special-case in case host bindings are shared via a variable.
// e.g. Material button shares host bindings as a constant in the same target.
if (ts.isIdentifier(hostField)) {
let symbol = checker.getSymbolAtLocation(hostField);
// Plain identifier references can point to alias symbols (e.g. imports).
if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) {
symbol = checker.getAliasedSymbol(symbol);
}
if (symbol !== undefined &&
symbol.valueDeclaration !== undefined &&
ts.isVariableDeclaration(symbol.valueDeclaration)) {
hostField = symbol?.valueDeclaration.initializer;
}
}
if (hostField === undefined || !ts.isObjectLiteralExpression(hostField)) {
return;
}
const hostMap = project_tsconfig_paths.reflectObjectLiteral(hostField);
const expressionResult = [];
const expressionVisitor = new TemplateExpressionReferenceVisitor(checker, null, node, knownFields, fieldNamesToConsiderForReferenceLookup);
for (const [rawName, expression] of hostMap.entries()) {
if (!ts.isStringLiteralLike(expression)) {
continue;
}
const isEventBinding = rawName.startsWith('(');
const isPropertyBinding = rawName.startsWith('[');
// Only migrate property or event bindings.
if (!isPropertyBinding && !isEventBinding) {
continue;
}
const parser = project_tsconfig_paths.makeBindingParser();
const sourceSpan = new project_tsconfig_paths.ParseSourceSpan(
// Fake source span to keep parsing offsets zero-based.
// We then later combine these with the expression TS node offsets.
new project_tsconfig_paths.ParseLocation({ content: '', url: '' }, 0, 0, 0), new project_tsconfig_paths.ParseLocation({ content: '', url: '' }, 0, 0, 0));
const name = rawName.substring(1, rawName.length - 1);
let parsed = undefined;
if (isEventBinding) {
const result = [];
parser.parseEvent(name.substring(1, name.length - 1), expression.text, false, sourceSpan, sourceSpan, [], result, sourceSpan);
parsed = result[0].handler;
}
else {
const result = [];
parser.parsePropertyBinding(name, expression.text, true,
/* isTwoWayBinding */ false, sourceSpan, 0, sourceSpan, [], result, sourceSpan);
parsed = result[0].expression;
}
if (parsed != null) {
expressionResult.push(...expressionVisitor.checkTemplateExpression(expression, parsed));
}
}
for (const ref of expressionResult) {
result.references.push({
kind: exports.ReferenceKind.InHostBinding,
from: {
read: ref.read,
readAstPath: ref.readAstPath,
isObjectShorthandExpression: ref.isObjectShorthandExpression,
isWrite: ref.isWrite,
file: project_paths.projectFile(ref.context.getSourceFile(), programInfo),
hostPropertyNode: ref.context,
},
target: ref.targetField,
});
}
}
/**
* Attempts to extract the `TemplateDefinition` for the given
* class, if possible.
*
* The definition can then be used with the Angular compiler to
* load/parse the given template.
*/
function attemptExtractTemplateDefinition(node, checker, reflector, resourceLoader) {
const classDecorators = reflector.getDecoratorsOfDeclaration(node);
const evaluator = new index.PartialEvaluator(reflector, checker, null);
const ngDecorators = classDecorators !== null
? project_tsconfig_paths.getAngularDecorators(classDecorators, ['Component'], /* isAngularCore */ false)
: [];
if (ngDecorators.length === 0 ||
ngDecorators[0].args === null ||
ngDecorators[0].args.length === 0 ||
!ts.isObjectLiteralExpression(ngDecorators[0].args[0])) {
return null;
}
const properties = project_tsconfig_paths.reflectObjectLiteral(ngDecorators[0].args[0]);
const templateProp = properties.get('template');
const templateUrlProp = properties.get('templateUrl');
const containingFile = node.getSourceFile().fileName;
// inline template.
if (templateProp !== undefined) {
const templateStr = evaluator.evaluate(templateProp);
if (typeof templateStr === 'string') {
return {
isInline: true,
expression: templateProp,
interpolationConfig: project_tsconfig_paths.DEFAULT_INTERPOLATION_CONFIG,
preserveWhitespaces: false,
resolvedTemplateUrl: containingFile,
templateUrl: containingFile,
};
}
}
try {
// external template.
if (templateUrlProp !== undefined) {
const templateUrl = evaluator.evaluate(templateUrlProp);
if (typeof templateUrl === 'string') {
return {
isInline: false,
interpolationConfig: project_tsconfig_paths.DEFAULT_INTERPOLATION_CONFIG,
preserveWhitespaces: false,
templateUrlExpression: templateUrlProp,
templateUrl,
resolvedTemplateUrl: resourceLoader.resolve(templateUrl, containingFile),
};
}
}
}
catch (e) {
console.error(`Could not parse external template: ${e}`);
}
return null;
}
/**
* Checks whether the given class has an Angular template, and resolves
* all of the references to inputs.
*/
function identifyTemplateReferences(programInfo, node, reflector, checker, evaluator, templateTypeChecker, resourceLoader, options, result, knownFields, fieldNamesToConsiderForReferenceLookup) {
const template = templateTypeChecker.getTemplate(node, project_tsconfig_paths.OptimizeFor.WholeProgram) ??
// If there is no template registered in the TCB or compiler, the template may
// be skipped due to an explicit `jit: true` setting. We try to detect this case
// and parse the template manually.
extractTemplateWithoutCompilerAnalysis(node, checker, reflector, resourceLoader, evaluator, options);
if (template !== null) {
const visitor = new TemplateReferenceVisitor(checker, templateTypeChecker, node, knownFields, fieldNamesToConsiderForReferenceLookup);
template.forEach((node) => node.visit(visitor));
for (const res of visitor.result) {
const templateFilePath = res.context.sourceSpan.start.file.url;
// Templates without an URL are non-mappable artifacts of e.g.
// string concatenated templates. See the `indirect` template
// source mapping concept in the compiler. We skip such references
// as those cannot be migrated, but print an error for now.
if (templateFilePath === '') {
// TODO: Incorporate a TODO potentially.
console.error(`Found reference to field ${res.targetField.key} that cannot be ` +
`migrated because the template cannot be parsed with source map information ` +
`(in file: ${node.getSourceFile().fileName}).`);
continue;
}
result.references.push({
kind: exports.ReferenceKind.InTemplate,
from: {
read: res.read,
readAstPath: res.readAstPath,
node: res.context,
isObjectShorthandExpression: res.isObjectShorthandExpression,
originatingTsFile: project_paths.projectFile(node.getSourceFile(), programInfo),
templateFile: project_paths.projectFile(project_tsconfig_paths.absoluteFrom(templateFilePath), programInfo),
isLikelyPartOfNarrowing: res.isLikelyNarrowed,
isWrite: res.isWrite,
},
target: res.targetField,
});
}
}
}
/**
* Attempts to extract a `@Component` template from the given class,
* without relying on the `NgCompiler` program analysis.
*
* This is useful for JIT components using `jit: true` which were not
* processed by the Angular compiler, but may still have templates that
* contain references to inputs that we can resolve via the fallback
* reference resolutions (that does not use the type check block).
*/
function extractTemplateWithoutCompilerAnalysis(node, checker, reflector, resourceLoader, evaluator, options) {
if (node.name === undefined) {
return null;
}
const tmplDef = attemptExtractTemplateDefinition(node, checker, reflector, resourceLoader);
if (tmplDef === null) {
return null;
}
return index.extractTemplate(node, tmplDef, evaluator, null, resourceLoader, {
enableBlockSyntax: true,
enableLetSyntax: true,
usePoisonedData: true,
enableI18nLegacyMessageIdFormat: options.enableI18nLegacyMessageIdFormat !== false,
i18nNormalizeLineEndingsInICUs: options.i18nNormalizeLineEndingsInICUs === true,
enableSelectorless: false,
}, project_tsconfig_paths.CompilationMode.FULL).nodes;
}
/** Gets the pattern and property name for a given binding element. */
function resolveBindingElement(node) {
const name = node.propertyName ?? node.name;
// If we are discovering a non-analyzable element in the path, abort.
if (!ts.isStringLiteralLike(name) && !ts.isIdentifier(name)) {
return null;
}
return {
pattern: node.parent,
propertyName: name.text,
};
}
/** Gets the declaration node of the given binding element. */
function getBindingElementDeclaration(node) {
while (true) {
if (ts.isBindingElement(node.parent.parent)) {
node = node.parent.parent;
}
else {
return node.parent.parent;
}
}
}
/**
* Expands the given reference to its containing expression, capturing
* the full context.
*
* E.g. `traverseAccess(ref<`bla`>)` may return `this.bla`
* or `traverseAccess(ref<`bla`>)` may return `this.someObj.a.b.c.bla`.
*
* This helper is useful as we will replace the full access with a temporary
* variable for narrowing. Replacing just the identifier is wrong.
*/
function traverseAccess(access) {
if (ts.isPropertyAccessExpression(access.parent) && access.parent.name === access) {
return access.parent;
}
else if (ts.isElementAccessExpression(access.parent) &&
access.parent.argumentExpression === access) {
return access.parent;
}
return access;
}
/**
* Unwraps the parent of the given node, if it's a
* parenthesized expression or `as` expression.
*/
function unwrapParent(node) {
if (ts.isParenthesizedExpression(node.parent)) {
return unwrapParent(node.parent);
}
else if (ts.isAsExpression(node.parent)) {
return unwrapParent(node.parent);
}
return node;
}
/**
* List of binary operators that indicate a write operation.
*
* Useful for figuring out whether an expression assigns to
* something or not.
*/
const writeBinaryOperators = [
ts.SyntaxKind.EqualsToken,
ts.SyntaxKind.BarBarEqualsToken,
ts.SyntaxKind.BarEqualsToken,
ts.SyntaxKind.AmpersandEqualsToken,
ts.SyntaxKind.AmpersandAmpersandEqualsToken,
ts.SyntaxKind.SlashEqualsToken,
ts.SyntaxKind.MinusEqualsToken,
ts.SyntaxKind.PlusEqualsToken,
ts.SyntaxKind.CaretEqualsToken,
ts.SyntaxKind.PercentEqualsToken,
ts.SyntaxKind.AsteriskEqualsToken,
ts.SyntaxKind.ExclamationEqualsToken,
];
/**
* Checks whether given TypeScript reference refers to an Angular input, and captures
* the reference if possible.
*
* @param fieldNamesToConsiderForReferenceLookup List of field names that should be
* respected when expensively looking up references to known fields.
* May be null if all identifiers should be inspected.
*/
function identifyPotentialTypeScriptReference(node, programInfo, checker, knownFields, result, fieldNamesToConsiderForReferenceLookup, advisors) {
// Skip all identifiers that never can point to a migrated field.
// TODO: Capture these assumptions and performance optimizations in the design doc.
if (fieldNamesToConsiderForReferenceLookup !== null &&
!fieldNamesToConsiderForReferenceLookup.has(node.text)) {
return;
}
let target = undefined;
try {
// Resolve binding elements to their declaration symbol.
// Commonly inputs are accessed via object expansion. e.g. `const {input} = this;`.
if (ts.isBindingElement(node.parent)) {
// Skip binding elements that are using spread.
if (node.parent.dotDotDotToken !== undefined) {
return;
}
const bindingInfo = resolveBindingElement(node.parent);
if (bindingInfo === null) {
// The declaration could not be resolved. Skip analyzing this.
return;
}
const bindingType = checker.getTypeAtLocation(bindingInfo.pattern);
const resolved = lookupPropertyAccess(checker, bindingType, [bindingInfo.propertyName]);
target = resolved?.symbol;
}
else {
target = checker.getSymbolAtLocation(node);
}
}
catch (e) {
console.error('Unexpected error while trying to resolve identifier reference:');
console.error(e);
// Gracefully skip analyzing. This can happen when e.g. a reference is named similar
// to an input, but is dependant on `.d.ts` that is not necessarily available (clutz dts).
return;
}
noTargetSymbolCheck: if (target === undefined) {
if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
const propAccessSymbol = checker.getSymbolAtLocation(node.parent.expression);
if (propAccessSymbol !== undefined &&
propAccessSymbol.valueDeclaration !== undefined &&
ts.isVariableDeclaration(propAccessSymbol.valueDeclaration) &&
propAccessSymbol.valueDeclaration.initializer !== undefined) {
target = advisors.debugElComponentInstanceTracker
.detect(propAccessSymbol.valueDeclaration.initializer)
?.getProperty(node.text);
// We found a target in the fallback path. Break out.
if (target !== undefined) {
break noTargetSymbolCheck;
}
}
}
return;
}
let targetInput = knownFields.attemptRetrieveDescriptorFromSymbol(target);
if (targetInput === null) {
return;
}
const access = unwrapParent(traverseAccess(node));
const accessParent = access.parent;
const isWriteReference = ts.isBinaryExpression(accessParent) &&
accessParent.left === access &&
writeBinaryOperators.includes(accessParent.operatorToken.kind);
// track accesses from source files to known fields.
result.references.push({
kind: exports.ReferenceKind.TsReference,
from: {
node,
file: project_paths.projectFile(node.getSourceFile(), programInfo),
isWrite: isWriteReference,
isPartOfElementBinding: ts.isBindingElement(node.parent),
},
target: targetInput,
});
}
/**
* Phase where we iterate through all source file references and
* detect references to known fields (e.g. commonly inputs).
*
* This is useful, for example in the signal input migration whe
* references need to be migrated to unwrap signals, given that
* their target properties is no longer holding a raw value, but
* instead an `InputSignal`.
*
* This phase detects references in all types of locations:
* - TS source files
* - Angular templates (inline or external)
* - Host binding expressions.
*/
function createFindAllSourceFileReferencesVisitor(programInfo, checker, reflector, resourceLoader, evaluator, templateTypeChecker, knownFields, fieldNamesToConsiderForReferenceLookup, result) {
const debugElComponentInstanceTracker = new DebugElementComponentInstance(checker);
const partialDirectiveCatalystTracker = new PartialDirectiveTypeInCatalystTests(checker, knownFields);
const perfCounters = {
template: 0,
hostBindings: 0,
tsReferences: 0,
tsTypes: 0,
};
// Schematic NodeJS execution may not have `global.performance` defined.
const currentTimeInMs = () => typeof global.performance !== 'undefined' ? global.performance.now() : Date.now();
const visitor = (node) => {
let lastTime = currentTimeInMs();
// Note: If there is no template type checker and resource loader, we aren't processing
// an Angular program, and can skip template detection.
if (ts.isClassDeclaration(node) && templateTypeChecker !== null && resourceLoader !== null) {
identifyTemplateReferences(programInfo, node, reflector, checker, evaluator, templateTypeChecker, resourceLoader, programInfo.userOptions, result, knownFields, fieldNamesToConsiderForReferenceLookup);
perfCounters.template += (currentTimeInMs() - lastTime) / 1000;
lastTime = currentTimeInMs();
identifyHostBindingReferences(node, programInfo, checker, reflector, result, knownFields, fieldNamesToConsiderForReferenceLookup);
perfCounters.hostBindings += (currentTimeInMs() - lastTime) / 1000;
lastTime = currentTimeInMs();
}
lastTime = currentTimeInMs();
// find references, but do not capture input declarations itself.
if (ts.isIdentifier(node) &&
!(isInputContainerNode(node.parent) && node.parent.name === node)) {
identifyPotentialTypeScriptReference(node, programInfo, checker, knownFields, result, fieldNamesToConsiderForReferenceLookup, {
debugElComponentInstanceTracker,
});
}
perfCounters.tsReferences += (currentTimeInMs() - lastTime) / 1000;
lastTime = currentTimeInMs();
// Detect `Partial<T>` references.
// Those are relevant to be tracked as they may be updated in Catalyst to
// unwrap signal inputs. Commonly people use `Partial` in Catalyst to type
// some "component initial values".
const partialDirectiveInCatalyst = partialDirectiveCatalystTracker.detect(node);
if (partialDirectiveInCatalyst !== null) {
result.references.push({
kind: exports.ReferenceKind.TsClassTypeReference,
from: {
file: project_paths.projectFile(partialDirectiveInCatalyst.referenceNode.getSourceFile(), programInfo),
node: partialDirectiveInCatalyst.referenceNode,
},
isPartialReference: true,
isPartOfCatalystFile: true,
target: partialDirectiveInCatalyst.targetClass,
});
}
perfCounters.tsTypes += (currentTimeInMs() - lastTime) / 1000;
};
return {
visitor,
debugPrintMetrics: () => {
console.info('Source file analysis performance', perfCounters);
},
};
}
exports.createFindAllSourceFileReferencesVisitor = createFindAllSourceFileReferencesVisitor;
exports.getBindingElementDeclaration = getBindingElementDeclaration;
exports.getMemberName = getMemberName;
exports.isHostBindingReference = isHostBindingReference;
exports.isInputContainerNode = isInputContainerNode;
exports.isTemplateReference = isTemplateReference;
exports.isTsClassTypeReference = isTsClassTypeReference;
exports.isTsReference = isTsReference;
exports.traverseAccess = traverseAccess;
exports.unwrapParent = unwrapParent;

Sorry, the diff of this file is too big to display

'use strict';
/**
* @license Angular v20.3.15
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var index = require('./index-Cu2VLWJC.cjs');
var schematics = require('@angular-devkit/schematics');
var core = require('@angular-devkit/core');
var posixPath = require('node:path/posix');
var os = require('os');
var ts = require('typescript');
var project_tsconfig_paths = require('./project_tsconfig_paths-CgKwP2MX.cjs');
require('path');
var path = require('node:path');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var posixPath__namespace = /*#__PURE__*/_interopNamespaceDefault(posixPath);
var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
/// <reference types="node" />
class NgtscCompilerHost {
fs;
options;
constructor(fs, options = {}) {
this.fs = fs;
this.options = options;
}
getSourceFile(fileName, languageVersion) {
const text = this.readFile(fileName);
return text !== undefined
? ts.createSourceFile(fileName, text, languageVersion, true)
: undefined;
}
getDefaultLibFileName(options) {
return this.fs.join(this.getDefaultLibLocation(), ts.getDefaultLibFileName(options));
}
getDefaultLibLocation() {
return this.fs.getDefaultLibLocation();
}
writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles) {
const path = project_tsconfig_paths.absoluteFrom(fileName);
this.fs.ensureDir(this.fs.dirname(path));
this.fs.writeFile(path, data);
}
getCurrentDirectory() {
return this.fs.pwd();
}
getCanonicalFileName(fileName) {
return this.useCaseSensitiveFileNames() ? fileName : fileName.toLowerCase();
}
useCaseSensitiveFileNames() {
return this.fs.isCaseSensitive();
}
getNewLine() {
switch (this.options.newLine) {
case ts.NewLineKind.CarriageReturnLineFeed:
return '\r\n';
case ts.NewLineKind.LineFeed:
return '\n';
default:
return os__namespace.EOL;
}
}
fileExists(fileName) {
const absPath = this.fs.resolve(fileName);
return this.fs.exists(absPath) && this.fs.stat(absPath).isFile();
}
readFile(fileName) {
const absPath = this.fs.resolve(fileName);
if (!this.fileExists(absPath)) {
return undefined;
}
return this.fs.readFile(absPath);
}
realpath(path) {
return this.fs.realpath(this.fs.resolve(path));
}
}
// We use TypeScript's native `ts.matchFiles` utility for the virtual file systems
// and their TypeScript compiler host `readDirectory` implementation. TypeScript's
// function implements complex logic for matching files with respect to root
// directory, extensions, excludes, includes etc. The function is currently
// internal but we can use it as the API most likely will not change any time soon,
// nor does it seem like this is being made public any time soon.
// Related issue for tracking: https://github.com/microsoft/TypeScript/issues/13793.
/**
* Creates a {@link ts.CompilerHost#readDirectory} implementation function,
* that leverages the specified file system (that may be e.g. virtual).
*/
function createFileSystemTsReadDirectoryFn(fs) {
if (ts.matchFiles === undefined) {
throw Error('Unable to read directory in configured file system. This means that ' +
'TypeScript changed its file matching internals.\n\nPlease consider downgrading your ' +
'TypeScript version, and report an issue in the Angular framework repository.');
}
const matchFilesFn = ts.matchFiles.bind(ts);
return (rootDir, extensions, excludes, includes, depth) => {
const directoryExists = (p) => {
const resolvedPath = fs.resolve(p);
return fs.exists(resolvedPath) && fs.stat(resolvedPath).isDirectory();
};
return matchFilesFn(rootDir, extensions, excludes, includes, fs.isCaseSensitive(), fs.pwd(), depth, (p) => {
const resolvedPath = fs.resolve(p);
// TS also gracefully returns an empty file set.
if (!directoryExists(resolvedPath)) {
return { directories: [], files: [] };
}
const children = fs.readdir(resolvedPath);
const files = [];
const directories = [];
for (const child of children) {
if (fs.stat(fs.join(resolvedPath, child))?.isDirectory()) {
directories.push(child);
}
else {
files.push(child);
}
}
return { files, directories };
}, (p) => fs.resolve(p), (p) => directoryExists(p));
};
}
function calcProjectFileAndBasePath(project, host = project_tsconfig_paths.getFileSystem()) {
const absProject = host.resolve(project);
const projectIsDir = host.lstat(absProject).isDirectory();
const projectFile = projectIsDir ? host.join(absProject, 'tsconfig.json') : absProject;
const projectDir = projectIsDir ? absProject : host.dirname(absProject);
const basePath = host.resolve(projectDir);
return { projectFile, basePath };
}
function readConfiguration(project, existingOptions, host = project_tsconfig_paths.getFileSystem()) {
try {
const fs = project_tsconfig_paths.getFileSystem();
const readConfigFile = (configFile) => ts.readConfigFile(configFile, (file) => host.readFile(host.resolve(file)));
const readAngularCompilerOptions = (configFile, parentOptions = {}) => {
const { config, error } = readConfigFile(configFile);
if (error) {
// Errors are handled later on by 'parseJsonConfigFileContent'
return parentOptions;
}
// Note: In Google, `angularCompilerOptions` are stored in `bazelOptions`.
// This function typically doesn't run for actual Angular compilations, but
// tooling like Tsurge, or schematics may leverage this helper, so we account
// for this here.
const angularCompilerOptions = config.angularCompilerOptions ?? config.bazelOptions?.angularCompilerOptions;
// we are only interested into merging 'angularCompilerOptions' as
// other options like 'compilerOptions' are merged by TS
let existingNgCompilerOptions = { ...angularCompilerOptions, ...parentOptions };
if (!config.extends) {
return existingNgCompilerOptions;
}
const extendsPaths = typeof config.extends === 'string' ? [config.extends] : config.extends;
// Call readAngularCompilerOptions recursively to merge NG Compiler options
// Reverse the array so the overrides happen from right to left.
return [...extendsPaths].reverse().reduce((prevOptions, extendsPath) => {
const extendedConfigPath = getExtendedConfigPath(configFile, extendsPath, host, fs);
return extendedConfigPath === null
? prevOptions
: readAngularCompilerOptions(extendedConfigPath, prevOptions);
}, existingNgCompilerOptions);
};
const { projectFile, basePath } = calcProjectFileAndBasePath(project, host);
const configFileName = host.resolve(host.pwd(), projectFile);
const { config, error } = readConfigFile(projectFile);
if (error) {
return {
project,
errors: [error],
rootNames: [],
options: {},
emitFlags: index.EmitFlags.Default,
};
}
const existingCompilerOptions = {
genDir: basePath,
basePath,
...readAngularCompilerOptions(configFileName),
...existingOptions,
};
const parseConfigHost = createParseConfigHost(host, fs);
const { options, errors, fileNames: rootNames, projectReferences, } = ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, existingCompilerOptions, configFileName);
let emitFlags = index.EmitFlags.Default;
if (!(options['skipMetadataEmit'] || options['flatModuleOutFile'])) {
emitFlags |= index.EmitFlags.Metadata;
}
if (options['skipTemplateCodegen']) {
emitFlags = emitFlags & ~index.EmitFlags.Codegen;
}
return { project: projectFile, rootNames, projectReferences, options, errors, emitFlags };
}
catch (e) {
const errors = [
{
category: ts.DiagnosticCategory.Error,
messageText: e.stack ?? e.message,
file: undefined,
start: undefined,
length: undefined,
source: 'angular',
code: index.UNKNOWN_ERROR_CODE,
},
];
return { project: '', errors, rootNames: [], options: {}, emitFlags: index.EmitFlags.Default };
}
}
function createParseConfigHost(host, fs = project_tsconfig_paths.getFileSystem()) {
return {
fileExists: host.exists.bind(host),
readDirectory: createFileSystemTsReadDirectoryFn(fs),
readFile: host.readFile.bind(host),
useCaseSensitiveFileNames: fs.isCaseSensitive(),
};
}
function getExtendedConfigPath(configFile, extendsValue, host, fs) {
const result = getExtendedConfigPathWorker(configFile, extendsValue, host, fs);
if (result !== null) {
return result;
}
// Try to resolve the paths with a json extension append a json extension to the file in case if
// it is missing and the resolution failed. This is to replicate TypeScript behaviour, see:
// https://github.com/microsoft/TypeScript/blob/294a5a7d784a5a95a8048ee990400979a6bc3a1c/src/compiler/commandLineParser.ts#L2806
return getExtendedConfigPathWorker(configFile, `${extendsValue}.json`, host, fs);
}
function getExtendedConfigPathWorker(configFile, extendsValue, host, fs) {
if (extendsValue.startsWith('.') || fs.isRooted(extendsValue)) {
const extendedConfigPath = host.resolve(host.dirname(configFile), extendsValue);
if (host.exists(extendedConfigPath)) {
return extendedConfigPath;
}
}
else {
const parseConfigHost = createParseConfigHost(host, fs);
// Path isn't a rooted or relative path, resolve like a module.
const { resolvedModule } = ts.nodeModuleNameResolver(extendsValue, configFile, { moduleResolution: ts.ModuleResolutionKind.Node10, resolveJsonModule: true }, parseConfigHost);
if (resolvedModule) {
return project_tsconfig_paths.absoluteFrom(resolvedModule.resolvedFileName);
}
}
return null;
}
/**
* Angular compiler file system implementation that leverages an
* CLI schematic virtual file tree.
*/
class DevkitMigrationFilesystem {
tree;
constructor(tree) {
this.tree = tree;
}
extname(path) {
return core.extname(path);
}
isRoot(path) {
return path === core.normalize('/');
}
isRooted(path) {
return this.normalize(path).startsWith('/');
}
dirname(file) {
return this.normalize(core.dirname(file));
}
join(basePath, ...paths) {
return this.normalize(core.join(basePath, ...paths));
}
relative(from, to) {
return this.normalize(core.relative(from, to));
}
basename(filePath, extension) {
return posixPath__namespace.basename(filePath, extension);
}
normalize(path) {
return core.normalize(path);
}
resolve(...paths) {
const normalizedPaths = paths.map((p) => core.normalize(p));
// In dev-kit, the NodeJS working directory should never be
// considered, so `/` is the last resort over `cwd`.
return this.normalize(posixPath__namespace.resolve(core.normalize('/'), ...normalizedPaths));
}
pwd() {
return '/';
}
isCaseSensitive() {
return true;
}
exists(path) {
return statPath(this.tree, path) !== null;
}
readFile(path) {
return this.tree.readText(path);
}
readFileBuffer(path) {
const buffer = this.tree.read(path);
if (buffer === null) {
throw new Error(`File does not exist: ${path}`);
}
return buffer;
}
readdir(path) {
const dir = this.tree.getDir(path);
return [
...dir.subdirs,
...dir.subfiles,
];
}
lstat(path) {
const stat = statPath(this.tree, path);
if (stat === null) {
throw new Error(`File does not exist for "lstat": ${path}`);
}
return stat;
}
stat(path) {
const stat = statPath(this.tree, path);
if (stat === null) {
throw new Error(`File does not exist for "stat": ${path}`);
}
return stat;
}
realpath(filePath) {
return filePath;
}
getDefaultLibLocation() {
return 'node_modules/typescript/lib';
}
ensureDir(path) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#ensureDir is not supported.');
}
writeFile(path, data) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#writeFile is not supported.');
}
removeFile(path) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#removeFile is not supported.');
}
copyFile(from, to) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#copyFile is not supported.');
}
moveFile(from, to) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#moveFile is not supported.');
}
removeDeep(path) {
// Migrations should compute replacements and not write directly.
throw new Error('DevkitFilesystem#removeDeep is not supported.');
}
chdir(_path) {
throw new Error('FileSystem#chdir is not supported.');
}
symlink() {
throw new Error('FileSystem#symlink is not supported.');
}
}
/** Stats the given path in the virtual tree. */
function statPath(tree, path) {
let fileInfo = null;
let dirInfo = null;
try {
fileInfo = tree.get(path);
}
catch (e) {
if (e.constructor.name === 'PathIsDirectoryException') {
dirInfo = tree.getDir(path);
}
else {
throw e;
}
}
if (fileInfo !== null || dirInfo !== null) {
return {
isDirectory: () => dirInfo !== null,
isFile: () => fileInfo !== null,
isSymbolicLink: () => false,
};
}
return null;
}
/**
* Groups the given replacements per project relative
* file path.
*
* This allows for simple execution of the replacements
* against a given file. E.g. via {@link applyTextUpdates}.
*/
function groupReplacementsByFile(replacements) {
const result = new Map();
for (const { projectFile, update } of replacements) {
if (!result.has(projectFile.rootRelativePath)) {
result.set(projectFile.rootRelativePath, []);
}
result.get(projectFile.rootRelativePath).push(update);
}
return result;
}
/**
* Synchronously combines unit data for the given migration.
*
* Note: This helper is useful for testing and execution of
* Tsurge migrations in non-batchable environments. In general,
* prefer parallel execution of combining via e.g. Beam combiners.
*/
async function synchronouslyCombineUnitData(migration, unitDatas) {
if (unitDatas.length === 0) {
return null;
}
if (unitDatas.length === 1) {
return unitDatas[0];
}
let combined = unitDatas[0];
for (let i = 1; i < unitDatas.length; i++) {
const other = unitDatas[i];
combined = await migration.combine(combined, other);
}
return combined;
}
/** Whether we are executing inside Google */
function isGoogle3() {
return process.env['GOOGLE3_TSURGE'] === '1';
}
/**
* By default, Tsurge will always create an Angular compiler program
* for projects analyzed and migrated. This works perfectly fine in
* third-party where Tsurge migrations run in Angular CLI projects.
*
* In first party, when running against full Google3, creating an Angular
* program for e.g. plain `ts_library` targets is overly expensive and
* can result in out of memory issues for large TS targets. In 1P we can
* reliably distinguish between TS and Angular targets via the `angularCompilerOptions`.
*/
function google3UsePlainTsProgramIfNoKnownAngularOption() {
return process.env['GOOGLE3_TSURGE'] === '1';
}
/** Options that are good defaults for Tsurge migrations. */
const defaultMigrationTsOptions = {
// Avoid checking libraries to speed up migrations.
skipLibCheck: true,
skipDefaultLibCheck: true,
noEmit: true,
// Does not apply to g3 and externally is enforced when the app is built by the compiler.
disableTypeScriptVersionCheck: true,
};
/**
* Creates an instance of a TypeScript program for the given project.
*/
function createPlainTsProgram(tsHost, tsconfig, optionOverrides) {
const program = ts.createProgram({
rootNames: tsconfig.rootNames,
options: {
...tsconfig.options,
...defaultMigrationTsOptions,
...optionOverrides,
},
});
return {
ngCompiler: null,
program,
userOptions: tsconfig.options,
__programAbsoluteRootFileNames: tsconfig.rootNames,
host: tsHost,
};
}
/**
* Parses the configuration of the given TypeScript project and creates
* an instance of the Angular compiler for the project.
*/
function createNgtscProgram(tsHost, tsconfig, optionOverrides) {
const ngtscProgram = new index.NgtscProgram(tsconfig.rootNames, {
...tsconfig.options,
...defaultMigrationTsOptions,
...optionOverrides,
}, tsHost);
// Expose an easy way to debug-print ng semantic diagnostics.
if (process.env['DEBUG_NG_SEMANTIC_DIAGNOSTICS'] === '1') {
console.error(ts.formatDiagnosticsWithColorAndContext(ngtscProgram.getNgSemanticDiagnostics(), tsHost));
}
return {
ngCompiler: ngtscProgram.compiler,
program: ngtscProgram.getTsProgram(),
userOptions: tsconfig.options,
__programAbsoluteRootFileNames: tsconfig.rootNames,
host: tsHost,
};
}
/** Code of the error raised by TypeScript when a tsconfig doesn't match any files. */
const NO_INPUTS_ERROR_CODE = 18003;
/** Parses the given tsconfig file, supporting Angular compiler options. */
function parseTsconfigOrDie(absoluteTsconfigPath, fs) {
const tsconfig = readConfiguration(absoluteTsconfigPath, {}, fs);
// Skip the "No inputs found..." error since we don't want to interrupt the migration if a
// tsconfig doesn't match a file. This will result in an empty `Program` which is still valid.
const errors = tsconfig.errors.filter((diag) => diag.code !== NO_INPUTS_ERROR_CODE);
if (errors.length) {
throw new Error(`Tsconfig could not be parsed or is invalid:\n\n` + `${errors.map((e) => e.messageText)}`);
}
return tsconfig;
}
// Note: Try to keep mostly in sync with
// //depot/google3/javascript/angular2/tools/ngc_wrapped/tsc_plugin.ts
// TODO: Consider moving this logic into the 1P launcher.
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
function fileNameToModuleNameFactory(rootDirs, workspaceName) {
return (importedFilePath) => {
let relativePath = '';
for (const rootDir of rootDirs) {
const rel = path__namespace.posix.relative(rootDir, importedFilePath);
if (!rel.startsWith('.')) {
relativePath = rel;
break;
}
}
if (relativePath) {
return `${workspaceName}/${relativePath.replace(EXT, '')}`;
}
else {
return importedFilePath.replace(EXT, '');
}
};
}
/** Creates the base program info for the given tsconfig path. */
function createBaseProgramInfo(absoluteTsconfigPath, fs, optionOverrides = {}) {
// Make sure the FS becomes globally available. Some code paths
// of the Angular compiler, or tsconfig parsing aren't leveraging
// the specified file system.
project_tsconfig_paths.setFileSystem(fs);
const tsconfig = parseTsconfigOrDie(absoluteTsconfigPath, fs);
const tsHost = new NgtscCompilerHost(fs, tsconfig.options);
// When enabled, use a plain TS program if we are sure it's not
// an Angular project based on the `tsconfig.json`.
if (google3UsePlainTsProgramIfNoKnownAngularOption() &&
tsconfig.options['_useHostForImportGeneration'] === undefined) {
return createPlainTsProgram(tsHost, tsconfig, optionOverrides);
}
// The Angular program may try to emit references during analysis or migration.
// To replicate the Google3 import emission here, ensure the unified module resolution
// can be enabled by the compiler.
if (isGoogle3() && tsconfig.options.rootDirs) {
tsHost.fileNameToModuleName = fileNameToModuleNameFactory(tsconfig.options.rootDirs,
/* workspaceName*/ 'google3');
}
return createNgtscProgram(tsHost, tsconfig, optionOverrides);
}
/**
* Creates the {@link ProgramInfo} from the given base information.
*
* This function purely exists to support custom programs that are
* intended to be injected into Tsurge migrations. e.g. for language
* service refactorings.
*/
function getProgramInfoFromBaseInfo(baseInfo) {
const fullProgramSourceFiles = [...baseInfo.program.getSourceFiles()];
const sourceFiles = fullProgramSourceFiles.filter((f) => !f.isDeclarationFile &&
// Note `isShim` will work for the initial program, but for TCB programs, the shims are no longer annotated.
!project_tsconfig_paths.isShim(f) &&
!f.fileName.endsWith('.ngtypecheck.ts'));
// Sort it by length in reverse order (longest first). This speeds up lookups,
// since there's no need to keep going through the array once a match is found.
const sortedRootDirs = project_tsconfig_paths.getRootDirs(baseInfo.host, baseInfo.userOptions).sort((a, b) => b.length - a.length);
// TODO: Consider also following TS's logic here, finding the common source root.
// See: Program#getCommonSourceDirectory.
const primaryRoot = project_tsconfig_paths.absoluteFrom(baseInfo.userOptions.rootDir ?? sortedRootDirs.at(-1) ?? baseInfo.program.getCurrentDirectory());
return {
...baseInfo,
sourceFiles,
fullProgramSourceFiles,
sortedRootDirs,
projectRoot: primaryRoot,
};
}
/**
* @private
*
* Base class for the possible Tsurge migration variants.
*
* For example, this class exposes methods to conveniently create
* TypeScript programs, while also allowing migration authors to override.
*/
class TsurgeBaseMigration {
/**
* Creates the TypeScript program for a given compilation unit.
*
* By default:
* - In 3P: Ngtsc programs are being created.
* - In 1P: Ngtsc or TS programs are created based on the Blaze target.
*/
createProgram(tsconfigAbsPath, fs, optionsOverride) {
return getProgramInfoFromBaseInfo(createBaseProgramInfo(tsconfigAbsPath, fs, optionsOverride));
}
}
/**
* A simpler variant of a {@link TsurgeComplexMigration} that does not
* fan-out into multiple workers per compilation unit to compute
* the final migration replacements.
*
* This is faster and less resource intensive as workers and TS programs
* are only ever created once.
*
* This is commonly the case when migrations are refactored to eagerly
* compute replacements in the analyze stage, and then leverage the
* global unit data to filter replacements that turned out to be "invalid".
*/
class TsurgeFunnelMigration extends TsurgeBaseMigration {
}
/**
* Complex variant of a `Tsurge` migration.
*
* For example, every analyze worker may contribute to a list of TS
* references that are later combined. The migrate phase can then compute actual
* file updates for all individual compilation units, leveraging the global metadata
* to e.g. see if there are any references from other compilation units that may be
* problematic and prevent migration of a given file.
*/
class TsurgeComplexMigration extends TsurgeBaseMigration {
}
/*!
* @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
*/
exports.MigrationStage = void 0;
(function (MigrationStage) {
/** The migration is analyzing an entrypoint */
MigrationStage[MigrationStage["Analysis"] = 0] = "Analysis";
/** The migration is about to migrate an entrypoint */
MigrationStage[MigrationStage["Migrate"] = 1] = "Migrate";
})(exports.MigrationStage || (exports.MigrationStage = {}));
/** Runs a Tsurge within an Angular Devkit context. */
async function runMigrationInDevkit(config) {
const { buildPaths, testPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(config.tree);
if (!buildPaths.length && !testPaths.length) {
throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the migration.');
}
const tsconfigPaths = [...buildPaths, ...testPaths];
const fs = new DevkitMigrationFilesystem(config.tree);
project_tsconfig_paths.setFileSystem(fs);
const migration = config.getMigration(fs);
const unitResults = [];
const isFunnelMigration = migration instanceof TsurgeFunnelMigration;
const compilationUnitAssignments = new Map();
for (const tsconfigPath of tsconfigPaths) {
config.beforeProgramCreation?.(tsconfigPath, exports.MigrationStage.Analysis);
const info = migration.createProgram(tsconfigPath, fs);
modifyProgramInfoToEnsureNonOverlappingFiles(tsconfigPath, info, compilationUnitAssignments);
config.afterProgramCreation?.(info, fs, exports.MigrationStage.Analysis);
config.beforeUnitAnalysis?.(tsconfigPath);
unitResults.push(await migration.analyze(info));
}
config.afterAllAnalyzed?.();
const combined = await synchronouslyCombineUnitData(migration, unitResults);
if (combined === null) {
config.afterAnalysisFailure?.();
return;
}
const globalMeta = await migration.globalMeta(combined);
let replacements;
if (isFunnelMigration) {
replacements = (await migration.migrate(globalMeta)).replacements;
}
else {
replacements = [];
for (const tsconfigPath of tsconfigPaths) {
config.beforeProgramCreation?.(tsconfigPath, exports.MigrationStage.Migrate);
const info = migration.createProgram(tsconfigPath, fs);
modifyProgramInfoToEnsureNonOverlappingFiles(tsconfigPath, info, compilationUnitAssignments);
config.afterProgramCreation?.(info, fs, exports.MigrationStage.Migrate);
const result = await migration.migrate(globalMeta, info);
replacements.push(...result.replacements);
}
}
const replacementsPerFile = new Map();
const changesPerFile = groupReplacementsByFile(replacements);
for (const [file, changes] of changesPerFile) {
if (!replacementsPerFile.has(file)) {
replacementsPerFile.set(file, changes);
}
}
for (const [file, changes] of replacementsPerFile) {
const recorder = config.tree.beginUpdate(file);
for (const c of changes) {
recorder
.remove(c.data.position, c.data.end - c.data.position)
.insertRight(c.data.position, c.data.toInsert);
}
config.tree.commitUpdate(recorder);
}
config.whenDone?.(await migration.stats(globalMeta));
}
/**
* Special logic for devkit migrations. In the Angular CLI, or in 3P precisely,
* projects can have tsconfigs with overlapping source files. i.e. two tsconfigs
* like e.g. build or test include the same `ts.SourceFile` (`.ts`). Migrations
* should never have 2+ compilation units with overlapping source files as this
* can result in duplicated replacements or analysis— hence we only ever assign a
* source file to a compilation unit *once*.
*
* Note that this is fine as we expect Tsurge migrations to work together as
* isolated compilation units— so it shouldn't matter if worst case a `.ts`
* file ends up in the e.g. test program.
*/
function modifyProgramInfoToEnsureNonOverlappingFiles(tsconfigPath, info, compilationUnitAssignments) {
const sourceFiles = [];
for (const sf of info.sourceFiles) {
const assignment = compilationUnitAssignments.get(sf.fileName);
// File is already assigned to a different compilation unit.
if (assignment !== undefined && assignment !== tsconfigPath) {
continue;
}
compilationUnitAssignments.set(sf.fileName, tsconfigPath);
sourceFiles.push(sf);
}
info.sourceFiles = sourceFiles;
}
/** A text replacement for the given file. */
class Replacement {
projectFile;
update;
constructor(projectFile, update) {
this.projectFile = projectFile;
this.update = update;
}
}
/** An isolated text update that may be applied to a file. */
class TextUpdate {
data;
constructor(data) {
this.data = data;
}
}
/** Confirms that the given data `T` is serializable. */
function confirmAsSerializable(data) {
return data;
}
/**
* Gets a project file instance for the given file.
*
* Use this helper for dealing with project paths throughout your
* migration. The return type is serializable.
*
* See {@link ProjectFile}.
*/
function projectFile(file, { sortedRootDirs, projectRoot }) {
const fs = project_tsconfig_paths.getFileSystem();
const filePath = fs.resolve(typeof file === 'string' ? file : file.fileName);
// Sorted root directories are sorted longest to shortest. First match
// is the appropriate root directory for ID computation.
for (const rootDir of sortedRootDirs) {
if (!isWithinBasePath(fs, rootDir, filePath)) {
continue;
}
return {
id: fs.relative(rootDir, filePath),
rootRelativePath: fs.relative(projectRoot, filePath),
};
}
// E.g. project directory may be `src/`, but files may be looked up
// from `node_modules/`. This is fine, but in those cases, no root
// directory matches.
const rootRelativePath = fs.relative(projectRoot, filePath);
return {
id: rootRelativePath,
rootRelativePath: rootRelativePath,
};
}
/**
* Whether `path` is a descendant of the `base`?
* E.g. `a/b/c` is within `a/b` but not within `a/x`.
*/
function isWithinBasePath(fs, base, path) {
return project_tsconfig_paths.isLocalRelativePath(fs.relative(base, path));
}
exports.Replacement = Replacement;
exports.TextUpdate = TextUpdate;
exports.TsurgeComplexMigration = TsurgeComplexMigration;
exports.TsurgeFunnelMigration = TsurgeFunnelMigration;
exports.confirmAsSerializable = confirmAsSerializable;
exports.projectFile = projectFile;
exports.runMigrationInDevkit = runMigrationInDevkit;

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display