Initial commit: notification-elements-demo app

Interactive Angular 19 demo for @sda/notification-elements-ui with
6 sections: Bell & Feed, Notification Center, Inbox, Comments &
Threads, Mention Input, and Full-Featured layout. Includes mock
data, dark mode toggle, and real-time event log.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Giuliano Silvestro
2026-02-13 21:49:19 +10:00
commit 5d0c9ec7eb
36473 changed files with 3778146 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var schematics = require('@angular-devkit/schematics');
var p = require('path');
var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.cjs');
var compiler_host = require('./compiler_host-CAfDJO3W.cjs');
var checker = require('./checker-B7WeiY-n.cjs');
var ts = require('typescript');
require('os');
require('@angular-devkit/core');
require('fs');
require('module');
require('url');
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
function findArrowFunction(node) {
let current = node;
while (current) {
if (ts.isArrowFunction(current)) {
return current;
}
current = current.parent;
}
return undefined;
}
function migrateFile(sourceFile, rewriter) {
if (!sourceFile.fileName.endsWith('main.server.ts')) {
return;
}
const bootstrapAppCalls = [];
ts.forEachChild(sourceFile, function findCalls(node) {
if (ts.isCallExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === 'bootstrapApplication' &&
node.arguments.length < 3) {
bootstrapAppCalls.push(node);
}
ts.forEachChild(node, findCalls);
});
if (bootstrapAppCalls.length === 0) {
return;
}
const importManager = new checker.ImportManager({
generateUniqueIdentifier: () => null,
shouldUseSingleQuotes: () => true,
});
for (const node of bootstrapAppCalls) {
const end = node.arguments[node.arguments.length - 1].getEnd();
rewriter(end, 0, ', context');
const arrowFunction = findArrowFunction(node);
if (arrowFunction && arrowFunction.parameters.length === 0) {
const pos = arrowFunction.parameters.end;
rewriter(pos, 0, 'context: BootstrapContext');
}
}
importManager.addImport({
exportSymbolName: 'BootstrapContext',
exportModuleSpecifier: '@angular/platform-browser',
requestedFile: sourceFile,
});
const finalization = importManager.finalize();
const printer = ts.createPrinter();
for (const [oldBindings, newBindings] of finalization.updatedImports) {
const newText = printer.printNode(ts.EmitHint.Unspecified, newBindings, sourceFile);
const start = oldBindings.getStart();
const width = oldBindings.getWidth();
rewriter(start, width, newText);
}
}
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
function migrate() {
return async (tree) => {
const { buildPaths, testPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(tree);
const basePath = process.cwd();
const allPaths = [...buildPaths, ...testPaths];
if (!allPaths.length) {
throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the add-bootstrap-context-to-server-main migration.');
}
for (const tsconfigPath of allPaths) {
runMigration(tree, tsconfigPath, basePath);
}
};
}
function runMigration(tree, tsconfigPath, basePath) {
const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
const sourceFiles = program
.getSourceFiles()
.filter((sourceFile) => compiler_host.canMigrateFile(basePath, sourceFile, program));
for (const sourceFile of sourceFiles) {
let update = null;
const rewriter = (startPos, width, text) => {
if (update === null) {
// Lazily initialize update, because most files will not require migration.
update = tree.beginUpdate(p.relative(basePath, sourceFile.fileName));
}
update.remove(startPos, width);
if (text !== null) {
update.insertLeft(startPos, text);
}
};
migrateFile(sourceFile, rewriter);
if (update !== null) {
tree.commitUpdate(update);
}
}
}
exports.migrate = migrate;

View File

@@ -0,0 +1,67 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
require('os');
var checker = require('./checker-B7WeiY-n.cjs');
var project_paths = require('./project_paths-DIZUxMOI.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(checker.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;

View File

@@ -0,0 +1,200 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
var checker = require('./checker-B7WeiY-n.cjs');
require('os');
/** 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 checker.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, '/');
}
exports.ChangeTracker = ChangeTracker;
exports.normalizePath = normalizePath;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,310 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
require('@angular-devkit/core');
require('node:path/posix');
var project_paths = require('./project_paths-DIZUxMOI.cjs');
var ts = require('typescript');
require('os');
var checker = require('./checker-B7WeiY-n.cjs');
var index = require('./index-DeAjKhiO.cjs');
require('path');
var apply_import_manager = require('./apply_import_manager-BDckZ-aO.cjs');
var leading_space = require('./leading_space-D9nQ8UQC.cjs');
require('@angular-devkit/schematics');
require('./project_tsconfig_paths-CDVxT6Ov.cjs');
require('fs');
require('module');
require('url');
/** Migration that cleans up unused imports from a project. */
class UnusedImportsMigration extends project_paths.TsurgeFunnelMigration {
printer = ts.createPrinter();
createProgram(tsconfigAbsPath, fs) {
return super.createProgram(tsconfigAbsPath, fs, {
extendedDiagnostics: {
checks: {
// Ensure that the diagnostic is enabled.
unusedStandaloneImports: index.DiagnosticCategoryLabel.Warning,
},
},
});
}
async analyze(info) {
const nodePositions = new Map();
const replacements = [];
let removedImports = 0;
let changedFiles = 0;
info.ngCompiler?.getDiagnostics().forEach((diag) => {
if (diag.file !== undefined &&
diag.start !== undefined &&
diag.length !== undefined &&
diag.code === checker.ngErrorCode(checker.ErrorCode.UNUSED_STANDALONE_IMPORTS)) {
// Skip files that aren't owned by this compilation unit.
if (!info.sourceFiles.includes(diag.file)) {
return;
}
if (!nodePositions.has(diag.file)) {
nodePositions.set(diag.file, new Set());
}
nodePositions.get(diag.file).add(this.getNodeKey(diag.start, diag.length));
}
});
nodePositions.forEach((locations, sourceFile) => {
const resolvedLocations = this.resolveRemovalLocations(sourceFile, locations);
const usageAnalysis = this.analyzeUsages(sourceFile, resolvedLocations);
if (resolvedLocations.allRemovedIdentifiers.size > 0) {
removedImports += resolvedLocations.allRemovedIdentifiers.size;
changedFiles++;
}
this.generateReplacements(sourceFile, resolvedLocations, usageAnalysis, info, replacements);
});
return project_paths.confirmAsSerializable({ replacements, removedImports, changedFiles });
}
async migrate(globalData) {
return project_paths.confirmAsSerializable(globalData);
}
async combine(unitA, unitB) {
return project_paths.confirmAsSerializable({
replacements: [...unitA.replacements, ...unitB.replacements],
removedImports: unitA.removedImports + unitB.removedImports,
changedFiles: unitA.changedFiles + unitB.changedFiles,
});
}
async globalMeta(combinedData) {
return project_paths.confirmAsSerializable(combinedData);
}
async stats(globalMetadata) {
return {
counters: {
removedImports: globalMetadata.removedImports,
changedFiles: globalMetadata.changedFiles,
},
};
}
/** Gets a key that can be used to look up a node based on its location. */
getNodeKey(start, length) {
return `${start}/${length}`;
}
/**
* Resolves a set of node locations to the actual AST nodes that need to be migrated.
* @param sourceFile File in which to resolve the locations.
* @param locations Location keys that should be resolved.
*/
resolveRemovalLocations(sourceFile, locations) {
const result = {
fullRemovals: new Set(),
partialRemovals: new Map(),
allRemovedIdentifiers: new Set(),
};
const walk = (node) => {
if (!ts.isIdentifier(node)) {
node.forEachChild(walk);
return;
}
// The TS typings don't reflect that the parent can be undefined.
const parent = node.parent;
if (!parent) {
return;
}
if (locations.has(this.getNodeKey(node.getStart(), node.getWidth()))) {
// When the entire array needs to be cleared, the diagnostic is
// reported on the property assignment, rather than an array element.
if (ts.isPropertyAssignment(parent) &&
parent.name === node &&
ts.isArrayLiteralExpression(parent.initializer)) {
result.fullRemovals.add(parent.initializer);
parent.initializer.elements.forEach((element) => {
if (ts.isIdentifier(element)) {
result.allRemovedIdentifiers.add(element.text);
}
});
}
else if (ts.isArrayLiteralExpression(parent)) {
if (!result.partialRemovals.has(parent)) {
result.partialRemovals.set(parent, new Set());
}
result.partialRemovals.get(parent).add(node);
result.allRemovedIdentifiers.add(node.text);
}
}
};
walk(sourceFile);
return result;
}
/**
* Analyzes how identifiers are used across a file.
* @param sourceFile File to be analyzed.
* @param locations Locations that will be changed as a part of this migration.
*/
analyzeUsages(sourceFile, locations) {
const { partialRemovals, fullRemovals } = locations;
const result = {
importedSymbols: new Map(),
identifierCounts: new Map(),
};
const walk = (node) => {
if (ts.isIdentifier(node) &&
node.parent &&
// Don't track individual identifiers marked for removal.
(!ts.isArrayLiteralExpression(node.parent) ||
!partialRemovals.has(node.parent) ||
!partialRemovals.get(node.parent).has(node))) {
result.identifierCounts.set(node.text, (result.identifierCounts.get(node.text) ?? 0) + 1);
}
// Don't track identifiers in array literals that are about to be removed.
if (ts.isArrayLiteralExpression(node) && fullRemovals.has(node)) {
return;
}
if (ts.isImportDeclaration(node)) {
const namedBindings = node.importClause?.namedBindings;
const moduleName = ts.isStringLiteral(node.moduleSpecifier)
? node.moduleSpecifier.text
: null;
if (namedBindings && ts.isNamedImports(namedBindings) && moduleName !== null) {
namedBindings.elements.forEach((imp) => {
if (!result.importedSymbols.has(moduleName)) {
result.importedSymbols.set(moduleName, new Map());
}
const symbolName = (imp.propertyName || imp.name).text;
const localName = imp.name.text;
result.importedSymbols.get(moduleName).set(localName, symbolName);
});
}
// Don't track identifiers in imports.
return;
}
// Track identifiers in all other node kinds.
node.forEachChild(walk);
};
walk(sourceFile);
return result;
}
/**
* Generates text replacements based on the data produced by the migration.
* @param sourceFile File being migrated.
* @param removalLocations Data about nodes being removed.
* @param usages Data about identifier usage.
* @param info Information about the current program.
* @param replacements Array tracking all text replacements.
*/
generateReplacements(sourceFile, removalLocations, usages, info, replacements) {
const { fullRemovals, partialRemovals, allRemovedIdentifiers } = removalLocations;
const { importedSymbols, identifierCounts } = usages;
const importManager = new checker.ImportManager();
const sourceText = sourceFile.getFullText();
// Replace full arrays with empty ones. This allows preserves more of the user's formatting.
fullRemovals.forEach((node) => {
replacements.push(new project_paths.Replacement(project_paths.projectFile(sourceFile, info), new project_paths.TextUpdate({
position: node.getStart(),
end: node.getEnd(),
toInsert: '[]',
})));
});
// Filter out the unused identifiers from an array.
partialRemovals.forEach((toRemove, parent) => {
toRemove.forEach((node) => {
replacements.push(new project_paths.Replacement(project_paths.projectFile(sourceFile, info), getArrayElementRemovalUpdate(node, parent, sourceText)));
});
});
// Attempt to clean up unused import declarations. Note that this isn't foolproof, because we
// do the matching based on identifier text, rather than going through the type checker which
// can be expensive. This should be enough for the vast majority of cases in this schematic
// since we're dealing exclusively with directive/pipe class names which tend to be very
// specific. In the worst case we may end up not removing an import declaration which would
// still be valid code that the user can clean up themselves.
importedSymbols.forEach((names, moduleName) => {
names.forEach((symbolName, localName) => {
// Note that in the `identifierCounts` lookup both zero and undefined
// are valid and mean that the identifiers isn't being used anymore.
if (allRemovedIdentifiers.has(localName) && !identifierCounts.get(localName)) {
importManager.removeImport(sourceFile, symbolName, moduleName);
}
});
});
apply_import_manager.applyImportManagerChanges(importManager, replacements, [sourceFile], info);
}
}
/** Generates a `TextUpdate` for the removal of an array element. */
function getArrayElementRemovalUpdate(node, parent, sourceText) {
let position = node.getStart();
let end = node.getEnd();
let toInsert = '';
const whitespaceOrLineFeed = /\s/;
// Usually the way we'd remove the nodes would be to recreate the `parent` while excluding
// the nodes that should be removed. The problem with this is that it'll strip out comments
// inside the array which can have special meaning internally. We work around it by removing
// only the node's own offsets. This comes with another problem in that it won't remove the commas
// that separate array elements which in turn can look weird if left in place (e.g.
// `[One, Two, Three, Four]` can turn into `[One,,Four]`). To account for them, we start with the
// node's end offset and then expand it to include trailing commas, whitespace and line breaks.
for (let i = end; i < sourceText.length; i++) {
if (sourceText[i] === ',' || whitespaceOrLineFeed.test(sourceText[i])) {
end++;
}
else {
break;
}
}
// If we're removing the last element in the array, adjust the starting offset so that
// it includes the previous comma on the same line. This avoids turning something like
// `[One, Two, Three]` into `[One,]`. We only do this within the same like, because
// trailing comma at the end of the line is fine.
if (parent.elements[parent.elements.length - 1] === node) {
for (let i = position - 1; i >= 0; i--) {
if (sourceText[i] === ',' || sourceText[i] === ' ') {
position--;
}
else {
break;
}
}
// Replace the node with its leading whitespace to preserve the formatting.
toInsert = leading_space.getLeadingLineWhitespaceOfNode(node);
}
return new project_paths.TextUpdate({ position, end, toInsert });
}
function migrate() {
return async (tree, context) => {
await project_paths.runMigrationInDevkit({
getMigration: () => new UnusedImportsMigration(),
tree,
beforeProgramCreation: (tsconfigPath, stage) => {
if (stage === project_paths.MigrationStage.Analysis) {
context.logger.info(`Preparing analysis for: ${tsconfigPath}...`);
}
else {
context.logger.info(`Running migration for: ${tsconfigPath}...`);
}
},
beforeUnitAnalysis: (tsconfigPath) => {
context.logger.info(`Scanning for unused imports using ${tsconfigPath}`);
},
afterAnalysisFailure: () => {
context.logger.error('Schematic failed unexpectedly with no analysis data');
},
whenDone: (stats) => {
const { removedImports, changedFiles } = stats.counters;
let statsMessage;
if (removedImports === 0) {
statsMessage = 'Schematic could not find unused imports in the project';
}
else {
statsMessage =
`Removed ${removedImports} import${removedImports !== 1 ? 's' : ''} ` +
`in ${changedFiles} file${changedFiles !== 1 ? 's' : ''}`;
}
context.logger.info('');
context.logger.info(statsMessage);
},
});
};
}
exports.migrate = migrate;

View File

@@ -0,0 +1,129 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var p = require('path');
var ts = require('typescript');
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);
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.canMigrateFile = canMigrateFile;
exports.createMigrationProgram = createMigrationProgram;
exports.createProgramOptions = createProgramOptions;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,179 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var schematics = require('@angular-devkit/schematics');
var p = require('path');
var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.cjs');
var compiler_host = require('./compiler_host-CAfDJO3W.cjs');
var ts = require('typescript');
var change_tracker = require('./change_tracker-EC4_OX_3.cjs');
var imports = require('./imports-CIX-JgAN.cjs');
require('@angular-devkit/core');
require('./checker-B7WeiY-n.cjs');
require('os');
require('fs');
require('module');
require('url');
const CORE = '@angular/core';
const DIRECTIVE = 'Directive';
const COMPONENT = 'Component';
const PIPE = 'Pipe';
function migrateFile(sourceFile, rewriteFn) {
const changeTracker = new change_tracker.ChangeTracker(ts.createPrinter());
// Check if there are any imports of the `AfterRenderPhase` enum.
const coreImports = imports.getNamedImports(sourceFile, CORE);
if (!coreImports) {
return;
}
const directive = imports.getImportSpecifier(sourceFile, CORE, DIRECTIVE);
const component = imports.getImportSpecifier(sourceFile, CORE, COMPONENT);
const pipe = imports.getImportSpecifier(sourceFile, CORE, PIPE);
if (!directive && !component && !pipe) {
return;
}
ts.forEachChild(sourceFile, function visit(node) {
ts.forEachChild(node, visit);
// First we need to check for class declarations
// Decorators will come after
if (!ts.isClassDeclaration(node)) {
return;
}
ts.getDecorators(node)?.forEach((decorator) => {
if (!ts.isDecorator(decorator)) {
return;
}
const callExpression = decorator.expression;
if (!ts.isCallExpression(callExpression)) {
return;
}
const decoratorIdentifier = callExpression.expression;
if (!ts.isIdentifier(decoratorIdentifier)) {
return;
}
// Checking the identifier of the decorator by comparing to the import specifier
switch (decoratorIdentifier.text) {
case directive?.name.text:
case component?.name.text:
case pipe?.name.text:
break;
default:
// It's not a decorator to migrate
return;
}
const [decoratorArgument] = callExpression.arguments;
if (!decoratorArgument || !ts.isObjectLiteralExpression(decoratorArgument)) {
return;
}
const properties = decoratorArgument.properties;
const standaloneProp = getStandaloneProperty(properties);
const hasImports = decoratorHasImports(decoratorArgument);
// We'll use the presence of imports to keep the migration idempotent
// We need to take care of 3 cases
// - standalone: true => remove the property if we have imports
// - standalone: false => nothing
// - No standalone property => add a standalone: false property if there are no imports
let newProperties;
if (!standaloneProp) {
if (!hasImports) {
const standaloneFalseProperty = ts.factory.createPropertyAssignment('standalone', ts.factory.createFalse());
newProperties = [...properties, standaloneFalseProperty];
}
}
else if (standaloneProp.value === ts.SyntaxKind.TrueKeyword && hasImports) {
// To keep the migration idempotent, we'll only remove the standalone prop when there are imports
newProperties = properties.filter((p) => p !== standaloneProp.property);
}
if (newProperties) {
// At this point we know that we need to add standalone: false or
// remove an existing standalone: true property.
const newPropsArr = ts.factory.createNodeArray(newProperties);
const newFirstArg = ts.factory.createObjectLiteralExpression(newPropsArr, true);
changeTracker.replaceNode(decoratorArgument, newFirstArg);
}
});
});
// Write the changes.
for (const changesInFile of changeTracker.recordChanges().values()) {
for (const change of changesInFile) {
rewriteFn(change.start, change.removeLength ?? 0, change.text);
}
}
}
function getStandaloneProperty(properties) {
for (const prop of properties) {
if (ts.isShorthandPropertyAssignment(prop) && prop.name.text) {
return { property: prop, value: prop.objectAssignmentInitializer };
}
if (isStandaloneProperty(prop)) {
if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
return { property: prop, value: prop.initializer.kind };
}
else {
return { property: prop, value: prop.initializer };
}
}
}
return undefined;
}
function isStandaloneProperty(prop) {
return (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'standalone');
}
function decoratorHasImports(decoratorArgument) {
for (const prop of decoratorArgument.properties) {
if (ts.isPropertyAssignment(prop) &&
ts.isIdentifier(prop.name) &&
prop.name.text === 'imports') {
if (prop.initializer.kind === ts.SyntaxKind.ArrayLiteralExpression ||
prop.initializer.kind === ts.SyntaxKind.Identifier) {
return true;
}
}
}
return false;
}
function migrate() {
return async (tree) => {
const { buildPaths, testPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(tree);
const basePath = process.cwd();
const allPaths = [...buildPaths, ...testPaths];
if (!allPaths.length) {
throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the explicit-standalone-flag migration.');
}
for (const tsconfigPath of allPaths) {
runMigration(tree, tsconfigPath, basePath);
}
};
}
function runMigration(tree, tsconfigPath, basePath) {
const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
const sourceFiles = program
.getSourceFiles()
.filter((sourceFile) => compiler_host.canMigrateFile(basePath, sourceFile, program));
for (const sourceFile of sourceFiles) {
let update = null;
const rewriter = (startPos, width, text) => {
if (update === null) {
// Lazily initialize update, because most files will not require migration.
update = tree.beginUpdate(p.relative(basePath, sourceFile.fileName));
}
update.remove(startPos, width);
if (text !== null) {
update.insertLeft(startPos, text);
}
};
migrateFile(sourceFile, rewriter);
if (update !== null) {
tree.commitUpdate(update);
}
}
}
exports.migrate = migrate;

View File

@@ -0,0 +1,105 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
/** Gets import information about the specified identifier by using the Type checker. */
function getImportOfIdentifier(typeChecker, node) {
const symbol = typeChecker.getSymbolAtLocation(node);
if (!symbol || symbol.declarations === undefined || !symbol.declarations.length) {
return null;
}
const decl = symbol.declarations[0];
if (!ts.isImportSpecifier(decl)) {
return null;
}
const importDecl = decl.parent.parent.parent;
if (!ts.isImportDeclaration(importDecl) || !ts.isStringLiteral(importDecl.moduleSpecifier)) {
return null;
}
return {
// Handles aliased imports: e.g. "import {Component as myComp} from ...";
name: decl.propertyName ? decl.propertyName.text : decl.name.text,
importModule: importDecl.moduleSpecifier.text,
node: importDecl,
};
}
/**
* Gets a top-level import specifier with a specific name that is imported from a particular module.
* E.g. given a file that looks like:
*
* ```ts
* import { Component, Directive } from '@angular/core';
* import { Foo } from './foo';
* ```
*
* Calling `getImportSpecifier(sourceFile, '@angular/core', 'Directive')` will yield the node
* referring to `Directive` in the top import.
*
* @param sourceFile File in which to look for imports.
* @param moduleName Name of the import's module.
* @param specifierName Original name of the specifier to look for. Aliases will be resolved to
* their original name.
*/
function getImportSpecifier(sourceFile, moduleName, specifierName) {
return getImportSpecifiers(sourceFile, moduleName, specifierName)[0] ?? null;
}
function getImportSpecifiers(sourceFile, moduleName, specifierOrSpecifiers) {
const matches = [];
for (const node of sourceFile.statements) {
if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) {
continue;
}
const namedBindings = node.importClause?.namedBindings;
const isMatch = typeof moduleName === 'string'
? node.moduleSpecifier.text === moduleName
: moduleName.test(node.moduleSpecifier.text);
if (!isMatch || !namedBindings || !ts.isNamedImports(namedBindings)) {
continue;
}
if (typeof specifierOrSpecifiers === 'string') {
const match = findImportSpecifier(namedBindings.elements, specifierOrSpecifiers);
if (match) {
matches.push(match);
}
}
else {
for (const specifierName of specifierOrSpecifiers) {
const match = findImportSpecifier(namedBindings.elements, specifierName);
if (match) {
matches.push(match);
}
}
}
}
return matches;
}
function getNamedImports(sourceFile, moduleName) {
for (const node of sourceFile.statements) {
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
const isMatch = node.moduleSpecifier.text === moduleName
;
const namedBindings = node.importClause?.namedBindings;
if (isMatch && namedBindings && ts.isNamedImports(namedBindings)) {
return namedBindings;
}
}
}
return null;
}
/** Finds an import specifier with a particular name. */
function findImportSpecifier(nodes, specifierName) {
return nodes.find((element) => {
const { name, propertyName } = element;
return propertyName ? propertyName.text === specifierName : name.text === specifierName;
});
}
exports.getImportOfIdentifier = getImportOfIdentifier;
exports.getImportSpecifier = getImportSpecifier;
exports.getNamedImports = getNamedImports;

