Merge remote-tracking branch 'origin/next' into valentin/introduce-instances-vitest-3

This commit is contained in:
Valentin Palkovic 2025-01-20 16:05:59 +01:00
commit 678fb8fb5d
17 changed files with 3980 additions and 1414 deletions

View File

@ -1,7 +1,7 @@
<h1>Migration</h1> <h1>Migration</h1>
- [From version 8.4.x to 8.5.x](#from-version-84x-to-85x) - [From version 8.5.x to 8.6.x](#from-version-85x-to-86x)
- [Introducing features.developmentModeForBuild](#introducing-featuresdevelopmentmodeforbuild) - [Angular: Support experimental zoneless support](#angular-support-experimental-zoneless-support)
- [Added source code panel to docs](#added-source-code-panel-to-docs) - [Added source code panel to docs](#added-source-code-panel-to-docs)
- [Addon-a11y: Component test integration](#addon-a11y-component-test-integration) - [Addon-a11y: Component test integration](#addon-a11y-component-test-integration)
- [Addon-a11y: Changing the default element selector](#addon-a11y-changing-the-default-element-selector) - [Addon-a11y: Changing the default element selector](#addon-a11y-changing-the-default-element-selector)
@ -427,6 +427,37 @@
- [Packages renaming](#packages-renaming) - [Packages renaming](#packages-renaming)
- [Deprecated embedded addons](#deprecated-embedded-addons) - [Deprecated embedded addons](#deprecated-embedded-addons)
## From version 8.5.x to 8.6.x
### Angular: Support experimental zoneless support
Storybook now supports [Angular's experimental zoneless mode](https://angular.dev/guide/experimental/zoneless). This mode is intended to improve performance by removing Angular's zone.js dependency. To enable zoneless mode in your Angular Storybook, set the `experimentalZoneless` config in your `angular.json` file:
````diff
{
"projects": {
"your-project": {
"architect": {
"storybook": {
...
"options": {
...
+ "experimentalZoneless": true
}
}
"build-storybook": {
...
"options": {
...
+ "experimentalZoneless": true
}
}
}
}
}
}
```
## From version 8.4.x to 8.5.x ## From version 8.4.x to 8.5.x
### Introducing features.developmentModeForBuild ### Introducing features.developmentModeForBuild
@ -442,7 +473,7 @@ export default {
developmentModeForBuild: true, developmentModeForBuild: true,
}, },
}; };
``` ````
### Added source code panel to docs ### Added source code panel to docs

View File

@ -58,27 +58,28 @@
"webpack": "5" "webpack": "5"
}, },
"devDependencies": { "devDependencies": {
"@analogjs/vite-plugin-angular": "^0.2.24", "@analogjs/vite-plugin-angular": "^1.12.1",
"@angular-devkit/architect": "^0.1703.0", "@angular-devkit/architect": "^0.1901.1",
"@angular-devkit/build-angular": "^17.3.0", "@angular-devkit/build-angular": "^19.1.1",
"@angular-devkit/core": "^17.3.0", "@angular-devkit/core": "^19.1.1",
"@angular/animations": "^17.3.0", "@angular/animations": "^19.1.1",
"@angular/cli": "^17.3.0", "@angular/cli": "^19.1.1",
"@angular/common": "^17.3.0", "@angular/common": "^19.1.1",
"@angular/compiler": "^17.3.0", "@angular/compiler": "^19.1.1",
"@angular/compiler-cli": "^17.3.0", "@angular/compiler-cli": "^19.1.1",
"@angular/core": "^17.3.0", "@angular/core": "^19.1.1",
"@angular/forms": "^17.3.0", "@angular/forms": "^19.1.1",
"@angular/platform-browser": "^17.3.0", "@angular/platform-browser": "^19.1.1",
"@angular/platform-browser-dynamic": "^17.3.0", "@angular/platform-browser-dynamic": "^19.1.1",
"@types/cross-spawn": "^6.0.2", "@types/cross-spawn": "^6.0.2",
"@types/node": "^22.0.0", "@types/node": "^22.0.0",
"@types/tmp": "^0.2.3", "@types/tmp": "^0.2.3",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"rimraf": "^6.0.1",
"tmp": "^0.2.1", "tmp": "^0.2.1",
"typescript": "^5.3.2", "typescript": "^5.3.2",
"webpack": "5", "webpack": "5",
"zone.js": "^0.14.2" "zone.js": "^0.15.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular-devkit/architect": ">=0.1500.0 < 0.2000.0", "@angular-devkit/architect": ">=0.1500.0 < 0.2000.0",
@ -100,6 +101,9 @@
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@angular/cli": { "@angular/cli": {
"optional": true "optional": true
},
"zone.js": {
"optional": true
} }
}, },
"engines": { "engines": {

View File

@ -43,6 +43,7 @@ export type StorybookBuilderOptions = JsonObject & {
preserveSymlinks?: boolean; preserveSymlinks?: boolean;
assets?: AssetPattern[]; assets?: AssetPattern[];
sourceMap?: SourceMapUnion; sourceMap?: SourceMapUnion;
experimentalZoneless?: boolean;
} & Pick< } & Pick<
// makes sure the option exists // makes sure the option exists
CLIOptions, CLIOptions,
@ -104,6 +105,7 @@ const commandBuilder: BuilderHandlerFn<StorybookBuilderOptions> = (
previewUrl, previewUrl,
sourceMap = false, sourceMap = false,
preserveSymlinks = false, preserveSymlinks = false,
experimentalZoneless = false,
} = options; } = options;
const standaloneOptions: StandaloneBuildOptions = { const standaloneOptions: StandaloneBuildOptions = {
@ -124,6 +126,7 @@ const commandBuilder: BuilderHandlerFn<StorybookBuilderOptions> = (
...(assets ? { assets } : {}), ...(assets ? { assets } : {}),
sourceMap, sourceMap,
preserveSymlinks, preserveSymlinks,
experimentalZoneless,
}, },
tsConfig, tsConfig,
webpackStatsJson, webpackStatsJson,

View File

@ -121,6 +121,11 @@
"type": ["boolean", "object"], "type": ["boolean", "object"],
"description": "Configure sourcemaps. See: https://angular.io/guide/workspace-config#source-map-configuration", "description": "Configure sourcemaps. See: https://angular.io/guide/workspace-config#source-map-configuration",
"default": false "default": false
},
"experimentalZoneless": {
"type": "boolean",
"description": "Experimental: Use zoneless change detection.",
"default": false
} }
}, },
"additionalProperties": false, "additionalProperties": false,

View File

@ -40,6 +40,7 @@ export type StorybookBuilderOptions = JsonObject & {
assets?: AssetPattern[]; assets?: AssetPattern[];
preserveSymlinks?: boolean; preserveSymlinks?: boolean;
sourceMap?: SourceMapUnion; sourceMap?: SourceMapUnion;
experimentalZoneless?: boolean;
} & Pick< } & Pick<
// makes sure the option exists // makes sure the option exists
CLIOptions, CLIOptions,
@ -121,6 +122,7 @@ const commandBuilder: BuilderHandlerFn<StorybookBuilderOptions> = (options, cont
previewUrl, previewUrl,
sourceMap = false, sourceMap = false,
preserveSymlinks = false, preserveSymlinks = false,
experimentalZoneless = false,
} = options; } = options;
const standaloneOptions: StandaloneOptions = { const standaloneOptions: StandaloneOptions = {
@ -146,6 +148,7 @@ const commandBuilder: BuilderHandlerFn<StorybookBuilderOptions> = (options, cont
...(assets ? { assets } : {}), ...(assets ? { assets } : {}),
preserveSymlinks, preserveSymlinks,
sourceMap, sourceMap,
experimentalZoneless,
}, },
tsConfig, tsConfig,
initialPath, initialPath,

View File

@ -157,6 +157,11 @@
"type": ["boolean", "object"], "type": ["boolean", "object"],
"description": "Configure sourcemaps. See: https://angular.io/guide/workspace-config#source-map-configuration", "description": "Configure sourcemaps. See: https://angular.io/guide/workspace-config#source-map-configuration",
"default": false "default": false
},
"experimentalZoneless": {
"type": "boolean",
"description": "Experimental: Use zoneless change detection.",
"default": false
} }
}, },
"additionalProperties": false, "additionalProperties": false,

