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>
- [From version 8.4.x to 8.5.x](#from-version-84x-to-85x)
- [Introducing features.developmentModeForBuild](#introducing-featuresdevelopmentmodeforbuild)
- [From version 8.5.x to 8.6.x](#from-version-85x-to-86x)
- [Angular: Support experimental zoneless support](#angular-support-experimental-zoneless-support)
- [Added source code panel to docs](#added-source-code-panel-to-docs)
- [Addon-a11y: Component test integration](#addon-a11y-component-test-integration)
- [Addon-a11y: Changing the default element selector](#addon-a11y-changing-the-default-element-selector)
@ -427,6 +427,37 @@
- [Packages renaming](#packages-renaming)
- [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
### Introducing features.developmentModeForBuild
@ -442,7 +473,7 @@ export default {
developmentModeForBuild: true,
},
};
```
````
### Added source code panel to docs

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,7 @@ export type StandaloneOptions = CLIOptions &
assets?: AssetPattern[];
sourceMap?: SourceMapUnion;
preserveSymlinks?: boolean;
experimentalZoneless?: boolean;
};
angularBuilderContext?: BuilderContext | null;
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 { BehaviorSubject, Subject } from 'rxjs';
import { stringify } from 'telejson';
@ -14,6 +14,12 @@ type StoryRenderInfo = {
moduleMetadataSnapshot: string;
};
declare global {
const STORYBOOK_ANGULAR_OPTIONS: {
experimentalZoneless: boolean;
};
}
const applicationRefs = new Map<HTMLElement, ApplicationRef>();
/**
@ -112,14 +118,25 @@ export abstract class AbstractRenderer {
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(() => {
return bootstrapApplication(application, {
...storyFnAngular.applicationConfig,
providers: [
storyPropsProvider(newStoryProps$),
...analyzedMetadata.applicationProviders,
...(storyFnAngular.applicationConfig?.providers ?? []),
],
providers,
});
});

View File

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

View File

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

View File

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

View File

@ -22,9 +22,6 @@ import { global } from '@storybook/global';
*/
// 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.
/** APPLICATION IMPORTS */

View File

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

View File

@ -1,5 +1,6 @@
import { logger } from 'storybook/internal/node-logger';
import { AngularLegacyBuildOptionsError } from 'storybook/internal/server-errors';
import { WebpackDefinePlugin } from '@storybook/builder-webpack5';
import { BuilderContext, targetFromTargetString } from '@angular-devkit/architect';
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 builderOptions = await getBuilderOptions(options, builderContext);
return getCustomWebpackConfig(baseConfig, {
const webpackConfig = await getCustomWebpackConfig(baseConfig, {
builderOptions: {
watch: options.configType === 'DEVELOPMENT',
...builderOptions,
},
} as any,
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 */
@ -45,10 +58,7 @@ function getBuilderContext(options: PresetOptions): BuilderContext {
}
/** Get builder options Merge target options from browser target and from storybook options */
async function getBuilderOptions(
options: PresetOptions,
builderContext: BuilderContext
): Promise<JsonObject> {
async function getBuilderOptions(options: PresetOptions, builderContext: BuilderContext) {
/** Get Browser Target options */
let browserTargetOptions: JsonObject = {};
if (options.angularBrowserTarget) {
@ -65,7 +75,7 @@ async function getBuilderOptions(
/** Merge target options from browser target options and from storybook options */
const builderOptions = {
...browserTargetOptions,
...(options.angularBuilderOptions as JsonObject),
...options.angularBuilderOptions,
tsConfig:
options.tsConfig ??
findUpSync('tsconfig.json', { cwd: options.configDir }) ??

View File

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

View File

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

File diff suppressed because it is too large Load Diff