20301
node_modules/@angular/core/schematics/bundles/index-DeAjKhiO.cjs generated vendored Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,997 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
require('os');
var checker = require('./checker-B7WeiY-n.cjs');
var index = require('./index-DeAjKhiO.cjs');
require('path');
var project_paths = require('./project_paths-DIZUxMOI.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 checker.RecursiveVisitor$1 {
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 = [];
checker.visitAll$1(this, template.attributes);
checker.visitAll$1(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') {
checker.visitAll$1(this, template.inputs);
checker.visitAll$1(this, template.outputs);
}
const referencedInputs = this.templateAttributeReferencedFields;
this.templateAttributeReferencedFields = null;
this.descendAndCheckForNarrowedSimilarReferences(referencedInputs, () => {
checker.visitAll$1(this, template.children);
checker.visitAll$1(this, template.references);
checker.visitAll$1(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 checker.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, context);
super.visitPropertyRead(ast, context);
}
visitSafePropertyRead(ast, context) {
this._inspectPropertyAccess(ast, context);
super.visitPropertyRead(ast, context);
}
visitPropertyWrite(ast, context) {
this._inspectPropertyAccess(ast, context);
super.visitPropertyWrite(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, astPath) {
if (this.fieldNamesToConsiderForReferenceLookup !== null &&
!this.fieldNamesToConsiderForReferenceLookup.has(ast.name)) {
return;
}
const isWrite = !!(ast instanceof checker.PropertyWrite ||
(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 !== checker.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 checker.PropertyRead ||
r instanceof checker.PropertyWrite ||
r instanceof checker.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$1) {
const path = [readOrWrite.name];
let node = readOrWrite;
while (node.receiver instanceof checker.PropertyRead || node.receiver instanceof checker.PropertyWrite) {
node = node.receiver;
path.unshift(node.name);
}
if (!(node.receiver instanceof checker.ImplicitReceiver || node.receiver instanceof checker.ThisReceiver)) {
return null;
}
const classType = checker$1.getTypeAtLocation(componentClass.name);
return (lookupPropertyAccess(checker$1, 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 checker.BoundAttribute && node.type === checker.BindingType.TwoWay) ||
(node instanceof checker.BoundEvent && node.type === checker.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$1, reflector, result, knownFields, fieldNamesToConsiderForReferenceLookup) {
if (node.name === undefined) {
return;
}
const decorators = reflector.getDecoratorsOfDeclaration(node);
if (decorators === null) {
return;
}
const angularDecorators = checker.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 = checker.unwrapExpression(ngDecorator.args[0]);
if (!ts.isObjectLiteralExpression(metadataNode)) {
return;
}
const metadata = checker.reflectObjectLiteral(metadataNode);
if (!metadata.has('host')) {
return;
}
let hostField = checker.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$1.getSymbolAtLocation(hostField);
// Plain identifier references can point to alias symbols (e.g. imports).
if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) {
symbol = checker$1.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 = checker.reflectObjectLiteral(hostField);
const expressionResult = [];
const expressionVisitor = new TemplateExpressionReferenceVisitor(checker$1, 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 = checker.makeBindingParser();
const sourceSpan = new checker.ParseSourceSpan(
// Fake source span to keep parsing offsets zero-based.
// We then later combine these with the expression TS node offsets.
new checker.ParseLocation({ content: '', url: '' }, 0, 0, 0), new checker.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$1, reflector, resourceLoader) {
const classDecorators = reflector.getDecoratorsOfDeclaration(node);
const evaluator = new index.PartialEvaluator(reflector, checker$1, null);
const ngDecorators = classDecorators !== null
? checker.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 = checker.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: checker.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: checker.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$1, evaluator, templateTypeChecker, resourceLoader, options, result, knownFields, fieldNamesToConsiderForReferenceLookup) {
const template = templateTypeChecker.getTemplate(node, checker.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$1, reflector, resourceLoader, evaluator, options);
if (template !== null) {
const visitor = new TemplateReferenceVisitor(checker$1, 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(checker.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$1, reflector, resourceLoader, evaluator, options) {
if (node.name === undefined) {
return null;
}
const tmplDef = attemptExtractTemplateDefinition(node, checker$1, 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,
}, checker.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;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
/**
* Gets the leading line whitespace of a given node.
*
* Useful for inserting e.g. TODOs without breaking indentation.
*/
function getLeadingLineWhitespaceOfNode(node) {
const fullText = node.getFullText().substring(0, node.getStart() - node.getFullStart());
let result = '';
for (let i = fullText.length - 1; i > -1; i--) {
// Note: LF line endings are `\n` while CRLF are `\r\n`. This logic should cover both, because
// we start from the beginning of the node and go backwards so will always hit `\n` first.
if (fullText[i] !== '\n') {
result = fullText[i] + result;
}
else {
break;
}
}
return result;
}
exports.getLeadingLineWhitespaceOfNode = getLeadingLineWhitespaceOfNode;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
var imports = require('./imports-CIX-JgAN.cjs');
function getCallDecoratorImport(typeChecker, decorator) {
// Note that this does not cover the edge case where decorators are called from
// a namespace import: e.g. "@core.Component()". This is not handled by Ngtsc either.
if (!ts.isCallExpression(decorator.expression) ||
!ts.isIdentifier(decorator.expression.expression)) {
return null;
}
const identifier = decorator.expression.expression;
return imports.getImportOfIdentifier(typeChecker, identifier);
}
/**
* Gets all decorators which are imported from an Angular package (e.g. "@angular/core")
* from a list of decorators.
*/
function getAngularDecorators(typeChecker, decorators) {
return decorators
.map((node) => ({ node, importData: getCallDecoratorImport(typeChecker, node) }))
.filter(({ importData }) => importData && importData.importModule.startsWith('@angular/'))
.map(({ node, importData }) => ({
node: node,
name: importData.name,
moduleName: importData.importModule,
importNode: importData.node,
}));
}
exports.getAngularDecorators = getAngularDecorators;

View File

@@ -0,0 +1,23 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
/** Find the closest parent node of a particular kind. */
function closestNode(node, predicate) {
let current = node.parent;
while (current && !ts.isSourceFile(current)) {
if (predicate(current)) {
return current;
}
current = current.parent;
}
return null;
}
exports.closestNode = closestNode;

View File

@@ -0,0 +1,608 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
require('os');
var checker = require('./checker-B7WeiY-n.cjs');
var index$1 = require('./index-DeAjKhiO.cjs');
require('path');
var project_paths = require('./project_paths-DIZUxMOI.cjs');
var apply_import_manager = require('./apply_import_manager-BDckZ-aO.cjs');
var index = require('./index-DyN3x_2s.cjs');
require('@angular-devkit/core');
require('node:path/posix');
require('fs');
require('module');
require('url');
require('@angular-devkit/schematics');
require('./project_tsconfig_paths-CDVxT6Ov.cjs');
function isOutputDeclarationEligibleForMigration(node) {
return (node.initializer !== undefined &&
ts.isNewExpression(node.initializer) &&
ts.isIdentifier(node.initializer.expression) &&
node.initializer.expression.text === 'EventEmitter');
}
function isPotentialOutputCallUsage(node, name) {
if (ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
ts.isIdentifier(node.expression.name)) {
return node.expression?.name.text === name;
}
else {
return false;
}
}
function isPotentialPipeCallUsage(node) {
return isPotentialOutputCallUsage(node, 'pipe');
}
function isPotentialNextCallUsage(node) {
return isPotentialOutputCallUsage(node, 'next');
}
function isPotentialCompleteCallUsage(node) {
return isPotentialOutputCallUsage(node, 'complete');
}
function isTargetOutputDeclaration(node, checker, reflector, dtsReader) {
const targetSymbol = checker.getSymbolAtLocation(node);
if (targetSymbol !== undefined) {
const propertyDeclaration = getTargetPropertyDeclaration(targetSymbol);
if (propertyDeclaration !== null &&
isOutputDeclaration(propertyDeclaration, reflector, dtsReader)) {
return propertyDeclaration;
}
}
return null;
}
/** Gets whether the given property is an Angular `@Output`. */
function isOutputDeclaration(node, reflector, dtsReader) {
// `.d.ts` file, so we check the `static ecmp` metadata on the `declare class`.
if (node.getSourceFile().isDeclarationFile) {
if (!ts.isIdentifier(node.name) ||
!ts.isClassDeclaration(node.parent) ||
node.parent.name === undefined) {
return false;
}
const ref = new checker.Reference(node.parent);
const directiveMeta = dtsReader.getDirectiveMetadata(ref);
return !!directiveMeta?.outputs.getByClassPropertyName(node.name.text);
}
// `.ts` file, so we check for the `@Output()` decorator.
return getOutputDecorator(node, reflector) !== null;
}
function getTargetPropertyDeclaration(targetSymbol) {
const valDeclaration = targetSymbol.valueDeclaration;
if (valDeclaration !== undefined && ts.isPropertyDeclaration(valDeclaration)) {
return valDeclaration;
}
return null;
}
/** Returns Angular `@Output` decorator or null when a given property declaration is not an @Output */
function getOutputDecorator(node, reflector) {
const decorators = reflector.getDecoratorsOfDeclaration(node);
const ngDecorators = decorators !== null ? checker.getAngularDecorators(decorators, ['Output'], /* isCore */ false) : [];
return ngDecorators.length > 0 ? ngDecorators[0] : null;
}
// THINK: this utility + type is not specific to @Output, really, maybe move it to tsurge?
/** Computes an unique ID for a given Angular `@Output` property. */
function getUniqueIdForProperty(info, prop) {
const { id } = project_paths.projectFile(prop.getSourceFile(), info);
id.replace(/\.d\.ts$/, '.ts');
return `${id}@@${prop.parent.name ?? 'unknown-class'}@@${prop.name.getText()}`;
}
function isTestRunnerImport(node) {
if (ts.isImportDeclaration(node)) {
const moduleSpecifier = node.moduleSpecifier.getText();
return moduleSpecifier.includes('jasmine') || moduleSpecifier.includes('catalyst');
}
return false;
}
// TODO: code duplication with signals migration - sort it out
/**
* Gets whether the given read is used to access
* the specified field.
*
* E.g. whether `<my-read>.toArray` is detected.
*/
function checkNonTsReferenceAccessesField(ref, fieldName) {
const readFromPath = ref.from.readAstPath.at(-1);
const parentRead = ref.from.readAstPath.at(-2);
if (ref.from.read !== readFromPath) {
return null;
}
if (!(parentRead instanceof checker.PropertyRead) || parentRead.name !== fieldName) {
return null;
}
return parentRead;
}
/**
* Gets whether the given reference is accessed to call the
* specified function on it.
*
* E.g. whether `<my-read>.toArray()` is detected.
*/
function checkNonTsReferenceCallsField(ref, fieldName) {
const propertyAccess = checkNonTsReferenceAccessesField(ref, fieldName);
if (propertyAccess === null) {
return null;
}
const accessIdx = ref.from.readAstPath.indexOf(propertyAccess);
if (accessIdx === -1) {
return null;
}
const potentialRead = ref.from.readAstPath[accessIdx];
if (potentialRead === undefined) {
return null;
}
return potentialRead;
}
const printer = ts.createPrinter();
function calculateDeclarationReplacement(info, node, aliasParam) {
const sf = node.getSourceFile();
let payloadTypes;
if (node.initializer && ts.isNewExpression(node.initializer) && node.initializer.typeArguments) {
payloadTypes = node.initializer.typeArguments;
}
else if (node.type && ts.isTypeReferenceNode(node.type) && node.type.typeArguments) {
payloadTypes = ts.factory.createNodeArray(node.type.typeArguments);
}
const outputCall = ts.factory.createCallExpression(ts.factory.createIdentifier('output'), payloadTypes, aliasParam !== undefined
? [
ts.factory.createObjectLiteralExpression([
ts.factory.createPropertyAssignment('alias', ts.factory.createStringLiteral(aliasParam, true)),
], false),
]
: []);
const existingModifiers = (node.modifiers ?? []).filter((modifier) => !ts.isDecorator(modifier) && modifier.kind !== ts.SyntaxKind.ReadonlyKeyword);
const updatedOutputDeclaration = ts.factory.createPropertyDeclaration(
// Think: this logic of dealing with modifiers is applicable to all signal-based migrations
ts.factory.createNodeArray([
...existingModifiers,
ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword),
]), node.name, undefined, undefined, outputCall);
return prepareTextReplacementForNode(info, node, printer.printNode(ts.EmitHint.Unspecified, updatedOutputDeclaration, sf));
}
function calculateImportReplacements(info, sourceFiles) {
const importReplacements = {};
for (const sf of sourceFiles) {
const importManager = new checker.ImportManager();
const addOnly = [];
const addRemove = [];
const file = project_paths.projectFile(sf, info);
importManager.addImport({
requestedFile: sf,
exportModuleSpecifier: '@angular/core',
exportSymbolName: 'output',
});
apply_import_manager.applyImportManagerChanges(importManager, addOnly, [sf], info);
importManager.removeImport(sf, 'Output', '@angular/core');
importManager.removeImport(sf, 'EventEmitter', '@angular/core');
apply_import_manager.applyImportManagerChanges(importManager, addRemove, [sf], info);
importReplacements[file.id] = {
add: addOnly,
addAndRemove: addRemove,
};
}
return importReplacements;
}
function calculateNextFnReplacement(info, node) {
return prepareTextReplacementForNode(info, node, 'emit');
}
function calculateNextFnReplacementInTemplate(file, span) {
return prepareTextReplacement(file, 'emit', span.start, span.end);
}
function calculateNextFnReplacementInHostBinding(file, offset, span) {
return prepareTextReplacement(file, 'emit', offset + span.start, offset + span.end);
}
function calculateCompleteCallReplacement(info, node) {
return prepareTextReplacementForNode(info, node, '', node.getFullStart());
}
function calculatePipeCallReplacement(info, node) {
if (ts.isPropertyAccessExpression(node.expression)) {
const sf = node.getSourceFile();
const importManager = new checker.ImportManager();
const outputToObservableIdent = importManager.addImport({
requestedFile: sf,
exportModuleSpecifier: '@angular/core/rxjs-interop',
exportSymbolName: 'outputToObservable',
});
const toObsCallExp = ts.factory.createCallExpression(outputToObservableIdent, undefined, [
node.expression.expression,
]);
const pipePropAccessExp = ts.factory.updatePropertyAccessExpression(node.expression, toObsCallExp, node.expression.name);
const pipeCallExp = ts.factory.updateCallExpression(node, pipePropAccessExp, [], node.arguments);
const replacements = [
prepareTextReplacementForNode(info, node, printer.printNode(ts.EmitHint.Unspecified, pipeCallExp, sf)),
];
apply_import_manager.applyImportManagerChanges(importManager, replacements, [sf], info);
return replacements;
}
else {
// TODO: assert instead?
throw new Error(`Unexpected call expression for .pipe - expected a property access but got "${node.getText()}"`);
}
}
function prepareTextReplacementForNode(info, node, replacement, start) {
const sf = node.getSourceFile();
return new project_paths.Replacement(project_paths.projectFile(sf, info), new project_paths.TextUpdate({
position: start ?? node.getStart(),
end: node.getEnd(),
toInsert: replacement,
}));
}
function prepareTextReplacement(file, replacement, start, end) {
return new project_paths.Replacement(file, new project_paths.TextUpdate({
position: start,
end: end,
toInsert: replacement,
}));
}
class OutputMigration extends project_paths.TsurgeFunnelMigration {
config;
constructor(config = {}) {
super();
this.config = config;
}
async analyze(info) {
const { sourceFiles, program } = info;
const outputFieldReplacements = {};
const problematicUsages = {};
let problematicDeclarationCount = 0;
const filesWithOutputDeclarations = new Set();
const checker$1 = program.getTypeChecker();
const reflector = new checker.TypeScriptReflectionHost(checker$1);
const dtsReader = new index$1.DtsMetadataReader(checker$1, reflector);
const evaluator = new index$1.PartialEvaluator(reflector, checker$1, null);
const resourceLoader = info.ngCompiler?.['resourceManager'] ?? null;
// Pre-analyze the program and get access to the template type checker.
// If we are processing a non-Angular target, there is no template info.
const { templateTypeChecker } = info.ngCompiler?.['ensureAnalyzed']() ?? {
templateTypeChecker: null,
};
const knownFields = {
// Note: We don't support cross-target migration of `Partial<T>` usages.
// This is an acceptable limitation for performance reasons.
shouldTrackClassReference: () => false,
attemptRetrieveDescriptorFromSymbol: (s) => {
const propDeclaration = getTargetPropertyDeclaration(s);
if (propDeclaration !== null) {
const classFieldID = getUniqueIdForProperty(info, propDeclaration);
if (classFieldID !== null) {
return {
node: propDeclaration,
key: classFieldID,
};
}
}
return null;
},
};
let isTestFile = false;
const outputMigrationVisitor = (node) => {
// detect output declarations
if (ts.isPropertyDeclaration(node)) {
const outputDecorator = getOutputDecorator(node, reflector);
if (outputDecorator !== null) {
if (isOutputDeclarationEligibleForMigration(node)) {
const outputDef = {
id: getUniqueIdForProperty(info, node),
aliasParam: outputDecorator.args?.at(0),
};
const outputFile = project_paths.projectFile(node.getSourceFile(), info);
if (this.config.shouldMigrate === undefined ||
this.config.shouldMigrate({
key: outputDef.id,
node: node,
}, outputFile)) {
const aliasParam = outputDef.aliasParam;
const aliasOptionValue = aliasParam ? evaluator.evaluate(aliasParam) : undefined;
if (aliasOptionValue == undefined || typeof aliasOptionValue === 'string') {
filesWithOutputDeclarations.add(node.getSourceFile());
addOutputReplacement(outputFieldReplacements, outputDef.id, outputFile, calculateDeclarationReplacement(info, node, aliasOptionValue?.toString()));
}
else {
problematicUsages[outputDef.id] = true;
problematicDeclarationCount++;
}
}
}
else {
problematicDeclarationCount++;
}
}
}
// detect .next usages that should be migrated to .emit
if (isPotentialNextCallUsage(node) && ts.isPropertyAccessExpression(node.expression)) {
const propertyDeclaration = isTargetOutputDeclaration(node.expression.expression, checker$1, reflector, dtsReader);
if (propertyDeclaration !== null) {
const id = getUniqueIdForProperty(info, propertyDeclaration);
const outputFile = project_paths.projectFile(node.getSourceFile(), info);
addOutputReplacement(outputFieldReplacements, id, outputFile, calculateNextFnReplacement(info, node.expression.name));
}
}
// detect .complete usages that should be removed
if (isPotentialCompleteCallUsage(node) && ts.isPropertyAccessExpression(node.expression)) {
const propertyDeclaration = isTargetOutputDeclaration(node.expression.expression, checker$1, reflector, dtsReader);
if (propertyDeclaration !== null) {
const id = getUniqueIdForProperty(info, propertyDeclaration);
const outputFile = project_paths.projectFile(node.getSourceFile(), info);
if (ts.isExpressionStatement(node.parent)) {
addOutputReplacement(outputFieldReplacements, id, outputFile, calculateCompleteCallReplacement(info, node.parent));
}
else {
problematicUsages[id] = true;
}
}
}
addCommentForEmptyEmit(node, info, checker$1, reflector, dtsReader, outputFieldReplacements);
// detect imports of test runners
if (isTestRunnerImport(node)) {
isTestFile = true;
}
// detect unsafe access of the output property
if (isPotentialPipeCallUsage(node) && ts.isPropertyAccessExpression(node.expression)) {
const propertyDeclaration = isTargetOutputDeclaration(node.expression.expression, checker$1, reflector, dtsReader);
if (propertyDeclaration !== null) {
const id = getUniqueIdForProperty(info, propertyDeclaration);
if (isTestFile) {
const outputFile = project_paths.projectFile(node.getSourceFile(), info);
addOutputReplacement(outputFieldReplacements, id, outputFile, ...calculatePipeCallReplacement(info, node));
}
else {
problematicUsages[id] = true;
}
}
}
ts.forEachChild(node, outputMigrationVisitor);
};
// calculate output migration replacements
for (const sf of sourceFiles) {
isTestFile = false;
ts.forEachChild(sf, outputMigrationVisitor);
}
// take care of the references in templates and host bindings
const referenceResult = { references: [] };
const { visitor: templateHostRefVisitor } = index.createFindAllSourceFileReferencesVisitor(info, checker$1, reflector, resourceLoader, evaluator, templateTypeChecker, knownFields, null, // TODO: capture known output names as an optimization
referenceResult);
// calculate template / host binding replacements
for (const sf of sourceFiles) {
ts.forEachChild(sf, templateHostRefVisitor);
}
for (const ref of referenceResult.references) {
// detect .next usages that should be migrated to .emit in template and host binding expressions
if (ref.kind === index.ReferenceKind.InTemplate) {
const callExpr = checkNonTsReferenceCallsField(ref, 'next');
// TODO: here and below for host bindings, we should ideally filter in the global meta stage
// (instead of using the `outputFieldReplacements` map)
// as technically, the call expression could refer to an output
// from a whole different compilation unit (e.g. tsconfig.json).
if (callExpr !== null && outputFieldReplacements[ref.target.key] !== undefined) {
addOutputReplacement(outputFieldReplacements, ref.target.key, ref.from.templateFile, calculateNextFnReplacementInTemplate(ref.from.templateFile, callExpr.nameSpan));
}
}
else if (ref.kind === index.ReferenceKind.InHostBinding) {
const callExpr = checkNonTsReferenceCallsField(ref, 'next');
if (callExpr !== null && outputFieldReplacements[ref.target.key] !== undefined) {
addOutputReplacement(outputFieldReplacements, ref.target.key, ref.from.file, calculateNextFnReplacementInHostBinding(ref.from.file, ref.from.hostPropertyNode.getStart() + 1, callExpr.nameSpan));
}
}
}
// calculate import replacements but do so only for files that have output declarations
const importReplacements = calculateImportReplacements(info, filesWithOutputDeclarations);
return project_paths.confirmAsSerializable({
problematicDeclarationCount,
outputFields: outputFieldReplacements,
importReplacements,
problematicUsages,
});
}
async combine(unitA, unitB) {
const outputFields = {};
const importReplacements = {};
const problematicUsages = {};
let problematicDeclarationCount = 0;
for (const unit of [unitA, unitB]) {
for (const declIdStr of Object.keys(unit.outputFields)) {
const declId = declIdStr;
// THINK: detect clash? Should we have an utility to merge data based on unique IDs?
outputFields[declId] = unit.outputFields[declId];
}
for (const fileIDStr of Object.keys(unit.importReplacements)) {
const fileID = fileIDStr;
importReplacements[fileID] = unit.importReplacements[fileID];
}
problematicDeclarationCount += unit.problematicDeclarationCount;
}
for (const unit of [unitA, unitB]) {
for (const declIdStr of Object.keys(unit.problematicUsages)) {
const declId = declIdStr;
problematicUsages[declId] = unit.problematicUsages[declId];
}
}
return project_paths.confirmAsSerializable({
problematicDeclarationCount,
outputFields,
importReplacements,
problematicUsages,
});
}
async globalMeta(combinedData) {
const globalMeta = {
importReplacements: combinedData.importReplacements,
outputFields: combinedData.outputFields,
problematicDeclarationCount: combinedData.problematicDeclarationCount,
problematicUsages: {},
};
for (const keyStr of Object.keys(combinedData.problematicUsages)) {
const key = keyStr;
// it might happen that a problematic usage is detected but we didn't see the declaration - skipping those
if (globalMeta.outputFields[key] !== undefined) {
globalMeta.problematicUsages[key] = true;
}
}
// Noop here as we don't have any form of special global metadata.
return project_paths.confirmAsSerializable(combinedData);
}
async stats(globalMetadata) {
const detectedOutputs = new Set(Object.keys(globalMetadata.outputFields)).size +
globalMetadata.problematicDeclarationCount;
const problematicOutputs = new Set(Object.keys(globalMetadata.problematicUsages)).size +
globalMetadata.problematicDeclarationCount;
const successRate = detectedOutputs > 0 ? (detectedOutputs - problematicOutputs) / detectedOutputs : 1;
return {
counters: {
detectedOutputs,
problematicOutputs,
successRate,
},
};
}
async migrate(globalData) {
const migratedFiles = new Set();
const problematicFiles = new Set();
const replacements = [];
for (const declIdStr of Object.keys(globalData.outputFields)) {
const declId = declIdStr;
const outputField = globalData.outputFields[declId];
if (!globalData.problematicUsages[declId]) {
replacements.push(...outputField.replacements);
migratedFiles.add(outputField.file.id);
}
else {
problematicFiles.add(outputField.file.id);
}
}
for (const fileIDStr of Object.keys(globalData.importReplacements)) {
const fileID = fileIDStr;
if (migratedFiles.has(fileID)) {
const importReplacements = globalData.importReplacements[fileID];
if (problematicFiles.has(fileID)) {
replacements.push(...importReplacements.add);
}
else {
replacements.push(...importReplacements.addAndRemove);
}
}
}
return { replacements };
}
}
function addOutputReplacement(outputFieldReplacements, outputId, file, ...replacements) {
let existingReplacements = outputFieldReplacements[outputId];
if (existingReplacements === undefined) {
outputFieldReplacements[outputId] = existingReplacements = {
file: file,
replacements: [],
};
}
existingReplacements.replacements.push(...replacements);
}
function addCommentForEmptyEmit(node, info, checker, reflector, dtsReader, outputFieldReplacements) {
if (!isEmptyEmitCall(node))
return;
const propertyAccess = getPropertyAccess(node);
if (!propertyAccess)
return;
const symbol = checker.getSymbolAtLocation(propertyAccess.name);
if (!symbol || !symbol.declarations?.length)
return;
const propertyDeclaration = isTargetOutputDeclaration(propertyAccess, checker, reflector, dtsReader);
if (!propertyDeclaration)
return;
const eventEmitterType = getEventEmitterArgumentType(propertyDeclaration);
if (!eventEmitterType)
return;
const id = getUniqueIdForProperty(info, propertyDeclaration);
const file = project_paths.projectFile(node.getSourceFile(), info);
const formatter = getFormatterText(node);
const todoReplacement = new project_paths.TextUpdate({
toInsert: `${formatter.indent}// TODO: The 'emit' function requires a mandatory ${eventEmitterType} argument\n`,
end: formatter.lineStartPos,
position: formatter.lineStartPos,
});
addOutputReplacement(outputFieldReplacements, id, file, new project_paths.Replacement(file, todoReplacement));
}
function isEmptyEmitCall(node) {
return (ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'emit' &&
node.arguments.length === 0);
}
function getPropertyAccess(node) {
const propertyAccessExpression = node.expression.expression;
return ts.isPropertyAccessExpression(propertyAccessExpression) ? propertyAccessExpression : null;
}
function getEventEmitterArgumentType(propertyDeclaration) {
const initializer = propertyDeclaration.initializer;
if (!initializer || !ts.isNewExpression(initializer))
return null;
const isEventEmitter = ts.isIdentifier(initializer.expression) && initializer.expression.getText() === 'EventEmitter';
if (!isEventEmitter)
return null;
const [typeArg] = initializer.typeArguments ?? [];
return typeArg ? typeArg.getText() : null;
}
function getFormatterText(node) {
const sourceFile = node.getSourceFile();
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
const lineStartPos = sourceFile.getPositionOfLineAndCharacter(line, 0);
const indent = sourceFile.text.slice(lineStartPos, node.getStart());
return { indent, lineStartPos };
}
function migrate(options) {
return async (tree, context) => {
await project_paths.runMigrationInDevkit({
tree,
getMigration: (fs) => new OutputMigration({
shouldMigrate: (_, file) => {
return (file.rootRelativePath.startsWith(fs.normalize(options.path)) &&
!/(^|\/)node_modules\//.test(file.rootRelativePath));
},
}),
beforeProgramCreation: (tsconfigPath, stage) => {
if (stage === project_paths.MigrationStage.Analysis) {
context.logger.info(`Preparing analysis for: ${tsconfigPath}...`);
}
else {
context.logger.info(`Running migration for: ${tsconfigPath}...`);
}
},
afterProgramCreation: (info, fs) => {
const analysisPath = fs.resolve(options.analysisDir);
// Support restricting the analysis to subfolders for larger projects.
if (analysisPath !== '/') {
info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath));
info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath));
}
},
beforeUnitAnalysis: (tsconfigPath) => {
context.logger.info(`Scanning for outputs: ${tsconfigPath}...`);
},
afterAllAnalyzed: () => {
context.logger.info(``);
context.logger.info(`Processing analysis data between targets...`);
context.logger.info(``);
},
afterAnalysisFailure: () => {
context.logger.error('Migration failed unexpectedly with no analysis data');
},
whenDone: ({ counters }) => {
const { detectedOutputs, problematicOutputs, successRate } = counters;
const migratedOutputs = detectedOutputs - problematicOutputs;
const successRatePercent = (successRate * 100).toFixed(2);
context.logger.info('');
context.logger.info(`Successfully migrated to outputs as functions 🎉`);
context.logger.info(` -> Migrated ${migratedOutputs} out of ${detectedOutputs} detected outputs (${successRatePercent} %).`);
},
});
};
}
exports.migrate = migrate;

View File

@@ -0,0 +1,98 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var schematics = require('@angular-devkit/schematics');
var p = require('path');
var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.cjs');
var compiler_host = require('./compiler_host-CAfDJO3W.cjs');
var ts = require('typescript');
var change_tracker = require('./change_tracker-EC4_OX_3.cjs');
var imports = require('./imports-CIX-JgAN.cjs');
require('@angular-devkit/core');
require('./checker-B7WeiY-n.cjs');
require('os');
require('fs');
require('module');
require('url');
const CORE = '@angular/core';
const EXPERIMENTAL_PENDING_TASKS = 'ExperimentalPendingTasks';
function migrateFile(sourceFile, typeChecker, rewriteFn) {
const changeTracker = new change_tracker.ChangeTracker(ts.createPrinter());
// Check if there are any imports of the `AfterRenderPhase` enum.
const coreImports = imports.getNamedImports(sourceFile, CORE);
if (!coreImports) {
return;
}
const importSpecifier = imports.getImportSpecifier(sourceFile, CORE, EXPERIMENTAL_PENDING_TASKS);
if (!importSpecifier) {
return;
}
const nodeToReplace = importSpecifier.propertyName ?? importSpecifier.name;
if (!ts.isIdentifier(nodeToReplace)) {
return;
}
changeTracker.replaceNode(nodeToReplace, ts.factory.createIdentifier('PendingTasks'));
ts.forEachChild(sourceFile, function visit(node) {
// import handled above
if (ts.isImportDeclaration(node)) {
return;
}
if (ts.isIdentifier(node) &&
node.text === EXPERIMENTAL_PENDING_TASKS &&
imports.getImportOfIdentifier(typeChecker, node)?.name === EXPERIMENTAL_PENDING_TASKS) {
changeTracker.replaceNode(node, ts.factory.createIdentifier('PendingTasks'));
}
ts.forEachChild(node, visit);
});
// Write the changes.
for (const changesInFile of changeTracker.recordChanges().values()) {
for (const change of changesInFile) {
rewriteFn(change.start, change.removeLength ?? 0, change.text);
}
}
}
function migrate() {
return async (tree) => {
const { buildPaths, testPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(tree);
const basePath = process.cwd();
const allPaths = [...buildPaths, ...testPaths];
if (!allPaths.length) {
throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the afterRender phase migration.');
}
for (const tsconfigPath of allPaths) {
runMigration(tree, tsconfigPath, basePath);
}
};
}
function runMigration(tree, tsconfigPath, basePath) {
const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
const sourceFiles = program
.getSourceFiles()
.filter((sourceFile) => compiler_host.canMigrateFile(basePath, sourceFile, program));
for (const sourceFile of sourceFiles) {
let update = null;
const rewriter = (startPos, width, text) => {
if (update === null) {
// Lazily initialize update, because most files will not require migration.
update = tree.beginUpdate(p.relative(basePath, sourceFile.fileName));
}
update.remove(startPos, width);
if (text !== null) {
update.insertLeft(startPos, text);
}
};
migrateFile(sourceFile, program.getTypeChecker(), rewriter);
if (update !== null) {
tree.commitUpdate(update);
}
}
}
exports.migrate = migrate;

View File

@@ -0,0 +1,784 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var index = require('./index-DeAjKhiO.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 checker = require('./checker-B7WeiY-n.cjs');
require('path');
var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.cjs');
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);
/// <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 = checker.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 = checker.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 = checker.getFileSystem()) {
try {
const fs = checker.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 = checker.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 checker.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;
}
/**
* 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;
}
/** Creates the base program info for the given tsconfig path. */
function createBaseProgramInfo(absoluteTsconfigPath, fs, optionOverrides = {}) {
if (fs === undefined) {
fs = new checker.NodeJSFileSystem();
checker.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);
}
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.
!checker.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 = checker.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 = checker.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);
checker.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 = checker.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 checker.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;

View File

@@ -0,0 +1,90 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var core = require('@angular-devkit/core');
/**
* Gets all tsconfig paths from a CLI project by reading the workspace configuration
* and looking for common tsconfig locations.
*/
async function getProjectTsConfigPaths(tree) {
// Start with some tsconfig paths that are generally used within CLI projects. Note
// that we are not interested in IDE-specific tsconfig files (e.g. /tsconfig.json)
const buildPaths = new Set();
const testPaths = new Set();
const workspace = await getWorkspace(tree);
for (const [, project] of workspace.projects) {
for (const [name, target] of project.targets) {
if (name !== 'build' && name !== 'test') {
continue;
}
for (const [, options] of allTargetOptions(target)) {
const tsConfig = options['tsConfig'];
// Filter out tsconfig files that don't exist in the CLI project.
if (typeof tsConfig !== 'string' || !tree.exists(tsConfig)) {
continue;
}
if (name === 'build') {
buildPaths.add(core.normalize(tsConfig));
}
else {
testPaths.add(core.normalize(tsConfig));
}
}
}
}
return {
buildPaths: [...buildPaths],
testPaths: [...testPaths],
};
}
/** Get options for all configurations for the passed builder target. */
function* allTargetOptions(target) {
if (target.options) {
yield [undefined, target.options];
}
if (!target.configurations) {
return;
}
for (const [name, options] of Object.entries(target.configurations)) {
if (options) {
yield [name, options];
}
}
}
function createHost(tree) {
return {
async readFile(path) {
const data = tree.read(path);
if (!data) {
throw new Error('File not found.');
}
return core.virtualFs.fileBufferToString(data);
},
async writeFile(path, data) {
return tree.overwrite(path, data);
},
async isDirectory(path) {
// Approximate a directory check.
// We don't need to consider empty directories and hence this is a good enough approach.
// This is also per documentation, see:
// https://angular.dev/tools/cli/schematics-for-libraries#get-the-project-configuration
return !tree.exists(path) && tree.getDir(path).subfiles.length > 0;
},
async isFile(path) {
return tree.exists(path);
},
};
}
async function getWorkspace(tree) {
const host = createHost(tree);
const { workspace } = await core.workspaces.readWorkspace('/', host);
return workspace;
}
exports.getProjectTsConfigPaths = getProjectTsConfigPaths;

View File

@@ -0,0 +1,27 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
/**
* Gets the text of the given property name. Returns null if the property
* name couldn't be determined statically.
*/
function getPropertyNameText(node) {
if (ts.isIdentifier(node) || ts.isStringLiteralLike(node)) {
return node.text;
}
return null;
}
/** Finds a property with a specific name in an object literal expression. */
function findLiteralProperty(literal, name) {
return literal.properties.find((prop) => prop.name && ts.isIdentifier(prop.name) && prop.name.text === name);
}
exports.findLiteralProperty = findLiteralProperty;
exports.getPropertyNameText = getPropertyNameText;

View File

@@ -0,0 +1,181 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var schematics = require('@angular-devkit/schematics');
var p = require('path');
var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.cjs');
var compiler_host = require('./compiler_host-CAfDJO3W.cjs');
var ts = require('typescript');
var change_tracker = require('./change_tracker-EC4_OX_3.cjs');
var imports = require('./imports-CIX-JgAN.cjs');
require('@angular-devkit/core');
require('./checker-B7WeiY-n.cjs');
require('os');
require('fs');
require('module');
require('url');
function migrateFile(sourceFile, rewriteFn) {
const changeTracker = new change_tracker.ChangeTracker(ts.createPrinter());
const visitNode = (node) => {
const provider = tryParseProviderExpression(node);
if (provider) {
replaceProviderWithNewApi({
sourceFile,
node,
provider,
changeTracker,
});
return;
}
ts.forEachChild(node, visitNode);
};
ts.forEachChild(sourceFile, visitNode);
for (const change of changeTracker.recordChanges().get(sourceFile)?.values() ?? []) {
rewriteFn(change.start, change.removeLength ?? 0, change.text);
}
}
function replaceProviderWithNewApi({ sourceFile, node, provider, changeTracker, }) {
const { initializerCode, importInject, provideInitializerFunctionName, initializerToken } = provider;
const initializerTokenSpecifier = imports.getImportSpecifier(sourceFile, angularCoreModule, initializerToken);
// The token doesn't come from `@angular/core`.
if (!initializerTokenSpecifier) {
return;
}
// Replace the provider with the new provide function.
changeTracker.replaceText(sourceFile, node.getStart(), node.getWidth(), `${provideInitializerFunctionName}(${initializerCode})`);
// Remove the `*_INITIALIZER` token from imports.
changeTracker.removeImport(sourceFile, initializerToken, angularCoreModule);
// Add the `inject` function to imports if needed.
if (importInject) {
changeTracker.addImport(sourceFile, 'inject', angularCoreModule);
}
// Add the `provide*Initializer` function to imports.
changeTracker.addImport(sourceFile, provideInitializerFunctionName, angularCoreModule);
}
function tryParseProviderExpression(node) {
if (!ts.isObjectLiteralExpression(node)) {
return;
}
let deps = [];
let initializerToken;
let useExisting;
let useFactoryCode;
let useValue;
let multi = false;
for (const property of node.properties) {
if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name)) {
switch (property.name.text) {
case 'deps':
if (ts.isArrayLiteralExpression(property.initializer)) {
deps = property.initializer.elements.map((el) => el.getText());
}
break;
case 'provide':
initializerToken = property.initializer.getText();
break;
case 'useExisting':
useExisting = property.initializer;
break;
case 'useFactory':
useFactoryCode = property.initializer.getText();
break;
case 'useValue':
useValue = property.initializer;
break;
case 'multi':
multi = property.initializer.kind === ts.SyntaxKind.TrueKeyword;
break;
}
}
// Handle the `useFactory() {}` shorthand case.
if (ts.isMethodDeclaration(property) && property.name.getText() === 'useFactory') {
const params = property.parameters.map((param) => param.getText()).join(', ');
useFactoryCode = `(${params}) => ${property.body?.getText()}`;
}
}
if (!initializerToken || !multi) {
return;
}
const provideInitializerFunctionName = initializerTokenToFunctionMap.get(initializerToken);
if (!provideInitializerFunctionName) {
return;
}
const info = {
initializerToken,
provideInitializerFunctionName,
importInject: false,
};
if (useExisting) {
return {
...info,
importInject: true,
initializerCode: `() => inject(${useExisting.getText()})()`,
};
}
if (useFactoryCode) {
const args = deps.map((dep) => `inject(${dep})`);
return {
...info,
importInject: deps.length > 0,
initializerCode: `() => {
const initializerFn = (${useFactoryCode})(${args.join(', ')});
return initializerFn();
}`,
};
}
if (useValue) {
return { ...info, initializerCode: useValue.getText() };
}
return;
}
const angularCoreModule = '@angular/core';
const initializerTokenToFunctionMap = new Map([
['APP_INITIALIZER', 'provideAppInitializer'],
['ENVIRONMENT_INITIALIZER', 'provideEnvironmentInitializer'],
['PLATFORM_INITIALIZER', 'providePlatformInitializer'],
]);
function migrate() {
return async (tree) => {
const { buildPaths, testPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(tree);
const basePath = process.cwd();
const allPaths = [...buildPaths, ...testPaths];
if (!allPaths.length) {
throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the provide initializer migration.');
}
for (const tsconfigPath of allPaths) {
runMigration(tree, tsconfigPath, basePath);
}
};
}
function runMigration(tree, tsconfigPath, basePath) {
const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
const sourceFiles = program
.getSourceFiles()
.filter((sourceFile) => compiler_host.canMigrateFile(basePath, sourceFile, program));
for (const sourceFile of sourceFiles) {
let update = null;
const rewriter = (startPos, width, text) => {
if (update === null) {
// Lazily initialize update, because most files will not require migration.
update = tree.beginUpdate(p.relative(basePath, sourceFile.fileName));
}
update.remove(startPos, width);
if (text !== null) {
update.insertLeft(startPos, text);
}
};
migrateFile(sourceFile, rewriter);
if (update !== null) {
tree.commitUpdate(update);
}
}
}
exports.migrate = migrate;

View File

@@ -0,0 +1,412 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var schematics = require('@angular-devkit/schematics');
var fs = require('fs');
var p = require('path');
var change_tracker = require('./change_tracker-EC4_OX_3.cjs');
var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.cjs');
var compiler_host = require('./compiler_host-CAfDJO3W.cjs');
var ts = require('typescript');
var checker = require('./checker-B7WeiY-n.cjs');
var property_name = require('./property_name-BBwFuqMe.cjs');
require('os');
require('@angular-devkit/core');
require('module');
require('url');
/**
* Finds the class declaration that is being referred to by a node.
* @param reference Node referring to a class declaration.
* @param typeChecker
*/
function findClassDeclaration(reference, typeChecker) {
return (typeChecker
.getTypeAtLocation(reference)
.getSymbol()
?.declarations?.find(ts.isClassDeclaration) || null);
}
/*!
* @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
*/
/**
* Checks whether a component is standalone.
* @param node Class being checked.
* @param reflector The reflection host to use.
*/
function isStandaloneComponent(node, reflector) {
const decorators = reflector.getDecoratorsOfDeclaration(node);
if (decorators === null) {
return false;
}
const decorator = checker.findAngularDecorator(decorators, 'Component', false);
if (decorator === undefined || decorator.args === null || decorator.args.length !== 1) {
return false;
}
const arg = decorator.args[0];
if (ts.isObjectLiteralExpression(arg)) {
const property = property_name.findLiteralProperty(arg, 'standalone');
if (property) {
return property.initializer.getText() === 'true';
}
else {
return true; // standalone is true by default in v19
}
}
return false;
}
/**
* Checks whether a node is variable declaration of type Routes or Route[] and comes from @angular/router
* @param node Variable declaration being checked.
* @param typeChecker
*/
function isAngularRoutesArray(node, typeChecker) {
if (ts.isVariableDeclaration(node)) {
const type = typeChecker.getTypeAtLocation(node);
if (type && typeChecker.isArrayType(type)) {
// Route[] is an array type
const typeArguments = typeChecker.getTypeArguments(type);
const symbol = typeArguments[0]?.getSymbol();
return (symbol?.name === 'Route' &&
symbol?.declarations?.some((decl) => {
return decl.getSourceFile().fileName.includes('@angular/router');
}));
}
}
return false;
}
/**
* Checks whether a node is a call expression to a router module method.
* Examples:
* - RouterModule.forRoot(routes)
* - RouterModule.forChild(routes)
*/
function isRouterModuleCallExpression(node, typeChecker) {
if (ts.isPropertyAccessExpression(node.expression)) {
const propAccess = node.expression;
const moduleSymbol = typeChecker.getSymbolAtLocation(propAccess.expression);
return (moduleSymbol?.name === 'RouterModule' &&
(propAccess.name.text === 'forRoot' || propAccess.name.text === 'forChild'));
}
return false;
}
/**
* Checks whether a node is a call expression to a router method.
* Example: this.router.resetConfig(routes)
*/
function isRouterCallExpression(node, typeChecker) {
if (ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'resetConfig') {
const calleeExpression = node.expression.expression;
const symbol = typeChecker.getSymbolAtLocation(calleeExpression);
if (symbol) {
const type = typeChecker.getTypeOfSymbolAtLocation(symbol, calleeExpression);
// if type of router is Router, then it is a router call expression
return type.aliasSymbol?.escapedName === 'Router';
}
}
return false;
}
/**
* Checks whether a node is a call expression to router provide function.
* Example: provideRoutes(routes)
*/
function isRouterProviderCallExpression(node, typeChecker) {
if (ts.isIdentifier(node.expression)) {
const moduleSymbol = typeChecker.getSymbolAtLocation(node.expression);
return moduleSymbol && moduleSymbol.name === 'provideRoutes';
}
return false;
}
/**
* Checks whether a node is a call expression to provideRouter function.
* Example: provideRouter(routes)
*/
function isProvideRoutesCallExpression(node, typeChecker) {
if (ts.isIdentifier(node.expression)) {
const moduleSymbol = typeChecker.getSymbolAtLocation(node.expression);
return moduleSymbol && moduleSymbol.name === 'provideRouter';
}
return false;
}
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* Converts all application routes that are using standalone components to be lazy loaded.
* @param sourceFile File that should be migrated.
* @param program
*/
function migrateFileToLazyRoutes(sourceFile, program) {
const typeChecker = program.getTypeChecker();
const reflector = new checker.TypeScriptReflectionHost(typeChecker);
const printer = ts.createPrinter();
const tracker = new change_tracker.ChangeTracker(printer);
const routeArraysToMigrate = findRoutesArrayToMigrate(sourceFile, typeChecker);
if (routeArraysToMigrate.length === 0) {
return { pendingChanges: [], skippedRoutes: [], migratedRoutes: [] };
}
const { skippedRoutes, migratedRoutes } = migrateRoutesArray(routeArraysToMigrate, typeChecker, reflector, tracker);
return {
pendingChanges: tracker.recordChanges().get(sourceFile) || [],
skippedRoutes,
migratedRoutes,
};
}
/** Finds route object that can be migrated */
function findRoutesArrayToMigrate(sourceFile, typeChecker) {
const routesArrays = [];
sourceFile.forEachChild(function walk(node) {
if (ts.isCallExpression(node)) {
if (isRouterModuleCallExpression(node, typeChecker) ||
isRouterProviderCallExpression(node, typeChecker) ||
isRouterCallExpression(node, typeChecker) ||
isProvideRoutesCallExpression(node, typeChecker)) {
const arg = node.arguments[0]; // ex: RouterModule.forRoot(routes) or provideRouter(routes)
const routeFileImports = sourceFile.statements.filter(ts.isImportDeclaration);
if (ts.isArrayLiteralExpression(arg) && arg.elements.length > 0) {
// ex: inline routes array: RouterModule.forRoot([{ path: 'test', component: TestComponent }])
routesArrays.push({
routeFilePath: sourceFile.fileName,
array: arg,
routeFileImports,
});
}
else if (ts.isIdentifier(arg)) {
// ex: reference to routes array: RouterModule.forRoot(routes)
// RouterModule.forRoot(routes), provideRouter(routes), provideRoutes(routes)
const symbol = typeChecker.getSymbolAtLocation(arg);
if (!symbol?.declarations)
return;
for (const declaration of symbol.declarations) {
if (ts.isVariableDeclaration(declaration)) {
const initializer = declaration.initializer;
if (initializer && ts.isArrayLiteralExpression(initializer)) {
// ex: const routes = [{ path: 'test', component: TestComponent }];
routesArrays.push({
routeFilePath: sourceFile.fileName,
array: initializer,
routeFileImports,
});
}
}
}
}
}
}
if (ts.isVariableDeclaration(node)) {
if (isAngularRoutesArray(node, typeChecker)) {
const initializer = node.initializer;
if (initializer &&
ts.isArrayLiteralExpression(initializer) &&
initializer.elements.length > 0) {
// ex: const routes: Routes = [{ path: 'test', component: TestComponent }];
if (routesArrays.find((x) => x.array === initializer)) {
// already exists
return;
}
routesArrays.push({
routeFilePath: sourceFile.fileName,
array: initializer,
routeFileImports: sourceFile.statements.filter(ts.isImportDeclaration),
});
}
}
}
node.forEachChild(walk);
});
return routesArrays;
}
/** Migrate a routes object standalone components to be lazy loaded. */
function migrateRoutesArray(routesArray, typeChecker, reflector, tracker) {
const migratedRoutes = [];
const skippedRoutes = [];
const importsToRemove = [];
for (const route of routesArray) {
route.array.elements.forEach((element) => {
if (ts.isObjectLiteralExpression(element)) {
const { migratedRoutes: migrated, skippedRoutes: toBeSkipped, importsToRemove: toBeRemoved, } = migrateRoute(element, route, typeChecker, reflector, tracker);
migratedRoutes.push(...migrated);
skippedRoutes.push(...toBeSkipped);
importsToRemove.push(...toBeRemoved);
}
});
}
for (const importToRemove of importsToRemove) {
tracker.removeNode(importToRemove);
}
return { migratedRoutes, skippedRoutes };
}
/**
* Migrates a single route object and returns the results of the migration
* It recursively migrates the children routes if they exist
*/
function migrateRoute(element, route, typeChecker, reflector, tracker) {
const skippedRoutes = [];
const migratedRoutes = [];
const importsToRemove = [];
const component = property_name.findLiteralProperty(element, 'component');
// this can be empty string or a variable that is not a string, or not present at all
const routePath = property_name.findLiteralProperty(element, 'path')?.getText() ?? '';
const children = property_name.findLiteralProperty(element, 'children');
// recursively migrate children routes first if they exist
if (children && ts.isArrayLiteralExpression(children.initializer)) {
for (const childRoute of children.initializer.elements) {
if (ts.isObjectLiteralExpression(childRoute)) {
const { migratedRoutes: migrated, skippedRoutes: toBeSkipped, importsToRemove: toBeRemoved, } = migrateRoute(childRoute, route, typeChecker, reflector, tracker);
migratedRoutes.push(...migrated);
skippedRoutes.push(...toBeSkipped);
importsToRemove.push(...toBeRemoved);
}
}
}
const routeMigrationResults = { migratedRoutes, skippedRoutes, importsToRemove };
if (!component) {
return routeMigrationResults;
}
const componentDeclaration = findClassDeclaration(component, typeChecker);
if (!componentDeclaration) {
return routeMigrationResults;
}
// if component is not a standalone component, skip it
if (!isStandaloneComponent(componentDeclaration, reflector)) {
skippedRoutes.push({ path: routePath, file: route.routeFilePath });
return routeMigrationResults;
}
const componentClassName = componentDeclaration.name && ts.isIdentifier(componentDeclaration.name)
? componentDeclaration.name.text
: null;
if (!componentClassName) {
return routeMigrationResults;
}
// if component is in the same file as the routes array, skip it
if (componentDeclaration.getSourceFile().fileName === route.routeFilePath) {
return routeMigrationResults;
}
const componentImport = route.routeFileImports.find((importDecl) => importDecl.importClause?.getText().includes(componentClassName));
// remove single and double quotes from the import path
let componentImportPath = ts.isStringLiteral(componentImport?.moduleSpecifier)
? componentImport.moduleSpecifier.text
: null;
// if the import path is not a string literal, skip it
if (!componentImportPath) {
skippedRoutes.push({ path: routePath, file: route.routeFilePath });
return routeMigrationResults;
}
const isDefaultExport = componentDeclaration.modifiers?.some((x) => x.kind === ts.SyntaxKind.DefaultKeyword) ?? false;
const loadComponent = createLoadComponentPropertyAssignment(componentImportPath, componentClassName, isDefaultExport);
tracker.replaceNode(component, loadComponent);
// Add the import statement for the standalone component
if (!importsToRemove.includes(componentImport)) {
importsToRemove.push(componentImport);
}
migratedRoutes.push({ path: routePath, file: route.routeFilePath });
// the component was migrated, so we return the results
return routeMigrationResults;
}
/**
* Generates the loadComponent property assignment for a given component.
*
* Example:
* loadComponent: () => import('./path').then(m => m.componentName)
* or
* loadComponent: () => import('./path') // when isDefaultExport is true
*/
function createLoadComponentPropertyAssignment(componentImportPath, componentDeclarationName, isDefaultExport) {
return ts.factory.createPropertyAssignment('loadComponent', ts.factory.createArrowFunction(undefined, undefined, [], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), isDefaultExport
? createImportCallExpression(componentImportPath) // will generate import('./path) and will skip the then() call
: ts.factory.createCallExpression(
// will generate import('./path).then(m => m.componentName)
ts.factory.createPropertyAccessExpression(createImportCallExpression(componentImportPath), 'then'), undefined, [createImportThenCallExpression(componentDeclarationName)])));
}
// import('./path)
const createImportCallExpression = (componentImportPath) => ts.factory.createCallExpression(ts.factory.createIdentifier('import'), undefined, [
ts.factory.createStringLiteral(componentImportPath, true),
]);
// m => m.componentName
const createImportThenCallExpression = (componentDeclarationName) => ts.factory.createArrowFunction(undefined, undefined, [ts.factory.createParameterDeclaration(undefined, undefined, 'm', undefined, undefined)], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('m'), componentDeclarationName));
function migrate(options) {
return async (tree, context) => {
const { buildPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(tree);
const basePath = process.cwd();
// TS and Schematic use paths in POSIX format even on Windows. This is needed as otherwise
// string matching such as `sourceFile.fileName.startsWith(pathToMigrate)` might not work.
const pathToMigrate = change_tracker.normalizePath(p.join(basePath, options.path));
if (!buildPaths.length) {
throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the route lazy loading migration.');
}
let migratedRoutes = [];
let skippedRoutes = [];
for (const tsconfigPath of buildPaths) {
const { migratedRoutes: migrated, skippedRoutes: skipped } = standaloneRoutesMigration(tree, tsconfigPath, basePath, pathToMigrate, options);
migratedRoutes.push(...migrated);
skippedRoutes.push(...skipped);
}
if (migratedRoutes.length === 0 && skippedRoutes.length === 0) {
throw new schematics.SchematicsException(`Could not find any files to migrate under the path ${pathToMigrate}.`);
}
context.logger.info('🎉 Automated migration step has finished! 🎉');
context.logger.info(`Number of updated routes: ${migratedRoutes.length}`);
context.logger.info(`Number of skipped routes: ${skippedRoutes.length}`);
if (skippedRoutes.length > 0) {
context.logger.info(`Note: this migration was unable to optimize the following routes, since they use components declared in NgModules:`);
for (const route of skippedRoutes) {
context.logger.info(`- \`${route.path}\` path at \`${route.file}\``);
}
context.logger.info(`Consider making those components standalone and run this migration again. More information about standalone migration can be found at https://angular.dev/reference/migrations/standalone`);
}
context.logger.info('IMPORTANT! Please verify manually that your application builds and behaves as expected.');
context.logger.info(`See https://angular.dev/reference/migrations/route-lazy-loading for more information.`);
};
}
function standaloneRoutesMigration(tree, tsconfigPath, basePath, pathToMigrate, schematicOptions) {
if (schematicOptions.path.startsWith('..')) {
throw new schematics.SchematicsException('Cannot run route lazy loading migration outside of the current project.');
}
if (fs.existsSync(pathToMigrate) && !fs.statSync(pathToMigrate).isDirectory()) {
throw new schematics.SchematicsException(`Migration path ${pathToMigrate} has to be a directory. Cannot run the route lazy loading migration.`);
}
const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
const sourceFiles = program
.getSourceFiles()
.filter((sourceFile) => sourceFile.fileName.startsWith(pathToMigrate) &&
compiler_host.canMigrateFile(basePath, sourceFile, program));
const migratedRoutes = [];
const skippedRoutes = [];
if (sourceFiles.length === 0) {
return { migratedRoutes, skippedRoutes };
}
for (const sourceFile of sourceFiles) {
const { pendingChanges, skippedRoutes: skipped, migratedRoutes: migrated, } = migrateFileToLazyRoutes(sourceFile, program);
skippedRoutes.push(...skipped);
migratedRoutes.push(...migrated);
const update = tree.beginUpdate(p.relative(basePath, sourceFile.fileName));
pendingChanges.forEach((change) => {
if (change.removeLength != null) {
update.remove(change.start, change.removeLength);
}
update.insertRight(change.start, change.text);
});
tree.commitUpdate(update);
}
return { migratedRoutes, skippedRoutes };
}
exports.migrate = migrate;

View File

@@ -0,0 +1,419 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var ts = require('typescript');
require('os');
var checker = require('./checker-B7WeiY-n.cjs');
require('./index-DeAjKhiO.cjs');
require('path');
var project_paths = require('./project_paths-DIZUxMOI.cjs');
var ng_decorators = require('./ng_decorators-B5HCqr20.cjs');
var property_name = require('./property_name-BBwFuqMe.cjs');
require('@angular-devkit/core');
require('node:path/posix');
require('fs');
require('module');
require('url');
require('@angular-devkit/schematics');
require('./project_tsconfig_paths-CDVxT6Ov.cjs');
require('./imports-CIX-JgAN.cjs');
/**
* Unwraps a given expression TypeScript node. Expressions can be wrapped within multiple
* parentheses or as expression. e.g. "(((({exp}))))()". The function should return the
* TypeScript node referring to the inner expression. e.g "exp".
*/
function unwrapExpression(node) {
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node)) {
return unwrapExpression(node.expression);
}
else {
return node;
}
}
/** Extracts `@Directive` or `@Component` metadata from the given class. */
function extractAngularClassMetadata(typeChecker, node) {
const decorators = ts.getDecorators(node);
if (!decorators || !decorators.length) {
return null;
}
const ngDecorators = ng_decorators.getAngularDecorators(typeChecker, decorators);
const componentDecorator = ngDecorators.find((dec) => dec.name === 'Component');
const directiveDecorator = ngDecorators.find((dec) => dec.name === 'Directive');
const decorator = componentDecorator ?? directiveDecorator;
// In case no decorator could be found on the current class, skip.
if (!decorator) {
return null;
}
const decoratorCall = decorator.node.expression;
// In case the decorator call is not valid, skip this class declaration.
if (decoratorCall.arguments.length !== 1) {
return null;
}
const metadata = unwrapExpression(decoratorCall.arguments[0]);
// Ensure that the metadata is an object literal expression.
if (!ts.isObjectLiteralExpression(metadata)) {
return null;
}
return {
type: componentDecorator ? 'component' : 'directive',
node: metadata,
};
}
const LF_CHAR = 10;
const CR_CHAR = 13;
const LINE_SEP_CHAR = 8232;
const PARAGRAPH_CHAR = 8233;
/** Gets the line and character for the given position from the line starts map. */
function getLineAndCharacterFromPosition(lineStartsMap, position) {
const lineIndex = findClosestLineStartPosition(lineStartsMap, position);
return { character: position - lineStartsMap[lineIndex], line: lineIndex };
}
/**
* Computes the line start map of the given text. This can be used in order to
* retrieve the line and character of a given text position index.
*/
function computeLineStartsMap(text) {
const result = [0];
let pos = 0;
while (pos < text.length) {
const char = text.charCodeAt(pos++);
// Handles the "CRLF" line break. In that case we peek the character
// after the "CR" and check if it is a line feed.
if (char === CR_CHAR) {
if (text.charCodeAt(pos) === LF_CHAR) {
pos++;
}
result.push(pos);
}
else if (char === LF_CHAR || char === LINE_SEP_CHAR || char === PARAGRAPH_CHAR) {
result.push(pos);
}
}
result.push(pos);
return result;
}
/** Finds the closest line start for the given position. */
function findClosestLineStartPosition(linesMap, position, low = 0, high = linesMap.length - 1) {
while (low <= high) {
const pivotIdx = Math.floor((low + high) / 2);
const pivotEl = linesMap[pivotIdx];
if (pivotEl === position) {
return pivotIdx;
}
else if (position > pivotEl) {
low = pivotIdx + 1;
}
else {
high = pivotIdx - 1;
}
}
// In case there was no exact match, return the closest "lower" line index. We also
// subtract the index by one because want the index of the previous line start.
return low - 1;
}
/**
* Visitor that can be used to determine Angular templates referenced within given
* TypeScript source files (inline templates or external referenced templates)
*/
class NgComponentTemplateVisitor {
typeChecker;
resolvedTemplates = [];
fs = checker.getFileSystem();
constructor(typeChecker) {
this.typeChecker = typeChecker;
}
visitNode(node) {
if (node.kind === ts.SyntaxKind.ClassDeclaration) {
this.visitClassDeclaration(node);
}
ts.forEachChild(node, (n) => this.visitNode(n));
}
visitClassDeclaration(node) {
const metadata = extractAngularClassMetadata(this.typeChecker, node);
if (metadata === null || metadata.type !== 'component') {
return;
}
const sourceFile = node.getSourceFile();
const sourceFileName = sourceFile.fileName;
// Walk through all component metadata properties and determine the referenced
// HTML templates (either external or inline)
metadata.node.properties.forEach((property) => {
if (!ts.isPropertyAssignment(property)) {
return;
}
const propertyName = property_name.getPropertyNameText(property.name);
// In case there is an inline template specified, ensure that the value is statically
// analyzable by checking if the initializer is a string literal-like node.
if (propertyName === 'template' && ts.isStringLiteralLike(property.initializer)) {
// Need to add an offset of one to the start because the template quotes are
// not part of the template content.
// The `getText()` method gives us the original raw text.
// We could have used the `text` property, but if the template is defined as a backtick
// string then the `text` property contains a "cooked" version of the string. Such cooked
// strings will have converted CRLF characters to only LF. This messes up string
// replacements in template migrations.
// The raw text returned by `getText()` includes the enclosing quotes so we change the
// `content` and `start` values accordingly.
const content = property.initializer.getText().slice(1, -1);
const start = property.initializer.getStart() + 1;
this.resolvedTemplates.push({
filePath: sourceFileName,
container: node,
content,
inline: true,
start: start,
getCharacterAndLineOfPosition: (pos) => ts.getLineAndCharacterOfPosition(sourceFile, pos + start),
});
}
if (propertyName === 'templateUrl' && ts.isStringLiteralLike(property.initializer)) {
const absolutePath = this.fs.resolve(this.fs.dirname(sourceFileName), property.initializer.text);
if (!this.fs.exists(absolutePath)) {
return;
}
const fileContent = this.fs.readFile(absolutePath);
const lineStartsMap = computeLineStartsMap(fileContent);
this.resolvedTemplates.push({
filePath: absolutePath,
container: node,
content: fileContent,
inline: false,
start: 0,
getCharacterAndLineOfPosition: (pos) => getLineAndCharacterFromPosition(lineStartsMap, pos),
});
}
});
}
}
function parseTemplate(template) {
let parsed;
try {
// Note: we use the HtmlParser here, instead of the `parseTemplate` function, because the
// latter returns an Ivy AST, not an HTML AST. The HTML AST has the advantage of preserving
// interpolated text as text nodes containing a mixture of interpolation tokens and text tokens,
// rather than turning them into `BoundText` nodes like the Ivy AST does. This allows us to
// easily get the text-only ranges without having to reconstruct the original text.
parsed = new checker.HtmlParser().parse(template, '', {
// Allows for ICUs to be parsed.
tokenizeExpansionForms: true,
// Explicitly disable blocks so that their characters are treated as plain text.
tokenizeBlocks: true,
preserveLineEndings: true,
});
// Don't migrate invalid templates.
if (parsed.errors && parsed.errors.length > 0) {
const errors = parsed.errors.map((e) => ({ type: 'parse', error: e }));
return { tree: undefined, errors };
}
}
catch (e) {
return { tree: undefined, errors: [{ type: 'parse', error: e }] };
}
return { tree: parsed, errors: [] };
}
function migrateTemplateToSelfClosingTags(template) {
let parsed = parseTemplate(template);
if (parsed.tree === undefined) {
return { migrated: template, changed: false, replacementCount: 0 };
}
const visitor = new AngularElementCollector();
checker.visitAll(visitor, parsed.tree.rootNodes);
let newTemplate = template;
let changedOffset = 0;
let replacementCount = 0;
for (let element of visitor.elements) {
const { start, end, tagName } = element;
const currentLength = newTemplate.length;
const templatePart = newTemplate.slice(start + changedOffset, end + changedOffset);
const convertedTemplate = replaceWithSelfClosingTag(templatePart, tagName);
// if the template has changed, replace the original template with the new one
if (convertedTemplate.length !== templatePart.length) {
newTemplate = replaceTemplate(newTemplate, convertedTemplate, start, end, changedOffset);
changedOffset += newTemplate.length - currentLength;
replacementCount++;
}
}
return { migrated: newTemplate, changed: changedOffset !== 0, replacementCount };
}
function replaceWithSelfClosingTag(html, tagName) {
const pattern = new RegExp(`<\\s*${tagName}\\s*([^>]*?(?:"[^"]*"|'[^']*'|[^'">])*)\\s*>([\\s\\S]*?)<\\s*/\\s*${tagName}\\s*>`, 'gi');
return html.replace(pattern, (_, content) => `<${tagName}${content ? ` ${content}` : ''} />`);
}
/**
* Replace the value in the template with the new value based on the start and end position + offset
*/
function replaceTemplate(template, replaceValue, start, end, offset) {
return template.slice(0, start + offset) + replaceValue + template.slice(end + offset);
}
const ALL_HTML_TAGS = new checker.DomElementSchemaRegistry().allKnownElementNames();
class AngularElementCollector extends checker.RecursiveVisitor {
elements = [];
constructor() {
super();
}
visitElement(element) {
const isHtmlTag = ALL_HTML_TAGS.includes(element.name);
if (isHtmlTag) {
return;
}
const hasNoContent = this.elementHasNoContent(element);
const hasNoClosingTag = this.elementHasNoClosingTag(element);
if (hasNoContent && !hasNoClosingTag) {
this.elements.push({
tagName: element.name,
start: element.sourceSpan.start.offset,
end: element.sourceSpan.end.offset,
});
}
return super.visitElement(element, null);
}
elementHasNoContent(element) {
if (!element.children?.length) {
return true;
}
if (element.children.length === 1) {
const child = element.children[0];
return child instanceof checker.Text && /^\s*$/.test(child.value);
}
return false;
}
elementHasNoClosingTag(element) {
const { startSourceSpan, endSourceSpan } = element;
if (!endSourceSpan) {
return true;
}
return (startSourceSpan.start.offset === endSourceSpan.start.offset &&
startSourceSpan.end.offset === endSourceSpan.end.offset);
}
}
class SelfClosingTagsMigration extends project_paths.TsurgeFunnelMigration {
config;
constructor(config = {}) {
super();
this.config = config;
}
async analyze(info) {
const { sourceFiles, program } = info;
const typeChecker = program.getTypeChecker();
const tagReplacements = [];
for (const sf of sourceFiles) {
ts.forEachChild(sf, (node) => {
// Skipping any non component declarations
if (!ts.isClassDeclaration(node)) {
return;
}
const file = project_paths.projectFile(node.getSourceFile(), info);
if (this.config.shouldMigrate && this.config.shouldMigrate(file) === false) {
return;
}
const templateVisitor = new NgComponentTemplateVisitor(typeChecker);
templateVisitor.visitNode(node);
templateVisitor.resolvedTemplates.forEach((template) => {
const { migrated, changed, replacementCount } = migrateTemplateToSelfClosingTags(template.content);
if (!changed) {
return;
}
const fileToMigrate = template.inline
? file
: project_paths.projectFile(template.filePath, info);
const end = template.start + template.content.length;
const replacements = [
prepareTextReplacement(fileToMigrate, migrated, template.start, end),
];
const fileReplacements = tagReplacements.find((tagReplacement) => tagReplacement.file === file);
if (fileReplacements) {
fileReplacements.replacements.push(...replacements);
fileReplacements.replacementCount += replacementCount;
}
else {
tagReplacements.push({ file, replacements, replacementCount });
}
});
});
}
return project_paths.confirmAsSerializable({ tagReplacements });
}
async combine(unitA, unitB) {
return project_paths.confirmAsSerializable({
tagReplacements: [...unitA.tagReplacements, ...unitB.tagReplacements],
});
}
async globalMeta(combinedData) {
const globalMeta = {
tagReplacements: combinedData.tagReplacements,
};
return project_paths.confirmAsSerializable(globalMeta);
}
async stats(globalMetadata) {
const touchedFilesCount = globalMetadata.tagReplacements.length;
const replacementCount = globalMetadata.tagReplacements.reduce((acc, cur) => acc + cur.replacementCount, 0);
return {
counters: {
touchedFilesCount,
replacementCount,
},
};
}
async migrate(globalData) {
return { replacements: globalData.tagReplacements.flatMap(({ replacements }) => replacements) };
}
}
function prepareTextReplacement(file, replacement, start, end) {
return new project_paths.Replacement(file, new project_paths.TextUpdate({
position: start,
end: end,
toInsert: replacement,
}));
}
function migrate(options) {
return async (tree, context) => {
await project_paths.runMigrationInDevkit({
tree,
getMigration: (fs) => new SelfClosingTagsMigration({
shouldMigrate: (file) => {
return (file.rootRelativePath.startsWith(fs.normalize(options.path)) &&
!/(^|\/)node_modules\//.test(file.rootRelativePath));
},
}),
beforeProgramCreation: (tsconfigPath, stage) => {
if (stage === project_paths.MigrationStage.Analysis) {
context.logger.info(`Preparing analysis for: ${tsconfigPath}...`);
}
else {
context.logger.info(`Running migration for: ${tsconfigPath}...`);
}
},
beforeUnitAnalysis: (tsconfigPath) => {
context.logger.info(`Scanning for component tags: ${tsconfigPath}...`);
},
afterAllAnalyzed: () => {
context.logger.info(``);
context.logger.info(`Processing analysis data between targets...`);
context.logger.info(``);
},
afterAnalysisFailure: () => {
context.logger.error('Migration failed unexpectedly with no analysis data');
},
whenDone: ({ counters }) => {
const { touchedFilesCount, replacementCount } = counters;
context.logger.info('');
context.logger.info(`Successfully migrated to self-closing tags 🎉`);
context.logger.info(` -> Migrated ${replacementCount} components to self-closing tags in ${touchedFilesCount} component files.`);
},
});
};
}
exports.migrate = migrate;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

54
node_modules/@angular/core/schematics/bundles/signals.cjs generated vendored Executable file
View File

@@ -0,0 +1,54 @@
'use strict';
/**
* @license Angular v19.2.18
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var schematics = require('@angular-devkit/schematics');
var signalQueriesMigration = require('./signal-queries-migration.cjs');
var signalInputMigration = require('./signal-input-migration.cjs');
var outputMigration = require('./output-migration.cjs');
require('./checker-B7WeiY-n.cjs');
require('typescript');
require('os');
require('fs');
require('module');
require('path');
require('url');
require('./index-DeAjKhiO.cjs');
require('./project_paths-DIZUxMOI.cjs');
require('@angular-devkit/core');
require('node:path/posix');
require('./project_tsconfig_paths-CDVxT6Ov.cjs');
require('./apply_import_manager-BDckZ-aO.cjs');
require('./migrate_ts_type_references-L5PQQLA-.cjs');
require('assert');
require('./index-DyN3x_2s.cjs');
require('./leading_space-D9nQ8UQC.cjs');
function migrate(options) {
// The migrations are independent so we can run them in any order, but we sort them here
// alphabetically so we get a consistent execution order in case of issue reports.
const migrations = options.migrations.slice().sort();
const rules = [];
for (const migration of migrations) {
switch (migration) {
case "inputs" /* SupportedMigrations.inputs */:
rules.push(signalInputMigration.migrate(options));
break;
case "outputs" /* SupportedMigrations.outputs */:
rules.push(outputMigration.migrate(options));
break;
case "queries" /* SupportedMigrations.queries */:
rules.push(signalQueriesMigration.migrate(options));
break;
default:
throw new schematics.SchematicsException(`Unsupported migration "${migration}"`);
}
}
return schematics.chain(rules);
}
exports.migrate = migrate;

File diff suppressed because it is too large Load Diff

62
node_modules/@angular/core/schematics/collection.json generated vendored Executable file
View File

@@ -0,0 +1,62 @@
{
"schematics": {
"standalone-migration": {
"description": "Converts the entire application or a part of it to standalone",
"factory": "./bundles/standalone-migration.cjs#migrate",
"schema": "./ng-generate/standalone-migration/schema.json",
"aliases": ["standalone"]
},
"control-flow-migration": {
"description": "Converts the entire application to block control flow syntax",
"factory": "./bundles/control-flow-migration.cjs#migrate",
"schema": "./ng-generate/control-flow-migration/schema.json",
"aliases": ["control-flow"]
},
"inject-migration": {
"description": "Converts usages of constructor-based injection to the inject() function",
"factory": "./bundles/inject-migration.cjs#migrate",
"schema": "./ng-generate/inject-migration/schema.json",
"aliases": ["inject"]
},
"route-lazy-loading-migration": {
"description": "Updates route definitions to use lazy-loading of components instead of eagerly referencing them",
"factory": "./bundles/route-lazy-loading.cjs#migrate",
"schema": "./ng-generate/route-lazy-loading/schema.json",
"aliases": ["route-lazy-loading"]
},
"signal-input-migration": {
"description": "Updates `@Input` declarations to signal inputs, while also migrating all relevant references.",
"factory": "./bundles/signal-input-migration.cjs#migrate",
"schema": "./ng-generate/signal-input-migration/schema.json",
"aliases": ["signal-inputs", "signal-input"]
},
"signal-queries-migration": {
"description": "Updates query declarations to signal queries, while also migrating all relevant references.",
"factory": "./bundles/signal-queries-migration.cjs#migrate",
"schema": "./ng-generate/signal-queries-migration/schema.json",
"aliases": ["signal-queries", "signal-query", "signal-query-migration"]
},
"output-migration": {
"description": "Updates @output declarations to the functional equivalent, while also migrating all relevant references.",
"factory": "./bundles/output-migration.cjs#migrate",
"schema": "./ng-generate/output-migration/schema.json",
"aliases": ["outputs"]
},
"signals": {
"description": "Combines all signals-related migrations into a single migration",
"factory": "./bundles/signals.cjs#migrate",
"schema": "./ng-generate/signals/schema.json"
},
"cleanup-unused-imports": {
"description": "Removes unused imports from standalone components.",
"factory": "./bundles/cleanup-unused-imports.cjs#migrate",
"schema": "./ng-generate/cleanup-unused-imports/schema.json"
},
"self-closing-tags-migration": {
"description": "Updates the components templates to use self-closing tags where possible",
"factory": "./bundles/self-closing-tags-migration.cjs#migrate",
"schema": "./ng-generate/self-closing-tags-migration/schema.json",
"aliases": ["self-closing-tag"]
}
}
}

25
node_modules/@angular/core/schematics/migrations.json generated vendored Executable file
View File

@@ -0,0 +1,25 @@
{
"schematics": {
"explicit-standalone-flag": {
"version": "19.0.0",
"description": "Updates non-standalone Directives, Component and Pipes to 'standalone:false' and removes 'standalone:true' from those who are standalone",
"factory": "./bundles/explicit-standalone-flag.cjs#migrate"
},
"pending-tasks": {
"version": "19.0.0",
"description": "Updates ExperimentalPendingTasks to PendingTasks",
"factory": "./bundles/pending-tasks.cjs#migrate"
},
"provide-initializer": {
"version": "19.0.0",
"description": "Replaces `APP_INITIALIZER`, `ENVIRONMENT_INITIALIZER` & `PLATFORM_INITIALIZER` respectively with `provideAppInitializer`, `provideEnvironmentInitializer` & `providePlatformInitializer`.",
"factory": "./bundles/provide-initializer.cjs#migrate",
"optional": true
},
"add-bootstrap-context-to-server-main": {
"version": "19.2.15",
"description": "Adds `BootstrapContext` to `bootstrapApplication` calls in `main.server.ts` to support server rendering.",
"factory": "./bundles/add-bootstrap-context-to-server-main.cjs#migrate"
}
}
}

View File

@@ -0,0 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularCleanupUnusedImportsMigration",
"title": "Angular Cleanup Unused Imports Schema",
"type": "object",
"properties": {}
}

View File

@@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularControlFlowMigration",
"title": "Angular Control Flow Migration Schema",
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path relative to the project root which should be migrated",
"x-prompt": "Which path in your project should be migrated?",
"default": "./"
},
"format": {
"type": "boolean",
"description": "Enables reformatting of your templates",
"x-prompt": "Should the migration reformat your templates?",
"default": "true"
}
}
}

