diff --git a/CHANGELOG.md b/CHANGELOG.md index c4284d55bc65..162b33d1d6a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 9.1.5 + +- CSF: Support `satisfies x as y` syntax - [#32169](https://github.com/storybookjs/storybook/pull/32169), thanks @diagramatics! +- Vitest addon: Handle Playwright installation errors gracefully - [#32329](https://github.com/storybookjs/storybook/pull/32329), thanks @ndelangen! + ## 9.1.4 - Angular: Properly merge builder options and browserTarget options - [#32272](https://github.com/storybookjs/storybook/pull/32272), thanks @kroeder! diff --git a/code/addons/vitest/src/postinstall.ts b/code/addons/vitest/src/postinstall.ts index 020ac845a31e..38c22b3a422a 100644 --- a/code/addons/vitest/src/postinstall.ts +++ b/code/addons/vitest/src/postinstall.ts @@ -292,10 +292,14 @@ export default async function postInstall(options: PostinstallOptions) { } else { logger.plain(`${step} Configuring Playwright with Chromium (this might take some time):`); logger.plain(' npx playwright install chromium --with-deps'); - await packageManager.executeCommand({ - command: 'npx', - args: ['playwright', 'install', 'chromium', '--with-deps'], - }); + try { + await packageManager.executeCommand({ + command: 'npx', + args: ['playwright', 'install', 'chromium', '--with-deps'], + }); + } catch (e) { + console.error('Failed to install Playwright. Please install it manually'); + } } const fileExtension = diff --git a/code/core/src/csf-tools/CsfFile.test.ts b/code/core/src/csf-tools/CsfFile.test.ts index 4299b8d586ff..a3ff6cee8cee 100644 --- a/code/core/src/csf-tools/CsfFile.test.ts +++ b/code/core/src/csf-tools/CsfFile.test.ts @@ -515,6 +515,57 @@ describe('CsfFile', () => { `); }); + it('typescript satisfies as', () => { + expect( + parse( + dedent` + import type { Meta, StoryFn, StoryObj } from '@storybook/react'; + type PropTypes = {}; + export default { title: 'foo/bar' } satisfies Meta as Meta; + export const A = { name: 'AA' } satisfies StoryObj; + export const B = ((args) => {}) satisfies StoryFn; + `, + true + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + stories: + - id: foo-bar--a + name: AA + parameters: + __isArgsStory: true + __id: foo-bar--a + __stats: + factory: false + play: false + render: false + loaders: false + beforeEach: false + globals: false + tags: false + storyFn: false + mount: false + moduleMock: false + - id: foo-bar--b + name: B + parameters: + __isArgsStory: true + __id: foo-bar--b + __stats: + factory: false + play: false + render: false + loaders: false + beforeEach: false + globals: false + tags: false + storyFn: false + mount: false + moduleMock: false + `); + }); + it('typescript meta var', () => { expect( parse( @@ -605,6 +656,136 @@ describe('CsfFile', () => { `); }); + it('typescript satisfies as meta', () => { + expect( + parse( + dedent` + import type { Meta, StoryFn } from '@storybook/react'; + type PropTypes = {}; + const meta = { title: 'foo/bar/baz' } satisfies Meta as Meta; + export default meta; + export const A: StoryFn = () => <>A; + export const B: StoryFn = () => <>B; + ` + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar/baz + stories: + - id: foo-bar-baz--a + name: A + __stats: + factory: false + play: false + render: false + loaders: false + beforeEach: false + globals: false + tags: false + storyFn: true + mount: false + moduleMock: false + - id: foo-bar-baz--b + name: B + __stats: + factory: false + play: false + render: false + loaders: false + beforeEach: false + globals: false + tags: false + storyFn: true + mount: false + moduleMock: false + `); + }); + + it('typescript satisfies as stories', () => { + expect( + parse( + dedent` + import type { Meta, StoryFn, StoryObj } from '@storybook/react'; + type PropTypes = {}; + export default { title: 'foo/bar' } as Meta; + export const A = { name: 'AA' } satisfies StoryObj as StoryObj; + export const B = ((args) => {}) satisfies StoryFn as StoryFn; + `, + true + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + stories: + - id: foo-bar--a + name: AA + parameters: + __isArgsStory: true + __id: foo-bar--a + __stats: + factory: false + play: false + render: false + loaders: false + beforeEach: false + globals: false + tags: false + storyFn: false + mount: false + moduleMock: false + - id: foo-bar--b + name: B + parameters: + __isArgsStory: true + __id: foo-bar--b + __stats: + factory: false + play: false + render: false + loaders: false + beforeEach: false + globals: false + tags: false + storyFn: false + mount: false + moduleMock: false + `); + }); + + it('typescript satisfies as export specifier', () => { + expect( + parse( + dedent` + import type { Meta, StoryFn } from '@storybook/react'; + type PropTypes = {}; + const meta = { title: 'foo/bar/baz' } satisfies Meta as Meta; + const story = { name: 'Story A' }; + export { meta as default, story as A }; + `, + true + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar/baz + stories: + - id: foo-bar-baz--a + name: A + localName: story + parameters: + __id: foo-bar-baz--a + __stats: + play: false + render: false + loaders: false + beforeEach: false + globals: false + tags: false + storyFn: false + mount: false + moduleMock: false + `); + }); + it('component object', () => { expect( parse( @@ -1995,7 +2176,7 @@ describe('CsfFile', () => { const { indexInputs } = loadCsf( dedent` const Component = (props) =>
hello
; - + export default { title: 'custom foo title', component: Component, @@ -2513,7 +2694,7 @@ describe('CsfFile', () => { ).toThrowErrorMatchingInlineSnapshot(` [MultipleMetaError: CSF: multiple meta objects (line 4, col 24) - More info: https://storybook.js.org/docs/writing-stories#default-export?ref=error] + More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export] `); }); @@ -2531,7 +2712,7 @@ describe('CsfFile', () => { ).toThrowErrorMatchingInlineSnapshot(` [MultipleMetaError: CSF: multiple meta objects (line 3, col 25) - More info: https://storybook.js.org/docs/writing-stories#default-export?ref=error] + More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export] `); }); @@ -2549,7 +2730,7 @@ describe('CsfFile', () => { ).toThrowErrorMatchingInlineSnapshot(` [MultipleMetaError: CSF: multiple meta objects - More info: https://storybook.js.org/docs/writing-stories#default-export?ref=error] + More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export] `); }); @@ -2565,7 +2746,7 @@ describe('CsfFile', () => { ).toThrowErrorMatchingInlineSnapshot(` [BadMetaError: CSF: meta() factory must be imported from .storybook/preview configuration (line 1, col 0) - More info: https://storybook.js.org/docs/writing-stories#default-export?ref=error] + More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export] `); }); @@ -2582,7 +2763,7 @@ describe('CsfFile', () => { ).toThrowErrorMatchingInlineSnapshot(` [BadMetaError: CSF: meta() factory must be imported from .storybook/preview configuration (line 4, col 28) - More info: https://storybook.js.org/docs/writing-stories#default-export?ref=error] + More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export] `); }); @@ -2599,7 +2780,7 @@ describe('CsfFile', () => { ).toThrowErrorMatchingInlineSnapshot(` [MixedFactoryError: CSF: expected factory story (line 4, col 17) - More info: https://storybook.js.org/docs/writing-stories#default-export?ref=error] + More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export] `); }); @@ -2616,7 +2797,7 @@ describe('CsfFile', () => { ).toThrowErrorMatchingInlineSnapshot(` [MixedFactoryError: CSF: expected non-factory story (line 4, col 28) - More info: https://storybook.js.org/docs/writing-stories#default-export?ref=error] + More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export] `); }); }); diff --git a/code/core/src/csf-tools/CsfFile.ts b/code/core/src/csf-tools/CsfFile.ts index 7e88d731e6e9..ab23eb33045a 100644 --- a/code/core/src/csf-tools/CsfFile.ts +++ b/code/core/src/csf-tools/CsfFile.ts @@ -181,7 +181,7 @@ export class NoMetaError extends Error { super(dedent` CSF: ${msg} ${formatLocation(ast, fileName)} - More info: https://storybook.js.org/docs/writing-stories#default-export?ref=error + More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export `); this.name = this.constructor.name; } @@ -193,7 +193,7 @@ export class MultipleMetaError extends Error { super(dedent` CSF: ${message} ${formatLocation(ast, fileName)} - More info: https://storybook.js.org/docs/writing-stories#default-export?ref=error + More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export `); this.name = this.constructor.name; } @@ -205,7 +205,7 @@ export class MixedFactoryError extends Error { super(dedent` CSF: ${message} ${formatLocation(ast, fileName)} - More info: https://storybook.js.org/docs/writing-stories#default-export?ref=error + More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export `); this.name = this.constructor.name; } @@ -217,7 +217,7 @@ export class BadMetaError extends Error { super(dedent` CSF: ${message} ${formatLocation(ast, fileName)} - More info: https://storybook.js.org/docs/writing-stories#default-export?ref=error + More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export `); this.name = this.constructor.name; } @@ -442,10 +442,18 @@ export class CsfFile { metaNode = decl; } else if ( // export default { ... } as Meta<...> + // export default { ... } satisfies Meta<...> (t.isTSAsExpression(decl) || t.isTSSatisfiesExpression(decl)) && t.isObjectExpression(decl.expression) ) { metaNode = decl.expression; + } else if ( + // export default { ... } satisfies Meta as Meta<...> + t.isTSAsExpression(decl) && + t.isTSSatisfiesExpression(decl.expression) && + t.isObjectExpression(decl.expression.expression) + ) { + metaNode = decl.expression.expression; } if (metaNode && t.isProgram(parent)) { @@ -495,10 +503,22 @@ export class CsfFile { } let storyNode; if (t.isVariableDeclarator(decl)) { - storyNode = - t.isTSAsExpression(decl.init) || t.isTSSatisfiesExpression(decl.init) - ? decl.init.expression - : decl.init; + if ( + t.isTSAsExpression(decl.init) && + t.isTSSatisfiesExpression(decl.init.expression) + ) { + // { ... } satisfies Meta<...> as Meta<...> + storyNode = decl.init.expression.expression; + } else if ( + t.isTSAsExpression(decl.init) || + t.isTSSatisfiesExpression(decl.init) + ) { + // { ... } as Meta<...> + // { ... } satisfies Meta<...> + storyNode = decl.init.expression; + } else { + storyNode = decl.init; + } } else { storyNode = decl; } @@ -596,10 +616,18 @@ export class CsfFile { metaNode = decl; } else if ( // export default { ... } as Meta<...> - t.isTSAsExpression(decl) && + // export default { ... } satisfies Meta<...> + (t.isTSAsExpression(decl) || t.isTSSatisfiesExpression(decl)) && t.isObjectExpression(decl.expression) ) { metaNode = decl.expression; + } else if ( + // export default { ... } satisfies Meta as Meta<...> + t.isTSAsExpression(decl) && + t.isTSSatisfiesExpression(decl.expression) && + t.isObjectExpression(decl.expression.expression) + ) { + metaNode = decl.expression.expression; } if (metaNode && t.isProgram(parent)) { @@ -681,7 +709,7 @@ export class CsfFile { throw new Error(dedent` Unexpected \`storiesOf\` usage: ${formatLocation(node, self._options.fileName)}. - SB8 does not support \`storiesOf\`. + SB8 does not support \`storiesOf\`. `); } if ( diff --git a/code/package.json b/code/package.json index edaa0c62c025..be8ca70b5337 100644 --- a/code/package.json +++ b/code/package.json @@ -285,5 +285,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "9.1.5" } diff --git a/docs/_snippets/csf-factories-automigrate.md b/docs/_snippets/csf-factories-automigrate.md new file mode 100644 index 000000000000..d1da7f3966de --- /dev/null +++ b/docs/_snippets/csf-factories-automigrate.md @@ -0,0 +1,11 @@ +```shell renderer="common" language="js" packageManager="npm" +npx storybook automigrate csf-factories +``` + +```shell renderer="common" language="js" packageManager="pnpm" +pnpm dlx storybook automigrate csf-factories +``` + +```shell renderer="common" language="js" packageManager="yarn" +yarn dlx storybook automigrate csf-factories +``` diff --git a/docs/_snippets/csf-next-builder-aliases.md b/docs/_snippets/csf-next-builder-aliases.md new file mode 100644 index 000000000000..8646e5f647e2 --- /dev/null +++ b/docs/_snippets/csf-next-builder-aliases.md @@ -0,0 +1,39 @@ +```ts filename=".storybook/main.js|ts" renderer="common" language="ts" tabTitle="Vite" +import path from 'path'; +// Replace your-framework with the framework you are using, e.g. react-vite, nextjs-vite, vue3-vite, etc. +import { defineMain } from '@storybook/your-framework/node'; + +export default defineMain({ + // ...rest of config + async viteFinal(config) { + if (config.resolve) { + config.resolve.alias = { + ...config.resolve?.alias, + '@': path.resolve(__dirname, './'), + }; + } + + return config; + }, +}); +``` + +```ts filename=".storybook/main.js|ts" renderer="common" language="ts" tabTitle="Webpack" +import path from 'path'; +// Replace your-framework with the framework you are using, e.g. react-webpack, nextjs, angular, etc. +import { defineMain } from '@storybook/your-framework/node'; + +export default defineMain({ + // ...rest of config + async webpackFinal(config) { + if (config.resolve) { + config.resolve.alias = { + ...config.resolve?.alias, + '@$': path.resolve(__dirname, './'), + }; + } + + return config; + }, +}); +``` diff --git a/docs/api/csf/csf-factories.mdx b/docs/api/csf/csf-factories.mdx deleted file mode 100644 index 0f8f76f9cc43..000000000000 --- a/docs/api/csf/csf-factories.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: 'Component Story Format (CSF)' -isTab: true -tab: - order: 2 - title: CSF Factories (Experimental) ---- - -CSF Factories are the next evolution of Storybook's Component Story Format (CSF). This new API uses a pattern called factory functions to provide full type safety to your Storybook stories, making it easier to configure addons correctly and unlocking the full potential of Storybook's features. - -While this feature is experimental, we will be documenting progress in the [RFC](https://github.com/storybookjs/storybook/discussions/30112). We welcome your feedback! diff --git a/docs/api/csf/csf-next.mdx b/docs/api/csf/csf-next.mdx new file mode 100644 index 000000000000..29431f3b94db --- /dev/null +++ b/docs/api/csf/csf-next.mdx @@ -0,0 +1,460 @@ +--- +title: 'Component Story Format (CSF)' +isTab: true +tab: + order: 2 + title: CSF Next (Preview) +--- + + + + + CSF Next is currently only supported in [React](?renderer=react) projects. + + + + + + + + This is a [**preview**](../../releases/features.mdx#preview) feature and (though unlikely) the API may change in future releases. We [welcome feedback](https://github.com/storybookjs/storybook/discussions/30112) and contributions to help improve this feature. + + +CSF Next is the next evolution of Storybook's Component Story Format (CSF). This new API uses a pattern called factory functions to provide full type safety to your Storybook stories, making it easier to configure addons correctly and unlocking the full potential of Storybook's features. + +This reference provides an overview of the API and a migration guide to upgrade from prior CSF versions. + +## Overview + +The CSF Next API is composed of functions to help you write stories. Note how three of the functions operate as factories, each producing the next function in the chain (`definePreview` โ†’ `preview.meta` โ†’ `meta.story`), providing full type safety at each step. + +### `defineMain` + +With CSF Next, your [main Storybook config](../main-config/main-config.mdx) is specified by the `defineMain` function. This function is type-safe and will automatically infer types for your project. + +```ts title=".storybook/main.js|ts" +// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite) +import { defineMain } from '@storybook/your-framework/node'; + +export default defineMain({ + framework: '@storybook/your-framework', + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: ['@storybook/addon-a11y'], +}); +``` + +### `definePreview` + +Similarly, the `definePreview` function specifies your project's story configuration. This function is also type-safe and will infer types throughout your project. + +Importantly, by specifying addons here, their types will be available throughout your project, enabling autocompletion and type checking. + +You will import the result of this function, `preview`, in your story files to define the component meta. + +```ts title=".storybook/preview.js|ts" +// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite) +import { definePreview } from '@storybook/your-framework'; +import addonA11y from '@storybook/addon-a11y'; + +export default definePreview({ + // ๐Ÿ‘‡ Add your addons here + addons: [addonA11y()], + parameters: { + // type-safe! + a11y: { + options: { xpath: true }, + }, + }, +}); +``` + + + The preview configuration will be automatically updated to reference the [necessary addons](#preview-addons) when installing an addon via `npx storybook add ` or running `storybook dev`. + + +### `preview.meta` + +The `meta` function on the `preview` object is used to define the [metadata for your stories](./index.mdx#default-export). It accepts an object containing the `component`, `title`, `parameters`, and other story properties. + +```ts title="Button.stories.js|ts" +import preview from '../.storybook/preview'; + +import { Button } from './Button'; + +const meta = preview.meta({ + component: Button, + parameters: { + // type-safe! + layout: 'centered', + } +}); +``` + + + + + +If you would like to use absolute imports instead of relative imports for your preview config, like below, you can configure that using subpath imports or an alias. + +```ts +// โœ… Absolute imports won't break if you move story files around +import preview from '#.storybook/preview'; + +// โŒ Relative imports can break if you move story files around +import preview from '../../../.storybook/preview'; +``` + +
+Configuration + +Subpath imports are a Node.js standard that allows you to define custom import paths in your project, which you can then use throughout your codebase. + +To configure subpath imports, add the following to your `package.json`: + +```json title="package.json" +{ + "imports": { + "#*": ["./*", "./*.ts", "./*.tsx"], + }, +} +``` + +For more information, refer to the [subpath imports documentation](../../writing-stories/mocking-data-and-modules/mocking-modules.mdx#subpath-imports). + +--- + +Alternatively, you can [configure an alias in your builder](../../writing-stories/mocking-data-and-modules/mocking-modules.mdx#builder-aliases) (Vite or Webpack). + +
+ +
+ +### `meta.story` + +Finally, the `story` function on the `meta` object defines the stories. This function accepts an object containing the [`name`](../../writing-stories/index.mdx#rename-stories), [`args`](../../writing-stories/args.mdx), [`parameters`](../../writing-stories/parameters.mdx), and other story properties. + +```ts title="Button.stories.js|ts" +// ...from above +const meta = preview.meta({ /* ... */ }); + +export const Primary = meta.story({ + args: { + primary: true, + }, +}); +``` + +#### `.extend` + +You can use the `.extend` method to create a new story based on an existing one, with the option to override or add new properties. + +
+Property merging details + +Properties are merged intelligently: + +- [`args`](../../writing-stories/args.mdx) are shallow merged +- [`parameters`](../../writing-stories/parameters.mdx) are deep merged, except for arrays, which are replaced +- [`decorators`](../../writing-stories/decorators.mdx) and [`tags`](../../writing-stories/tags.mdx) are concatenated + +```ts title="Button.stories.js|ts" +// ...from above +const meta = preview.meta({ /* ... */ }); + +export const Example = meta.story({ + args: { + primary: true, + exampleArray: ['a', 'b'], + exampleObject: { a: 'a', b: 'b' }, + }, + parameters: { + exampleArray: ['a', 'b'], + exampleObject: { a: 'a', b: 'b' }, + }, + tags: ['a'], +}); + +/* + * ๐Ÿ‘‡ Final values applied: + * { + * args: { + * primary: true, + * disabled: true, + * exampleArray: ['c'], + * exampleObject: { a: 'c' } + * }, + * parameters: { + * exampleArray: ['c'], + * exampleObject: { a: 'c', b: 'b' } + * }, + * tags: ['a', 'b'] + * } + */ +export const ExtendedExample = Example.extend({ + args: { + disabled: true, + exampleArray: ['c'], + exampleObject: { a: 'c' }, + }, + parameters: { + disabled: true, + exampleArray: ['c'], + exampleObject: { a: 'c' }, + }, + tags: ['b'], +}); +``` + +
+ +```ts title="Button.stories.js|ts" +// ...from above +const meta = preview.meta({ /* ... */ }); + +export const Primary = meta.story({ + args: { + primary: true, + }, +}); + +export const PrimaryDisabled = Primary.extend({ + args: { + disabled: true, + }, +}); +``` + +## Upgrade to CSF Next + +You can upgrade your stories to CSF Next either automatically (from CSF 3) or manually (from CSF 1, 2, or 3). CSF Next is designed to be usable incrementally; you do not have to upgrade all of your story files at once. However, you cannot mix story formats within the same file. + +Before proceeding, be sure you're using the latest version of Storybook. You can upgrade your Storybook automatically with this command: + + + +### Automatically + +You can automatically upgrade all of your project's stories from CSF 3 to CSF Next with this command: + + + +It will run through each of the manual upgrade steps below on all of your story files. + +
+Upgrading from CSF 2 to 3 + +You must be using CSF 3 to automatically upgrade to CSF Next. If you are using CSF 2, you can upgrade to CSF 3 first using this command: + + + +
+ +### Manual + +You can also upgrade your project's story files to CSF Next manually. Before using CSF Next in a story file, you must upgrade your `.storybook/main.js|ts` and `.storybook/preview.js|ts` files. + +**1. Update your main Storybook config file** + +Update your `.storybook/main.js|ts` file to use the new [`defineMain`](#definemain) function. + +```diff title=".storybook/main.js|ts" +// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite) ++ import { defineMain } from '@storybook/your-framework/node'; +- import { StorybookConfig } from '@storybook/your-framework'; + ++ export default defineMain({ +- export const config: StorybookConfig = { + // ...config itself is unchanged ++ }); +- }; +- export default config; +``` + +**2. Update your preview config file** +
+ +Update your `.storybook/preview.js|ts` file to use the new [`definePreview`](#definepreview) function. + +
+Which addons should be specified in `preview`? + +The ability for an addon to provide annotation types (`parameters`, `globals`, etc.) is new and not all addons support it yet. + +If an addon provides annotations (i.e. it distributes a `./preview` export), it can be imported in two ways: + +1. For official Storybook addons, you import the default export: + `import addonName from '@storybook/addon-name'` + +2. For community addons, you should import the entire module and access the addon from there: + `import * as addonName from 'community-addon-name'` + +
+ +```diff title=".storybook/preview.js|ts" +// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite) ++ import { definePreview } from '@storybook/your-framework'; +- import type { Preview } from '@storybook/your-framework'; +// ๐Ÿ‘‡ Import the addons you are using ++ import addonA11y from '@storybook/addon-a11y'; + ++ export default definePreview({ +- export const preview: Preview = { + // ...current config + // ๐Ÿ‘‡ Add your addons here ++ addons: [addonA11y()], ++ }); +- }; +- export default preview; +``` + +**3. Update your story files** + +Story files have been updated for improved usability. With the new format: + +- Import the preview construct from the Storybook preview file +- The meta object is now created via the [`preview.meta`](#previewmeta) function and does not have to be exported as a default export +- Stories are now created from the meta object, via the [`meta.story`](#metastory) function + + +The examples below show the changes needed to upgrade a story file from CSF 3 to CSF Next. You can also upgrade from CSF 1 or 2 using similar steps. + + +```diff title="Button.stories.js|ts" ++ import preview from '../.storybook/preview'; +- import type { Meta, StoryObj } from '@storybook/your-framework'; + +import { Button } from './Button'; + ++ const meta = preview.meta({ +- const meta = { + // ...meta object is unchanged ++ }); +- } satisfies Meta; +- export default meta; + +- type Story = StoryObj; + ++ export const Primary = meta.story({ +- export const Primary: Story = { + // ...story object is unchanged ++ }); +- }; +``` + +Note that importing or manually applying any type to the meta or stories is no longer necessary. Thanks to the factory function pattern, the types are now inferred automatically. + +**3.1 Reusing story properties** + + + +If you are reusing story properties to create a new story based on another, the [`.extend`](#storyextend) method is the recommended way to do so. + + + +Previously, story properties such as `Story.args` or `Story.parameters` were accessed directly when reusing them in another story. While accessing them like this is still supported, it is deprecated in CSF Next. + +All of the story properties are now contained within a new property called `composed` and should be accessed from that property instead. For instance, `Story.composed.args` or `Story.composed.parameters`. + +```diff title="Button.stories.js|ts" +// ...rest of file + ++ export const Primary = meta.story({ +- export const Primary: Story = { + args: { primary: true }, ++ }); +- }; + ++ export const PrimaryDisabled = meta.story({ +- export const PrimaryDisabled: Story = { + args: { ++ ...Primary.composed.args, +- ...Primary.args, + disabled: true, + } ++ }); +- }; +``` + + + The property name "composed" was chosen because the values within are composed from the story, its component meta, and the preview configuration. + + If you want to access the direct input to the story, you can use `Story.input` instead of `Story.composed`. + + +**4. Update your Vitest setup file** + +If you're using [Storybook's Vitest addon](../../writing-tests/integrations/vitest-addon.mdx), you can remove your Vitest setup file (`.storybook/vitest.setup.ts`). + +If you are using [portable stories in Vitest](../portable-stories/portable-stories-vitest.mdx), you may use a Vitest setup file to configure your stories. This file must be updated to use the new CSF Next format. + + +Note that this only applies if you use CSF Next for all your tested stories. If you use a mix of CSF 1, 2, or 3 and CSF Next, you must maintain two separate setup files. + + +```diff title="vitest.setup.js|ts" +import { beforeAll } from 'vitest'; +// ๐Ÿ‘‡ No longer necessary +- // Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc. +import { setProjectAnnotations } from '@storybook/your-framework'; +- import * as addonAnnotations from 'my-addon/preview'; ++ import preview from './.storybook/preview'; +- import * as previewAnnotations from './.storybook/preview'; + +// No longer necessary +- const annotations = setProjectAnnotations([previewAnnotations, addonAnnotations]); + +// Run Storybook's beforeAll hook ++ beforeAll(preview.composed.beforeAll); +- beforeAll(annotations.beforeAll); +``` + +**5. Reusing stories in test files** + +[Storybook's Vitest addon](../../writing-tests/integrations/vitest-addon.mdx) allows you to test your components directly inside Storybook. All the stories are automatically turned into Vitest tests, making integration seamless in your testing suite. + +If you cannot use Storybook Test, you can still reuse the stories in your test files using [portable stories](../portable-stories/portable-stories-vitest.mdx). In prior story formats, you had to compose the stories before rendering them in your test files. With CSF Next, you can now reuse the stories directly. + +```diff title="Button.test.js|ts" +import { test, expect } from 'vitest'; +import { screen } from '@testing-library/react'; +- import { composeStories } from '@storybook/your-framework'; + +// Import all stories from the stories file +import * as stories from './Button.stories'; + ++ const { Primary } = stories; +- const { Primary } = composeStories(stories); + +test('renders primary button with default args', async () => { + // The run function will mount the component and run all of Storybook's lifecycle hooks + await Primary.run(); + const buttonElement = screen.getByText('Text coming from args in stories file!'); + expect(buttonElement).not.toBeNull(); +}); +``` + +The `Story` object also provides a `Component` property, enabling you to render the component with any method you choose, such as [Testing Library](https://testing-library.com/). You can also access its composed properties ([`args`](../../writing-stories/args.mdx), [`parameters`](../../writing-stories/parameters.mdx), etc.) via the `composed` property. + +Here's an example of how you can reuse a story in a test file by rendering its component: + +{/* prettier-ignore-start */} + +{/* prettier-ignore-end */} + +## Frequently asked questions (FAQ) + +### Will I have to migrate all of my stories to this new format? + +Storybook will continue to support CSF 1, [CSF 2](../../../release-6-5/docs/api/stories/csf.mdx), and [CSF 3](./index.mdx) for the foreseeable future. None of these prior formats are deprecated. + +While using CSF Next, you can still use the older formats, as long as they are not mixed in the same file. If you want to migrate your existing files to the new format, refer to [the upgrade section](#upgrading-from-csf-1-2-or-3), above. + +### Will this format work with MDX docs pages? + +Yes, the [doc blocks](../../writing-docs/doc-blocks.mdx) used to reference stories in MDX files support the CSF Next format with no changes needed. + +### How can I know more about this format and provide feedback? + +For more information on this experimental format's original proposal, refer to its [RFC on GitHub](https://github.com/storybookjs/storybook/discussions/30112). We welcome your comments! + +
\ No newline at end of file diff --git a/docs/versions/latest.json b/docs/versions/latest.json index 0cb911c467d9..593dd5af945f 100644 --- a/docs/versions/latest.json +++ b/docs/versions/latest.json @@ -1 +1 @@ -{"version":"9.1.4","info":{"plain":"- Angular: Properly merge builder options and browserTarget options - [#32272](https://github.com/storybookjs/storybook/pull/32272), thanks @kroeder!\n- Core: Optimize bundlesize, by reusing internal/babel in mocking-utils - [#32350](https://github.com/storybookjs/storybook/pull/32350), thanks @ndelangen!\n- Svelte & Vue: Add framework-specific `docgen` option to disable docgen processing - [#32319](https://github.com/storybookjs/storybook/pull/32319), thanks @copilot-swe-agent!\n- Svelte: Support `@sveltejs/vite-plugin-svelte` v6 - [#32320](https://github.com/storybookjs/storybook/pull/32320), thanks @JReinhold!"}} +{"version":"9.1.5","info":{"plain":"- CSF: Support `satisfies x as y` syntax - [#32169](https://github.com/storybookjs/storybook/pull/32169), thanks @diagramatics!\n- Vitest addon: Handle Playwright installation errors gracefully - [#32329](https://github.com/storybookjs/storybook/pull/32329), thanks @ndelangen!"}} diff --git a/docs/versions/next.json b/docs/versions/next.json index 70686af6baf3..aca7c902f6a8 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"10.0.0-beta.0","info":{"plain":"- Core: Fix staticCopy not copying `index.html` to sub directory - [#32259](https://github.com/storybookjs/storybook/pull/32259), thanks @ndelangen!\n- Core: Remove CJS bundles, only ship ESM - [#31819](https://github.com/storybookjs/storybook/pull/31819), thanks @ndelangen!\n- Docs: Move button in ArgsTable heading to fix screenreader announcements - [#32238](https://github.com/storybookjs/storybook/pull/32238), thanks @Sidnioulz!\n- Telemetry: Disambiguate traffic coming from error/upgrade links - [#32287](https://github.com/storybookjs/storybook/pull/32287), thanks @shilman!\n- Telemetry: Disambiguate unattributed traffic from Onboarding - [#32286](https://github.com/storybookjs/storybook/pull/32286), thanks @shilman!"}} +{"version":"10.0.0-beta.2","info":{"plain":"- Build: Fix dts bundling external detection - [#32366](https://github.com/storybookjs/storybook/pull/32366), thanks @mrginglymus!\n- Codemod: Replace `globby` with `tinyglobby` - [#31407](https://github.com/storybookjs/storybook/pull/31407), thanks @benmccann!\n- Next.js-vite: Use `fileURLToPath` for module resolution in preset - [#32386](https://github.com/storybookjs/storybook/pull/32386), thanks @ndelangen!\n- Tags: Remove undocumented x-only tags - [#32360](https://github.com/storybookjs/storybook/pull/32360), thanks @shilman!\n- Vitest addon: Handle Playwright installation errors gracefully - [#32329](https://github.com/storybookjs/storybook/pull/32329), thanks @ndelangen!"}}