Skip to content

Commit d69a6f5

Browse files
authored
fix(material/schematics): use provider functions in ng add (#26920)
Switches to generating `provideAnimations` and `provideNoopAnimations` instead of `importProvidersFrom(BrowserAnimationsModule)` in `ng add`.
1 parent df839e6 commit d69a6f5

File tree

2 files changed

+67
-47
lines changed

2 files changed

+67
-47
lines changed

src/material/schematics/ng-add/index.spec.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -219,47 +219,64 @@ describe('ng-add schematic', () => {
219219
expect(errorOutput[0]).toMatch(/Could not set up "BrowserAnimationsModule"/);
220220
});
221221

222-
it('should add the BrowserAnimationsModule to a bootstrapApplication call', async () => {
222+
it('should add the provideAnimations to a bootstrapApplication call', async () => {
223223
appTree.delete('/projects/material/src/app/app.module.ts');
224+
appTree.create(
225+
'/projects/material/src/app/app.config.ts',
226+
`
227+
export const appConfig = {
228+
providers: [{ provide: 'foo', useValue: 1 }]
229+
};
230+
`,
231+
);
224232
appTree.overwrite(
225233
'/projects/material/src/main.ts',
226234
`
227-
import { importProvidersFrom } from '@angular/core';
228-
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
235+
import { bootstrapApplication } from '@angular/platform-browser';
229236
import { AppComponent } from './app/app.component';
237+
import { appConfig } from './app/app.config';
230238
231-
bootstrapApplication(AppComponent, {
232-
providers: [{provide: 'foo', useValue: 1}, importProvidersFrom(BrowserModule)]
233-
});
239+
bootstrapApplication(AppComponent, appConfig);
234240
`,
235241
);
236242

237243
const tree = await runner.runSchematic('ng-add-setup-project', baseOptions, appTree);
238-
const fileContent = getFileContent(tree, '/projects/material/src/main.ts');
239-
expect(fileContent).toContain('importProvidersFrom(BrowserModule, BrowserAnimationsModule)');
244+
const fileContent = getFileContent(tree, '/projects/material/src/app/app.config.ts');
245+
246+
expect(fileContent).toContain(
247+
`import { provideAnimations } from '@angular/platform-browser/animations';`,
248+
);
249+
expect(fileContent).toContain(`[{ provide: 'foo', useValue: 1 }, provideAnimations()]`);
240250
});
241251

242-
it('should not add BrowserAnimationsModule if NoopAnimationsModule is set up in a bootstrapApplication call', async () => {
252+
it('should not add provideAnimations if provideNoopAnimations is set up in a bootstrapApplication call', async () => {
243253
appTree.delete('/projects/material/src/app/app.module.ts');
254+
appTree.create(
255+
'/projects/material/src/app/app.config.ts',
256+
`
257+
import { provideNoopAnimations } from '@angular/platform-browser/animations';
258+
259+
export const appConfig = {
260+
providers: [{ provide: 'foo', useValue: 1 }, provideNoopAnimations()]
261+
};
262+
`,
263+
);
244264
appTree.overwrite(
245265
'/projects/material/src/main.ts',
246266
`
247-
import { importProvidersFrom } from '@angular/core';
248267
import { bootstrapApplication } from '@angular/platform-browser';
249-
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
250268
import { AppComponent } from './app/app.component';
269+
import { appConfig } from './app/app.config';
251270
252-
bootstrapApplication(AppComponent, {
253-
providers: [{provide: 'foo', useValue: 1}, importProvidersFrom(NoopAnimationsModule)]
254-
});
271+
bootstrapApplication(AppComponent, appConfig);
255272
`,
256273
);
257274

258275
await runner.runSchematic('ng-add-setup-project', baseOptions, appTree);
259276

260277
expect(errorOutput.length).toBe(1);
261278
expect(errorOutput[0]).toMatch(
262-
/Could not set up "BrowserAnimationsModule" because "NoopAnimationsModule" is already imported/,
279+
/Could not add "provideAnimations" because "provideNoopAnimations" is already provided/,
263280
);
264281
});
265282
});

src/material/schematics/ng-add/setup-project.ts

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,16 @@ import {
1717
} from '@angular/cdk/schematics';
1818
import {
1919
importsProvidersFrom,
20-
addModuleImportToStandaloneBootstrap,
20+
findBootstrapApplicationCall,
21+
addFunctionalProvidersToStandaloneBootstrap,
22+
callsProvidersFunction,
2123
} from '@schematics/angular/private/components';
2224
import {getWorkspace, ProjectDefinition} from '@schematics/angular/utility/workspace';
2325
import {ProjectType} from '@schematics/angular/utility/workspace-models';
2426
import {addFontsToIndex} from './fonts/material-fonts';
2527
import {Schema} from './schema';
2628
import {addThemeToAppStyles, addTypographyClass} from './theming/theming';
27-
28-
/** Name of the Angular module that enables Angular browser animations. */
29-
const browserAnimationsModuleName = 'BrowserAnimationsModule';
30-
31-
/** Name of the module that switches Angular animations to a noop implementation. */
32-
const noopAnimationsModuleName = 'NoopAnimationsModule';
29+
import * as ts from 'typescript';
3330

3431
/**
3532
* Scaffolds the basics of a Angular Material application, this includes:
@@ -70,57 +67,60 @@ function addAnimationsModule(options: Schema) {
7067
return async (host: Tree, context: SchematicContext) => {
7168
const workspace = await getWorkspace(host);
7269
const project = getProjectFromWorkspace(workspace, options.project);
70+
const mainFilePath = getProjectMainFile(project);
71+
const mainSourceFile = ts.createSourceFile(
72+
mainFilePath,
73+
host.readText(mainFilePath),
74+
ts.ScriptTarget.Latest,
75+
);
7376

74-
try {
75-
addAnimationsModuleToNonStandaloneApp(host, project, context, options);
76-
} catch (e) {
77-
if ((e as {message?: string}).message?.includes('Bootstrap call not found')) {
78-
addAnimationsModuleToStandaloneApp(host, project, context, options);
79-
} else {
80-
throw e;
81-
}
77+
if (findBootstrapApplicationCall(mainSourceFile)) {
78+
addAnimationsToStandaloneApp(host, mainFilePath, context, options);
79+
} else {
80+
addAnimationsToNonStandaloneApp(host, project, mainFilePath, context, options);
8281
}
8382
};
8483
}
8584

8685
/** Adds the animations module to an app that is bootstrap using the standalone component APIs. */
87-
function addAnimationsModuleToStandaloneApp(
86+
function addAnimationsToStandaloneApp(
8887
host: Tree,
89-
project: ProjectDefinition,
88+
mainFile: string,
9089
context: SchematicContext,
9190
options: Schema,
9291
) {
93-
const mainFile = getProjectMainFile(project);
92+
const animationsFunction = 'provideAnimations';
93+
const noopAnimationsFunction = 'provideNoopAnimations';
9494

9595
if (options.animations === 'enabled') {
96-
// In case the project explicitly uses the NoopAnimationsModule, we should print a warning
96+
// In case the project explicitly uses provideNoopAnimations, we should print a warning
9797
// message that makes the user aware of the fact that we won't automatically set up
98-
// animations. If we would add the BrowserAnimationsModule while the NoopAnimationsModule
98+
// animations. If we would add provideAnimations while provideNoopAnimations
9999
// is already configured, we would cause unexpected behavior and runtime exceptions.
100-
if (importsProvidersFrom(host, mainFile, noopAnimationsModuleName)) {
100+
if (callsProvidersFunction(host, mainFile, noopAnimationsFunction)) {
101101
context.logger.error(
102-
`Could not set up "${browserAnimationsModuleName}" ` +
103-
`because "${noopAnimationsModuleName}" is already imported.`,
102+
`Could not add "${animationsFunction}" ` +
103+
`because "${noopAnimationsFunction}" is already provided.`,
104104
);
105105
context.logger.info(`Please manually set up browser animations.`);
106106
} else {
107-
addModuleImportToStandaloneBootstrap(
107+
addFunctionalProvidersToStandaloneBootstrap(
108108
host,
109109
mainFile,
110-
browserAnimationsModuleName,
110+
animationsFunction,
111111
'@angular/platform-browser/animations',
112112
);
113113
}
114114
} else if (
115115
options.animations === 'disabled' &&
116-
!importsProvidersFrom(host, mainFile, browserAnimationsModuleName)
116+
!importsProvidersFrom(host, mainFile, animationsFunction)
117117
) {
118-
// Do not add the NoopAnimationsModule module if the project already explicitly uses
119-
// the BrowserAnimationsModule.
120-
addModuleImportToStandaloneBootstrap(
118+
// Do not add the provideNoopAnimations if the project already explicitly uses
119+
// the provideAnimations.
120+
addFunctionalProvidersToStandaloneBootstrap(
121121
host,
122122
mainFile,
123-
noopAnimationsModuleName,
123+
noopAnimationsFunction,
124124
'@angular/platform-browser/animations',
125125
);
126126
}
@@ -130,13 +130,16 @@ function addAnimationsModuleToStandaloneApp(
130130
* Adds the animations module to an app that is bootstrap
131131
* using the non-standalone component APIs.
132132
*/
133-
function addAnimationsModuleToNonStandaloneApp(
133+
function addAnimationsToNonStandaloneApp(
134134
host: Tree,
135135
project: ProjectDefinition,
136+
mainFile: string,
136137
context: SchematicContext,
137138
options: Schema,
138139
) {
139-
const appModulePath = getAppModulePath(host, getProjectMainFile(project));
140+
const browserAnimationsModuleName = 'BrowserAnimationsModule';
141+
const noopAnimationsModuleName = 'NoopAnimationsModule';
142+
const appModulePath = getAppModulePath(host, mainFile);
140143

141144
if (options.animations === 'enabled') {
142145
// In case the project explicitly uses the NoopAnimationsModule, we should print a warning

0 commit comments

Comments
 (0)