View File

@@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularInjectMigration",
"title": "Angular Inject Migration Schema",
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path relative to the project root which should be migrated",
"x-prompt": "Which path in your project should be migrated?",
"default": "./"
},
"migrateAbstractClasses": {
"type": "boolean",
"description": "Whether abstract classes should be migrated",
"x-prompt": "Do you want to migrate abstract classes? Abstract classes are not migrated by default, because their parameters aren't guaranteed to be injectable",
"default": false
},
"backwardsCompatibleConstructors": {
"type": "boolean",
"description": "Whether to clean up constructors or keep their signatures backwards compatible",
"x-prompt": "Do you want to clean up all constructors or keep them backwards compatible? Enabling this option will include an additional signature of `constructor(...args: unknown[]);` that will avoid errors for sub-classes, but will increase the amount of generated code by the migration",
"default": false
},
"nonNullableOptional": {
"type": "boolean",
"description": "Whether to cast the optional inject sites to be non-nullable",
"x-prompt": "Do you want optional inject calls to be non-nullable? Enable this option if you want the return type to be identical to @Optional(), at the expense of worse type safety",
"default": false
}
}
}

View File

@@ -0,0 +1,19 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularOutputMigration",
"title": "Angular Output migration",
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the directory where all outputs should be migrated.",
"x-prompt": "Which directory do you want to migrate?",
"default": "./"
},
"analysisDir": {
"type": "string",
"description": "Path to the directory that should be analyzed. References to migrated outputs are migrated based on this folder. Useful for larger projects if the analysis takes too long and the analysis scope can be narrowed.",
"default": "./"
}
}
}