View File

@ -20,6 +20,7 @@ export type StandaloneOptions = CLIOptions &
assets?: AssetPattern[]; assets?: AssetPattern[];
sourceMap?: SourceMapUnion; sourceMap?: SourceMapUnion;
preserveSymlinks?: boolean; preserveSymlinks?: boolean;
experimentalZoneless?: boolean;
}; };
angularBuilderContext?: BuilderContext | null; angularBuilderContext?: BuilderContext | null;
tsConfig?: string; tsConfig?: string;

View File

@ -1,4 +1,4 @@
import { ApplicationRef, NgModule, enableProdMode } from '@angular/core'; import { ApplicationRef, NgModule } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser'; import { bootstrapApplication } from '@angular/platform-browser';
import { BehaviorSubject, Subject } from 'rxjs'; import { BehaviorSubject, Subject } from 'rxjs';
import { stringify } from 'telejson'; import { stringify } from 'telejson';
@ -14,6 +14,12 @@ type StoryRenderInfo = {
moduleMetadataSnapshot: string; moduleMetadataSnapshot: string;
}; };
declare global {
const STORYBOOK_ANGULAR_OPTIONS: {
experimentalZoneless: boolean;
};
}
const applicationRefs = new Map<HTMLElement, ApplicationRef>(); const applicationRefs = new Map<HTMLElement, ApplicationRef>();
/** /**
@ -112,14 +118,25 @@ export abstract class AbstractRenderer {
analyzedMetadata, analyzedMetadata,
}); });
const providers = [
storyPropsProvider(newStoryProps$),
...analyzedMetadata.applicationProviders,
...(storyFnAngular.applicationConfig?.providers ?? []),
];
if (STORYBOOK_ANGULAR_OPTIONS?.experimentalZoneless) {
const { provideExperimentalZonelessChangeDetection } = await import('@angular/core');
if (!provideExperimentalZonelessChangeDetection) {
throw new Error('Experimental zoneless change detection requires Angular 18 or higher');
} else {
providers.unshift(provideExperimentalZonelessChangeDetection());
}
}
const applicationRef = await queueBootstrapping(() => { const applicationRef = await queueBootstrapping(() => {
return bootstrapApplication(application, { return bootstrapApplication(application, {
...storyFnAngular.applicationConfig, ...storyFnAngular.applicationConfig,
providers: [ providers,
storyPropsProvider(newStoryProps$),
...analyzedMetadata.applicationProviders,
...(storyFnAngular.applicationConfig?.providers ?? []),
],
}); });
}); });

View File

@ -26,6 +26,8 @@ describe('RendererFactory', () => {
rootDocstargetDOMNode = global.document.getElementById('root-docs'); rootDocstargetDOMNode = global.document.getElementById('root-docs');
(platformBrowserDynamic as any).mockImplementation(platformBrowserDynamicTesting); (platformBrowserDynamic as any).mockImplementation(platformBrowserDynamicTesting);
vi.spyOn(console, 'log').mockImplementation(() => {}); vi.spyOn(console, 'log').mockImplementation(() => {});
// @ts-expect-error Ignore
globalThis.STORYBOOK_ANGULAR_OPTIONS = { experimentalZoneless: false };
}); });
afterEach(() => { afterEach(() => {

View File

@ -11,6 +11,8 @@ import {
Input, Input,
Output, Output,
Pipe, Pipe,
input,
output,
} from '@angular/core'; } from '@angular/core';
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
@ -26,7 +28,9 @@ import {
describe('getComponentInputsOutputs', () => { describe('getComponentInputsOutputs', () => {
it('should return empty if no I/O found', () => { it('should return empty if no I/O found', () => {
@Component({}) @Component({
standalone: false,
})
class FooComponent {} class FooComponent {}
expect(getComponentInputsOutputs(FooComponent)).toEqual({ expect(getComponentInputsOutputs(FooComponent)).toEqual({
@ -47,11 +51,18 @@ describe('getComponentInputsOutputs', () => {
template: '', template: '',
inputs: ['inputInComponentMetadata'], inputs: ['inputInComponentMetadata'],
outputs: ['outputInComponentMetadata'], outputs: ['outputInComponentMetadata'],
standalone: false,
}) })
class FooComponent { class FooComponent {
@Input() @Input()
public input: string; public input: string;
public signalInput = input<string>();
public signalInputAliased = input<string>('signalInputAliased', {
alias: 'signalInputAliasedAlias',
});
@Input('inputPropertyName') @Input('inputPropertyName')
public inputWithBindingPropertyName: string; public inputWithBindingPropertyName: string;
@ -60,6 +71,8 @@ describe('getComponentInputsOutputs', () => {
@Output('outputPropertyName') @Output('outputPropertyName')
public outputWithBindingPropertyName = new EventEmitter<Event>(); public outputWithBindingPropertyName = new EventEmitter<Event>();
public signalOutput = output<string>();
} }
const fooComponentFactory = resolveComponentFactory(FooComponent); const fooComponentFactory = resolveComponentFactory(FooComponent);
@ -79,7 +92,9 @@ describe('getComponentInputsOutputs', () => {
], ],
}); });
expect(sortByPropName(inputs)).toEqual(sortByPropName(fooComponentFactory.inputs)); expect(sortByPropName(inputs)).toEqual(
sortByPropName(fooComponentFactory.inputs.map(({ isSignal, ...rest }) => rest))
);
expect(sortByPropName(outputs)).toEqual(sortByPropName(fooComponentFactory.outputs)); expect(sortByPropName(outputs)).toEqual(sortByPropName(fooComponentFactory.outputs));
}); });
@ -88,6 +103,7 @@ describe('getComponentInputsOutputs', () => {
template: '', template: '',
inputs: ['input', 'inputWithBindingPropertyName'], inputs: ['input', 'inputWithBindingPropertyName'],
outputs: ['outputWithBindingPropertyName'], outputs: ['outputWithBindingPropertyName'],
standalone: false,
}) })
class FooComponent { class FooComponent {
@Input() @Input()
@ -107,13 +123,16 @@ describe('getComponentInputsOutputs', () => {
const { inputs, outputs } = getComponentInputsOutputs(FooComponent); const { inputs, outputs } = getComponentInputsOutputs(FooComponent);
expect(sortByPropName(inputs)).toEqual(sortByPropName(fooComponentFactory.inputs)); expect(sortByPropName(inputs)).toEqual(
sortByPropName(fooComponentFactory.inputs.map(({ isSignal, ...rest }) => rest))
);
expect(sortByPropName(outputs)).toEqual(sortByPropName(fooComponentFactory.outputs)); expect(sortByPropName(outputs)).toEqual(sortByPropName(fooComponentFactory.outputs));
}); });
it('should return I/O in the presence of multiple decorators', () => { it('should return I/O in the presence of multiple decorators', () => {
@Component({ @Component({
template: '', template: '',
standalone: false,
}) })
class FooComponent { class FooComponent {
@Input() @Input()
@ -137,13 +156,16 @@ describe('getComponentInputsOutputs', () => {
outputs: [], outputs: [],
}); });
expect(sortByPropName(inputs)).toEqual(sortByPropName(fooComponentFactory.inputs)); expect(sortByPropName(inputs)).toEqual(
sortByPropName(fooComponentFactory.inputs.map(({ isSignal, ...rest }) => rest))
);
expect(sortByPropName(outputs)).toEqual(sortByPropName(fooComponentFactory.outputs)); expect(sortByPropName(outputs)).toEqual(sortByPropName(fooComponentFactory.outputs));
}); });
it('should return I/O with extending classes', () => { it('should return I/O with extending classes', () => {
@Component({ @Component({
template: '', template: '',
standalone: false,
}) })
class BarComponent { class BarComponent {
@Input() @Input()
@ -155,6 +177,7 @@ describe('getComponentInputsOutputs', () => {
@Component({ @Component({
template: '', template: '',
standalone: false,
}) })
class FooComponent extends BarComponent { class FooComponent extends BarComponent {
@Input() @Input()
@ -177,7 +200,9 @@ describe('getComponentInputsOutputs', () => {
outputs: [], outputs: [],
}); });
expect(sortByPropName(inputs)).toEqual(sortByPropName(fooComponentFactory.inputs)); expect(sortByPropName(inputs)).toEqual(
sortByPropName(fooComponentFactory.inputs.map(({ isSignal, ...rest }) => rest))
);
expect(sortByPropName(outputs)).toEqual(sortByPropName(fooComponentFactory.outputs)); expect(sortByPropName(outputs)).toEqual(sortByPropName(fooComponentFactory.outputs));
}); });
}); });

View File

@ -16,15 +16,13 @@ import { PropertyExtractor } from './PropertyExtractor';
const TEST_TOKEN = new InjectionToken('testToken'); const TEST_TOKEN = new InjectionToken('testToken');
const TestTokenProvider = { provide: TEST_TOKEN, useValue: 123 }; const TestTokenProvider = { provide: TEST_TOKEN, useValue: 123 };
const TestService = Injectable()(class {}); const TestService = Injectable()(class {});
const TestComponent1 = Component({})(class {}); const TestComponent1 = Component({ standalone: false })(class {});
const TestComponent2 = Component({})(class {}); const TestComponent2 = Component({ standalone: false })(class {});
const StandaloneTestComponent = Component({ standalone: true })(class {}); const StandaloneTestComponent = Component({})(class {});
const StandaloneTestDirective = Directive({ standalone: true })(class {}); const StandaloneTestDirective = Directive({})(class {});
const MixedTestComponent1 = Component({ standalone: true })( const MixedTestComponent1 = Component({})(class extends StandaloneTestComponent {});
class extends StandaloneTestComponent {} const MixedTestComponent2 = Component({ standalone: false })(class extends MixedTestComponent1 {});
); const MixedTestComponent3 = Component({})(class extends MixedTestComponent2 {});
const MixedTestComponent2 = Component({})(class extends MixedTestComponent1 {});
const MixedTestComponent3 = Component({ standalone: true })(class extends MixedTestComponent2 {});
const TestModuleWithDeclarations = NgModule({ declarations: [TestComponent1] })(class {}); const TestModuleWithDeclarations = NgModule({ declarations: [TestComponent1] })(class {});
const TestModuleWithImportsAndProviders = NgModule({ const TestModuleWithImportsAndProviders = NgModule({
imports: [TestModuleWithDeclarations], imports: [TestModuleWithDeclarations],

View File

@ -22,9 +22,6 @@ import { global } from '@storybook/global';
*/ */
// import 'web-animations-js'; // Run `npm install --save web-animations-js`. // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/** Zone JS is required by Angular itself. */
import 'zone.js';
// Included with Angular CLI. // Included with Angular CLI.
/** APPLICATION IMPORTS */ /** APPLICATION IMPORTS */

