Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Angular: Support v19.2 when @angular/animations is not installed
  • Loading branch information
valentinpalkovic committed Feb 21, 2025
commit d0b17133abb3accd9f8a9a0efc7f0a5bd962c81e
3 changes: 2 additions & 1 deletion code/builders/builder-webpack5/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ import prettyTime from 'pretty-hrtime';
import sirv from 'sirv';
import { corePath } from 'storybook/core-path';
import type { Configuration, Stats, StatsOptions } from 'webpack';
import webpackDep, { DefinePlugin, ProgressPlugin } from 'webpack';
import webpackDep, { DefinePlugin, IgnorePlugin, ProgressPlugin } from 'webpack';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';

export * from './types';
export * from './preview/virtual-module-mapping';

export const WebpackDefinePlugin = DefinePlugin;
export const WebpackIgnorePlugin = IgnorePlugin;

export const printDuration = (startTime: [number, number]) =>
prettyTime(process.hrtime(startTime))
Expand Down
4 changes: 4 additions & 0 deletions code/frameworks/angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"@angular-devkit/architect": ">=0.1500.0 < 0.2000.0",
"@angular-devkit/build-angular": ">=15.0.0 < 20.0.0",
"@angular-devkit/core": ">=15.0.0 < 20.0.0",
"@angular/animations": ">=15.0.0 < 20.0.0",
"@angular/cli": ">=15.0.0 < 20.0.0",
"@angular/common": ">=15.0.0 < 20.0.0",
"@angular/compiler": ">=15.0.0 < 20.0.0",
Expand All @@ -115,6 +116,9 @@
"zone.js": ">= 0.11.1 < 1.0.0"
},
"peerDependenciesMeta": {
"@angular/animations": {
"optional": true
},
"@angular/cli": {
"optional": true
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export abstract class AbstractRenderer {
this.initAngularRootElement(targetDOMNode, targetSelector);

const analyzedMetadata = new PropertyExtractor(storyFnAngular.moduleMetadata, component);
await analyzedMetadata.init();

const storyUid = this.generateStoryUIdFromRawStoryUid(
targetDOMNode.getAttribute(STORY_UID_ATTRIBUTE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@ import {
VERSION,
} from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import {
BrowserAnimationsModule,
NoopAnimationsModule,
provideAnimations,
provideNoopAnimations,
} from '@angular/platform-browser/animations';
import { dedent } from 'ts-dedent';

import { NgModuleMetadata } from '../../types';
Expand All @@ -45,9 +39,7 @@ export class PropertyExtractor implements NgModuleMetadata {
constructor(
private metadata: NgModuleMetadata,
private component?: any
) {
this.init();
}
) {}

// With the new way of mounting standalone components to the DOM via bootstrapApplication API,
// we should now pass ModuleWithProviders to the providers array of the bootstrapApplication function.
Expand All @@ -71,8 +63,8 @@ export class PropertyExtractor implements NgModuleMetadata {
}
}

private init() {
const analyzed = this.analyzeMetadata(this.metadata);
public async init() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: init() is now public and async but constructor no longer calls it - this could cause issues if callers forget to call init()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of requiring everyone to call PropertyExtractor.init() manually after constructing, maybe you can do:

  constructor(
    private metadata: NgModuleMetadata,
    private component?: any
  ) {
    return this.init().then(() => this);
  }

so you can do await new PropertyExtractor(), that will run init and return the instance.

const analyzed = await this.analyzeMetadata(this.metadata);
this.imports = uniqueArray([CommonModule, analyzed.imports]);
this.providers = uniqueArray(analyzed.providers);
this.applicationProviders = uniqueArray(analyzed.applicationProviders);
Expand Down Expand Up @@ -101,28 +93,28 @@ export class PropertyExtractor implements NgModuleMetadata {
* - Extracts providers from ModuleWithProviders
* - Returns a new NgModuleMetadata object
*/
private analyzeMetadata = (metadata: NgModuleMetadata) => {
private analyzeMetadata = async (metadata: NgModuleMetadata) => {
const declarations = [...(metadata?.declarations || [])];
const providers = [...(metadata?.providers || [])];
const applicationProviders: Provider[] = [];
const imports = [...(metadata?.imports || [])].reduce((acc, imported) => {
// remove ngModule and use only its providers if it is restricted
// (e.g. BrowserModule, BrowserAnimationsModule, NoopAnimationsModule, ...etc)
const [isRestricted, restrictedProviders] = PropertyExtractor.analyzeRestricted(imported);
if (isRestricted) {
applicationProviders.unshift(restrictedProviders || []);
return acc;
}

acc.push(imported);

return acc;
}, []);
const imports = await Promise.all(
[...(metadata?.imports || [])].map(async (imported) => {
const [isRestricted, restrictedProviders] =
await PropertyExtractor.analyzeRestricted(imported);
if (isRestricted) {
applicationProviders.unshift(restrictedProviders || []);
return null;
}
return imported;
})
).then((results) => results.filter(Boolean));

return { ...metadata, imports, providers, applicationProviders, declarations };
};

static analyzeRestricted = (ngModule: NgModule): [boolean] | [boolean, Provider] => {
static analyzeRestricted = async (
ngModule: NgModule
): Promise<[boolean] | [boolean, Provider]> => {
if (ngModule === BrowserModule) {
console.warn(
dedent`
Expand All @@ -136,32 +128,38 @@ export class PropertyExtractor implements NgModuleMetadata {
return [true];
}

if (ngModule === BrowserAnimationsModule) {
console.warn(
dedent`
Storybook Warning:
You have added the "BrowserAnimationsModule" to the list of "imports" in your moduleMetadata definition of your Story.
In Storybook 7.0 we use Angular's new 'bootstrapApplication' API to mount the component to the DOM, which accepts a list of providers to set up application-wide providers.
Use the 'applicationConfig' decorator from '@storybook/angular' and add the "provideAnimations" function to the list of "providers".
If your Angular version does not support "provide-like" functions, use the helper function importProvidersFrom instead to set up animations. For this case, please add "importProvidersFrom(BrowserAnimationsModule)" to the list of providers of your applicationConfig definition.
Please visit https://angular.io/guide/standalone-components#configuring-dependency-injection for more information.
`
);
return [true, provideAnimations()];
}
try {
const animations = await import('@angular/platform-browser/animations');

if (ngModule === animations.BrowserAnimationsModule) {
console.warn(
dedent`
Storybook Warning:
You have added the "BrowserAnimationsModule" to the list of "imports" in your moduleMetadata definition of your Story.
In Storybook 7.0 we use Angular's new 'bootstrapApplication' API to mount the component to the DOM, which accepts a list of providers to set up application-wide providers.
Use the 'applicationConfig' decorator from '@storybook/angular' and add the "provideAnimations" function to the list of "providers".
If your Angular version does not support "provide-like" functions, use the helper function importProvidersFrom instead to set up animations. For this case, please add "importProvidersFrom(BrowserAnimationsModule)" to the list of providers of your applicationConfig definition.
Please visit https://angular.io/guide/standalone-components#configuring-dependency-injection for more information.
`
);
return [true, animations.provideAnimations()];
}

if (ngModule === NoopAnimationsModule) {
console.warn(
dedent`
Storybook Warning:
You have added the "NoopAnimationsModule" to the list of "imports" in your moduleMetadata definition of your Story.
In Storybook v7.0 we are using Angular's new bootstrapApplication API to mount an Angular application to the DOM, which accepts a list of providers to set up application-wide providers.
Use the 'applicationConfig' decorator from '@storybook/angular' and add the "provideNoopAnimations" function to the list of "providers".
If your Angular version does not support "provide-like" functions, use the helper function importProvidersFrom instead to set up noop animations and to extract all necessary providers from NoopAnimationsModule. For this case, please add "importProvidersFrom(NoopAnimationsModule)" to the list of providers of your applicationConfig definition.
Please visit https://angular.io/guide/standalone-components#configuring-dependency-injection for more information.
`
);
return [true, provideNoopAnimations()];
if (ngModule === animations.NoopAnimationsModule) {
console.warn(
dedent`
Storybook Warning:
You have added the "NoopAnimationsModule" to the list of "imports" in your moduleMetadata definition of your Story.
In Storybook v7.0 we are using Angular's new bootstrapApplication API to mount an Angular application to the DOM, which accepts a list of providers to set up application-wide providers.
Use the 'applicationConfig' decorator from '@storybook/angular' and add the "provideNoopAnimations" function to the list of "providers".
If your Angular version does not support "provide-like" functions, use the helper function importProvidersFrom instead to set up noop animations and to extract all necessary providers from NoopAnimationsModule. For this case, please add "importProvidersFrom(NoopAnimationsModule)" to the list of providers of your applicationConfig definition.
Please visit https://angular.io/guide/standalone-components#configuring-dependency-injection for more information.
`
);
return [true, animations.provideNoopAnimations()];
}
} catch (e) {
return [false];
}

return [false];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { logger } from 'storybook/internal/node-logger';
import { AngularLegacyBuildOptionsError } from 'storybook/internal/server-errors';
import { WebpackDefinePlugin } from '@storybook/builder-webpack5';
import { WebpackDefinePlugin, WebpackIgnorePlugin } from '@storybook/builder-webpack5';

import { BuilderContext, targetFromTargetString } from '@angular-devkit/architect';
import { JsonObject, logging } from '@angular-devkit/core';
Expand Down Expand Up @@ -40,6 +40,16 @@ export async function webpackFinal(baseConfig: webpack.Configuration, options: P
})
);

try {
await import('@angular/platform-browser/animations');
} catch (e) {
webpackConfig.plugins.push(
new WebpackIgnorePlugin({
resourceRegExp: /@angular\/platform-browser\/animations$/,
})
);
}
Comment on lines +43 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider catching a more specific error type than just 'e' to avoid accidentally catching unrelated errors


return webpackConfig;
}

Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Removing this template file will break the OpenCloseComponent and its stories unless corresponding changes are made to move this template inline or provide an alternative.

This file was deleted.

This file was deleted.

Loading
Loading