View File

@@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularStandaloneRoutesMigration",
"title": "Angular Standalone Routes Migration Schema",
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path relative to the project root which should be migrated",
"x-prompt": "Which path in your project should be migrated?",
"default": "./"
}
}
}

View File

@@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularSelfClosingTagMigration",
"title": "Angular Self Closing Tag Migration Schema",
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the directory where all templates should be migrated.",
"x-prompt": "Which directory do you want to migrate?",
"default": "./"
}
}
}

View File

@@ -0,0 +1,30 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularSignalInputMigration",
"title": "Angular Signal Input migration",
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the directory where all inputs should be migrated.",
"x-prompt": "Which directory do you want to migrate?",
"default": "./"
},
"analysisDir": {
"type": "string",
"description": "Path to the directory that should be analyzed. References to migrated inputs are migrated based on this folder. Useful for larger projects if the analysis takes too long and the analysis scope can be narrowed.",
"default": "./"
},
"bestEffortMode": {
"type": "boolean",
"description": "Whether to eagerly migrate as much as possible, ignoring problematic patterns that would otherwise prevent migration.",
"x-prompt": "Do you want to migrate as much as possible, even if it may break your build?",
"default": false
},
"insertTodos": {
"type": "boolean",
"description": "Whether the migration should add TODOs for inputs that could not be migrated",
"default": false
}
}
}

