Skip to content

Commit

Permalink
fix(material/schematics): use provider functions in ng add (#26920)
Browse files Browse the repository at this point in the history
Switches to generating `provideAnimations` and `provideNoopAnimations` instead of `importProvidersFrom(BrowserAnimationsModule)` in `ng add`.

(cherry picked from commit d69a6f5)
  • Loading branch information
crisbeto committed Apr 13, 2023
1 parent 987bdd3 commit 22410a9
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 47 deletions.
47 changes: 32 additions & 15 deletions src/material/schematics/ng-add/index.spec.ts
Expand Up @@ -219,47 +219,64 @@ describe('ng-add schematic', () => {
expect(errorOutput[0]).toMatch(/Could not set up "BrowserAnimationsModule"/);
});

it('should add the BrowserAnimationsModule to a bootstrapApplication call', async () => {
it('should add the provideAnimations to a bootstrapApplication call', async () => {
appTree.delete('/projects/material/src/app/app.module.ts');
appTree.create(
'/projects/material/src/app/app.config.ts',
`
export const appConfig = {
providers: [{ provide: 'foo', useValue: 1 }]
};
`,
);
appTree.overwrite(
'/projects/material/src/main.ts',
`
import { importProvidersFrom } from '@angular/core';
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, {
providers: [{provide: 'foo', useValue: 1}, importProvidersFrom(BrowserModule)]
});
bootstrapApplication(AppComponent, appConfig);
`,
);

const tree = await runner.runSchematic('ng-add-setup-project', baseOptions, appTree);
const fileContent = getFileContent(tree, '/projects/material/src/main.ts');
expect(fileContent).toContain('importProvidersFrom(BrowserModule, BrowserAnimationsModule)');
const fileContent = getFileContent(tree, '/projects/material/src/app/app.config.ts');

expect(fileContent).toContain(
`import { provideAnimations } from '@angular/platform-browser/animations';`,
);
expect(fileContent).toContain(`[{ provide: 'foo', useValue: 1 }, provideAnimations()]`);
});

it('should not add BrowserAnimationsModule if NoopAnimationsModule is set up in a bootstrapApplication call', async () => {
it('should not add provideAnimations if provideNoopAnimations is set up in a bootstrapApplication call', async () => {
appTree.delete('/projects/material/src/app/app.module.ts');
appTree.create(
'/projects/material/src/app/app.config.ts',
`
import { provideNoopAnimations } from '@angular/platform-browser/animations';
export const appConfig = {
providers: [{ provide: 'foo', useValue: 1 }, provideNoopAnimations()]
};
`,
);
appTree.overwrite(
'/projects/material/src/main.ts',
`
import { importProvidersFrom } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, {
providers: [{provide: 'foo', useValue: 1}, importProvidersFrom(NoopAnimationsModule)]
});
bootstrapApplication(AppComponent, appConfig);
`,
);

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

expect(errorOutput.length).toBe(1);
expect(errorOutput[0]).toMatch(
/Could not set up "BrowserAnimationsModule" because "NoopAnimationsModule" is already imported/,
/Could not add "provideAnimations" because "provideNoopAnimations" is already provided/,
);
});
});
Expand Down
67 changes: 35 additions & 32 deletions src/material/schematics/ng-add/setup-project.ts
Expand Up @@ -17,19 +17,16 @@ import {
} from '@angular/cdk/schematics';
import {
importsProvidersFrom,
addModuleImportToStandaloneBootstrap,
findBootstrapApplicationCall,
addFunctionalProvidersToStandaloneBootstrap,
callsProvidersFunction,
} from '@schematics/angular/private/components';
import {getWorkspace, ProjectDefinition} from '@schematics/angular/utility/workspace';
import {ProjectType} from '@schematics/angular/utility/workspace-models';
import {addFontsToIndex} from './fonts/material-fonts';
import {Schema} from './schema';
import {addThemeToAppStyles, addTypographyClass} from './theming/theming';

/** Name of the Angular module that enables Angular browser animations. */
const browserAnimationsModuleName = 'BrowserAnimationsModule';

/** Name of the module that switches Angular animations to a noop implementation. */
const noopAnimationsModuleName = 'NoopAnimationsModule';
import * as ts from 'typescript';

/**
* Scaffolds the basics of a Angular Material application, this includes:
Expand Down Expand Up @@ -70,57 +67,60 @@ function addAnimationsModule(options: Schema) {
return async (host: Tree, context: SchematicContext) => {
const workspace = await getWorkspace(host);
const project = getProjectFromWorkspace(workspace, options.project);
const mainFilePath = getProjectMainFile(project);
const mainSourceFile = ts.createSourceFile(
mainFilePath,
host.readText(mainFilePath),
ts.ScriptTarget.Latest,
);

try {
addAnimationsModuleToNonStandaloneApp(host, project, context, options);
} catch (e) {
if ((e as {message?: string}).message?.includes('Bootstrap call not found')) {
addAnimationsModuleToStandaloneApp(host, project, context, options);
} else {
throw e;
}
if (findBootstrapApplicationCall(mainSourceFile)) {
addAnimationsToStandaloneApp(host, mainFilePath, context, options);
} else {
addAnimationsToNonStandaloneApp(host, project, mainFilePath, context, options);
}
};
}

/** Adds the animations module to an app that is bootstrap using the standalone component APIs. */
function addAnimationsModuleToStandaloneApp(
function addAnimationsToStandaloneApp(
host: Tree,
project: ProjectDefinition,
mainFile: string,
context: SchematicContext,
options: Schema,
) {
const mainFile = getProjectMainFile(project);
const animationsFunction = 'provideAnimations';
const noopAnimationsFunction = 'provideNoopAnimations';

if (options.animations === 'enabled') {
// In case the project explicitly uses the NoopAnimationsModule, we should print a warning
// In case the project explicitly uses provideNoopAnimations, we should print a warning
// message that makes the user aware of the fact that we won't automatically set up
// animations. If we would add the BrowserAnimationsModule while the NoopAnimationsModule
// animations. If we would add provideAnimations while provideNoopAnimations
// is already configured, we would cause unexpected behavior and runtime exceptions.
if (importsProvidersFrom(host, mainFile, noopAnimationsModuleName)) {
if (callsProvidersFunction(host, mainFile, noopAnimationsFunction)) {
context.logger.error(
`Could not set up "${browserAnimationsModuleName}" ` +
`because "${noopAnimationsModuleName}" is already imported.`,
`Could not add "${animationsFunction}" ` +
`because "${noopAnimationsFunction}" is already provided.`,
);
context.logger.info(`Please manually set up browser animations.`);
} else {
addModuleImportToStandaloneBootstrap(
addFunctionalProvidersToStandaloneBootstrap(
host,
mainFile,
browserAnimationsModuleName,
animationsFunction,
'@angular/platform-browser/animations',
);
}
} else if (
options.animations === 'disabled' &&
!importsProvidersFrom(host, mainFile, browserAnimationsModuleName)
!importsProvidersFrom(host, mainFile, animationsFunction)
) {
// Do not add the NoopAnimationsModule module if the project already explicitly uses
// the BrowserAnimationsModule.
addModuleImportToStandaloneBootstrap(
// Do not add the provideNoopAnimations if the project already explicitly uses
// the provideAnimations.
addFunctionalProvidersToStandaloneBootstrap(
host,
mainFile,
noopAnimationsModuleName,
noopAnimationsFunction,
'@angular/platform-browser/animations',
);
}
Expand All @@ -130,13 +130,16 @@ function addAnimationsModuleToStandaloneApp(
* Adds the animations module to an app that is bootstrap
* using the non-standalone component APIs.
*/
function addAnimationsModuleToNonStandaloneApp(
function addAnimationsToNonStandaloneApp(
host: Tree,
project: ProjectDefinition,
mainFile: string,
context: SchematicContext,
options: Schema,
) {
const appModulePath = getAppModulePath(host, getProjectMainFile(project));
const browserAnimationsModuleName = 'BrowserAnimationsModule';
const noopAnimationsModuleName = 'NoopAnimationsModule';
const appModulePath = getAppModulePath(host, mainFile);

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

0 comments on commit 22410a9

Please sign in to comment.