mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-07 23:22:10 +08:00
Merge remote-tracking branch 'origin/next' into valentin/introduce-instances-vitest-3
This commit is contained in:
commit
678fb8fb5d
37
MIGRATION.md
37
MIGRATION.md
@ -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
|
||||
|
||||
|
@ -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": {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -20,6 +20,7 @@ export type StandaloneOptions = CLIOptions &
|
||||
assets?: AssetPattern[];
|
||||
sourceMap?: SourceMapUnion;
|
||||
preserveSymlinks?: boolean;
|
||||
experimentalZoneless?: boolean;
|
||||
};
|
||||
angularBuilderContext?: BuilderContext | null;
|
||||
tsConfig?: string;
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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(() => {
|
||||
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
@ -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],
|
||||
|
@ -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 */
|
||||
|
@ -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;
|
||||
|
@ -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 }) ??
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
|
5189
code/yarn.lock
5189
code/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user