diff --git a/packages/core/schematics/migrations/bootstrap-options-migration/migration.spec.ts b/packages/core/schematics/migrations/bootstrap-options-migration/migration.spec.ts index d51c9a17267..c26c9e1c1ab 100644 --- a/packages/core/schematics/migrations/bootstrap-options-migration/migration.spec.ts +++ b/packages/core/schematics/migrations/bootstrap-options-migration/migration.spec.ts @@ -749,6 +749,55 @@ describe('bootstrap options migration', () => { .toEqual(expected.replace(/\s+/g, '')); }); }); + + it('should not migrate a SSR config that has provideZonelessChangeDetection in the base config', async () => { + return runTsurgeMigration(new BootstrapOptionsMigration(), [ + ...typeFiles, + { + name: absoluteFrom('/app/app.config.ts'), + contents: ` + import { provideZonelessChangeDetection } from '@angular/core'; + export const appConfig = { + providers: [provideZonelessChangeDetection()], + }; + `, + }, + { + name: absoluteFrom('/app/app.config.server.ts'), + contents: ` + import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; + import { appConfig } from './app.config'; + const serverConfig: ApplicationConfig = { + providers: [] + }; + export const appServerConfig = mergeApplicationConfig(appConfig, serverConfig); + `, + }, + { + name: absoluteFrom('/main.server.ts'), + isProgramRootFile: true, + contents: ` + import { bootstrapApplication } from '@angular/platform-browser'; + import { AppComponent } from './app/app.component'; + import { appServerConfig } from './app/app.config.server'; + const bootstrap = () => bootstrapApplication(AppComponent, appServerConfig); + export default bootstrap; + `, + }, + ]).then(({fs}) => { + const actualMainServer = fs.readFile(absoluteFrom('/main.server.ts')); + const expectedMainServer = ` + import { bootstrapApplication } from '@angular/platform-browser'; + import { AppComponent } from './app/app.component'; + import { appServerConfig } from './app/app.config.server'; + const bootstrap = () => bootstrapApplication(AppComponent, appServerConfig); + export default bootstrap; + `; + expect(actualMainServer.replace(/\s+/g, '')) + .withContext(diffText(expectedMainServer, actualMainServer)) + .toEqual(expectedMainServer.replace(/\s+/g, '')); + }); + }); }); describe('bootstrapModule', () => { diff --git a/packages/core/schematics/migrations/bootstrap-options-migration/migration.ts b/packages/core/schematics/migrations/bootstrap-options-migration/migration.ts index c9b85fc0b95..0c323e3060c 100644 --- a/packages/core/schematics/migrations/bootstrap-options-migration/migration.ts +++ b/packages/core/schematics/migrations/bootstrap-options-migration/migration.ts @@ -197,15 +197,10 @@ export class BootstrapOptionsMigration extends TsurgeFunnelMigration< if (ts.isObjectLiteralExpression(optionsNode)) { optionLiteral = optionsNode; addProvidersToBootstrapOption(optionProjectFile, optionLiteral, providerFn, replacements); + } else if (this.isServerConfigZoneless(optionsNode, typeChecker)) { + // Nothing to migrate for the SSR bootstrap + return; } else if (ts.isIdentifier(optionsNode)) { - // This case handled both `bootstrapApplication(App, appConfig)` and the server () => bootstrapApplication(App, appConfig) - // where appConfig is the result of a `mergeApplicationConfig` call. - - // This is tricky case to handle, in G3 we're might not be able to resolve the identifier's value - // Our best alternative is to assume there is not CD providers set and add the ZoneChangeDetection provider - // In the cases where it is, we'll just override the zone provider we just set by re-used inthe appConfig providers - - // TODO: Should we insert a TODO to clean this up ? const text = `{...${optionsNode.getText()}, providers: [${providerFn}, ...${optionsNode.getText()}.providers]}`; replacements.push( new Replacement( @@ -239,6 +234,63 @@ export class BootstrapOptionsMigration extends TsurgeFunnelMigration< }); } + /** + * The optionsNode can be a appConfig built with mergeApplicationConfig + * In this case we need to analyze if the base config uses provideZonelessChangeDetection + */ + private isServerConfigZoneless(optionsNode: ts.Expression, typeChecker: ts.TypeChecker): boolean { + // Check if optionsNode is a result of mergeApplicationConfig + let symbol = typeChecker.getSymbolAtLocation(optionsNode); + if (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0) { + symbol = typeChecker.getAliasedSymbol(symbol); + } + const optionDeclaration = symbol?.getDeclarations()?.[0]; + if (!optionDeclaration) { + return false; + } + + if ( + !ts.isVariableDeclaration(optionDeclaration) || + !optionDeclaration.initializer || + !ts.isCallExpression(optionDeclaration.initializer) || + !ts.isIdentifier(optionDeclaration.initializer.expression) || + optionDeclaration.initializer.expression.text !== 'mergeApplicationConfig' + ) { + // We didn't find a mergeApplicationConfig call, this isn't a server config + return false; + } + + let maybeAppConfig = optionDeclaration.initializer.arguments[0]; + if (ts.isIdentifier(maybeAppConfig)) { + const resolved = getObjectLiteralFromIdentifier(maybeAppConfig, typeChecker); + if (resolved) { + maybeAppConfig = resolved; + } + } + + if (maybeAppConfig && ts.isObjectLiteralExpression(maybeAppConfig)) { + for (const prop of maybeAppConfig.properties) { + if ( + ts.isPropertyAssignment(prop) && + ts.isIdentifier(prop.name) && + prop.name.text === 'providers' && + ts.isArrayLiteralExpression(prop.initializer) + ) { + for (const el of prop.initializer.elements) { + if ( + ts.isCallExpression(el) && + ts.isIdentifier(el.expression) && + el.expression.text === 'provideZonelessChangeDetection' + ) { + return true; + } + } + } + } + } + return false; + } + private analyzeCreateApplication( node: ts.CallExpression, sourceFile: ts.SourceFile,