View File

@ -3,7 +3,6 @@ import { PresetProperty } from 'storybook/internal/types';
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
import { StandaloneOptions } from './builders/utils/standalone-options'; import { StandaloneOptions } from './builders/utils/standalone-options';
import { StorybookConfig } from './types';
const getAbsolutePath = <I extends string>(input: I): I => const getAbsolutePath = <I extends string>(input: I): I =>
dirname(require.resolve(join(input, 'package.json'))) as any; dirname(require.resolve(join(input, 'package.json'))) as any;

View File

@ -1,5 +1,6 @@
import { logger } from 'storybook/internal/node-logger'; import { logger } from 'storybook/internal/node-logger';
import { AngularLegacyBuildOptionsError } from 'storybook/internal/server-errors'; import { AngularLegacyBuildOptionsError } from 'storybook/internal/server-errors';
import { WebpackDefinePlugin } from '@storybook/builder-webpack5';
import { BuilderContext, targetFromTargetString } from '@angular-devkit/architect'; import { BuilderContext, targetFromTargetString } from '@angular-devkit/architect';
import { JsonObject, logging } from '@angular-devkit/core'; import { JsonObject, logging } from '@angular-devkit/core';
@ -21,13 +22,25 @@ export async function webpackFinal(baseConfig: webpack.Configuration, options: P
const builderContext = getBuilderContext(options); const builderContext = getBuilderContext(options);
const builderOptions = await getBuilderOptions(options, builderContext); const builderOptions = await getBuilderOptions(options, builderContext);
return getCustomWebpackConfig(baseConfig, { const webpackConfig = await getCustomWebpackConfig(baseConfig, {
builderOptions: { builderOptions: {
watch: options.configType === 'DEVELOPMENT', watch: options.configType === 'DEVELOPMENT',
...builderOptions, ...builderOptions,
}, } as any,
builderContext, builderContext,
}); });
webpackConfig.plugins = webpackConfig.plugins ?? [];
webpackConfig.plugins.push(
new WebpackDefinePlugin({
STORYBOOK_ANGULAR_OPTIONS: JSON.stringify({
experimentalZoneless: builderOptions.experimentalZoneless,
}),
})
);
return webpackConfig;
} }
/** Get Builder Context If storybook is not start by angular builder create dumb BuilderContext */ /** Get Builder Context If storybook is not start by angular builder create dumb BuilderContext */
@ -45,10 +58,7 @@ function getBuilderContext(options: PresetOptions): BuilderContext {
} }
/** Get builder options Merge target options from browser target and from storybook options */ /** Get builder options Merge target options from browser target and from storybook options */
async function getBuilderOptions( async function getBuilderOptions(options: PresetOptions, builderContext: BuilderContext) {
options: PresetOptions,
builderContext: BuilderContext
): Promise<JsonObject> {
/** Get Browser Target options */ /** Get Browser Target options */
let browserTargetOptions: JsonObject = {}; let browserTargetOptions: JsonObject = {};
if (options.angularBrowserTarget) { if (options.angularBrowserTarget) {
@ -65,7 +75,7 @@ async function getBuilderOptions(
/** Merge target options from browser target options and from storybook options */ /** Merge target options from browser target options and from storybook options */
const builderOptions = { const builderOptions = {
...browserTargetOptions, ...browserTargetOptions,
...(options.angularBuilderOptions as JsonObject), ...options.angularBuilderOptions,
tsConfig: tsConfig:
options.tsConfig ?? options.tsConfig ??
findUpSync('tsconfig.json', { cwd: options.configDir }) ?? findUpSync('tsconfig.json', { cwd: options.configDir }) ??

View File

@ -1,17 +1,13 @@
import { Options as CoreOptions } from 'storybook/internal/types'; import { Options as CoreOptions } from 'storybook/internal/types';
import { BuilderContext } from '@angular-devkit/architect'; import { BuilderContext } from '@angular-devkit/architect';
import { StylePreprocessorOptions } from '@angular-devkit/build-angular'; import { StandaloneOptions } from '../builders/utils/standalone-options';
import { StyleElement } from '@angular-devkit/build-angular/src/builders/browser/schema';
export type PresetOptions = CoreOptions & { export type PresetOptions = CoreOptions & {
/* Allow to get the options of a targeted "browser builder" */ /* Allow to get the options of a targeted "browser builder" */
angularBrowserTarget?: string | null; angularBrowserTarget?: string | null;
/* Defined set of options. These will take over priority from angularBrowserTarget options */ /* Defined set of options. These will take over priority from angularBrowserTarget options */
angularBuilderOptions?: { angularBuilderOptions?: StandaloneOptions['angularBuilderOptions'];
styles?: StyleElement[];
stylePreprocessorOptions?: StylePreprocessorOptions;
};
/* Angular context from builder */ /* Angular context from builder */
angularBuilderContext?: BuilderContext | null; angularBuilderContext?: BuilderContext | null;
tsConfig?: string; tsConfig?: string;

View File

@ -212,6 +212,7 @@
"process": "^0.11.10", "process": "^0.11.10",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"rimraf": "^6.0.1",
"semver": "^7.3.7", "semver": "^7.3.7",
"serve-static": "^1.14.1", "serve-static": "^1.14.1",
"slash": "^5.0.0", "slash": "^5.0.0",

File diff suppressed because it is too large Load Diff