diff --git a/code/addons/a11y/src/index.ts b/code/addons/a11y/src/index.ts
index 583f0841fb86..48ffbe5e2c06 100644
--- a/code/addons/a11y/src/index.ts
+++ b/code/addons/a11y/src/index.ts
@@ -1,9 +1,10 @@
-import { definePreview } from 'storybook/preview-api';
+import { definePreviewAddon } from 'storybook/internal/csf';
import * as addonAnnotations from './preview';
+import type { A11yTypes } from './types';
export { PARAM_KEY } from './constants';
export * from './params';
-export type { A11yParameters } from './types';
+export type { A11yTypes } from './types';
-export default () => definePreview(addonAnnotations);
+export default () => definePreviewAddon(addonAnnotations);
diff --git a/code/addons/a11y/src/types.ts b/code/addons/a11y/src/types.ts
index d1a818bd2ad4..de2472006a03 100644
--- a/code/addons/a11y/src/types.ts
+++ b/code/addons/a11y/src/types.ts
@@ -19,7 +19,7 @@ export interface A11yGlobals {
*
* @see https://storybook.js.org/docs/writing-tests/accessibility-testing
*/
- a11y: {
+ a11y?: {
/**
* Prevent the addon from executing automated accessibility checks upon visiting a story. You
* can still trigger the checks from the addon panel.
@@ -51,3 +51,8 @@ export type EnhancedResults = Omit definePreview(addonAnnotations);
+export default () => definePreviewAddon(addonAnnotations);
diff --git a/code/addons/docs/src/types.ts b/code/addons/docs/src/types.ts
index 7ce603d7cf12..ac6ab06ddc98 100644
--- a/code/addons/docs/src/types.ts
+++ b/code/addons/docs/src/types.ts
@@ -164,7 +164,7 @@ export interface DocsParameters {
*
* @see https://storybook.js.org/docs/api/doc-blocks/doc-block-canvas
*/
- canvas?: CanvasBlockParameters;
+ canvas?: Partial;
/**
* Controls block configuration
@@ -195,14 +195,14 @@ export interface DocsParameters {
*
* @see https://storybook.js.org/docs/api/doc-blocks/doc-block-source
*/
- source?: SourceBlockParameters;
+ source?: Partial;
/**
* Story configuration
*
* @see https://storybook.js.org/docs/api/doc-blocks/doc-block-story
*/
- story?: StoryBlockParameters;
+ story?: Partial;
/**
* The subtitle displayed when shown in docs page
@@ -219,3 +219,7 @@ export interface DocsParameters {
title?: string;
};
}
+
+export interface DocsTypes {
+ parameters: DocsParameters;
+}
diff --git a/code/addons/links/src/index.ts b/code/addons/links/src/index.ts
index ec01a5177326..869923579cdb 100644
--- a/code/addons/links/src/index.ts
+++ b/code/addons/links/src/index.ts
@@ -1,7 +1,7 @@
-import { definePreview } from 'storybook/preview-api';
+import { definePreviewAddon } from 'storybook/internal/csf';
import * as addonAnnotations from './preview';
export { linkTo, hrefTo, withLinks, navigate } from './utils';
-export default () => definePreview(addonAnnotations);
+export default () => definePreviewAddon(addonAnnotations);
diff --git a/code/addons/pseudo-states/src/index.ts b/code/addons/pseudo-states/src/index.ts
index f02987a12309..40fcb0b5511a 100644
--- a/code/addons/pseudo-states/src/index.ts
+++ b/code/addons/pseudo-states/src/index.ts
@@ -1,7 +1,7 @@
-import { definePreview } from 'storybook/preview-api';
+import { definePreviewAddon } from 'storybook/internal/csf';
import * as addonAnnotations from './preview';
export { PARAM_KEY } from './constants';
-export default () => definePreview(addonAnnotations);
+export default () => definePreviewAddon(addonAnnotations);
diff --git a/code/addons/themes/src/index.ts b/code/addons/themes/src/index.ts
index 4a02b63fdb12..428b8e12eb58 100644
--- a/code/addons/themes/src/index.ts
+++ b/code/addons/themes/src/index.ts
@@ -1,9 +1,10 @@
-import { definePreview } from 'storybook/preview-api';
+import { definePreviewAddon } from 'storybook/internal/csf';
import * as addonAnnotations from './preview';
+import type { ThemesTypes } from './types';
-export type { ThemesGlobals, ThemesParameters } from './types';
+export type { ThemesTypes } from './types';
-export default () => definePreview(addonAnnotations);
+export default () => definePreviewAddon(addonAnnotations);
export * from './decorators';
diff --git a/code/addons/themes/src/theme-switcher.tsx b/code/addons/themes/src/theme-switcher.tsx
index ee3f63597b20..10eca3b4057d 100644
--- a/code/addons/themes/src/theme-switcher.tsx
+++ b/code/addons/themes/src/theme-switcher.tsx
@@ -17,7 +17,7 @@ import {
} from './constants';
import type { ThemesParameters as Parameters, ThemeAddonState } from './types';
-type ThemesParameters = Parameters['themes'];
+type ThemesParameters = NonNullable;
const IconButtonLabel = styled.div(({ theme }) => ({
fontSize: theme.typography.size.s2 - 1,
diff --git a/code/addons/themes/src/types.ts b/code/addons/themes/src/types.ts
index 3a825e37983e..34b5c7e60979 100644
--- a/code/addons/themes/src/types.ts
+++ b/code/addons/themes/src/types.ts
@@ -9,7 +9,7 @@ export interface ThemesParameters {
*
* @see https://github.com/storybookjs/storybook/blob/next/code/addons/themes/README.md
*/
- themes: {
+ themes?: {
/** Remove the addon panel and disable the addon's behavior */
disable?: boolean;
/** Which theme to override for the story */
@@ -21,3 +21,8 @@ export interface ThemesGlobals {
/** Which theme to override for the story */
theme?: string;
}
+
+export interface ThemesTypes {
+ parameters: ThemesParameters;
+ globals: ThemesGlobals;
+}
diff --git a/code/addons/vitest/src/index.ts b/code/addons/vitest/src/index.ts
index bfd4b00bc8be..ed9c6eedc4b4 100644
--- a/code/addons/vitest/src/index.ts
+++ b/code/addons/vitest/src/index.ts
@@ -1,5 +1,3 @@
-import { definePreview } from 'storybook/preview-api';
+import { definePreviewAddon } from 'storybook/internal/csf';
-export default () => definePreview({});
-
-export type { TestParameters } from './types';
+export default () => definePreviewAddon({});
diff --git a/code/addons/vitest/src/types.ts b/code/addons/vitest/src/types.ts
index 77ba1c26653c..b6eca96f92de 100644
--- a/code/addons/vitest/src/types.ts
+++ b/code/addons/vitest/src/types.ts
@@ -2,21 +2,6 @@ import type { experimental_UniversalStore } from 'storybook/internal/core-server
import type { PreviewAnnotation, StoryId } from 'storybook/internal/types';
import type { API_HashEntry } from 'storybook/internal/types';
-export interface TestParameters {
- /**
- * Test addon configuration
- *
- * @see https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
- */
- test: {
- /** Ignore unhandled errors during test execution */
- dangerouslyIgnoreUnhandledErrors?: boolean;
-
- /** Whether to throw exceptions coming from the play function */
- throwPlayFunctionExceptions?: boolean;
- };
-}
-
export interface VitestError extends Error {
VITEST_TEST_PATH?: string;
VITEST_TEST_NAME?: string;
diff --git a/code/core/src/actions/index.ts b/code/core/src/actions/index.ts
index c9d2c4e55a37..d2d3261dc960 100644
--- a/code/core/src/actions/index.ts
+++ b/code/core/src/actions/index.ts
@@ -1,5 +1,3 @@
export * from './constants';
export * from './models';
export * from './runtime';
-
-export type { ActionsParameters } from './types';
diff --git a/code/core/src/actions/preview.ts b/code/core/src/actions/preview.ts
index 620d72b24ed9..8b841c1f0826 100644
--- a/code/core/src/actions/preview.ts
+++ b/code/core/src/actions/preview.ts
@@ -1,10 +1,13 @@
-import { definePreview } from 'storybook/preview-api';
+import { definePreviewAddon } from 'storybook/internal/csf';
import * as addArgs from './addArgs';
import * as loaders from './loaders';
+import type { ActionsTypes } from './types';
+
+export { ActionsTypes };
export default () =>
- definePreview({
+ definePreviewAddon({
...addArgs,
...loaders,
});
diff --git a/code/core/src/actions/types.ts b/code/core/src/actions/types.ts
index 47b3bb9ddd84..b47e7219bfe3 100644
--- a/code/core/src/actions/types.ts
+++ b/code/core/src/actions/types.ts
@@ -4,7 +4,7 @@ export interface ActionsParameters {
*
* @see https://storybook.js.org/docs/essentials/actions#parameters
*/
- actions: {
+ actions?: {
/**
* Create actions for each arg that matches the regex. (**NOT recommended, see below**)
*
@@ -36,3 +36,7 @@ export interface ActionsParameters {
handles?: string[];
};
}
+
+export interface ActionsTypes {
+ parameters: ActionsParameters;
+}
diff --git a/code/core/src/backgrounds/decorator.ts b/code/core/src/backgrounds/decorator.ts
index e7bdbb1d167d..bb0d35de9cfc 100644
--- a/code/core/src/backgrounds/decorator.ts
+++ b/code/core/src/backgrounds/decorator.ts
@@ -24,7 +24,7 @@ export const withBackgroundAndGrid: DecoratorFunction = (StoryFn, context) => {
options = DEFAULT_BACKGROUNDS,
disable,
grid = defaultGrid,
- } = (parameters[PARAM_KEY] || {}) as BackgroundsParameters['backgrounds'];
+ } = (parameters[PARAM_KEY] || {}) as NonNullable;
const data = globals[PARAM_KEY] || {};
const backgroundName: string | undefined = typeof data === 'string' ? data : data?.value;
diff --git a/code/core/src/backgrounds/index.ts b/code/core/src/backgrounds/index.ts
index 49fd7f86cdd5..cb0ff5c3b541 100644
--- a/code/core/src/backgrounds/index.ts
+++ b/code/core/src/backgrounds/index.ts
@@ -1,5 +1 @@
-import addonAnnotations from './preview';
-
-export default addonAnnotations;
-
-export type { BackgroundsParameters, BackgroundsGlobals } from './types';
+export {};
diff --git a/code/core/src/backgrounds/preview.ts b/code/core/src/backgrounds/preview.ts
index ffa9db6924d1..f7f094531073 100644
--- a/code/core/src/backgrounds/preview.ts
+++ b/code/core/src/backgrounds/preview.ts
@@ -1,8 +1,8 @@
-import { definePreview } from 'storybook/preview-api';
+import { definePreviewAddon } from 'storybook/internal/csf';
import { PARAM_KEY } from './constants';
import { withBackgroundAndGrid } from './decorator';
-import type { BackgroundsParameters, GlobalState } from './types';
+import type { BackgroundTypes, BackgroundsParameters, GlobalState } from './types';
const decorators = globalThis.FEATURES?.backgrounds ? [withBackgroundAndGrid] : [];
@@ -21,8 +21,10 @@ const initialGlobals: Record = {
[PARAM_KEY]: { value: undefined, grid: false },
};
+export type { BackgroundTypes };
+
export default () =>
- definePreview({
+ definePreviewAddon({
decorators,
parameters,
initialGlobals,
diff --git a/code/core/src/backgrounds/types.ts b/code/core/src/backgrounds/types.ts
index e82bd400204e..713a1f1d6e52 100644
--- a/code/core/src/backgrounds/types.ts
+++ b/code/core/src/backgrounds/types.ts
@@ -15,11 +15,19 @@ export interface GridConfig {
offsetY?: number;
}
-export type GlobalState = { value: string | undefined; grid: boolean };
+export type GlobalState = { value: string | undefined; grid?: boolean };
export type GlobalStateUpdate = Partial;
export interface BackgroundsParameters {
- [PARAM_KEY]: {
+ /**
+ * Backgrounds configuration
+ *
+ * @see https://storybook.js.org/docs/essentials/backgrounds#parameters
+ */
+ backgrounds?: {
+ /** Default background color */
+ default?: string;
+
/** Remove the addon panel and disable the addon's behavior */
disable?: boolean;
@@ -37,5 +45,10 @@ export interface BackgroundsGlobals {
*
* @see https://storybook.js.org/docs/essentials/backgrounds#globals
*/
- [PARAM_KEY]: GlobalState | GlobalState['value'];
+ [PARAM_KEY]?: GlobalState | GlobalState['value'];
+}
+
+export interface BackgroundTypes {
+ parameters: BackgroundsParameters;
+ globals: BackgroundsGlobals;
}
diff --git a/code/core/src/common/utils/get-addon-annotations.ts b/code/core/src/common/utils/get-addon-annotations.ts
index 1798fe231c31..f82a13bdd437 100644
--- a/code/core/src/common/utils/get-addon-annotations.ts
+++ b/code/core/src/common/utils/get-addon-annotations.ts
@@ -23,26 +23,24 @@ export function getAnnotationsName(addonName: string): string {
return cleanedUpName;
}
-export async function getAddonAnnotations(addon: string) {
+export async function getAddonAnnotations(addon: string, configDir: string) {
+ const data = {
+ // core addons will have a function as default export in index entrypoint
+ importPath: addon,
+ importName: getAnnotationsName(addon),
+ isCoreAddon: isCorePackage(addon),
+ };
+
+ if (!data.isCoreAddon) {
+ // for backwards compatibility, if it's not a core addon we use /preview entrypoint
+ data.importPath = `${addon}/preview`;
+ }
+
+ // If the preview endpoint doesn't exist, we don't need to add the addon to definePreview
try {
- const data = {
- // core addons will have a function as default export in index entrypoint
- importPath: addon,
- importName: getAnnotationsName(addon),
- isCoreAddon: isCorePackage(addon),
- };
-
- if (addon === '@storybook/addon-essentials') {
- return data;
- } else if (!data.isCoreAddon) {
- // for backwards compatibility, if it's not a core addon we use /preview entrypoint
- data.importPath = `${addon}/preview`;
- }
-
- require.resolve(path.join(addon, 'preview'));
-
- return data;
- } catch (err) {}
-
- return null;
+ require.resolve(path.join(addon, 'preview'), { paths: [configDir] });
+ } catch (err) {
+ return null;
+ }
+ return data;
}
diff --git a/code/core/src/common/utils/sync-main-preview-addons.test.ts b/code/core/src/common/utils/sync-main-preview-addons.test.ts
index 35891d10778f..505d9d5cc794 100644
--- a/code/core/src/common/utils/sync-main-preview-addons.test.ts
+++ b/code/core/src/common/utils/sync-main-preview-addons.test.ts
@@ -22,6 +22,8 @@ describe('getSyncedStorybookAddons', () => {
addons: ['custom-addon', '@storybook/addon-a11y'],
};
+ const configDir = '/user/storybook/.storybook';
+
it('should sync addons between main and preview', async () => {
const preview = loadConfig(`
import * as myAddonAnnotations from "custom-addon/preview";
@@ -36,7 +38,7 @@ describe('getSyncedStorybookAddons', () => {
return { importName: 'addonA11yAnnotations', importPath: '@storybook/addon-a11y/preview' };
});
- const result = await getSyncedStorybookAddons(mainConfig, preview);
+ const result = await getSyncedStorybookAddons(mainConfig, preview, configDir);
expect(printConfig(result).code).toMatchInlineSnapshot(`
import * as addonA11yAnnotations from "@storybook/addon-a11y/preview";
import * as myAddonAnnotations from "custom-addon/preview";
@@ -66,7 +68,7 @@ describe('getSyncedStorybookAddons', () => {
};
});
- const result = await getSyncedStorybookAddons(mainConfig, preview);
+ const result = await getSyncedStorybookAddons(mainConfig, preview, configDir);
expect(printConfig(result).code).toMatchInlineSnapshot(`
import addonA11yAnnotations from "@storybook/addon-a11y";
import * as myAddonAnnotations from "custom-addon/preview";
@@ -92,7 +94,7 @@ describe('getSyncedStorybookAddons', () => {
return { importName: 'addonA11yAnnotations', importPath: '@storybook/addon-a11y/preview' };
});
- const result = await getSyncedStorybookAddons(mainConfig, preview);
+ const result = await getSyncedStorybookAddons(mainConfig, preview, configDir);
expect(printConfig(result).code).toMatchInlineSnapshot(`
import * as addonA11yAnnotations from "@storybook/addon-a11y/preview";
import { definePreview } from "@storybook/react/preview";
@@ -122,7 +124,7 @@ describe('getSyncedStorybookAddons', () => {
return { importName: 'addonA11yAnnotations', importPath: '@storybook/addon-a11y/preview' };
});
- const result = await getSyncedStorybookAddons(mainConfig, preview);
+ const result = await getSyncedStorybookAddons(mainConfig, preview, configDir);
const transformedCode = normalizeLineBreaks(printConfig(result).code);
expect(transformedCode).toMatch(originalCode);
@@ -144,7 +146,7 @@ describe('getSyncedStorybookAddons', () => {
return { importName: 'addonA11yAnnotations', importPath: '@storybook/addon-a11y/preview' };
});
- const result = await getSyncedStorybookAddons(mainConfig, preview);
+ const result = await getSyncedStorybookAddons(mainConfig, preview, configDir);
const transformedCode = normalizeLineBreaks(printConfig(result).code);
expect(transformedCode).toMatch(originalCode);
diff --git a/code/core/src/common/utils/sync-main-preview-addons.ts b/code/core/src/common/utils/sync-main-preview-addons.ts
index 4f6ee1dd478b..86891fbbd8ef 100644
--- a/code/core/src/common/utils/sync-main-preview-addons.ts
+++ b/code/core/src/common/utils/sync-main-preview-addons.ts
@@ -14,16 +14,21 @@ import { getAddonNames } from './get-addon-names';
const logger = console;
-export async function syncStorybookAddons(mainConfig: StorybookConfig, previewConfigPath: string) {
+export async function syncStorybookAddons(
+ mainConfig: StorybookConfig,
+ previewConfigPath: string,
+ configDir: string
+) {
const previewConfig = await readConfig(previewConfigPath!);
- const modifiedConfig = await getSyncedStorybookAddons(mainConfig, previewConfig);
+ const modifiedConfig = await getSyncedStorybookAddons(mainConfig, previewConfig, configDir);
await writeConfig(modifiedConfig);
}
export async function getSyncedStorybookAddons(
mainConfig: StorybookConfig,
- previewConfig: ConfigFile
+ previewConfig: ConfigFile,
+ configDir: string
): Promise {
const isCsfFactory = isCsfFactoryPreview(previewConfig);
@@ -43,7 +48,7 @@ export async function getSyncedStorybookAddons(
* exports map called preview, if so add to the array
*/
for (const addon of addons) {
- const annotations = await getAddonAnnotations(addon);
+ const annotations = await getAddonAnnotations(addon, configDir);
if (annotations) {
const hasAlreadyImportedAddonAnnotations = previewConfig._ast.program.body.find(
(node) => t.isImportDeclaration(node) && node.source.value === annotations.importPath
diff --git a/code/core/src/component-testing/preview.ts b/code/core/src/component-testing/preview.ts
index 3bf37a7b2c97..6f99880cd53c 100644
--- a/code/core/src/component-testing/preview.ts
+++ b/code/core/src/component-testing/preview.ts
@@ -1,8 +1,7 @@
+import { definePreviewAddon } from 'storybook/internal/csf';
import { instrument } from 'storybook/internal/instrumenter';
import type { PlayFunction, StepLabel, StoryContext } from 'storybook/internal/types';
-import { definePreview } from 'storybook/preview-api';
-
const { step } = instrument(
{
// It seems like the label is unused, but the instrumenter has access to it
@@ -16,7 +15,7 @@ const { step } = instrument(
);
export default () =>
- definePreview({
+ definePreviewAddon({
parameters: {
throwPlayFunctionExceptions: false,
},
diff --git a/code/core/src/controls/index.ts b/code/core/src/controls/index.ts
index 4fbce9564d5f..c94f80f843a1 100644
--- a/code/core/src/controls/index.ts
+++ b/code/core/src/controls/index.ts
@@ -1,2 +1 @@
export * from './constants';
-export * from './types';
diff --git a/code/core/src/controls/preview.ts b/code/core/src/controls/preview.ts
index 450e37646b18..fc919081569a 100644
--- a/code/core/src/controls/preview.ts
+++ b/code/core/src/controls/preview.ts
@@ -1,6 +1,8 @@
-import { definePreview } from 'storybook/preview-api';
+import { definePreviewAddon } from 'storybook/internal/csf';
-export default definePreview({
+import type { ControlsTypes } from './types';
+
+export default definePreviewAddon({
// Controls addon doesn't need any preview-side configuration
// It operates entirely through the manager UI
});
diff --git a/code/core/src/controls/types.ts b/code/core/src/controls/types.ts
index d12dc06ad802..7623c40ec871 100644
--- a/code/core/src/controls/types.ts
+++ b/code/core/src/controls/types.ts
@@ -4,7 +4,7 @@ export interface ControlsParameters {
*
* @see https://storybook.js.org/docs/essentials/controls#parameters-1
*/
- controls: {
+ controls?: {
/** Remove the addon panel and disable the addon's behavior */
disable?: boolean;
@@ -35,3 +35,7 @@ export interface ControlsParameters {
sort?: 'none' | 'alpha' | 'requiredFirst';
};
}
+
+export interface ControlsTypes {
+ parameters: ControlsParameters;
+}
diff --git a/code/core/src/csf-tools/CsfFile.test.ts b/code/core/src/csf-tools/CsfFile.test.ts
index 9a864e6ae79e..47f661991e36 100644
--- a/code/core/src/csf-tools/CsfFile.test.ts
+++ b/code/core/src/csf-tools/CsfFile.test.ts
@@ -2351,6 +2351,51 @@ describe('CsfFile', () => {
`);
});
+ it('extend story', () => {
+ expect(
+ parse(
+ dedent`
+ import { config } from '#.storybook/preview'
+ const meta = config.meta({ component: 'foo' });
+ export default meta;
+ export const A = meta.story({})
+ export const B = A.extend({})
+ `
+ )
+ ).toMatchInlineSnapshot(`
+ meta:
+ component: '''foo'''
+ title: Default Title
+ stories:
+ - id: default-title--a
+ name: A
+ __stats:
+ factory: true
+ play: false
+ render: false
+ loaders: false
+ beforeEach: false
+ globals: false
+ tags: false
+ storyFn: false
+ mount: false
+ moduleMock: false
+ - id: default-title--b
+ name: B
+ __stats:
+ factory: true
+ play: false
+ render: false
+ loaders: false
+ beforeEach: false
+ globals: false
+ tags: false
+ storyFn: false
+ mount: false
+ moduleMock: false
+ `);
+ });
+
it('story name', () => {
expect(
parse(
diff --git a/code/core/src/csf-tools/CsfFile.ts b/code/core/src/csf-tools/CsfFile.ts
index d2a2965b5cea..fcfabd2b3e35 100644
--- a/code/core/src/csf-tools/CsfFile.ts
+++ b/code/core/src/csf-tools/CsfFile.ts
@@ -507,7 +507,8 @@ export class CsfFile {
t.isCallExpression(storyNode) &&
t.isMemberExpression(storyNode.callee) &&
t.isIdentifier(storyNode.callee.property) &&
- storyNode.callee.property.name === 'story'
+ (storyNode.callee.property.name === 'story' ||
+ storyNode.callee.property.name === 'extend')
) {
storyIsFactory = true;
storyNode = storyNode.arguments[0];
diff --git a/code/core/src/shared/preview/core-annotations.ts b/code/core/src/csf/core-annotations.ts
similarity index 53%
rename from code/core/src/shared/preview/core-annotations.ts
rename to code/core/src/csf/core-annotations.ts
index 2b4e1f8bea0e..6737a567c6c9 100644
--- a/code/core/src/shared/preview/core-annotations.ts
+++ b/code/core/src/csf/core-annotations.ts
@@ -1,12 +1,22 @@
import componentTestingAnnotations from 'storybook/internal/component-testing/preview';
+import type { StorybookTypes } from 'storybook/internal/types';
-import actionAnnotations from 'storybook/actions/preview';
-import backgroundsAnnotations from 'storybook/backgrounds/preview';
-import highlightAnnotations from 'storybook/highlight/preview';
-import measureAnnotations from 'storybook/measure/preview';
-import outlineAnnotations from 'storybook/outline/preview';
-import testAnnotations from 'storybook/test/preview';
-import viewportAnnotations from 'storybook/viewport/preview';
+import actionAnnotations, { type ActionsTypes } from 'storybook/actions/preview';
+import backgroundsAnnotations, { type BackgroundTypes } from 'storybook/backgrounds/preview';
+import highlightAnnotations, { type HighlightTypes } from 'storybook/highlight/preview';
+import measureAnnotations, { type MeasureTypes } from 'storybook/measure/preview';
+import outlineAnnotations, { type OutlineTypes } from 'storybook/outline/preview';
+import testAnnotations, { type TestTypes } from 'storybook/test/preview';
+import viewportAnnotations, { type ViewportTypes } from 'storybook/viewport/preview';
+
+export type CoreTypes = StorybookTypes &
+ ActionsTypes &
+ BackgroundTypes &
+ HighlightTypes &
+ MeasureTypes &
+ OutlineTypes &
+ TestTypes &
+ ViewportTypes;
export function getCoreAnnotations() {
return [
diff --git a/code/core/src/csf/csf-factories.test.ts b/code/core/src/csf/csf-factories.test.ts
new file mode 100644
index 000000000000..1ddce9f0f70b
--- /dev/null
+++ b/code/core/src/csf/csf-factories.test.ts
@@ -0,0 +1,43 @@
+import { test } from 'vitest';
+
+import { definePreview, definePreviewAddon } from './csf-factories';
+
+interface Addon1Types {
+ parameters: { foo?: { value: string } };
+}
+
+const addon = definePreviewAddon({});
+
+interface Addon2Types {
+ parameters: { bar?: { value: string } };
+}
+
+const addon2 = definePreviewAddon({});
+
+const preview = definePreview({ addons: [addon, addon2] });
+
+const meta = preview.meta({});
+
+test('addon parameters are inferred', () => {
+ const MyStory = meta.story({
+ parameters: {
+ foo: {
+ value: '1',
+ },
+ bar: {
+ value: '1',
+ },
+ },
+ });
+ const MyStory2 = meta.story({
+ // @ts-expect-error can not assign numbers to strings
+ parameters: {
+ foo: {
+ value: 1,
+ },
+ bar: {
+ value: 1,
+ },
+ },
+ });
+});
diff --git a/code/core/src/csf/csf-factories.ts b/code/core/src/csf/csf-factories.ts
index f3f9072b9ef2..630af00586e1 100644
--- a/code/core/src/csf/csf-factories.ts
+++ b/code/core/src/csf/csf-factories.ts
@@ -1,46 +1,217 @@
+import type { AddonTypes, PlayFunction, StoryContext } from 'storybook/internal/csf';
+import { combineTags } from 'storybook/internal/csf';
import type {
Args,
ComponentAnnotations,
- NormalizedComponentAnnotations,
+ ComposedStoryFn,
NormalizedProjectAnnotations,
- NormalizedStoryAnnotations,
ProjectAnnotations,
Renderer,
StoryAnnotations,
} from 'storybook/internal/types';
+import {
+ combineParameters,
+ composeConfigs,
+ composeStory,
+ normalizeArrays,
+ normalizeProjectAnnotations,
+} from '../preview-api/index';
+import { getCoreAnnotations } from './core-annotations';
+
export interface Preview {
readonly _tag: 'Preview';
- input: ProjectAnnotations;
+ input: ProjectAnnotations & { addons?: PreviewAddon[] };
composed: NormalizedProjectAnnotations;
- meta(input: ComponentAnnotations): Meta;
+ meta>(
+ input: TInput
+ ): Meta;
+}
+
+export type InferTypes[]> = T extends PreviewAddon[]
+ ? C & { csf4: true }
+ : never;
+
+export function definePreview[]>(
+ input: ProjectAnnotations & { addons?: Addons }
+): Preview> {
+ let composed: NormalizedProjectAnnotations>;
+ const preview = {
+ _tag: 'Preview',
+ input: input,
+ get composed() {
+ if (composed) {
+ return composed;
+ }
+ const { addons, ...rest } = input;
+ composed = normalizeProjectAnnotations>(
+ composeConfigs([...getCoreAnnotations(), ...(addons ?? []), rest])
+ );
+ return composed;
+ },
+ meta(meta) {
+ // @ts-expect-error hard
+ return defineMeta(meta, this);
+ },
+ } as Preview>;
+ globalThis.globalProjectAnnotations = preview.composed;
+ return preview;
+}
+
+export interface PreviewAddon
+ extends ProjectAnnotations {}
+
+export function definePreviewAddon(
+ preview: ProjectAnnotations
+): PreviewAddon {
+ return preview;
}
export function isPreview(input: unknown): input is Preview {
return input != null && typeof input === 'object' && '_tag' in input && input?._tag === 'Preview';
}
-export interface Meta {
+export interface Meta<
+ TRenderer extends Renderer,
+ TInput extends ComponentAnnotations = ComponentAnnotations<
+ TRenderer,
+ TRenderer['args']
+ >,
+> {
readonly _tag: 'Meta';
- input: ComponentAnnotations;
- composed: NormalizedComponentAnnotations;
+ input: TInput;
+ // composed: NormalizedComponentAnnotations;
preview: Preview;
- story(input: StoryAnnotations): Story;
+ story(
+ input?: () => TRenderer['storyResult']
+ ): Story TRenderer['storyResult'] }>;
+
+ story>(
+ input?: TInput
+ ): Story;
}
export function isMeta(input: unknown): input is Meta {
return input != null && typeof input === 'object' && '_tag' in input && input?._tag === 'Meta';
}
-export interface Story {
+function defineMeta<
+ TRenderer extends Renderer,
+ TInput extends ComponentAnnotations = ComponentAnnotations<
+ TRenderer,
+ TRenderer['args']
+ >,
+>(input: TInput, preview: Preview): Meta {
+ return {
+ _tag: 'Meta',
+ input,
+ preview,
+ get composed(): never {
+ throw new Error('Not implemented');
+ },
+ // @ts-expect-error hard
+ story(
+ story: StoryAnnotations | (() => TRenderer['storyResult']) = {}
+ ) {
+ return defineStory(typeof story === 'function' ? { render: story } : story, this);
+ },
+ };
+}
+
+export interface Story<
+ TRenderer extends Renderer,
+ TInput extends StoryAnnotations = StoryAnnotations<
+ TRenderer,
+ TRenderer['args']
+ >,
+> {
readonly _tag: 'Story';
- input: StoryAnnotations;
- composed: NormalizedStoryAnnotations;
- meta: Meta;
+ input: TInput;
+ composed: Pick<
+ ComposedStoryFn,
+ 'argTypes' | 'parameters' | 'id' | 'tags' | 'globals'
+ > & {
+ args: TRenderer['args'];
+ name: string;
+ };
+ meta: Meta;
+ __compose: () => ComposedStoryFn;
+ play: TInput['play'];
+ run: (context?: Partial>>) => Promise;
+
+ extend>(
+ input: TInput
+ ): Story;
}
export function isStory(input: unknown): input is Story {
return input != null && typeof input === 'object' && '_tag' in input && input?._tag === 'Story';
}
+
+function defineStory<
+ TRenderer extends Renderer,
+ TInput extends StoryAnnotations,
+>(input: TInput, meta: Meta): Story {
+ let composed: ComposedStoryFn;
+
+ const compose = () => {
+ if (!composed) {
+ composed = composeStory(
+ input as StoryAnnotations,
+ meta.input as ComponentAnnotations,
+ undefined,
+ meta.preview.composed
+ );
+ }
+ return composed;
+ };
+ return {
+ _tag: 'Story',
+ input,
+ meta,
+ __compose: compose,
+ get composed() {
+ const composed = compose();
+ const { args, argTypes, parameters, id, tags, globals, storyName: name } = composed;
+ return { args, argTypes, parameters, id, tags, name, globals };
+ },
+ get play() {
+ return input.play ?? meta.input?.play ?? (async () => {});
+ },
+ get run() {
+ return compose().run ?? (async () => {});
+ },
+ extend>(input: TInput) {
+ return defineStory(
+ {
+ ...this.input,
+ ...input,
+ args: { ...this.input.args, ...input.args },
+ argTypes: combineParameters(this.input.argTypes, input.argTypes),
+ afterEach: [
+ ...normalizeArrays(this.input?.afterEach ?? []),
+ ...normalizeArrays(input.afterEach ?? []),
+ ],
+ beforeEach: [
+ ...normalizeArrays(this.input?.beforeEach ?? []),
+ ...normalizeArrays(input.beforeEach ?? []),
+ ],
+ decorators: [
+ ...normalizeArrays(this.input?.decorators ?? []),
+ ...normalizeArrays(input.decorators ?? []),
+ ],
+ globals: { ...this.input.globals, ...input.globals },
+ loaders: [
+ ...normalizeArrays(this.input?.loaders ?? []),
+ ...normalizeArrays(input.loaders ?? []),
+ ],
+ parameters: combineParameters(this.input.parameters, input.parameters),
+ tags: combineTags(...(this.input.tags ?? []), ...(input.tags ?? [])),
+ },
+ this.meta
+ );
+ },
+ };
+}
diff --git a/code/core/src/csf/index.ts b/code/core/src/csf/index.ts
index 4997de1b3705..5501ed1a8e00 100644
--- a/code/core/src/csf/index.ts
+++ b/code/core/src/csf/index.ts
@@ -89,3 +89,4 @@ export const combineTags = (...tags: string[]): string[] => {
export { includeConditionalArg } from './includeConditionalArg';
export * from './story';
export * from './csf-factories';
+export * from './core-annotations';
diff --git a/code/core/src/csf/story.ts b/code/core/src/csf/story.ts
index 3e685c4f1f92..76d48150a5bc 100644
--- a/code/core/src/csf/story.ts
+++ b/code/core/src/csf/story.ts
@@ -1,6 +1,7 @@
import type { RemoveIndexSignature, Simplify, UnionToIntersection } from 'type-fest';
import type { SBScalarType, SBType } from './SBType';
+import type { CoreTypes } from './core-annotations';
export * from './SBType';
export type StoryId = string;
@@ -170,7 +171,12 @@ export interface StrictGlobalTypes {
[name: string]: StrictInputType;
}
-export interface Renderer {
+export interface AddonTypes {
+ parameters?: Record;
+ globals?: Record;
+}
+
+export interface Renderer extends AddonTypes {
/** What is the type of the `component` annotation in this renderer? */
component: any;
@@ -187,6 +193,10 @@ export interface Renderer {
// This generic type will eventually be filled in with TArgs
// Credits to Michael Arnaldi.
T?: unknown;
+
+ args: unknown;
+
+ csf4: boolean;
}
/** @deprecated - Use `Renderer` */
@@ -328,7 +338,8 @@ export interface BaseAnnotations;
runStep?: StepRunner;
@@ -497,7 +509,8 @@ export interface ComponentAnnotations;
/** Override the globals values for all stories in this component */
- globals?: Globals;
+ globals?: Globals &
+ (TRenderer['csf4'] extends true ? CoreTypes['globals'] & TRenderer['globals'] : unknown);
}
export type StoryAnnotations<
@@ -515,7 +528,8 @@ export type StoryAnnotations<
play?: PlayFunction;
/** Override the globals values for this story */
- globals?: Globals;
+ globals?: Globals &
+ (TRenderer['csf4'] extends true ? CoreTypes['globals'] & TRenderer['globals'] : unknown);
/** @deprecated */
story?: Omit, 'story'>;
diff --git a/code/core/src/highlight/index.ts b/code/core/src/highlight/index.ts
index 938410188420..87b61478b4fa 100644
--- a/code/core/src/highlight/index.ts
+++ b/code/core/src/highlight/index.ts
@@ -1,7 +1,2 @@
export { HIGHLIGHT, REMOVE_HIGHLIGHT, RESET_HIGHLIGHT, SCROLL_INTO_VIEW } from './constants';
-export type {
- ClickEventDetails,
- HighlightMenuItem,
- HighlightOptions,
- HighlightParameters,
-} from './types';
+export type { ClickEventDetails, HighlightMenuItem, HighlightOptions } from './types';
diff --git a/code/core/src/highlight/preview.ts b/code/core/src/highlight/preview.ts
index 97298ccb3085..3e542656790c 100644
--- a/code/core/src/highlight/preview.ts
+++ b/code/core/src/highlight/preview.ts
@@ -1,10 +1,15 @@
/* eslint-env browser */
-import { addons, definePreview } from 'storybook/preview-api';
+import { definePreviewAddon } from 'storybook/internal/csf';
+import { addons } from 'storybook/preview-api';
+
+import type { HighlightTypes } from './types';
import { useHighlights } from './useHighlights';
if (globalThis?.FEATURES?.highlight && addons?.ready) {
addons.ready().then(useHighlights);
}
-export default () => definePreview({});
+export type { HighlightTypes };
+
+export default () => definePreviewAddon({});
diff --git a/code/core/src/highlight/types.ts b/code/core/src/highlight/types.ts
index 13967cea0306..12f6a9f864f1 100644
--- a/code/core/src/highlight/types.ts
+++ b/code/core/src/highlight/types.ts
@@ -1,12 +1,16 @@
import type { IconName } from './icons';
+export interface HighlightTypes {
+ parameters: HighlightParameters;
+}
+
export interface HighlightParameters {
/**
* Highlight configuration
*
* @see https://storybook.js.org/docs/essentials/highlight#parameters
*/
- highlight: {
+ highlight?: {
/** Remove the addon panel and disable the addon's behavior */
disable?: boolean;
};
diff --git a/code/core/src/measure/index.ts b/code/core/src/measure/index.ts
index 1e804e286e15..cb0ff5c3b541 100644
--- a/code/core/src/measure/index.ts
+++ b/code/core/src/measure/index.ts
@@ -1,7 +1 @@
-import { definePreview } from 'storybook/preview-api';
-
-import * as addonAnnotations from './preview';
-
-export type { MeasureParameters } from './types';
-
-export default () => definePreview(addonAnnotations);
+export {};
diff --git a/code/core/src/measure/preview.ts b/code/core/src/measure/preview.ts
index d8b5e485a39d..e08b9972b071 100644
--- a/code/core/src/measure/preview.ts
+++ b/code/core/src/measure/preview.ts
@@ -1,6 +1,7 @@
-import { definePreview } from 'storybook/preview-api';
+import { definePreviewAddon } from 'storybook/internal/csf';
import { PARAM_KEY } from './constants';
+import type { MeasureTypes } from './types';
import { withMeasure } from './withMeasure';
export const decorators = globalThis.FEATURES?.measure ? [withMeasure] : [];
@@ -9,8 +10,10 @@ export const initialGlobals = {
[PARAM_KEY]: false,
};
+export type { MeasureTypes };
+
export default () =>
- definePreview({
+ definePreviewAddon({
decorators,
initialGlobals,
});
diff --git a/code/core/src/measure/types.ts b/code/core/src/measure/types.ts
index e51cf69775b5..ee6a46062a09 100644
--- a/code/core/src/measure/types.ts
+++ b/code/core/src/measure/types.ts
@@ -4,8 +4,12 @@ export interface MeasureParameters {
*
* @see https://storybook.js.org/docs/essentials/measure-and-outline#parameters
*/
- measure: {
+ measure?: {
/** Remove the addon panel and disable the addon's behavior */
disable?: boolean;
};
}
+
+export interface MeasureTypes {
+ parameters: MeasureParameters;
+}
diff --git a/code/core/src/outline/index.ts b/code/core/src/outline/index.ts
index 225a831a8d08..cb0ff5c3b541 100644
--- a/code/core/src/outline/index.ts
+++ b/code/core/src/outline/index.ts
@@ -1,7 +1 @@
-import { definePreview } from 'storybook/preview-api';
-
-import * as addonAnnotations from './preview';
-
-export type { OutlineParameters } from './types';
-
-export default () => definePreview(addonAnnotations);
+export {};
diff --git a/code/core/src/outline/preview.ts b/code/core/src/outline/preview.ts
index fee4d770ecff..86dc934e14cd 100644
--- a/code/core/src/outline/preview.ts
+++ b/code/core/src/outline/preview.ts
@@ -1,6 +1,7 @@
-import { definePreview } from 'storybook/preview-api';
+import { definePreviewAddon } from 'storybook/internal/csf';
import { PARAM_KEY } from './constants';
+import type { OutlineTypes } from './types';
import { withOutline } from './withOutline';
export const decorators = globalThis.FEATURES?.outline ? [withOutline] : [];
@@ -9,4 +10,6 @@ export const initialGlobals = {
[PARAM_KEY]: false,
};
-export default () => definePreview({ decorators, initialGlobals });
+export type { OutlineTypes };
+
+export default () => definePreviewAddon({ decorators, initialGlobals });
diff --git a/code/core/src/outline/types.ts b/code/core/src/outline/types.ts
index b5b3d4b3d663..959fc653ea01 100644
--- a/code/core/src/outline/types.ts
+++ b/code/core/src/outline/types.ts
@@ -4,8 +4,12 @@ export interface OutlineParameters {
*
* @see https://storybook.js.org/docs/essentials/measure-and-outline#parameters
*/
- outline: {
+ outline?: {
/** Remove the addon panel and disable the addon's behavior */
disable?: boolean;
};
}
+
+export interface OutlineTypes {
+ parameters: OutlineParameters;
+}
diff --git a/code/core/src/preview-api/index.ts b/code/core/src/preview-api/index.ts
index 72dfe110670f..8e6b0ca382a7 100644
--- a/code/core/src/preview-api/index.ts
+++ b/code/core/src/preview-api/index.ts
@@ -27,9 +27,6 @@ export { makeDecorator } from './addons';
*/
export { addons, mockChannel } from './addons';
-/** ADDON ANNOTATIONS TYPE HELPER */
-export { definePreview } from './addons';
-
// TODO: Universal Stores are disabled in the preview, until we get automatic leader negotiation in place
// export { UniversalStore as experimental_UniversalStore } from '../shared/universal-store';
// export { useUniversalStore as experimental_useUniversalStore } from '../shared/universal-store/use-universal-store-preview';
@@ -57,6 +54,7 @@ export {
defaultDecorateStory,
prepareStory,
prepareMeta,
+ normalizeArrays,
normalizeStory,
filterArgTypes,
sanitizeStoryContextUpdate,
@@ -87,4 +85,3 @@ export {
waitForAnimations,
} from './preview-web';
export type { SelectionStore, View } from './preview-web';
-export { getCoreAnnotations } from '../shared/preview/core-annotations';
diff --git a/code/core/src/preview-api/modules/addons/definePreview.ts b/code/core/src/preview-api/modules/addons/definePreview.ts
deleted file mode 100644
index b9e74fe15a11..000000000000
--- a/code/core/src/preview-api/modules/addons/definePreview.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import type { ProjectAnnotations, Renderer } from 'storybook/internal/types';
-
-export function definePreview(config: ProjectAnnotations): ProjectAnnotations {
- return config;
-}
diff --git a/code/core/src/preview-api/modules/addons/index.ts b/code/core/src/preview-api/modules/addons/index.ts
index db494490d7c1..b32933b0c1c9 100644
--- a/code/core/src/preview-api/modules/addons/index.ts
+++ b/code/core/src/preview-api/modules/addons/index.ts
@@ -1,5 +1,4 @@
export * from './main';
-export * from './definePreview';
export * from './hooks';
export * from './make-decorator';
export * from './storybook-channel-mock';
diff --git a/code/core/src/preview-api/modules/preview-web/render/MdxDocsRender.ts b/code/core/src/preview-api/modules/preview-web/render/MdxDocsRender.ts
index f87c034cd621..060c08601e7d 100644
--- a/code/core/src/preview-api/modules/preview-web/render/MdxDocsRender.ts
+++ b/code/core/src/preview-api/modules/preview-web/render/MdxDocsRender.ts
@@ -104,7 +104,7 @@ export class MdxDocsRender implements Render(
- input: Preview['input']
-): Preview {
- let composed: NormalizedProjectAnnotations;
- const preview: Preview = {
- _tag: 'Preview',
- input,
- get composed() {
- if (composed) {
- return composed;
- }
- const { addons, ...rest } = input;
- composed = normalizeProjectAnnotations(
- composeConfigs([...getCoreAnnotations(), ...(addons ?? []), rest])
- );
- return composed;
- },
- meta(meta: ComponentAnnotations) {
- return defineMeta(meta, this);
- },
- };
- globalThis.globalProjectAnnotations = preview.composed;
- return preview;
-}
-
-function defineMeta(
- input: ComponentAnnotations,
- preview: Preview
-): Meta {
- return {
- _tag: 'Meta',
- input,
- preview,
- get composed(): never {
- throw new Error('Not implemented');
- },
- story(story: StoryAnnotations) {
- return defineStory(story, this);
- },
- };
-}
-
-function defineStory(
- input: ComponentAnnotations,
- meta: Meta
-): Story {
- return {
- _tag: 'Story',
- input,
- meta,
- get composed(): never {
- throw new Error('Not implemented');
- },
- };
-}
diff --git a/code/core/src/test/preview.ts b/code/core/src/test/preview.ts
index b0f90b4c2efc..b7e85a3ee435 100644
--- a/code/core/src/test/preview.ts
+++ b/code/core/src/test/preview.ts
@@ -1,7 +1,7 @@
import type { LoaderFunction } from 'storybook/internal/csf';
+import { definePreviewAddon } from 'storybook/internal/csf';
import { instrument } from 'storybook/internal/instrumenter';
-import { definePreview } from 'storybook/preview-api';
import {
clearAllMocks,
fn,
@@ -122,7 +122,21 @@ const enhanceContext: LoaderFunction = async (context) => {
}
};
+interface TestParameters {
+ test?: {
+ /** Ignore unhandled errors during test execution */
+ dangerouslyIgnoreUnhandledErrors?: boolean;
+
+ /** Whether to throw exceptions coming from the play function */
+ throwPlayFunctionExceptions?: boolean;
+ };
+}
+
+export interface TestTypes {
+ parameters: TestParameters;
+}
+
export default () =>
- definePreview({
+ definePreviewAddon({
loaders: [resetAllMocksLoader, nameSpiesAndWrapActionsInSpies, enhanceContext],
});
diff --git a/code/core/src/types/modules/csf.ts b/code/core/src/types/modules/csf.ts
index 9d5659aca020..7208efab6d25 100644
--- a/code/core/src/types/modules/csf.ts
+++ b/code/core/src/types/modules/csf.ts
@@ -1,7 +1,7 @@
import type { ViewMode as ViewModeBase } from 'storybook/internal/csf';
import type { Renderer as CSFRenderer } from 'storybook/internal/csf';
-import type { Addon_OptionsParameter } from './addons';
+import type { Addon_OptionsParameterV7 } from './addons';
// Fix https://github.com/storybookjs/storybook/issues/30540
// Can be removed once @storybook/core and storybook are merged in 9.0
@@ -77,7 +77,7 @@ export type ViewMode = OrString | undefined;
type Layout = 'centered' | 'fullscreen' | 'padded' | 'none';
export interface StorybookParameters {
- options?: Addon_OptionsParameter;
+ options?: Addon_OptionsParameterV7;
/**
* The layout property defines basic styles added to the preview body where the story is rendered.
*
@@ -86,6 +86,10 @@ export interface StorybookParameters {
layout?: Layout;
}
+export interface StorybookTypes {
+ parameters: StorybookParameters;
+}
+
export interface StorybookInternalParameters extends StorybookParameters {
fileName?: string;
docsOnly?: true;
diff --git a/code/core/src/types/modules/story.ts b/code/core/src/types/modules/story.ts
index 61a23559ef20..72b963431bc6 100644
--- a/code/core/src/types/modules/story.ts
+++ b/code/core/src/types/modules/story.ts
@@ -44,7 +44,6 @@ export type RenderToCanvas = (
export interface ProjectAnnotations
extends BaseProjectAnnotations {
- addons?: ProjectAnnotations[];
testingLibraryRender?: (...args: never[]) => { unmount: () => void };
renderToCanvas?: RenderToCanvas;
}
diff --git a/code/core/src/viewport/components/Tool.tsx b/code/core/src/viewport/components/Tool.tsx
index 0f978954cc2b..6a97c65ddd33 100644
--- a/code/core/src/viewport/components/Tool.tsx
+++ b/code/core/src/viewport/components/Tool.tsx
@@ -133,7 +133,7 @@ const Pure = React.memo(function PureTool(props: PureProps) {
...Object.entries(viewportMap).map(([k, value]) => ({
id: k,
title: value.name,
- icon: iconsMap[value.type],
+ icon: iconsMap[value.type!],
active: k === viewportName,
onClick: () => {
update({ value: k, isRotated: false });
diff --git a/code/core/src/viewport/index.ts b/code/core/src/viewport/index.ts
index f6d57c404fdd..097f071c30c1 100644
--- a/code/core/src/viewport/index.ts
+++ b/code/core/src/viewport/index.ts
@@ -1,5 +1,4 @@
export * from './constants';
export * from './types';
export * from './defaults';
-export * from './preview';
export * from './responsiveViewport';
diff --git a/code/core/src/viewport/preview.ts b/code/core/src/viewport/preview.ts
index 338373d21e4e..30ca59dadd2d 100644
--- a/code/core/src/viewport/preview.ts
+++ b/code/core/src/viewport/preview.ts
@@ -1,13 +1,15 @@
-import { definePreview } from 'storybook/preview-api';
+import { definePreviewAddon } from 'storybook/internal/csf';
import { PARAM_KEY } from './constants';
-import type { GlobalState } from './types';
+import type { GlobalState, ViewportTypes } from './types';
export const initialGlobals: Record = {
[PARAM_KEY]: { value: undefined, isRotated: false },
};
+export type { ViewportTypes };
+
export default () =>
- definePreview({
+ definePreviewAddon({
initialGlobals,
});
diff --git a/code/core/src/viewport/types.ts b/code/core/src/viewport/types.ts
index 3607e3c413bf..2388b0949d9b 100644
--- a/code/core/src/viewport/types.ts
+++ b/code/core/src/viewport/types.ts
@@ -1,7 +1,7 @@
export interface Viewport {
name: string;
styles: ViewportStyles;
- type: 'desktop' | 'mobile' | 'tablet' | 'other';
+ type?: 'desktop' | 'mobile' | 'tablet' | 'other';
}
export interface ViewportStyles {
@@ -22,7 +22,7 @@ export type GlobalState = {
* When true the viewport applied will be rotated 90°, e.g. it will rotate from portrait to
* landscape orientation.
*/
- isRotated: boolean;
+ isRotated?: boolean;
};
export type GlobalStateUpdate = Partial;
@@ -33,7 +33,7 @@ export interface ViewportParameters {
*
* @see https://storybook.js.org/docs/essentials/viewport#parameters
*/
- viewport: {
+ viewport?: {
/**
* Remove the addon panel and disable the addon's behavior . If you wish to turn off this addon
* for the entire Storybook, you should do so when registering addon-essentials
@@ -56,5 +56,10 @@ export interface ViewportGlobals {
*
* @see https://storybook.js.org/docs/essentials/viewport#globals
*/
- viewport: GlobalState | GlobalState['value'];
+ viewport?: GlobalState | GlobalState['value'];
+}
+
+export interface ViewportTypes {
+ parameters: ViewportParameters;
+ globals: ViewportGlobals;
}
diff --git a/code/core/src/viewport/utils.tsx b/code/core/src/viewport/utils.tsx
index 983965cd27c1..e3f66838aef5 100644
--- a/code/core/src/viewport/utils.tsx
+++ b/code/core/src/viewport/utils.tsx
@@ -37,7 +37,7 @@ export const IconButtonLabel = styled.div(({ theme }) => ({
marginLeft: 10,
}));
-export const iconsMap: Record = {
+export const iconsMap: Record, React.ReactNode> = {
desktop: ,
mobile: ,
tablet: ,
diff --git a/code/frameworks/nextjs-vite/src/index.ts b/code/frameworks/nextjs-vite/src/index.ts
index 09da2fa5df6b..b0d1de86853d 100644
--- a/code/frameworks/nextjs-vite/src/index.ts
+++ b/code/frameworks/nextjs-vite/src/index.ts
@@ -1,12 +1,14 @@
+import type { AddonTypes, InferTypes, PreviewAddon } from 'storybook/internal/csf';
import type { ProjectAnnotations } from 'storybook/internal/types';
import type { ReactPreview } from '@storybook/react';
import { __definePreview } from '@storybook/react';
-import type { ReactRenderer } from '@storybook/react';
+import type { ReactTypes } from '@storybook/react';
import type vitePluginStorybookNextJs from 'vite-plugin-storybook-nextjs';
import * as nextPreview from './preview';
+import type { NextJsTypes } from './types';
export * from '@storybook/react';
// @ts-expect-error (double exports)
@@ -19,14 +21,14 @@ declare module '@storybook/nextjs-vite/vite-plugin' {
export const storybookNextJsPlugin: typeof vitePluginStorybookNextJs;
}
-export function definePreview(preview: NextPreview['input']) {
+export function definePreview[]>(
+ preview: { addons?: Addons } & ProjectAnnotations>
+): NextPreview> {
+ // @ts-expect-error hard
return __definePreview({
...preview,
- addons: [
- nextPreview as unknown as ProjectAnnotations,
- ...(preview.addons ?? []),
- ],
- }) as NextPreview;
+ addons: [nextPreview, ...(preview.addons ?? [])],
+ });
}
-interface NextPreview extends ReactPreview {}
+interface NextPreview extends ReactPreview {}
diff --git a/code/frameworks/nextjs-vite/src/types.ts b/code/frameworks/nextjs-vite/src/types.ts
index 29e290ff71fe..dcd8136fe5b9 100644
--- a/code/frameworks/nextjs-vite/src/types.ts
+++ b/code/frameworks/nextjs-vite/src/types.ts
@@ -52,9 +52,13 @@ export interface NextJsParameters {
* Next.js navigation configuration when using `next/navigation`. Please note that it can only
* be used in components/pages in the app directory.
*/
- navigation?: NextRouter;
+ navigation?: Partial;
/** Next.js router configuration */
- router?: NextRouter;
+ router?: Partial;
};
}
+
+export interface NextJsTypes {
+ parameters: NextJsParameters;
+}
diff --git a/code/frameworks/nextjs/src/index.ts b/code/frameworks/nextjs/src/index.ts
index f91791742e5b..88aecbb315e6 100644
--- a/code/frameworks/nextjs/src/index.ts
+++ b/code/frameworks/nextjs/src/index.ts
@@ -1,18 +1,26 @@
+import type { AddonTypes, InferTypes, PreviewAddon } from 'storybook/internal/csf';
+import type { ProjectAnnotations } from 'storybook/internal/types';
+
import type { ReactPreview } from '@storybook/react';
import { __definePreview } from '@storybook/react';
+import type { ReactTypes } from '@storybook/react';
import * as nextPreview from './preview';
+import type { NextJsTypes } from './types';
export * from '@storybook/react';
export * from './types';
// @ts-expect-error (double exports)
export * from './portable-stories';
-export function definePreview(preview: NextPreview['input']) {
+export function definePreview[]>(
+ preview: { addons?: Addons } & ProjectAnnotations>
+): NextPreview> {
+ // @ts-expect-error hard
return __definePreview({
...preview,
addons: [nextPreview, ...(preview.addons ?? [])],
- }) as NextPreview;
+ });
}
-interface NextPreview extends ReactPreview {}
+interface NextPreview extends ReactPreview {}
diff --git a/code/frameworks/nextjs/src/types.ts b/code/frameworks/nextjs/src/types.ts
index 986c1a542746..2d9d0874b855 100644
--- a/code/frameworks/nextjs/src/types.ts
+++ b/code/frameworks/nextjs/src/types.ts
@@ -67,9 +67,13 @@ export interface NextJsParameters {
* Next.js navigation configuration when using `next/navigation`. Please note that it can only
* be used in components/pages in the app directory.
*/
- navigation?: NextRouter;
+ navigation?: Partial;
/** Next.js router configuration */
- router?: NextRouter;
+ router?: Partial;
};
}
+
+export interface NextJsTypes {
+ parameters: NextJsParameters;
+}
diff --git a/code/lib/cli-storybook/src/add.ts b/code/lib/cli-storybook/src/add.ts
index ba0e8999f79f..3b9f37fa52c2 100644
--- a/code/lib/cli-storybook/src/add.ts
+++ b/code/lib/cli-storybook/src/add.ts
@@ -180,7 +180,7 @@ export async function add(
// TODO: remove try/catch once CSF factories is shipped, for now gracefully handle any error
try {
- await syncStorybookAddons(mainConfig, previewConfigPath!);
+ await syncStorybookAddons(mainConfig, previewConfigPath!, configDir);
} catch (e) {
//
}
diff --git a/code/lib/cli-storybook/src/codemod/csf-factories.ts b/code/lib/cli-storybook/src/codemod/csf-factories.ts
index cb2913280cd4..f8d5abd4f54f 100644
--- a/code/lib/cli-storybook/src/codemod/csf-factories.ts
+++ b/code/lib/cli-storybook/src/codemod/csf-factories.ts
@@ -66,6 +66,7 @@ export const csfFactories: CommandFix = {
promptType: 'command',
async run({
dryRun,
+ configDir,
mainConfig,
mainConfigPath,
previewConfigPath,
@@ -137,7 +138,7 @@ export const csfFactories: CommandFix = {
configToCsfFactory(fileInfo, { configType: 'preview', frameworkPackage }, { dryRun })
);
- await syncStorybookAddons(mainConfig, previewConfigPath!);
+ await syncStorybookAddons(mainConfig, previewConfigPath!, configDir);
logger.log(
printBoxedMessage(
diff --git a/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.test.ts b/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.test.ts
index 0f2f0759d3b3..c7e35818d15a 100644
--- a/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.test.ts
+++ b/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.test.ts
@@ -183,7 +183,7 @@ describe('stories codemod', () => {
const someData = {};
- export const A = meta.story({});
+ export const A = meta.story();
export const B = meta.story({
...A.input,
@@ -280,7 +280,7 @@ describe('stories codemod', () => {
title: 'Component',
});
- export const A = meta.story({});
+ export const A = meta.story();
export const B = meta.story({
play: async () => {
await A.play();
@@ -375,9 +375,7 @@ describe('stories codemod', () => {
import preview from '#.storybook/preview';
const meta = preview.meta({ title: 'Component' });
- export const CSF1Story = meta.story({
- render: () => Hello
,
- });
+ export const CSF1Story = meta.story(() => Hello
);
`);
});
});
@@ -588,7 +586,7 @@ describe('stories codemod', () => {
const meta = preview.meta({});
- export const A = meta.story({});
+ export const A = meta.story();
`);
});
});
diff --git a/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.ts b/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.ts
index e62058da7bdc..a20c4688023c 100644
--- a/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.ts
+++ b/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.ts
@@ -113,19 +113,16 @@ export async function storyToCsfFactory(
if (t.isObjectExpression(init)) {
// Wrap the object in `meta.story()`
+
declarator.init = t.callExpression(
t.memberExpression(t.identifier(metaVariableName), t.identifier('story')),
- [init]
+ init.properties.length === 0 ? [] : [init]
);
} else if (t.isArrowFunctionExpression(init)) {
// Transform CSF1 to meta.story({ render: })
- const renderProperty = t.objectProperty(t.identifier('render'), init);
-
- const objectExpression = t.objectExpression([renderProperty]);
-
declarator.init = t.callExpression(
t.memberExpression(t.identifier(metaVariableName), t.identifier('story')),
- [objectExpression]
+ [init]
);
}
}
diff --git a/code/renderers/react/src/__test__/Button.csf4.stories.tsx b/code/renderers/react/src/__test__/Button.csf4.stories.tsx
index 714949efef1e..8b19ccb0d50b 100644
--- a/code/renderers/react/src/__test__/Button.csf4.stories.tsx
+++ b/code/renderers/react/src/__test__/Button.csf4.stories.tsx
@@ -7,7 +7,9 @@ import { expect, fn, mocked, userEvent, within } from 'storybook/test';
import { __definePreview } from '../preview';
import { Button } from './Button';
-const preview = __definePreview({});
+const preview = __definePreview({
+ addons: [],
+});
const meta = preview.meta({
id: 'button-component',
@@ -49,7 +51,7 @@ const getCaptionForLocale = (locale: string) => {
};
export const CSF2StoryWithLocale = meta.story({
- render: (args, { globals: { locale } }) => {
+ render: (_, { globals: { locale } }) => {
const caption = getCaptionForLocale(locale);
return (
<>
@@ -218,8 +220,8 @@ export const WithActionArg = meta.story({
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
- const buttonEl = await canvas.getByRole('button');
- await buttonEl.click();
+ const buttonEl = canvas.getByRole('button');
+ buttonEl.click();
},
});
@@ -290,7 +292,7 @@ export const Modal = meta.story({
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
- const openModalButton = await canvas.getByRole('button', { name: /open modal/i });
+ const openModalButton = canvas.getByRole('button', { name: /open modal/i });
await userEvent.click(openModalButton);
await expect(within(document.body).getByRole('dialog')).toBeInTheDocument();
},
diff --git a/code/renderers/react/src/__test__/__snapshots__/portable-stories-factory.test.tsx.snap b/code/renderers/react/src/__test__/__snapshots__/portable-stories-factory.test.tsx.snap
index 3f00ff746281..9882ac099027 100644
--- a/code/renderers/react/src/__test__/__snapshots__/portable-stories-factory.test.tsx.snap
+++ b/code/renderers/react/src/__test__/__snapshots__/portable-stories-factory.test.tsx.snap
@@ -13,6 +13,20 @@ exports[`Renders CSF2Secondary story 1`] = `
+
+
diff --git a/code/renderers/react/src/__test__/portable-stories-factory.test.tsx b/code/renderers/react/src/__test__/portable-stories-factory.test.tsx
index b61e935ca953..a338bcd7b755 100644
--- a/code/renderers/react/src/__test__/portable-stories-factory.test.tsx
+++ b/code/renderers/react/src/__test__/portable-stories-factory.test.tsx
@@ -1,79 +1,51 @@
// @vitest-environment happy-dom
import { cleanup, render, screen } from '@testing-library/react';
-import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
+import { afterEach, describe, expect, it, vi } from 'vitest';
import React from 'react';
-import type { ProjectAnnotations } from 'storybook/internal/csf';
-
-import type { Meta, ReactRenderer } from '@storybook/react';
-
-import { expectTypeOf } from 'expect-type';
-import { addons } from 'storybook/preview-api';
-
-import { composeStories, composeStory, setProjectAnnotations } from '..';
-import type { Button } from './Button';
-import * as ButtonStories from './Button.csf4.stories';
-import * as ComponentWithErrorStories from './ComponentWithError.stories';
-
-const HooksStory = composeStory(
- ButtonStories.HooksStory.input,
- ButtonStories.CSF3Primary.meta.input
-);
-
-const projectAnnotations = setProjectAnnotations([]);
-
-// example with composeStories, returns an object with all stories composed with args/decorators
-// @ts-expect-error TODO: add a way to provide custom args/argTypes
-const { CSF3Primary, LoaderStory, MountInPlayFunction, MountInPlayFunctionThrow } =
- // @ts-expect-error TODO: add a way to provide custom args/argTypes
- composeStories(ButtonStories);
-const { ThrowsError } = composeStories(ComponentWithErrorStories);
-
-beforeAll(async () => {
- await projectAnnotations.beforeAll?.();
-});
+import {
+ CSF2StoryWithLocale,
+ CSF3Button,
+ CSF3ButtonWithRender,
+ CSF3InputFieldFilled,
+ CSF3Primary,
+ MountInPlayFunction,
+ MountInPlayFunctionThrow,
+} from './Button.csf4.stories';
+import { CSF2Secondary, HooksStory } from './Button.csf4.stories';
afterEach(() => {
cleanup();
});
-// example with composeStory, returns a single story composed with args/decorators
-const Secondary = composeStory(
- ButtonStories.CSF2Secondary.input,
- ButtonStories.CSF3Primary.meta.input
-);
describe('renders', () => {
it('renders primary button', () => {
- render(
Hello world);
+ render(
Hello world);
const buttonElement = screen.getByText(/Hello world/i);
expect(buttonElement).not.toBeNull();
});
it('reuses args from composed story', () => {
- render(
);
+ render(
);
const buttonElement = screen.getByRole('button');
- expect(buttonElement.textContent).toEqual(Secondary.args.children);
+ expect(buttonElement.textContent).toEqual(CSF2Secondary.input.args.children);
});
it('onclick handler is called', async () => {
const onClickSpy = vi.fn();
- render(
);
+ render(
);
const buttonElement = screen.getByRole('button');
buttonElement.click();
expect(onClickSpy).toHaveBeenCalled();
});
it('reuses args from composeStories', () => {
- const { getByText } = render(
);
+ const { getByText } = render(
);
const buttonElement = getByText(/foo/i);
expect(buttonElement).not.toBeNull();
});
- it('should throw error when rendering a component with a render error', async () => {
- await expect(() => ThrowsError.run()).rejects.toThrowError('Error in render');
- });
-
it('should render component mounted in play function', async () => {
await MountInPlayFunction.run();
@@ -84,88 +56,21 @@ describe('renders', () => {
it('should throw an error in play function', async () => {
await expect(() => MountInPlayFunctionThrow.run()).rejects.toThrowError('Error thrown in play');
});
-
- it('should call and compose loaders data', async () => {
- await LoaderStory.load();
- const { getByTestId } = render(
);
- expect(getByTestId('spy-data').textContent).toEqual('mockFn return value');
- expect(getByTestId('loaded-data').textContent).toEqual('loaded data');
- // spy assertions happen in the play function and should work
- await LoaderStory.run!();
- });
-});
-
-describe('projectAnnotations', () => {
- it('renders with default projectAnnotations', () => {
- setProjectAnnotations([
- {
- parameters: { injected: true },
- initialGlobals: {
- locale: 'en',
- },
- },
- ]);
- const WithEnglishText = composeStory(
- ButtonStories.CSF2StoryWithLocale.input,
- ButtonStories.CSF3Primary.meta.input
- );
- const { getByText } = render(
);
- const buttonElement = getByText('Hello!');
- expect(buttonElement).not.toBeNull();
- expect(WithEnglishText.parameters?.injected).toBe(true);
- });
-
- it('renders with custom projectAnnotations via composeStory params', () => {
- const WithPortugueseText = composeStory(
- ButtonStories.CSF2StoryWithLocale.input,
- ButtonStories.CSF3Primary.meta.input,
- {
- initialGlobals: { locale: 'pt' },
- }
- );
- const { getByText } = render(
);
- const buttonElement = getByText('Olá!');
- expect(buttonElement).not.toBeNull();
- });
-
- it('has action arg from argTypes when addon-actions annotations are added', () => {
- const Story = composeStory(
- ButtonStories.WithActionArgType.input,
- ButtonStories.CSF3Primary.meta.input
- );
-
- // TODO: add a way to provide custom args/argTypes, right now it's type any
- expect(Story.args.someActionArg).toHaveProperty('isAction', true);
- });
});
describe('CSF3', () => {
it('renders with inferred globalRender', () => {
- const Primary = composeStory(
- ButtonStories.CSF3Button.input,
- ButtonStories.CSF3Primary.meta.input
- );
-
- render(
Hello world);
+ render(
Hello world);
const buttonElement = screen.getByText(/Hello world/i);
expect(buttonElement).not.toBeNull();
});
it('renders with custom render function', () => {
- const Primary = composeStory(
- ButtonStories.CSF3ButtonWithRender.input,
- ButtonStories.CSF3Primary.meta.input
- );
-
- render(
);
+ render(
);
expect(screen.getByTestId('custom-render')).not.toBeNull();
});
it('renders with play function without canvas element', async () => {
- const CSF3InputFieldFilled = composeStory(
- ButtonStories.CSF3InputFieldFilled.input,
- ButtonStories.CSF3Primary.meta.input
- );
await CSF3InputFieldFilled.run();
const input = screen.getByTestId('input') as HTMLInputElement;
@@ -173,11 +78,6 @@ describe('CSF3', () => {
});
it('renders with play function with canvas element', async () => {
- const CSF3InputFieldFilled = composeStory(
- ButtonStories.CSF3InputFieldFilled.input,
- ButtonStories.CSF3Primary.meta.input
- );
-
let divElement;
try {
divElement = document.createElement('div');
@@ -202,53 +102,15 @@ describe('CSF3', () => {
});
});
-// common in addons that need to communicate between manager and preview
-it('should pass with decorators that need addons channel', () => {
- const PrimaryWithChannels = composeStory(
- ButtonStories.CSF3Primary.input,
- ButtonStories.CSF3Primary.meta.input,
- {
- decorators: [
- (StoryFn: any) => {
- addons.getChannel();
- return StoryFn();
- },
- ],
- }
- );
- render(
Hello world);
- const buttonElement = screen.getByText(/Hello world/i);
- expect(buttonElement).not.toBeNull();
-});
-
-describe('ComposeStories types', () => {
- // this file tests Typescript types that's why there are no assertions
- it('Should support typescript operators', () => {
- type ComposeStoriesParam = Parameters
[0];
-
- expectTypeOf({
- ...ButtonStories,
- default: ButtonStories.CSF3Primary.meta.input as Meta,
- }).toMatchTypeOf();
-
- expectTypeOf({
- ...ButtonStories,
- default: ButtonStories.CSF3Primary.meta.input satisfies Meta,
- }).toMatchTypeOf();
- });
-});
-
-// @ts-expect-error TODO: fix the types for this
-const testCases = Object.values(composeStories(ButtonStories)).map(
- // @ts-expect-error TODO: fix the types for this
- (Story) => [Story.storyName, Story] as [string, typeof Story]
-);
+const testCases = Object.entries({
+ CSF2StoryWithLocale,
+ CSF3Button,
+ CSF3ButtonWithRender,
+ CSF3InputFieldFilled,
+ CSF3Primary,
+ MountInPlayFunction,
+}).map(([name, Story]) => [name, Story] as [string, typeof Story]);
it.each(testCases)('Renders %s story', async (_storyName, Story) => {
- if (_storyName === 'CSF2StoryWithLocale' || _storyName === 'MountInPlayFunctionThrow') {
- return;
- }
-
- // @ts-expect-error TODO: fix the types for this
await Story.run();
expect(document.body).toMatchSnapshot();
});
diff --git a/code/renderers/react/src/csf-factories.test.tsx b/code/renderers/react/src/csf-factories.test.tsx
index 7a9aa5093d7b..03b0de64da0a 100644
--- a/code/renderers/react/src/csf-factories.test.tsx
+++ b/code/renderers/react/src/csf-factories.test.tsx
@@ -3,7 +3,7 @@
import { describe, it } from 'vitest';
import { expect, test } from 'vitest';
-import type { KeyboardEventHandler, ReactElement, ReactNode } from 'react';
+import type { ComponentType, KeyboardEventHandler, ReactElement, ReactNode } from 'react';
import React from 'react';
import type { Canvas } from 'storybook/internal/csf';
@@ -16,10 +16,12 @@ import type { Mock } from 'storybook/test';
import { __definePreview } from './preview';
import type { Decorator } from './public-types';
-type ButtonProps = { label: string; disabled: boolean };
+type ButtonProps = { label: string; disabled: boolean; onKeyDown?: () => void };
const Button: (props: ButtonProps) => ReactElement = () => <>>;
-const preview = __definePreview({});
+const preview = __definePreview({
+ addons: [],
+});
test('csf factories', () => {
const config = __definePreview({
@@ -92,9 +94,10 @@ describe('Args can be provided in multiple ways', () => {
args: { label: 'good' },
});
const Basic = meta.story({
- args: {},
render: () => Hello world
,
});
+
+ const CSF1 = meta.story(() => Hello world
);
});
it('❌ Required args need to be provided when the user uses a non-empty render', () => {
@@ -214,6 +217,56 @@ describe('Story args can be inferred', () => {
args: { decoratorArg: 0, decoratorArg2: '', label: 'good' },
});
});
+
+ it('Component type can be overridden', () => {
+ const meta = preview.meta({
+ component: Button as unknown as ComponentType<
+ Omit & { onKeyDown?: boolean }
+ >,
+ render: ({ onKeyDown, ...args }) => {
+ return