1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086 |
- 'use strict';
- /**
- * @license Angular v19.2.13
- * (c) 2010-2025 Google LLC. https://angular.io/
- * License: MIT
- */
- 'use strict';
- var schematics = require('@angular-devkit/schematics');
- var index = require('./index-BIvVb6in.cjs');
- var fs = require('fs');
- var p = require('path');
- var ts = require('typescript');
- var compiler_host = require('./compiler_host-B1Gyeytz.cjs');
- var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.cjs');
- var ng_decorators = require('./ng_decorators-B5HCqr20.cjs');
- var nodes = require('./nodes-B16H9JUd.cjs');
- var imports = require('./imports-CIX-JgAN.cjs');
- var checker = require('./checker-5pyJrZ9G.cjs');
- require('os');
- require('@angular-devkit/core');
- require('module');
- require('url');
- function createProgram({ rootNames, options, host, oldProgram, }) {
- return new index.NgtscProgram(rootNames, options, host, oldProgram);
- }
- /** Checks whether a node is referring to a specific import specifier. */
- function isReferenceToImport(typeChecker, node, importSpecifier) {
- // If this function is called on an identifier (should be most cases), we can quickly rule out
- // non-matches by comparing the identifier's string and the local name of the import specifier
- // which saves us some calls to the type checker.
- if (ts.isIdentifier(node) && node.text !== importSpecifier.name.text) {
- return false;
- }
- const nodeSymbol = typeChecker.getTypeAtLocation(node).getSymbol();
- const importSymbol = typeChecker.getTypeAtLocation(importSpecifier).getSymbol();
- return (!!(nodeSymbol?.declarations?.[0] && importSymbol?.declarations?.[0]) &&
- nodeSymbol.declarations[0] === importSymbol.declarations[0]);
- }
- /*!
- * @license
- * Copyright Google LLC All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.dev/license
- */
- /** Utility class used to track a one-to-many relationship where all the items are unique. */
- class UniqueItemTracker {
- _nodes = new Map();
- track(key, item) {
- const set = this._nodes.get(key);
- if (set) {
- set.add(item);
- }
- else {
- this._nodes.set(key, new Set([item]));
- }
- }
- get(key) {
- return this._nodes.get(key);
- }
- getEntries() {
- return this._nodes.entries();
- }
- isEmpty() {
- return this._nodes.size === 0;
- }
- }
- /** Resolves references to nodes. */
- class ReferenceResolver {
- _program;
- _host;
- _rootFileNames;
- _basePath;
- _excludedFiles;
- _languageService;
- /**
- * If set, allows the language service to *only* read a specific file.
- * Used to speed up single-file lookups.
- */
- _tempOnlyFile = null;
- constructor(_program, _host, _rootFileNames, _basePath, _excludedFiles) {
- this._program = _program;
- this._host = _host;
- this._rootFileNames = _rootFileNames;
- this._basePath = _basePath;
- this._excludedFiles = _excludedFiles;
- }
- /** Finds all references to a node within the entire project. */
- findReferencesInProject(node) {
- const languageService = this._getLanguageService();
- const fileName = node.getSourceFile().fileName;
- const start = node.getStart();
- let referencedSymbols;
- // The language service can throw if it fails to read a file.
- // Silently continue since we're making the lookup on a best effort basis.
- try {
- referencedSymbols = languageService.findReferences(fileName, start) || [];
- }
- catch (e) {
- console.error('Failed reference lookup for node ' + node.getText(), e.message);
- referencedSymbols = [];
- }
- const results = new Map();
- for (const symbol of referencedSymbols) {
- for (const ref of symbol.references) {
- if (!ref.isDefinition || symbol.definition.kind === ts.ScriptElementKind.alias) {
- if (!results.has(ref.fileName)) {
- results.set(ref.fileName, []);
- }
- results
- .get(ref.fileName)
- .push([ref.textSpan.start, ref.textSpan.start + ref.textSpan.length]);
- }
- }
- }
- return results;
- }
- /** Finds all references to a node within a single file. */
- findSameFileReferences(node, fileName) {
- // Even though we're only passing in a single file into `getDocumentHighlights`, the language
- // service ends up traversing the entire project. Prevent it from reading any files aside from
- // the one we're interested in by intercepting it at the compiler host level.
- // This is an order of magnitude faster on a large project.
- this._tempOnlyFile = fileName;
- const nodeStart = node.getStart();
- const results = [];
- let highlights;
- // The language service can throw if it fails to read a file.
- // Silently continue since we're making the lookup on a best effort basis.
- try {
- highlights = this._getLanguageService().getDocumentHighlights(fileName, nodeStart, [
- fileName,
- ]);
- }
- catch (e) {
- console.error('Failed reference lookup for node ' + node.getText(), e.message);
- }
- if (highlights) {
- for (const file of highlights) {
- // We are pretty much guaranteed to only have one match from the current file since it is
- // the only one being passed in `getDocumentHighlight`, but we check here just in case.
- if (file.fileName === fileName) {
- for (const { textSpan: { start, length }, kind, } of file.highlightSpans) {
- if (kind !== ts.HighlightSpanKind.none) {
- results.push([start, start + length]);
- }
- }
- }
- }
- }
- // Restore full project access to the language service.
- this._tempOnlyFile = null;
- return results;
- }
- /** Used by the language service */
- _readFile(path) {
- if ((this._tempOnlyFile !== null && path !== this._tempOnlyFile) ||
- this._excludedFiles?.test(path)) {
- return '';
- }
- return this._host.readFile(path);
- }
- /** Gets a language service that can be used to perform lookups. */
- _getLanguageService() {
- if (!this._languageService) {
- const rootFileNames = this._rootFileNames.slice();
- this._program
- .getTsProgram()
- .getSourceFiles()
- .forEach(({ fileName }) => {
- if (!this._excludedFiles?.test(fileName) && !rootFileNames.includes(fileName)) {
- rootFileNames.push(fileName);
- }
- });
- this._languageService = ts.createLanguageService({
- getCompilationSettings: () => this._program.getTsProgram().getCompilerOptions(),
- getScriptFileNames: () => rootFileNames,
- // The files won't change so we can return the same version.
- getScriptVersion: () => '0',
- getScriptSnapshot: (path) => {
- const content = this._readFile(path);
- return content ? ts.ScriptSnapshot.fromString(content) : undefined;
- },
- getCurrentDirectory: () => this._basePath,
- getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
- readFile: (path) => this._readFile(path),
- fileExists: (path) => this._host.fileExists(path),
- }, ts.createDocumentRegistry(), ts.LanguageServiceMode.PartialSemantic);
- }
- return this._languageService;
- }
- }
- /** Creates a NodeLookup object from a source file. */
- function getNodeLookup(sourceFile) {
- const lookup = new Map();
- sourceFile.forEachChild(function walk(node) {
- const nodesAtStart = lookup.get(node.getStart());
- if (nodesAtStart) {
- nodesAtStart.push(node);
- }
- else {
- lookup.set(node.getStart(), [node]);
- }
- node.forEachChild(walk);
- });
- return lookup;
- }
- /**
- * Converts node offsets to the nodes they correspond to.
- * @param lookup Data structure used to look up nodes at particular positions.
- * @param offsets Offsets of the nodes.
- * @param results Set in which to store the results.
- */
- function offsetsToNodes(lookup, offsets, results) {
- for (const [start, end] of offsets) {
- const match = lookup.get(start)?.find((node) => node.getEnd() === end);
- if (match) {
- results.add(match);
- }
- }
- return results;
- }
- /**
- * Finds the class declaration that is being referred to by a node.
- * @param reference Node referring to a class declaration.
- * @param typeChecker
- */
- function findClassDeclaration(reference, typeChecker) {
- return (typeChecker
- .getTypeAtLocation(reference)
- .getSymbol()
- ?.declarations?.find(ts.isClassDeclaration) || null);
- }
- /** Finds a property with a specific name in an object literal expression. */
- function findLiteralProperty(literal, name) {
- return literal.properties.find((prop) => prop.name && ts.isIdentifier(prop.name) && prop.name.text === name);
- }
- /** Gets a relative path between two files that can be used inside a TypeScript import. */
- function getRelativeImportPath(fromFile, toFile) {
- let path = p.relative(p.dirname(fromFile), toFile).replace(/\.ts$/, '');
- // `relative` returns paths inside the same directory without `./`
- if (!path.startsWith('.')) {
- path = './' + path;
- }
- // Using the Node utilities can yield paths with forward slashes on Windows.
- return compiler_host.normalizePath(path);
- }
- /** Function used to remap the generated `imports` for a component to known shorter aliases. */
- function knownInternalAliasRemapper(imports) {
- return imports.map((current) => current.moduleSpecifier === '@angular/common' && current.symbolName === 'NgForOf'
- ? { ...current, symbolName: 'NgFor' }
- : current);
- }
- /**
- * Gets the closest node that matches a predicate, including the node that the search started from.
- * @param node Node from which to start the search.
- * @param predicate Predicate that the result needs to pass.
- */
- function closestOrSelf(node, predicate) {
- return predicate(node) ? node : nodes.closestNode(node, predicate);
- }
- /**
- * Checks whether a node is referring to a specific class declaration.
- * @param node Node that is being checked.
- * @param className Name of the class that the node might be referring to.
- * @param moduleName Name of the Angular module that should contain the class.
- * @param typeChecker
- */
- function isClassReferenceInAngularModule(node, className, moduleName, typeChecker) {
- const symbol = typeChecker.getTypeAtLocation(node).getSymbol();
- const externalName = `@angular/${moduleName}`;
- const internalName = `angular2/rc/packages/${moduleName}`;
- return !!symbol?.declarations?.some((decl) => {
- const closestClass = closestOrSelf(decl, ts.isClassDeclaration);
- const closestClassFileName = closestClass?.getSourceFile().fileName;
- if (!closestClass ||
- !closestClassFileName ||
- !closestClass.name ||
- !ts.isIdentifier(closestClass.name) ||
- (!closestClassFileName.includes(externalName) && !closestClassFileName.includes(internalName))) {
- return false;
- }
- return typeof className === 'string'
- ? closestClass.name.text === className
- : className.test(closestClass.name.text);
- });
- }
- /**
- * Finds the imports of testing libraries in a file.
- */
- function getTestingImports(sourceFile) {
- return {
- testBed: imports.getImportSpecifier(sourceFile, '@angular/core/testing', 'TestBed'),
- catalyst: imports.getImportSpecifier(sourceFile, /testing\/catalyst(\/(fake_)?async)?$/, 'setupModule'),
- };
- }
- /**
- * Determines if a node is a call to a testing API.
- * @param typeChecker Type checker to use when resolving references.
- * @param node Node to check.
- * @param testBedImport Import of TestBed within the file.
- * @param catalystImport Import of Catalyst within the file.
- */
- function isTestCall(typeChecker, node, testBedImport, catalystImport) {
- const isObjectLiteralCall = ts.isCallExpression(node) &&
- node.arguments.length > 0 &&
- // `arguments[0]` is the testing module config.
- ts.isObjectLiteralExpression(node.arguments[0]);
- const isTestBedCall = isObjectLiteralCall &&
- testBedImport &&
- ts.isPropertyAccessExpression(node.expression) &&
- node.expression.name.text === 'configureTestingModule' &&
- isReferenceToImport(typeChecker, node.expression.expression, testBedImport);
- const isCatalystCall = isObjectLiteralCall &&
- catalystImport &&
- ts.isIdentifier(node.expression) &&
- isReferenceToImport(typeChecker, node.expression, catalystImport);
- return !!(isTestBedCall || isCatalystCall);
- }
- /*!
- * @license
- * Copyright Google LLC All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.dev/license
- */
- /**
- * Converts all declarations in the specified files to standalone.
- * @param sourceFiles Files that should be migrated.
- * @param program
- * @param printer
- * @param fileImportRemapper Optional function that can be used to remap file-level imports.
- * @param declarationImportRemapper Optional function that can be used to remap declaration-level
- * imports.
- */
- function toStandalone(sourceFiles, program, printer, fileImportRemapper, declarationImportRemapper) {
- const templateTypeChecker = program.compiler.getTemplateTypeChecker();
- const typeChecker = program.getTsProgram().getTypeChecker();
- const modulesToMigrate = new Set();
- const testObjectsToMigrate = new Set();
- const declarations = new Set();
- const tracker = new compiler_host.ChangeTracker(printer, fileImportRemapper);
- for (const sourceFile of sourceFiles) {
- const modules = findNgModuleClassesToMigrate(sourceFile, typeChecker);
- const testObjects = findTestObjectsToMigrate(sourceFile, typeChecker);
- for (const module of modules) {
- const allModuleDeclarations = extractDeclarationsFromModule(module, templateTypeChecker);
- const unbootstrappedDeclarations = filterNonBootstrappedDeclarations(allModuleDeclarations, module, templateTypeChecker, typeChecker);
- if (unbootstrappedDeclarations.length > 0) {
- modulesToMigrate.add(module);
- unbootstrappedDeclarations.forEach((decl) => declarations.add(decl));
- }
- }
- testObjects.forEach((obj) => testObjectsToMigrate.add(obj));
- }
- for (const declaration of declarations) {
- convertNgModuleDeclarationToStandalone(declaration, declarations, tracker, templateTypeChecker, declarationImportRemapper);
- }
- for (const node of modulesToMigrate) {
- migrateNgModuleClass(node, declarations, tracker, typeChecker, templateTypeChecker);
- }
- migrateTestDeclarations(testObjectsToMigrate, declarations, tracker, templateTypeChecker, typeChecker);
- return tracker.recordChanges();
- }
- /**
- * Converts a single declaration defined through an NgModule to standalone.
- * @param decl Declaration being converted.
- * @param tracker Tracker used to track the file changes.
- * @param allDeclarations All the declarations that are being converted as a part of this migration.
- * @param typeChecker
- * @param importRemapper
- */
- function convertNgModuleDeclarationToStandalone(decl, allDeclarations, tracker, typeChecker, importRemapper) {
- const directiveMeta = typeChecker.getDirectiveMetadata(decl);
- if (directiveMeta && directiveMeta.decorator && !directiveMeta.isStandalone) {
- let decorator = markDecoratorAsStandalone(directiveMeta.decorator);
- if (directiveMeta.isComponent) {
- const importsToAdd = getComponentImportExpressions(decl, allDeclarations, tracker, typeChecker, importRemapper);
- if (importsToAdd.length > 0) {
- const hasTrailingComma = importsToAdd.length > 2 &&
- !!extractMetadataLiteral(directiveMeta.decorator)?.properties.hasTrailingComma;
- decorator = setPropertyOnAngularDecorator(decorator, 'imports', ts.factory.createArrayLiteralExpression(
- // Create a multi-line array when it has a trailing comma.
- ts.factory.createNodeArray(importsToAdd, hasTrailingComma), hasTrailingComma));
- }
- }
- tracker.replaceNode(directiveMeta.decorator, decorator);
- }
- else {
- const pipeMeta = typeChecker.getPipeMetadata(decl);
- if (pipeMeta && pipeMeta.decorator && !pipeMeta.isStandalone) {
- tracker.replaceNode(pipeMeta.decorator, markDecoratorAsStandalone(pipeMeta.decorator));
- }
- }
- }
- /**
- * Gets the expressions that should be added to a component's
- * `imports` array based on its template dependencies.
- * @param decl Component class declaration.
- * @param allDeclarations All the declarations that are being converted as a part of this migration.
- * @param tracker
- * @param typeChecker
- * @param importRemapper
- */
- function getComponentImportExpressions(decl, allDeclarations, tracker, typeChecker, importRemapper) {
- const templateDependencies = findTemplateDependencies(decl, typeChecker);
- const usedDependenciesInMigration = new Set(templateDependencies.filter((dep) => allDeclarations.has(dep.node)));
- const seenImports = new Set();
- const resolvedDependencies = [];
- for (const dep of templateDependencies) {
- const importLocation = findImportLocation(dep, decl, usedDependenciesInMigration.has(dep)
- ? checker.PotentialImportMode.ForceDirect
- : checker.PotentialImportMode.Normal, typeChecker);
- if (importLocation && !seenImports.has(importLocation.symbolName)) {
- seenImports.add(importLocation.symbolName);
- resolvedDependencies.push(importLocation);
- }
- }
- return potentialImportsToExpressions(resolvedDependencies, decl.getSourceFile(), tracker, importRemapper);
- }
- /**
- * Converts an array of potential imports to an array of expressions that can be
- * added to the `imports` array.
- * @param potentialImports Imports to be converted.
- * @param component Component class to which the imports will be added.
- * @param tracker
- * @param importRemapper
- */
- function potentialImportsToExpressions(potentialImports, toFile, tracker, importRemapper) {
- const processedDependencies = importRemapper
- ? importRemapper(potentialImports)
- : potentialImports;
- return processedDependencies.map((importLocation) => {
- if (importLocation.moduleSpecifier) {
- return tracker.addImport(toFile, importLocation.symbolName, importLocation.moduleSpecifier);
- }
- const identifier = ts.factory.createIdentifier(importLocation.symbolName);
- if (!importLocation.isForwardReference) {
- return identifier;
- }
- const forwardRefExpression = tracker.addImport(toFile, 'forwardRef', '@angular/core');
- const arrowFunction = ts.factory.createArrowFunction(undefined, undefined, [], undefined, undefined, identifier);
- return ts.factory.createCallExpression(forwardRefExpression, undefined, [arrowFunction]);
- });
- }
- /**
- * Moves all of the declarations of a class decorated with `@NgModule` to its imports.
- * @param node Class being migrated.
- * @param allDeclarations All the declarations that are being converted as a part of this migration.
- * @param tracker
- * @param typeChecker
- * @param templateTypeChecker
- */
- function migrateNgModuleClass(node, allDeclarations, tracker, typeChecker, templateTypeChecker) {
- const decorator = templateTypeChecker.getNgModuleMetadata(node)?.decorator;
- const metadata = decorator ? extractMetadataLiteral(decorator) : null;
- if (metadata) {
- moveDeclarationsToImports(metadata, allDeclarations, typeChecker, templateTypeChecker, tracker);
- }
- }
- /**
- * Moves all the symbol references from the `declarations` array to the `imports`
- * array of an `NgModule` class and removes the `declarations`.
- * @param literal Object literal used to configure the module that should be migrated.
- * @param allDeclarations All the declarations that are being converted as a part of this migration.
- * @param typeChecker
- * @param tracker
- */
- function moveDeclarationsToImports(literal, allDeclarations, typeChecker, templateTypeChecker, tracker) {
- const declarationsProp = findLiteralProperty(literal, 'declarations');
- if (!declarationsProp) {
- return;
- }
- const declarationsToPreserve = [];
- const declarationsToCopy = [];
- const properties = [];
- const importsProp = findLiteralProperty(literal, 'imports');
- const hasAnyArrayTrailingComma = literal.properties.some((prop) => ts.isPropertyAssignment(prop) &&
- ts.isArrayLiteralExpression(prop.initializer) &&
- prop.initializer.elements.hasTrailingComma);
- // Separate the declarations that we want to keep and ones we need to copy into the `imports`.
- if (ts.isPropertyAssignment(declarationsProp)) {
- // If the declarations are an array, we can analyze it to
- // find any classes from the current migration.
- if (ts.isArrayLiteralExpression(declarationsProp.initializer)) {
- for (const el of declarationsProp.initializer.elements) {
- if (ts.isIdentifier(el)) {
- const correspondingClass = findClassDeclaration(el, typeChecker);
- if (!correspondingClass ||
- // Check whether the declaration is either standalone already or is being converted
- // in this migration. We need to check if it's standalone already, in order to correct
- // some cases where the main app and the test files are being migrated in separate
- // programs.
- isStandaloneDeclaration(correspondingClass, allDeclarations, templateTypeChecker)) {
- declarationsToCopy.push(el);
- }
- else {
- declarationsToPreserve.push(el);
- }
- }
- else {
- declarationsToCopy.push(el);
- }
- }
- }
- else {
- // Otherwise create a spread that will be copied into the `imports`.
- declarationsToCopy.push(ts.factory.createSpreadElement(declarationsProp.initializer));
- }
- }
- // If there are no `imports`, create them with the declarations we want to copy.
- if (!importsProp && declarationsToCopy.length > 0) {
- properties.push(ts.factory.createPropertyAssignment('imports', ts.factory.createArrayLiteralExpression(ts.factory.createNodeArray(declarationsToCopy, hasAnyArrayTrailingComma && declarationsToCopy.length > 2))));
- }
- for (const prop of literal.properties) {
- if (!isNamedPropertyAssignment(prop)) {
- properties.push(prop);
- continue;
- }
- // If we have declarations to preserve, update the existing property, otherwise drop it.
- if (prop === declarationsProp) {
- if (declarationsToPreserve.length > 0) {
- const hasTrailingComma = ts.isArrayLiteralExpression(prop.initializer)
- ? prop.initializer.elements.hasTrailingComma
- : hasAnyArrayTrailingComma;
- properties.push(ts.factory.updatePropertyAssignment(prop, prop.name, ts.factory.createArrayLiteralExpression(ts.factory.createNodeArray(declarationsToPreserve, hasTrailingComma && declarationsToPreserve.length > 2))));
- }
- continue;
- }
- // If we have an `imports` array and declarations
- // that should be copied, we merge the two arrays.
- if (prop === importsProp && declarationsToCopy.length > 0) {
- let initializer;
- if (ts.isArrayLiteralExpression(prop.initializer)) {
- initializer = ts.factory.updateArrayLiteralExpression(prop.initializer, ts.factory.createNodeArray([...prop.initializer.elements, ...declarationsToCopy], prop.initializer.elements.hasTrailingComma));
- }
- else {
- initializer = ts.factory.createArrayLiteralExpression(ts.factory.createNodeArray([ts.factory.createSpreadElement(prop.initializer), ...declarationsToCopy],
- // Expect the declarations to be greater than 1 since
- // we have the pre-existing initializer already.
- hasAnyArrayTrailingComma && declarationsToCopy.length > 1));
- }
- properties.push(ts.factory.updatePropertyAssignment(prop, prop.name, initializer));
- continue;
- }
- // Retain any remaining properties.
- properties.push(prop);
- }
- tracker.replaceNode(literal, ts.factory.updateObjectLiteralExpression(literal, ts.factory.createNodeArray(properties, literal.properties.hasTrailingComma)), ts.EmitHint.Expression);
- }
- /** Sets a decorator node to be standalone. */
- function markDecoratorAsStandalone(node) {
- const metadata = extractMetadataLiteral(node);
- if (metadata === null || !ts.isCallExpression(node.expression)) {
- return node;
- }
- const standaloneProp = metadata.properties.find((prop) => {
- return isNamedPropertyAssignment(prop) && prop.name.text === 'standalone';
- });
- // In v19 standalone is the default so don't do anything if there's no `standalone`
- // property or it's initialized to anything other than `false`.
- if (!standaloneProp || standaloneProp.initializer.kind !== ts.SyntaxKind.FalseKeyword) {
- return node;
- }
- const newProperties = metadata.properties.filter((element) => element !== standaloneProp);
- // Use `createDecorator` instead of `updateDecorator`, because
- // the latter ends up duplicating the node's leading comment.
- return ts.factory.createDecorator(ts.factory.createCallExpression(node.expression.expression, node.expression.typeArguments, [
- ts.factory.createObjectLiteralExpression(ts.factory.createNodeArray(newProperties, metadata.properties.hasTrailingComma), newProperties.length > 1),
- ]));
- }
- /**
- * Sets a property on an Angular decorator node. If the property
- * already exists, its initializer will be replaced.
- * @param node Decorator to which to add the property.
- * @param name Name of the property to be added.
- * @param initializer Initializer for the new property.
- */
- function setPropertyOnAngularDecorator(node, name, initializer) {
- // Invalid decorator.
- if (!ts.isCallExpression(node.expression) || node.expression.arguments.length > 1) {
- return node;
- }
- let literalProperties;
- let hasTrailingComma = false;
- if (node.expression.arguments.length === 0) {
- literalProperties = [ts.factory.createPropertyAssignment(name, initializer)];
- }
- else if (ts.isObjectLiteralExpression(node.expression.arguments[0])) {
- const literal = node.expression.arguments[0];
- const existingProperty = findLiteralProperty(literal, name);
- hasTrailingComma = literal.properties.hasTrailingComma;
- if (existingProperty && ts.isPropertyAssignment(existingProperty)) {
- literalProperties = literal.properties.slice();
- literalProperties[literalProperties.indexOf(existingProperty)] =
- ts.factory.updatePropertyAssignment(existingProperty, existingProperty.name, initializer);
- }
- else {
- literalProperties = [
- ...literal.properties,
- ts.factory.createPropertyAssignment(name, initializer),
- ];
- }
- }
- else {
- // Unsupported case (e.g. `@Component(SOME_CONST)`). Return the original node.
- return node;
- }
- // Use `createDecorator` instead of `updateDecorator`, because
- // the latter ends up duplicating the node's leading comment.
- return ts.factory.createDecorator(ts.factory.createCallExpression(node.expression.expression, node.expression.typeArguments, [
- ts.factory.createObjectLiteralExpression(ts.factory.createNodeArray(literalProperties, hasTrailingComma), literalProperties.length > 1),
- ]));
- }
- /** Checks if a node is a `PropertyAssignment` with a name. */
- function isNamedPropertyAssignment(node) {
- return ts.isPropertyAssignment(node) && node.name && ts.isIdentifier(node.name);
- }
- /**
- * Finds the import from which to bring in a template dependency of a component.
- * @param target Dependency that we're searching for.
- * @param inContext Component in which the dependency is used.
- * @param importMode Mode in which to resolve the import target.
- * @param typeChecker
- */
- function findImportLocation(target, inContext, importMode, typeChecker) {
- const importLocations = typeChecker.getPotentialImportsFor(target, inContext, importMode);
- let firstSameFileImport = null;
- let firstModuleImport = null;
- for (const location of importLocations) {
- // Prefer a standalone import, if we can find one.
- // Otherwise fall back to the first module-based import.
- if (location.kind === checker.PotentialImportKind.Standalone) {
- return location;
- }
- if (!location.moduleSpecifier && !firstSameFileImport) {
- firstSameFileImport = location;
- }
- if (location.kind === checker.PotentialImportKind.NgModule &&
- !firstModuleImport &&
- // ɵ is used for some internal Angular modules that we want to skip over.
- !location.symbolName.startsWith('ɵ')) {
- firstModuleImport = location;
- }
- }
- return firstSameFileImport || firstModuleImport || importLocations[0] || null;
- }
- /**
- * Checks whether a node is an `NgModule` metadata element with at least one element.
- * E.g. `declarations: [Foo]` or `declarations: SOME_VAR` would match this description,
- * but not `declarations: []`.
- */
- function hasNgModuleMetadataElements(node) {
- return (ts.isPropertyAssignment(node) &&
- (!ts.isArrayLiteralExpression(node.initializer) || node.initializer.elements.length > 0));
- }
- /** Finds all modules whose declarations can be migrated. */
- function findNgModuleClassesToMigrate(sourceFile, typeChecker) {
- const modules = [];
- if (imports.getImportSpecifier(sourceFile, '@angular/core', 'NgModule')) {
- sourceFile.forEachChild(function walk(node) {
- if (ts.isClassDeclaration(node)) {
- const decorator = ng_decorators.getAngularDecorators(typeChecker, ts.getDecorators(node) || []).find((current) => current.name === 'NgModule');
- const metadata = decorator ? extractMetadataLiteral(decorator.node) : null;
- if (metadata) {
- const declarations = findLiteralProperty(metadata, 'declarations');
- if (declarations != null && hasNgModuleMetadataElements(declarations)) {
- modules.push(node);
- }
- }
- }
- node.forEachChild(walk);
- });
- }
- return modules;
- }
- /** Finds all testing object literals that need to be migrated. */
- function findTestObjectsToMigrate(sourceFile, typeChecker) {
- const testObjects = [];
- const { testBed, catalyst } = getTestingImports(sourceFile);
- if (testBed || catalyst) {
- sourceFile.forEachChild(function walk(node) {
- if (isTestCall(typeChecker, node, testBed, catalyst)) {
- const config = node.arguments[0];
- const declarations = findLiteralProperty(config, 'declarations');
- if (declarations &&
- ts.isPropertyAssignment(declarations) &&
- ts.isArrayLiteralExpression(declarations.initializer) &&
- declarations.initializer.elements.length > 0) {
- testObjects.push(config);
- }
- }
- node.forEachChild(walk);
- });
- }
- return testObjects;
- }
- /**
- * Finds the classes corresponding to dependencies used in a component's template.
- * @param decl Component in whose template we're looking for dependencies.
- * @param typeChecker
- */
- function findTemplateDependencies(decl, typeChecker) {
- const results = [];
- const usedDirectives = typeChecker.getUsedDirectives(decl);
- const usedPipes = typeChecker.getUsedPipes(decl);
- if (usedDirectives !== null) {
- for (const dir of usedDirectives) {
- if (ts.isClassDeclaration(dir.ref.node)) {
- results.push(dir.ref);
- }
- }
- }
- if (usedPipes !== null) {
- const potentialPipes = typeChecker.getPotentialPipes(decl);
- for (const pipe of potentialPipes) {
- if (ts.isClassDeclaration(pipe.ref.node) &&
- usedPipes.some((current) => pipe.name === current)) {
- results.push(pipe.ref);
- }
- }
- }
- return results;
- }
- /**
- * Removes any declarations that are a part of a module's `bootstrap`
- * array from an array of declarations.
- * @param declarations Anaalyzed declarations of the module.
- * @param ngModule Module whote declarations are being filtered.
- * @param templateTypeChecker
- * @param typeChecker
- */
- function filterNonBootstrappedDeclarations(declarations, ngModule, templateTypeChecker, typeChecker) {
- const metadata = templateTypeChecker.getNgModuleMetadata(ngModule);
- const metaLiteral = metadata && metadata.decorator ? extractMetadataLiteral(metadata.decorator) : null;
- const bootstrapProp = metaLiteral ? findLiteralProperty(metaLiteral, 'bootstrap') : null;
- // If there's no `bootstrap`, we can't filter.
- if (!bootstrapProp) {
- return declarations;
- }
- // If we can't analyze the `bootstrap` property, we can't safely determine which
- // declarations aren't bootstrapped so we assume that all of them are.
- if (!ts.isPropertyAssignment(bootstrapProp) ||
- !ts.isArrayLiteralExpression(bootstrapProp.initializer)) {
- return [];
- }
- const bootstrappedClasses = new Set();
- for (const el of bootstrapProp.initializer.elements) {
- const referencedClass = ts.isIdentifier(el) ? findClassDeclaration(el, typeChecker) : null;
- // If we can resolve an element to a class, we can filter it out,
- // otherwise assume that the array isn't static.
- if (referencedClass) {
- bootstrappedClasses.add(referencedClass);
- }
- else {
- return [];
- }
- }
- return declarations.filter((ref) => !bootstrappedClasses.has(ref));
- }
- /**
- * Extracts all classes that are referenced in a module's `declarations` array.
- * @param ngModule Module whose declarations are being extraced.
- * @param templateTypeChecker
- */
- function extractDeclarationsFromModule(ngModule, templateTypeChecker) {
- const metadata = templateTypeChecker.getNgModuleMetadata(ngModule);
- return metadata
- ? metadata.declarations
- .filter((decl) => ts.isClassDeclaration(decl.node))
- .map((decl) => decl.node)
- : [];
- }
- /**
- * Migrates the `declarations` from a unit test file to standalone.
- * @param testObjects Object literals used to configure the testing modules.
- * @param declarationsOutsideOfTestFiles Non-testing declarations that are part of this migration.
- * @param tracker
- * @param templateTypeChecker
- * @param typeChecker
- */
- function migrateTestDeclarations(testObjects, declarationsOutsideOfTestFiles, tracker, templateTypeChecker, typeChecker) {
- const { decorators, componentImports } = analyzeTestingModules(testObjects, typeChecker);
- const allDeclarations = new Set(declarationsOutsideOfTestFiles);
- for (const decorator of decorators) {
- const closestClass = nodes.closestNode(decorator.node, ts.isClassDeclaration);
- if (decorator.name === 'Pipe' || decorator.name === 'Directive') {
- tracker.replaceNode(decorator.node, markDecoratorAsStandalone(decorator.node));
- if (closestClass) {
- allDeclarations.add(closestClass);
- }
- }
- else if (decorator.name === 'Component') {
- const newDecorator = markDecoratorAsStandalone(decorator.node);
- const importsToAdd = componentImports.get(decorator.node);
- if (closestClass) {
- allDeclarations.add(closestClass);
- }
- if (importsToAdd && importsToAdd.size > 0) {
- const hasTrailingComma = importsToAdd.size > 2 &&
- !!extractMetadataLiteral(decorator.node)?.properties.hasTrailingComma;
- const importsArray = ts.factory.createNodeArray(Array.from(importsToAdd), hasTrailingComma);
- tracker.replaceNode(decorator.node, setPropertyOnAngularDecorator(newDecorator, 'imports', ts.factory.createArrayLiteralExpression(importsArray)));
- }
- else {
- tracker.replaceNode(decorator.node, newDecorator);
- }
- }
- }
- for (const obj of testObjects) {
- moveDeclarationsToImports(obj, allDeclarations, typeChecker, templateTypeChecker, tracker);
- }
- }
- /**
- * Analyzes a set of objects used to configure testing modules and returns the AST
- * nodes that need to be migrated and the imports that should be added to the imports
- * of any declared components.
- * @param testObjects Object literals that should be analyzed.
- */
- function analyzeTestingModules(testObjects, typeChecker) {
- const seenDeclarations = new Set();
- const decorators = [];
- const componentImports = new Map();
- for (const obj of testObjects) {
- const declarations = extractDeclarationsFromTestObject(obj, typeChecker);
- if (declarations.length === 0) {
- continue;
- }
- const importsProp = findLiteralProperty(obj, 'imports');
- const importElements = importsProp &&
- hasNgModuleMetadataElements(importsProp) &&
- ts.isArrayLiteralExpression(importsProp.initializer)
- ? importsProp.initializer.elements.filter((el) => {
- // Filter out calls since they may be a `ModuleWithProviders`.
- return (!ts.isCallExpression(el) &&
- // Also filter out the animations modules since they throw errors if they're imported
- // multiple times and it's common for apps to use the `NoopAnimationsModule` to
- // disable animations in screenshot tests.
- !isClassReferenceInAngularModule(el, /^BrowserAnimationsModule|NoopAnimationsModule$/, 'platform-browser/animations', typeChecker));
- })
- : null;
- for (const decl of declarations) {
- if (seenDeclarations.has(decl)) {
- continue;
- }
- const [decorator] = ng_decorators.getAngularDecorators(typeChecker, ts.getDecorators(decl) || []);
- if (decorator) {
- seenDeclarations.add(decl);
- decorators.push(decorator);
- if (decorator.name === 'Component' && importElements) {
- // We try to de-duplicate the imports being added to a component, because it may be
- // declared in different testing modules with a different set of imports.
- let imports = componentImports.get(decorator.node);
- if (!imports) {
- imports = new Set();
- componentImports.set(decorator.node, imports);
- }
- importElements.forEach((imp) => imports.add(imp));
- }
- }
- }
- }
- return { decorators, componentImports };
- }
- /**
- * Finds the class declarations that are being referred
- * to in the `declarations` of an object literal.
- * @param obj Object literal that may contain the declarations.
- * @param typeChecker
- */
- function extractDeclarationsFromTestObject(obj, typeChecker) {
- const results = [];
- const declarations = findLiteralProperty(obj, 'declarations');
- if (declarations &&
- hasNgModuleMetadataElements(declarations) &&
- ts.isArrayLiteralExpression(declarations.initializer)) {
- for (const element of declarations.initializer.elements) {
- const declaration = findClassDeclaration(element, typeChecker);
- // Note that we only migrate classes that are in the same file as the testing module,
- // because external fixture components are somewhat rare and handling them is going
- // to involve a lot of assumptions that are likely to be incorrect.
- if (declaration && declaration.getSourceFile().fileName === obj.getSourceFile().fileName) {
- results.push(declaration);
- }
- }
- }
- return results;
- }
- /** Extracts the metadata object literal from an Angular decorator. */
- function extractMetadataLiteral(decorator) {
- // `arguments[0]` is the metadata object literal.
- return ts.isCallExpression(decorator.expression) &&
- decorator.expression.arguments.length === 1 &&
- ts.isObjectLiteralExpression(decorator.expression.arguments[0])
- ? decorator.expression.arguments[0]
- : null;
- }
- /**
- * Checks whether a class is a standalone declaration.
- * @param node Class being checked.
- * @param declarationsInMigration Classes that are being converted to standalone in this migration.
- * @param templateTypeChecker
- */
- function isStandaloneDeclaration(node, declarationsInMigration, templateTypeChecker) {
- if (declarationsInMigration.has(node)) {
- return true;
- }
- const metadata = templateTypeChecker.getDirectiveMetadata(node) || templateTypeChecker.getPipeMetadata(node);
- return metadata != null && metadata.isStandalone;
- }
- /*!
- * @license
- * Copyright Google LLC All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.dev/license
- */
- function pruneNgModules(program, host, basePath, rootFileNames, sourceFiles, printer, importRemapper, referenceLookupExcludedFiles, declarationImportRemapper) {
- const filesToRemove = new Set();
- const tracker = new compiler_host.ChangeTracker(printer, importRemapper);
- const tsProgram = program.getTsProgram();
- const typeChecker = tsProgram.getTypeChecker();
- const templateTypeChecker = program.compiler.getTemplateTypeChecker();
- const referenceResolver = new ReferenceResolver(program, host, rootFileNames, basePath, referenceLookupExcludedFiles);
- const removalLocations = {
- arrays: new UniqueItemTracker(),
- imports: new UniqueItemTracker(),
- exports: new UniqueItemTracker(),
- unknown: new Set(),
- };
- const classesToRemove = new Set();
- const barrelExports = new UniqueItemTracker();
- const componentImportArrays = new UniqueItemTracker();
- const testArrays = new UniqueItemTracker();
- const nodesToRemove = new Set();
- sourceFiles.forEach(function walk(node) {
- if (ts.isClassDeclaration(node) && canRemoveClass(node, typeChecker)) {
- collectChangeLocations(node, removalLocations, componentImportArrays, testArrays, templateTypeChecker, referenceResolver, program);
- classesToRemove.add(node);
- }
- else if (ts.isExportDeclaration(node) &&
- !node.exportClause &&
- node.moduleSpecifier &&
- ts.isStringLiteralLike(node.moduleSpecifier) &&
- node.moduleSpecifier.text.startsWith('.')) {
- const exportedSourceFile = typeChecker
- .getSymbolAtLocation(node.moduleSpecifier)
- ?.valueDeclaration?.getSourceFile();
- if (exportedSourceFile) {
- barrelExports.track(exportedSourceFile, node);
- }
- }
- node.forEachChild(walk);
- });
- replaceInComponentImportsArray(componentImportArrays, classesToRemove, tracker, typeChecker, templateTypeChecker, declarationImportRemapper);
- replaceInTestImportsArray(testArrays, removalLocations, classesToRemove, tracker, typeChecker, templateTypeChecker, declarationImportRemapper);
- // We collect all the places where we need to remove references first before generating the
- // removal instructions since we may have to remove multiple references from one node.
- removeArrayReferences(removalLocations.arrays, tracker);
- removeImportReferences(removalLocations.imports, tracker);
- removeExportReferences(removalLocations.exports, tracker);
- addRemovalTodos(removalLocations.unknown, tracker);
- // Collect all the nodes to be removed before determining which files to delete since we need
- // to know it ahead of time when deleting barrel files that export other barrel files.
- (function trackNodesToRemove(nodes) {
- for (const node of nodes) {
- const sourceFile = node.getSourceFile();
- if (!filesToRemove.has(sourceFile) && canRemoveFile(sourceFile, nodes)) {
- const barrelExportsForFile = barrelExports.get(sourceFile);
- nodesToRemove.add(node);
- filesToRemove.add(sourceFile);
- barrelExportsForFile && trackNodesToRemove(barrelExportsForFile);
- }
- else {
- nodesToRemove.add(node);
- }
- }
- })(classesToRemove);
- for (const node of nodesToRemove) {
- const sourceFile = node.getSourceFile();
- if (!filesToRemove.has(sourceFile) && canRemoveFile(sourceFile, nodesToRemove)) {
- filesToRemove.add(sourceFile);
- }
- else {
- tracker.removeNode(node);
- }
- }
- return { pendingChanges: tracker.recordChanges(), filesToRemove };
- }
- /**
- * Collects all the nodes that a module needs to be removed from.
- * @param ngModule Module being removed.
- * @param removalLocations Tracks the different places from which the class should be removed.
- * @param componentImportArrays Set of `imports` arrays of components that need to be adjusted.
- * @param testImportArrays Set of `imports` arrays of tests that need to be adjusted.
- * @param referenceResolver
- * @param program
- */
- function collectChangeLocations(ngModule, removalLocations, componentImportArrays, testImportArrays, templateTypeChecker, referenceResolver, program) {
- const refsByFile = referenceResolver.findReferencesInProject(ngModule.name);
- const tsProgram = program.getTsProgram();
- const typeChecker = tsProgram.getTypeChecker();
- const nodes$1 = new Set();
- for (const [fileName, refs] of refsByFile) {
- const sourceFile = tsProgram.getSourceFile(fileName);
- if (sourceFile) {
- offsetsToNodes(getNodeLookup(sourceFile), refs, nodes$1);
- }
- }
- for (const node of nodes$1) {
- const closestArray = nodes.closestNode(node, ts.isArrayLiteralExpression);
- if (closestArray) {
- const closestAssignment = nodes.closestNode(closestArray, ts.isPropertyAssignment);
- if (closestAssignment && isInImportsArray(closestAssignment, closestArray)) {
- const closestCall = nodes.closestNode(closestAssignment, ts.isCallExpression);
- if (closestCall) {
- const closestDecorator = nodes.closestNode(closestCall, ts.isDecorator);
- const closestClass = closestDecorator
- ? nodes.closestNode(closestDecorator, ts.isClassDeclaration)
- : null;
- const directiveMeta = closestClass
- ? templateTypeChecker.getDirectiveMetadata(closestClass)
- : null;
- // If the module was flagged as being removable, but it's still being used in a
- // standalone component's `imports` array, it means that it was likely changed
- // outside of the migration and deleting it now will be breaking. Track it
- // separately so it can be handled properly.
- if (directiveMeta && directiveMeta.isComponent && directiveMeta.isStandalone) {
- componentImportArrays.track(closestArray, node);
- continue;
- }
- // If the module is removable and used inside a test's `imports`,
- // we track it separately so it can be replaced with its `exports`.
- const { testBed, catalyst } = getTestingImports(node.getSourceFile());
- if (isTestCall(typeChecker, closestCall, testBed, catalyst)) {
- testImportArrays.track(closestArray, node);
- continue;
- }
- }
- }
- removalLocations.arrays.track(closestArray, node);
- continue;
- }
- const closestImport = nodes.closestNode(node, ts.isNamedImports);
- if (closestImport) {
- removalLocations.imports.track(closestImport, node);
- continue;
- }
- const closestExport = nodes.closestNode(node, ts.isNamedExports);
- if (closestExport) {
- removalLocations.exports.track(closestExport, node);
- continue;
- }
- removalLocations.unknown.add(node);
- }
- }
- /**
- * Replaces all the leftover modules in component `imports` arrays with their exports.
- * @param componentImportArrays All the imports arrays and their nodes that represent NgModules.
- * @param classesToRemove Set of classes that were marked for removal.
- * @param tracker
- * @param typeChecker
- * @param templateTypeChecker
- * @param importRemapper
- */
- function replaceInComponentImportsArray(componentImportArrays, classesToRemove, tracker, typeChecker, templateTypeChecker, importRemapper) {
- for (const [array, toReplace] of componentImportArrays.getEntries()) {
- const closestClass = nodes.closestNode(array, ts.isClassDeclaration);
- if (!closestClass) {
- continue;
- }
- const replacements = new UniqueItemTracker();
- const usedImports = new Set(findTemplateDependencies(closestClass, templateTypeChecker).map((ref) => ref.node));
- for (const node of toReplace) {
- const moduleDecl = findClassDeclaration(node, typeChecker);
- if (moduleDecl) {
- const moduleMeta = templateTypeChecker.getNgModuleMetadata(moduleDecl);
- if (moduleMeta) {
- moduleMeta.exports.forEach((exp) => {
- if (usedImports.has(exp.node)) {
- replacements.track(node, exp);
- }
- });
- }
- else {
- // It's unlikely not to have module metadata at this point, but just in
- // case unmark the class for removal to reduce the chance of breakages.
- classesToRemove.delete(moduleDecl);
- }
- }
- }
- replaceModulesInImportsArray(array, replacements, tracker, templateTypeChecker, importRemapper);
- }
- }
- /**
- * Replaces all the leftover modules in testing `imports` arrays with their exports.
- * @param testImportArrays All test `imports` arrays and their nodes that represent modules.
- * @param classesToRemove Classes marked for removal by the migration.
- * @param tracker
- * @param typeChecker
- * @param templateTypeChecker
- * @param importRemapper
- */
- function replaceInTestImportsArray(testImportArrays, removalLocations, classesToRemove, tracker, typeChecker, templateTypeChecker, importRemapper) {
- for (const [array, toReplace] of testImportArrays.getEntries()) {
- const replacements = new UniqueItemTracker();
- for (const node of toReplace) {
- const moduleDecl = findClassDeclaration(node, typeChecker);
- if (moduleDecl) {
- const moduleMeta = templateTypeChecker.getNgModuleMetadata(moduleDecl);
- if (moduleMeta) {
- // Since we don't have access to the template type checker in tests,
- // we copy over all the `exports` that aren't flagged for removal.
- const exports = moduleMeta.exports.filter((exp) => !classesToRemove.has(exp.node));
- if (exports.length > 0) {
- exports.forEach((exp) => replacements.track(node, exp));
- }
- else {
- removalLocations.arrays.track(array, node);
- }
- }
- else {
- // It's unlikely not to have module metadata at this point, but just in
- // case unmark the class for removal to reduce the chance of breakages.
- classesToRemove.delete(moduleDecl);
- }
- }
- }
- replaceModulesInImportsArray(array, replacements, tracker, templateTypeChecker, importRemapper);
- }
- }
- /**
- * Replaces any leftover modules in an `imports` arrays with a set of specified exports
- * @param array Imports array which is being migrated.
- * @param replacements Map of NgModule references to their exports.
- * @param tracker
- * @param templateTypeChecker
- * @param importRemapper
- */
- function replaceModulesInImportsArray(array, replacements, tracker, templateTypeChecker, importRemapper) {
- if (replacements.isEmpty()) {
- return;
- }
- const newElements = [];
- const identifiers = new Set();
- for (const element of array.elements) {
- if (ts.isIdentifier(element)) {
- identifiers.add(element.text);
- }
- }
- for (const element of array.elements) {
- const replacementRefs = replacements.get(element);
- if (!replacementRefs) {
- newElements.push(element);
- continue;
- }
- const potentialImports = [];
- for (const ref of replacementRefs) {
- const importLocation = findImportLocation(ref, array, checker.PotentialImportMode.Normal, templateTypeChecker);
- if (importLocation) {
- potentialImports.push(importLocation);
- }
- }
- potentialImportsToExpressions(potentialImports, array.getSourceFile(), tracker, importRemapper).forEach((expr) => {
- if (!ts.isIdentifier(expr) || !identifiers.has(expr.text)) {
- newElements.push(expr);
- }
- });
- }
- tracker.replaceNode(array, ts.factory.updateArrayLiteralExpression(array, newElements));
- }
- /**
- * Removes all tracked array references.
- * @param locations Locations from which to remove the references.
- * @param tracker Tracker in which to register the changes.
- */
- function removeArrayReferences(locations, tracker) {
- for (const [array, toRemove] of locations.getEntries()) {
- const newElements = filterRemovedElements(array.elements, toRemove);
- tracker.replaceNode(array, ts.factory.updateArrayLiteralExpression(array, ts.factory.createNodeArray(newElements, array.elements.hasTrailingComma)));
- }
- }
- /**
- * Removes all tracked import references.
- * @param locations Locations from which to remove the references.
- * @param tracker Tracker in which to register the changes.
- */
- function removeImportReferences(locations, tracker) {
- for (const [namedImports, toRemove] of locations.getEntries()) {
- const newElements = filterRemovedElements(namedImports.elements, toRemove);
- // If no imports are left, we can try to drop the entire import.
- if (newElements.length === 0) {
- const importClause = nodes.closestNode(namedImports, ts.isImportClause);
- // If the import clause has a name we can only drop then named imports.
- // e.g. `import Foo, {ModuleToRemove} from './foo';` becomes `import Foo from './foo';`.
- if (importClause && importClause.name) {
- tracker.replaceNode(importClause, ts.factory.updateImportClause(importClause, importClause.isTypeOnly, importClause.name, undefined));
- }
- else {
- // Otherwise we can drop the entire declaration.
- const declaration = nodes.closestNode(namedImports, ts.isImportDeclaration);
- if (declaration) {
- tracker.removeNode(declaration);
- }
- }
- }
- else {
- // Otherwise we just drop the imported symbols and keep the declaration intact.
- tracker.replaceNode(namedImports, ts.factory.updateNamedImports(namedImports, newElements));
- }
- }
- }
- /**
- * Removes all tracked export references.
- * @param locations Locations from which to remove the references.
- * @param tracker Tracker in which to register the changes.
- */
- function removeExportReferences(locations, tracker) {
- for (const [namedExports, toRemove] of locations.getEntries()) {
- const newElements = filterRemovedElements(namedExports.elements, toRemove);
- // If no exports are left, we can drop the entire declaration.
- if (newElements.length === 0) {
- const declaration = nodes.closestNode(namedExports, ts.isExportDeclaration);
- if (declaration) {
- tracker.removeNode(declaration);
- }
- }
- else {
- // Otherwise we just drop the exported symbols and keep the declaration intact.
- tracker.replaceNode(namedExports, ts.factory.updateNamedExports(namedExports, newElements));
- }
- }
- }
- /**
- * Determines whether an `@NgModule` class is safe to remove. A module is safe to remove if:
- * 1. It has no `declarations`.
- * 2. It has no `providers`.
- * 3. It has no `bootstrap` components.
- * 4. It has no `ModuleWithProviders` in its `imports`.
- * 5. It has no class members. Empty construstors are ignored.
- * @param node Class that is being checked.
- * @param typeChecker
- */
- function canRemoveClass(node, typeChecker) {
- const decorator = findNgModuleDecorator(node, typeChecker)?.node;
- // We can't remove a declaration if it's not a valid `NgModule`.
- if (!decorator || !ts.isCallExpression(decorator.expression)) {
- return false;
- }
- // Unsupported case, e.g. `@NgModule(SOME_VALUE)`.
- if (decorator.expression.arguments.length > 0 &&
- !ts.isObjectLiteralExpression(decorator.expression.arguments[0])) {
- return false;
- }
- // We can't remove modules that have class members. We make an exception for an
- // empty constructor which may have been generated by a tool and forgotten.
- if (node.members.length > 0 && node.members.some((member) => !isEmptyConstructor(member))) {
- return false;
- }
- // An empty `NgModule` call can be removed.
- if (decorator.expression.arguments.length === 0) {
- return true;
- }
- const literal = decorator.expression.arguments[0];
- const imports = findLiteralProperty(literal, 'imports');
- if (imports && isNonEmptyNgModuleProperty(imports)) {
- // We can't remove the class if at least one import isn't identifier, because it may be a
- // `ModuleWithProviders` which is the equivalent of having something in the `providers` array.
- for (const dep of imports.initializer.elements) {
- if (!ts.isIdentifier(dep)) {
- return false;
- }
- const depDeclaration = findClassDeclaration(dep, typeChecker);
- const depNgModule = depDeclaration
- ? findNgModuleDecorator(depDeclaration, typeChecker)
- : null;
- // If any of the dependencies of the class is an `NgModule` that can't be removed, the class
- // itself can't be removed either, because it may be part of a transitive dependency chain.
- if (depDeclaration !== null &&
- depNgModule !== null &&
- !canRemoveClass(depDeclaration, typeChecker)) {
- return false;
- }
- }
- }
- // We can't remove classes that have any `declarations`, `providers` or `bootstrap` elements.
- // Also err on the side of caution and don't remove modules where any of the aforementioned
- // properties aren't initialized to an array literal.
- for (const prop of literal.properties) {
- if (isNonEmptyNgModuleProperty(prop) &&
- (prop.name.text === 'declarations' ||
- prop.name.text === 'providers' ||
- prop.name.text === 'bootstrap')) {
- return false;
- }
- }
- return true;
- }
- /**
- * Checks whether a node is a non-empty property from an NgModule's metadata. This is defined as a
- * property assignment with a static name, initialized to an array literal with more than one
- * element.
- * @param node Node to be checked.
- */
- function isNonEmptyNgModuleProperty(node) {
- return (ts.isPropertyAssignment(node) &&
- ts.isIdentifier(node.name) &&
- ts.isArrayLiteralExpression(node.initializer) &&
- node.initializer.elements.length > 0);
- }
- /**
- * Determines if a file is safe to delete. A file is safe to delete if all it contains are
- * import statements, class declarations that are about to be deleted and non-exported code.
- * @param sourceFile File that is being checked.
- * @param nodesToBeRemoved Nodes that are being removed as a part of the migration.
- */
- function canRemoveFile(sourceFile, nodesToBeRemoved) {
- for (const node of sourceFile.statements) {
- if (ts.isImportDeclaration(node) || nodesToBeRemoved.has(node)) {
- continue;
- }
- if (ts.isExportDeclaration(node) ||
- (ts.canHaveModifiers(node) &&
- ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))) {
- return false;
- }
- }
- return true;
- }
- /**
- * Gets whether an AST node contains another AST node.
- * @param parent Parent node that may contain the child.
- * @param child Child node that is being checked.
- */
- function contains(parent, child) {
- return (parent === child ||
- (parent.getSourceFile().fileName === child.getSourceFile().fileName &&
- child.getStart() >= parent.getStart() &&
- child.getStart() <= parent.getEnd()));
- }
- /**
- * Removes AST nodes from a node array.
- * @param elements Array from which to remove the nodes.
- * @param toRemove Nodes that should be removed.
- */
- function filterRemovedElements(elements, toRemove) {
- return elements.filter((el) => {
- for (const node of toRemove) {
- // Check that the element contains the node, despite knowing with relative certainty that it
- // does, because this allows us to unwrap some nodes. E.g. if we have `[((toRemove))]`, we
- // want to remove the entire parenthesized expression, rather than just `toRemove`.
- if (contains(el, node)) {
- return false;
- }
- }
- return true;
- });
- }
- /** Returns whether a node as an empty constructor. */
- function isEmptyConstructor(node) {
- return (ts.isConstructorDeclaration(node) &&
- node.parameters.length === 0 &&
- (node.body == null || node.body.statements.length === 0));
- }
- /**
- * Adds TODO comments to nodes that couldn't be removed manually.
- * @param nodes Nodes to which to add the TODO.
- * @param tracker Tracker in which to register the changes.
- */
- function addRemovalTodos(nodes, tracker) {
- for (const node of nodes) {
- // Note: the comment is inserted using string manipulation, instead of going through the AST,
- // because this way we preserve more of the app's original formatting.
- // Note: in theory this can duplicate comments if the module pruning runs multiple times on
- // the same node. In practice it is unlikely, because the second time the node won't be picked
- // up by the language service as a reference, because the class won't exist anymore.
- tracker.insertText(node.getSourceFile(), node.getFullStart(), ` /* TODO(standalone-migration): clean up removed NgModule reference manually. */ `);
- }
- }
- /** Finds the `NgModule` decorator in a class, if it exists. */
- function findNgModuleDecorator(node, typeChecker) {
- const decorators = ng_decorators.getAngularDecorators(typeChecker, ts.getDecorators(node) || []);
- return decorators.find((decorator) => decorator.name === 'NgModule') || null;
- }
- /**
- * Checks whether a node is used inside of an `imports` array.
- * @param closestAssignment The closest property assignment to the node.
- * @param closestArray The closest array to the node.
- */
- function isInImportsArray(closestAssignment, closestArray) {
- return (closestAssignment.initializer === closestArray &&
- (ts.isIdentifier(closestAssignment.name) || ts.isStringLiteralLike(closestAssignment.name)) &&
- closestAssignment.name.text === 'imports');
- }
- /*!
- * @license
- * Copyright Google LLC All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.dev/license
- */
- function toStandaloneBootstrap(program, host, basePath, rootFileNames, sourceFiles, printer, importRemapper, referenceLookupExcludedFiles, declarationImportRemapper) {
- const tracker = new compiler_host.ChangeTracker(printer, importRemapper);
- const typeChecker = program.getTsProgram().getTypeChecker();
- const templateTypeChecker = program.compiler.getTemplateTypeChecker();
- const referenceResolver = new ReferenceResolver(program, host, rootFileNames, basePath, referenceLookupExcludedFiles);
- const bootstrapCalls = [];
- const testObjects = new Set();
- const allDeclarations = new Set();
- // `bootstrapApplication` doesn't include Protractor support by default
- // anymore so we have to opt the app in, if we detect it being used.
- const additionalProviders = hasImport(program, rootFileNames, 'protractor')
- ? new Map([['provideProtractorTestingSupport', '@angular/platform-browser']])
- : null;
- for (const sourceFile of sourceFiles) {
- sourceFile.forEachChild(function walk(node) {
- if (ts.isCallExpression(node) &&
- ts.isPropertyAccessExpression(node.expression) &&
- node.expression.name.text === 'bootstrapModule' &&
- isClassReferenceInAngularModule(node.expression, 'PlatformRef', 'core', typeChecker)) {
- const call = analyzeBootstrapCall(node, typeChecker, templateTypeChecker);
- if (call) {
- bootstrapCalls.push(call);
- }
- }
- node.forEachChild(walk);
- });
- findTestObjectsToMigrate(sourceFile, typeChecker).forEach((obj) => testObjects.add(obj));
- }
- for (const call of bootstrapCalls) {
- call.declarations.forEach((decl) => allDeclarations.add(decl));
- migrateBootstrapCall(call, tracker, additionalProviders, referenceResolver, typeChecker, printer);
- }
- // The previous migrations explicitly skip over bootstrapped
- // declarations so we have to migrate them now.
- for (const declaration of allDeclarations) {
- convertNgModuleDeclarationToStandalone(declaration, allDeclarations, tracker, templateTypeChecker, declarationImportRemapper);
- }
- migrateTestDeclarations(testObjects, allDeclarations, tracker, templateTypeChecker, typeChecker);
- return tracker.recordChanges();
- }
- /**
- * Extracts all of the information from a `bootstrapModule` call
- * necessary to convert it to `bootstrapApplication`.
- * @param call Call to be analyzed.
- * @param typeChecker
- * @param templateTypeChecker
- */
- function analyzeBootstrapCall(call, typeChecker, templateTypeChecker) {
- if (call.arguments.length === 0 || !ts.isIdentifier(call.arguments[0])) {
- return null;
- }
- const declaration = findClassDeclaration(call.arguments[0], typeChecker);
- if (!declaration) {
- return null;
- }
- const decorator = ng_decorators.getAngularDecorators(typeChecker, ts.getDecorators(declaration) || []).find((decorator) => decorator.name === 'NgModule');
- if (!decorator ||
- decorator.node.expression.arguments.length === 0 ||
- !ts.isObjectLiteralExpression(decorator.node.expression.arguments[0])) {
- return null;
- }
- const metadata = decorator.node.expression.arguments[0];
- const bootstrapProp = findLiteralProperty(metadata, 'bootstrap');
- if (!bootstrapProp ||
- !ts.isPropertyAssignment(bootstrapProp) ||
- !ts.isArrayLiteralExpression(bootstrapProp.initializer) ||
- bootstrapProp.initializer.elements.length === 0 ||
- !ts.isIdentifier(bootstrapProp.initializer.elements[0])) {
- return null;
- }
- const component = findClassDeclaration(bootstrapProp.initializer.elements[0], typeChecker);
- if (component && component.name && ts.isIdentifier(component.name)) {
- return {
- module: declaration,
- metadata,
- component: component,
- call,
- declarations: extractDeclarationsFromModule(declaration, templateTypeChecker),
- };
- }
- return null;
- }
- /**
- * Converts a `bootstrapModule` call to `bootstrapApplication`.
- * @param analysis Analysis result of the call.
- * @param tracker Tracker in which to register the changes.
- * @param additionalFeatures Additional providers, apart from the auto-detected ones, that should
- * be added to the bootstrap call.
- * @param referenceResolver
- * @param typeChecker
- * @param printer
- */
- function migrateBootstrapCall(analysis, tracker, additionalProviders, referenceResolver, typeChecker, printer) {
- const sourceFile = analysis.call.getSourceFile();
- const moduleSourceFile = analysis.metadata.getSourceFile();
- const providers = findLiteralProperty(analysis.metadata, 'providers');
- const imports = findLiteralProperty(analysis.metadata, 'imports');
- const nodesToCopy = new Set();
- const providersInNewCall = [];
- const moduleImportsInNewCall = [];
- let nodeLookup = null;
- // Comment out the metadata so that it'll be removed when we run the module pruning afterwards.
- // If the pruning is left for some reason, the user will still have an actionable TODO.
- tracker.insertText(moduleSourceFile, analysis.metadata.getStart(), '/* TODO(standalone-migration): clean up removed NgModule class manually. \n');
- tracker.insertText(moduleSourceFile, analysis.metadata.getEnd(), ' */');
- if (providers && ts.isPropertyAssignment(providers)) {
- nodeLookup = nodeLookup || getNodeLookup(moduleSourceFile);
- if (ts.isArrayLiteralExpression(providers.initializer)) {
- providersInNewCall.push(...providers.initializer.elements);
- }
- else {
- providersInNewCall.push(ts.factory.createSpreadElement(providers.initializer));
- }
- addNodesToCopy(sourceFile, providers, nodeLookup, tracker, nodesToCopy, referenceResolver);
- }
- if (imports && ts.isPropertyAssignment(imports)) {
- nodeLookup = nodeLookup || getNodeLookup(moduleSourceFile);
- migrateImportsForBootstrapCall(sourceFile, imports, nodeLookup, moduleImportsInNewCall, providersInNewCall, tracker, nodesToCopy, referenceResolver, typeChecker);
- }
- if (additionalProviders) {
- additionalProviders.forEach((moduleSpecifier, name) => {
- providersInNewCall.push(ts.factory.createCallExpression(tracker.addImport(sourceFile, name, moduleSpecifier), undefined, undefined));
- });
- }
- if (nodesToCopy.size > 0) {
- let text = '\n\n';
- nodesToCopy.forEach((node) => {
- const transformedNode = remapDynamicImports(sourceFile.fileName, node);
- // Use `getText` to try an preserve the original formatting. This only works if the node
- // hasn't been transformed. If it has, we have to fall back to the printer.
- if (transformedNode === node) {
- text += transformedNode.getText() + '\n';
- }
- else {
- text += printer.printNode(ts.EmitHint.Unspecified, transformedNode, node.getSourceFile());
- }
- });
- text += '\n';
- tracker.insertText(sourceFile, getLastImportEnd(sourceFile), text);
- }
- replaceBootstrapCallExpression(analysis, providersInNewCall, moduleImportsInNewCall, tracker);
- }
- /**
- * Replaces a `bootstrapModule` call with `bootstrapApplication`.
- * @param analysis Analysis result of the `bootstrapModule` call.
- * @param providers Providers that should be added to the new call.
- * @param modules Modules that are being imported into the new call.
- * @param tracker Object keeping track of the changes to the different files.
- */
- function replaceBootstrapCallExpression(analysis, providers, modules, tracker) {
- const sourceFile = analysis.call.getSourceFile();
- const componentPath = getRelativeImportPath(sourceFile.fileName, analysis.component.getSourceFile().fileName);
- const args = [tracker.addImport(sourceFile, analysis.component.name.text, componentPath)];
- const bootstrapExpression = tracker.addImport(sourceFile, 'bootstrapApplication', '@angular/platform-browser');
- if (providers.length > 0 || modules.length > 0) {
- const combinedProviders = [];
- if (modules.length > 0) {
- const importProvidersExpression = tracker.addImport(sourceFile, 'importProvidersFrom', '@angular/core');
- combinedProviders.push(ts.factory.createCallExpression(importProvidersExpression, [], modules));
- }
- // Push the providers after `importProvidersFrom` call for better readability.
- combinedProviders.push(...providers);
- const providersArray = ts.factory.createNodeArray(combinedProviders, analysis.metadata.properties.hasTrailingComma && combinedProviders.length > 2);
- const initializer = remapDynamicImports(sourceFile.fileName, ts.factory.createArrayLiteralExpression(providersArray, combinedProviders.length > 1));
- args.push(ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment('providers', initializer)], true));
- }
- tracker.replaceNode(analysis.call, ts.factory.createCallExpression(bootstrapExpression, [], args),
- // Note: it's important to pass in the source file that the nodes originated from!
- // Otherwise TS won't print out literals inside of the providers that we're copying
- // over from the module file.
- undefined, analysis.metadata.getSourceFile());
- }
- /**
- * Processes the `imports` of an NgModule so that they can be used in the `bootstrapApplication`
- * call inside of a different file.
- * @param sourceFile File to which the imports will be moved.
- * @param imports Node declaring the imports.
- * @param nodeLookup Map used to look up nodes based on their positions in a file.
- * @param importsForNewCall Array keeping track of the imports that are being added to the new call.
- * @param providersInNewCall Array keeping track of the providers in the new call.
- * @param tracker Tracker in which changes to files are being stored.
- * @param nodesToCopy Nodes that should be copied to the new file.
- * @param referenceResolver
- * @param typeChecker
- */
- function migrateImportsForBootstrapCall(sourceFile, imports, nodeLookup, importsForNewCall, providersInNewCall, tracker, nodesToCopy, referenceResolver, typeChecker) {
- if (!ts.isArrayLiteralExpression(imports.initializer)) {
- importsForNewCall.push(imports.initializer);
- return;
- }
- for (const element of imports.initializer.elements) {
- // If the reference is to a `RouterModule.forRoot` call, we can try to migrate it.
- if (ts.isCallExpression(element) &&
- ts.isPropertyAccessExpression(element.expression) &&
- element.arguments.length > 0 &&
- element.expression.name.text === 'forRoot' &&
- isClassReferenceInAngularModule(element.expression.expression, 'RouterModule', 'router', typeChecker)) {
- const options = element.arguments[1];
- const features = options ? getRouterModuleForRootFeatures(sourceFile, options, tracker) : [];
- // If the features come back as null, it means that the router
- // has a configuration that can't be migrated automatically.
- if (features !== null) {
- providersInNewCall.push(ts.factory.createCallExpression(tracker.addImport(sourceFile, 'provideRouter', '@angular/router'), [], [element.arguments[0], ...features]));
- addNodesToCopy(sourceFile, element.arguments[0], nodeLookup, tracker, nodesToCopy, referenceResolver);
- if (options) {
- addNodesToCopy(sourceFile, options, nodeLookup, tracker, nodesToCopy, referenceResolver);
- }
- continue;
- }
- }
- if (ts.isIdentifier(element)) {
- // `BrowserAnimationsModule` can be replaced with `provideAnimations`.
- const animationsModule = 'platform-browser/animations';
- const animationsImport = `@angular/${animationsModule}`;
- if (isClassReferenceInAngularModule(element, 'BrowserAnimationsModule', animationsModule, typeChecker)) {
- providersInNewCall.push(ts.factory.createCallExpression(tracker.addImport(sourceFile, 'provideAnimations', animationsImport), [], []));
- continue;
- }
- // `NoopAnimationsModule` can be replaced with `provideNoopAnimations`.
- if (isClassReferenceInAngularModule(element, 'NoopAnimationsModule', animationsModule, typeChecker)) {
- providersInNewCall.push(ts.factory.createCallExpression(tracker.addImport(sourceFile, 'provideNoopAnimations', animationsImport), [], []));
- continue;
- }
- // `HttpClientModule` can be replaced with `provideHttpClient()`.
- const httpClientModule = 'common/http';
- const httpClientImport = `@angular/${httpClientModule}`;
- if (isClassReferenceInAngularModule(element, 'HttpClientModule', httpClientModule, typeChecker)) {
- const callArgs = [
- // we add `withInterceptorsFromDi()` to the call to ensure that class-based interceptors
- // still work
- ts.factory.createCallExpression(tracker.addImport(sourceFile, 'withInterceptorsFromDi', httpClientImport), [], []),
- ];
- providersInNewCall.push(ts.factory.createCallExpression(tracker.addImport(sourceFile, 'provideHttpClient', httpClientImport), [], callArgs));
- continue;
- }
- }
- const target =
- // If it's a call, it'll likely be a `ModuleWithProviders`
- // expression so the target is going to be call's expression.
- ts.isCallExpression(element) && ts.isPropertyAccessExpression(element.expression)
- ? element.expression.expression
- : element;
- const classDeclaration = findClassDeclaration(target, typeChecker);
- const decorators = classDeclaration
- ? ng_decorators.getAngularDecorators(typeChecker, ts.getDecorators(classDeclaration) || [])
- : undefined;
- if (!decorators ||
- decorators.length === 0 ||
- decorators.every(({ name }) => name !== 'Directive' && name !== 'Component' && name !== 'Pipe')) {
- importsForNewCall.push(element);
- addNodesToCopy(sourceFile, element, nodeLookup, tracker, nodesToCopy, referenceResolver);
- }
- }
- }
- /**
- * Generates the call expressions that can be used to replace the options
- * object that is passed into a `RouterModule.forRoot` call.
- * @param sourceFile File that the `forRoot` call is coming from.
- * @param options Node that is passed as the second argument to the `forRoot` call.
- * @param tracker Tracker in which to track imports that need to be inserted.
- * @returns Null if the options can't be migrated, otherwise an array of call expressions.
- */
- function getRouterModuleForRootFeatures(sourceFile, options, tracker) {
- // Options that aren't a static object literal can't be migrated.
- if (!ts.isObjectLiteralExpression(options)) {
- return null;
- }
- const featureExpressions = [];
- const configOptions = [];
- const inMemoryScrollingOptions = [];
- const features = new UniqueItemTracker();
- for (const prop of options.properties) {
- // We can't migrate options that we can't easily analyze.
- if (!ts.isPropertyAssignment(prop) ||
- (!ts.isIdentifier(prop.name) && !ts.isStringLiteralLike(prop.name))) {
- return null;
- }
- switch (prop.name.text) {
- // `preloadingStrategy` maps to the `withPreloading` function.
- case 'preloadingStrategy':
- features.track('withPreloading', prop.initializer);
- break;
- // `enableTracing: true` maps to the `withDebugTracing` feature.
- case 'enableTracing':
- if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
- features.track('withDebugTracing', null);
- }
- break;
- // `initialNavigation: 'enabled'` and `initialNavigation: 'enabledBlocking'` map to the
- // `withEnabledBlockingInitialNavigation` feature, while `initialNavigation: 'disabled'` maps
- // to the `withDisabledInitialNavigation` feature.
- case 'initialNavigation':
- if (!ts.isStringLiteralLike(prop.initializer)) {
- return null;
- }
- if (prop.initializer.text === 'enabledBlocking' || prop.initializer.text === 'enabled') {
- features.track('withEnabledBlockingInitialNavigation', null);
- }
- else if (prop.initializer.text === 'disabled') {
- features.track('withDisabledInitialNavigation', null);
- }
- break;
- // `useHash: true` maps to the `withHashLocation` feature.
- case 'useHash':
- if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
- features.track('withHashLocation', null);
- }
- break;
- // `errorHandler` maps to the `withNavigationErrorHandler` feature.
- case 'errorHandler':
- features.track('withNavigationErrorHandler', prop.initializer);
- break;
- // `anchorScrolling` and `scrollPositionRestoration` arguments have to be combined into an
- // object literal that is passed into the `withInMemoryScrolling` feature.
- case 'anchorScrolling':
- case 'scrollPositionRestoration':
- inMemoryScrollingOptions.push(prop);
- break;
- // All remaining properties can be passed through the `withRouterConfig` feature.
- default:
- configOptions.push(prop);
- break;
- }
- }
- if (inMemoryScrollingOptions.length > 0) {
- features.track('withInMemoryScrolling', ts.factory.createObjectLiteralExpression(inMemoryScrollingOptions));
- }
- if (configOptions.length > 0) {
- features.track('withRouterConfig', ts.factory.createObjectLiteralExpression(configOptions));
- }
- for (const [feature, featureArgs] of features.getEntries()) {
- const callArgs = [];
- featureArgs.forEach((arg) => {
- if (arg !== null) {
- callArgs.push(arg);
- }
- });
- featureExpressions.push(ts.factory.createCallExpression(tracker.addImport(sourceFile, feature, '@angular/router'), [], callArgs));
- }
- return featureExpressions;
- }
- /**
- * Finds all the nodes that are referenced inside a root node and would need to be copied into a
- * new file in order for the node to compile, and tracks them.
- * @param targetFile File to which the nodes will be copied.
- * @param rootNode Node within which to look for references.
- * @param nodeLookup Map used to look up nodes based on their positions in a file.
- * @param tracker Tracker in which changes to files are stored.
- * @param nodesToCopy Set that keeps track of the nodes being copied.
- * @param referenceResolver
- */
- function addNodesToCopy(targetFile, rootNode, nodeLookup, tracker, nodesToCopy, referenceResolver) {
- const refs = findAllSameFileReferences(rootNode, nodeLookup, referenceResolver);
- for (const ref of refs) {
- const importSpecifier = closestOrSelf(ref, ts.isImportSpecifier);
- const importDeclaration = importSpecifier
- ? nodes.closestNode(importSpecifier, ts.isImportDeclaration)
- : null;
- // If the reference is in an import, we need to add an import to the main file.
- if (importDeclaration &&
- importSpecifier &&
- ts.isStringLiteralLike(importDeclaration.moduleSpecifier)) {
- const moduleName = importDeclaration.moduleSpecifier.text.startsWith('.')
- ? remapRelativeImport(targetFile.fileName, importDeclaration.moduleSpecifier)
- : importDeclaration.moduleSpecifier.text;
- const symbolName = importSpecifier.propertyName
- ? importSpecifier.propertyName.text
- : importSpecifier.name.text;
- const alias = importSpecifier.propertyName ? importSpecifier.name.text : undefined;
- tracker.addImport(targetFile, symbolName, moduleName, alias);
- continue;
- }
- const variableDeclaration = closestOrSelf(ref, ts.isVariableDeclaration);
- const variableStatement = variableDeclaration
- ? nodes.closestNode(variableDeclaration, ts.isVariableStatement)
- : null;
- // If the reference is a variable, we can attempt to import it or copy it over.
- if (variableDeclaration && variableStatement && ts.isIdentifier(variableDeclaration.name)) {
- if (isExported(variableStatement)) {
- tracker.addImport(targetFile, variableDeclaration.name.text, getRelativeImportPath(targetFile.fileName, ref.getSourceFile().fileName));
- }
- else {
- nodesToCopy.add(variableStatement);
- }
- continue;
- }
- // Otherwise check if the reference is inside of an exportable declaration, e.g. a function.
- // This code that is safe to copy over into the new file or import it, if it's exported.
- const closestExportable = closestOrSelf(ref, isExportableDeclaration);
- if (closestExportable) {
- if (isExported(closestExportable) && closestExportable.name) {
- tracker.addImport(targetFile, closestExportable.name.text, getRelativeImportPath(targetFile.fileName, ref.getSourceFile().fileName));
- }
- else {
- nodesToCopy.add(closestExportable);
- }
- }
- }
- }
- /**
- * Finds all the nodes referenced within the root node in the same file.
- * @param rootNode Node from which to start looking for references.
- * @param nodeLookup Map used to look up nodes based on their positions in a file.
- * @param referenceResolver
- */
- function findAllSameFileReferences(rootNode, nodeLookup, referenceResolver) {
- const results = new Set();
- const traversedTopLevelNodes = new Set();
- const excludeStart = rootNode.getStart();
- const excludeEnd = rootNode.getEnd();
- (function walk(node) {
- if (!isReferenceIdentifier(node)) {
- node.forEachChild(walk);
- return;
- }
- const refs = referencesToNodeWithinSameFile(node, nodeLookup, excludeStart, excludeEnd, referenceResolver);
- if (refs === null) {
- return;
- }
- for (const ref of refs) {
- if (results.has(ref)) {
- continue;
- }
- results.add(ref);
- const closestTopLevel = nodes.closestNode(ref, isTopLevelStatement);
- // Avoid re-traversing the same top-level nodes since we know what the result will be.
- if (!closestTopLevel || traversedTopLevelNodes.has(closestTopLevel)) {
- continue;
- }
- // Keep searching, starting from the closest top-level node. We skip import declarations,
- // because we already know about them and they may put the search into an infinite loop.
- if (!ts.isImportDeclaration(closestTopLevel) &&
- isOutsideRange(excludeStart, excludeEnd, closestTopLevel.getStart(), closestTopLevel.getEnd())) {
- traversedTopLevelNodes.add(closestTopLevel);
- walk(closestTopLevel);
- }
- }
- })(rootNode);
- return results;
- }
- /**
- * Finds all the nodes referring to a specific node within the same file.
- * @param node Node whose references we're lookip for.
- * @param nodeLookup Map used to look up nodes based on their positions in a file.
- * @param excludeStart Start of a range that should be excluded from the results.
- * @param excludeEnd End of a range that should be excluded from the results.
- * @param referenceResolver
- */
- function referencesToNodeWithinSameFile(node, nodeLookup, excludeStart, excludeEnd, referenceResolver) {
- const offsets = referenceResolver
- .findSameFileReferences(node, node.getSourceFile().fileName)
- .filter(([start, end]) => isOutsideRange(excludeStart, excludeEnd, start, end));
- if (offsets.length > 0) {
- const nodes = offsetsToNodes(nodeLookup, offsets, new Set());
- if (nodes.size > 0) {
- return nodes;
- }
- }
- return null;
- }
- /**
- * Transforms a node so that any dynamic imports with relative file paths it contains are remapped
- * as if they were specified in a different file. If no transformations have occurred, the original
- * node will be returned.
- * @param targetFileName File name to which to remap the imports.
- * @param rootNode Node being transformed.
- */
- function remapDynamicImports(targetFileName, rootNode) {
- let hasChanged = false;
- const transformer = (context) => {
- return (sourceFile) => ts.visitNode(sourceFile, function walk(node) {
- if (ts.isCallExpression(node) &&
- node.expression.kind === ts.SyntaxKind.ImportKeyword &&
- node.arguments.length > 0 &&
- ts.isStringLiteralLike(node.arguments[0]) &&
- node.arguments[0].text.startsWith('.')) {
- hasChanged = true;
- return context.factory.updateCallExpression(node, node.expression, node.typeArguments, [
- context.factory.createStringLiteral(remapRelativeImport(targetFileName, node.arguments[0])),
- ...node.arguments.slice(1),
- ]);
- }
- return ts.visitEachChild(node, walk, context);
- });
- };
- const result = ts.transform(rootNode, [transformer]).transformed[0];
- return hasChanged ? result : rootNode;
- }
- /**
- * Checks whether a node is a statement at the top level of a file.
- * @param node Node to be checked.
- */
- function isTopLevelStatement(node) {
- return node.parent != null && ts.isSourceFile(node.parent);
- }
- /**
- * Asserts that a node is an identifier that might be referring to a symbol. This excludes
- * identifiers of named nodes like property assignments.
- * @param node Node to be checked.
- */
- function isReferenceIdentifier(node) {
- return (ts.isIdentifier(node) &&
- ((!ts.isPropertyAssignment(node.parent) && !ts.isParameter(node.parent)) ||
- node.parent.name !== node));
- }
- /**
- * Checks whether a range is completely outside of another range.
- * @param excludeStart Start of the exclusion range.
- * @param excludeEnd End of the exclusion range.
- * @param start Start of the range that is being checked.
- * @param end End of the range that is being checked.
- */
- function isOutsideRange(excludeStart, excludeEnd, start, end) {
- return (start < excludeStart && end < excludeStart) || start > excludeEnd;
- }
- /**
- * Remaps the specifier of a relative import from its original location to a new one.
- * @param targetFileName Name of the file that the specifier will be moved to.
- * @param specifier Specifier whose path is being remapped.
- */
- function remapRelativeImport(targetFileName, specifier) {
- return getRelativeImportPath(targetFileName, p.join(p.dirname(specifier.getSourceFile().fileName), specifier.text));
- }
- /**
- * Whether a node is exported.
- * @param node Node to be checked.
- */
- function isExported(node) {
- return ts.canHaveModifiers(node) && node.modifiers
- ? node.modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)
- : false;
- }
- /**
- * Asserts that a node is an exportable declaration, which means that it can either be exported or
- * it can be safely copied into another file.
- * @param node Node to be checked.
- */
- function isExportableDeclaration(node) {
- return (ts.isEnumDeclaration(node) ||
- ts.isClassDeclaration(node) ||
- ts.isFunctionDeclaration(node) ||
- ts.isInterfaceDeclaration(node) ||
- ts.isTypeAliasDeclaration(node));
- }
- /**
- * Gets the index after the last import in a file. Can be used to insert new code into the file.
- * @param sourceFile File in which to search for imports.
- */
- function getLastImportEnd(sourceFile) {
- let index = 0;
- for (const statement of sourceFile.statements) {
- if (ts.isImportDeclaration(statement)) {
- index = Math.max(index, statement.getEnd());
- }
- else {
- break;
- }
- }
- return index;
- }
- /** Checks if any of the program's files has an import of a specific module. */
- function hasImport(program, rootFileNames, moduleName) {
- const tsProgram = program.getTsProgram();
- const deepImportStart = moduleName + '/';
- for (const fileName of rootFileNames) {
- const sourceFile = tsProgram.getSourceFile(fileName);
- if (!sourceFile) {
- continue;
- }
- for (const statement of sourceFile.statements) {
- if (ts.isImportDeclaration(statement) &&
- ts.isStringLiteralLike(statement.moduleSpecifier) &&
- (statement.moduleSpecifier.text === moduleName ||
- statement.moduleSpecifier.text.startsWith(deepImportStart))) {
- return true;
- }
- }
- }
- return false;
- }
- var MigrationMode;
- (function (MigrationMode) {
- MigrationMode["toStandalone"] = "convert-to-standalone";
- MigrationMode["pruneModules"] = "prune-ng-modules";
- MigrationMode["standaloneBootstrap"] = "standalone-bootstrap";
- })(MigrationMode || (MigrationMode = {}));
- function migrate(options) {
- return async (tree, context) => {
- const { buildPaths, testPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(tree);
- const basePath = process.cwd();
- const allPaths = [...buildPaths, ...testPaths];
- // TS and Schematic use paths in POSIX format even on Windows. This is needed as otherwise
- // string matching such as `sourceFile.fileName.startsWith(pathToMigrate)` might not work.
- const pathToMigrate = compiler_host.normalizePath(p.join(basePath, options.path));
- let migratedFiles = 0;
- if (!allPaths.length) {
- throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the standalone migration.');
- }
- for (const tsconfigPath of allPaths) {
- migratedFiles += standaloneMigration(tree, tsconfigPath, basePath, pathToMigrate, options);
- }
- if (migratedFiles === 0) {
- throw new schematics.SchematicsException(`Could not find any files to migrate under the path ${pathToMigrate}. Cannot run the standalone migration.`);
- }
- context.logger.info('🎉 Automated migration step has finished! 🎉');
- context.logger.info('IMPORTANT! Please verify manually that your application builds and behaves as expected.');
- context.logger.info(`See https://angular.dev/reference/migrations/standalone for more information.`);
- };
- }
- function standaloneMigration(tree, tsconfigPath, basePath, pathToMigrate, schematicOptions, oldProgram) {
- if (schematicOptions.path.startsWith('..')) {
- throw new schematics.SchematicsException('Cannot run standalone migration outside of the current project.');
- }
- const { host, options, rootNames } = compiler_host.createProgramOptions(tree, tsconfigPath, basePath, undefined, undefined, {
- _enableTemplateTypeChecker: true, // Required for the template type checker to work.
- compileNonExportedClasses: true, // We want to migrate non-exported classes too.
- // Avoid checking libraries to speed up the migration.
- skipLibCheck: true,
- skipDefaultLibCheck: true,
- });
- const referenceLookupExcludedFiles = /node_modules|\.ngtypecheck\.ts/;
- const program = createProgram({ rootNames, host, options, oldProgram });
- const printer = ts.createPrinter();
- if (fs.existsSync(pathToMigrate) && !fs.statSync(pathToMigrate).isDirectory()) {
- throw new schematics.SchematicsException(`Migration path ${pathToMigrate} has to be a directory. Cannot run the standalone migration.`);
- }
- const sourceFiles = program
- .getTsProgram()
- .getSourceFiles()
- .filter((sourceFile) => sourceFile.fileName.startsWith(pathToMigrate) &&
- compiler_host.canMigrateFile(basePath, sourceFile, program.getTsProgram()));
- if (sourceFiles.length === 0) {
- return 0;
- }
- let pendingChanges;
- let filesToRemove = null;
- if (schematicOptions.mode === MigrationMode.pruneModules) {
- const result = pruneNgModules(program, host, basePath, rootNames, sourceFiles, printer, undefined, referenceLookupExcludedFiles, knownInternalAliasRemapper);
- pendingChanges = result.pendingChanges;
- filesToRemove = result.filesToRemove;
- }
- else if (schematicOptions.mode === MigrationMode.standaloneBootstrap) {
- pendingChanges = toStandaloneBootstrap(program, host, basePath, rootNames, sourceFiles, printer, undefined, referenceLookupExcludedFiles, knownInternalAliasRemapper);
- }
- else {
- // This shouldn't happen, but default to `MigrationMode.toStandalone` just in case.
- pendingChanges = toStandalone(sourceFiles, program, printer, undefined, knownInternalAliasRemapper);
- }
- for (const [file, changes] of pendingChanges.entries()) {
- // Don't attempt to edit a file if it's going to be deleted.
- if (filesToRemove?.has(file)) {
- continue;
- }
- const update = tree.beginUpdate(p.relative(basePath, file.fileName));
- changes.forEach((change) => {
- if (change.removeLength != null) {
- update.remove(change.start, change.removeLength);
- }
- update.insertRight(change.start, change.text);
- });
- tree.commitUpdate(update);
- }
- if (filesToRemove) {
- for (const file of filesToRemove) {
- tree.delete(p.relative(basePath, file.fileName));
- }
- }
- // Run the module pruning after the standalone bootstrap to automatically remove the root module.
- // Note that we can't run the module pruning internally without propagating the changes to disk,
- // because there may be conflicting AST node changes.
- if (schematicOptions.mode === MigrationMode.standaloneBootstrap) {
- return (standaloneMigration(tree, tsconfigPath, basePath, pathToMigrate, { ...schematicOptions, mode: MigrationMode.pruneModules }, program) + sourceFiles.length);
- }
- return sourceFiles.length;
- }
- exports.migrate = migrate;
|