View File

@@ -0,0 +1,30 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularSignalQueriesMigration",
"title": "Angular Signal Queries migration",
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the directory where all queries should be migrated.",
"x-prompt": "Which directory do you want to migrate?",
"default": "./"
},
"analysisDir": {
"type": "string",
"description": "Path to the directory that should be analyzed. References to migrated queries are migrated based on this folder. Useful for larger projects if the analysis takes too long and the analysis scope can be narrowed.",
"default": "./"
},
"bestEffortMode": {
"type": "boolean",
"description": "Whether to eagerly migrate as much as possible, ignoring problematic patterns that would otherwise prevent migration.",
"x-prompt": "Do you want to migrate as much as possible, even if it may break your build?",
"default": false
},
"insertTodos": {
"type": "boolean",
"description": "Whether the migration should add TODOs for queries that could not be migrated",
"default": false
}
}
}

View File

@@ -0,0 +1,66 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularSignalMigration",
"title": "Angular Signals migration",
"type": "object",
"properties": {
"migrations": {
"type": "array",
"default": [
"inputs",
"outputs",
"queries"
],
"items": {
"type": "string",
"enum": [
"inputs",
"outputs",
"queries"
]
},
"description": "Signals-related migrations that should be run",
"x-prompt": {
"message": "Which migrations do you want to run?",
"type": "list",
"multiselect": true,
"items": [
{
"value": "inputs",
"label": "Convert `@Input` to the signal-based `input`"
},
{
"value": "outputs",
"label": "Convert `@Output` to the new `output` function"
},
{
"value": "queries",
"label": "Convert `@ViewChild`/`@ViewChildren` and `@ContentChild`/`@ContentChildren` to the signal-based `viewChild`/`viewChildren` and `contentChild`/`contentChildren`"
}
]
}
},
"path": {
"type": "string",
"description": "Path to the directory that should be migrated.",
"x-prompt": "Which directory do you want to migrate?",
"default": "./"
},
"analysisDir": {
"type": "string",
"description": "Path to the directory that should be analyzed. Useful for larger projects if the analysis takes too long and the analysis scope can be narrowed.",
"default": "./"
},
"bestEffortMode": {
"type": "boolean",
"description": "Whether to eagerly migrate as much as possible, ignoring problematic patterns that would otherwise prevent migration.",
"x-prompt": "Do you want to migrate as much as possible, even if it may break your build?",
"default": false
},
"insertTodos": {
"type": "boolean",
"description": "Whether the migration should add TODOs for code that could not be migrated",
"default": false
}
}
}

View File

@@ -0,0 +1,38 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "AngularStandaloneMigration",
"title": "Angular Standalone Migration Schema",
"type": "object",
"properties": {
"mode": {
"description": "Operation that should be performed by the migrator",
"type": "string",
"enum": ["convert-to-standalone", "prune-ng-modules", "standalone-bootstrap"],
"default": "convert-to-standalone",
"x-prompt": {
"message": "Choose the type of migration:",
"type": "list",
"items": [
{
"value": "convert-to-standalone",
"label": "Convert all components, directives and pipes to standalone"
},
{
"value": "prune-ng-modules",
"label": "Remove unnecessary NgModule classes"
},
{
"value": "standalone-bootstrap",
"label": "Bootstrap the application using standalone APIs"
}
]
}
},
"path": {
"type": "string",
"description": "Path relative to the project root which should be migrated",
"x-prompt": "Which path in your project should be migrated?",
"default": "./"
}
}
}