diff --git a/MIGRATION.md b/MIGRATION.md index e60260d39e40..da8622f1cab7 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -2,6 +2,7 @@ - [From version 10.x to 11.0.0](#from-version-10x-to-1100) - [`storybook dev` no longer opens a browser by default](#storybook-dev-no-longer-opens-a-browser-by-default) + - [`parameters.componentSubtitle` removed](#parameterscomponentsubtitle-removed) - [Raised browser support floors](#raised-browser-support-floors) - [Docs Code panel enabled by default](#docs-code-panel-enabled-by-default) - [Node.js 22.12 or higher](#nodejs-2212-or-higher) @@ -574,6 +575,30 @@ To keep opening Storybook automatically, add `--open` to your command or package } ``` +### `parameters.componentSubtitle` removed + +The deprecated `parameters.componentSubtitle` fallback was removed. +Use `parameters.docs.subtitle` instead. + +The `component-subtitle` automigration moves it in your preview and story files when you upgrade, or when you run `npx storybook automigrate component-subtitle`: + +```diff +export default { + parameters: { +- componentSubtitle: 'Button variants', ++ docs: { subtitle: 'Button variants' }, + }, +}; +``` + +When the same object already sets `docs.subtitle`, the automigration keeps it and removes `componentSubtitle`, because `docs.subtitle` already took precedence. +A `componentSubtitle` set on a single story never affected the Subtitle block, so the automigration leaves it in place for you to delete. +Files it cannot edit safely, such as parameters built from a spread, are listed in `automigrations-summary.md` for you to change by hand. + +Before, any `docs.subtitle` took precedence over any `componentSubtitle`, even one set in the preview over one set in a component's meta. +After the migration, the more specific value wins, like every other parameter. +If your preview sets `docs.subtitle`, check the subtitles on your Docs pages. + ### Docs Code panel enabled by default When `@storybook/addon-docs` is installed, the Code panel is now available for stories without setting `parameters.docs.codePanel` to `true`. diff --git a/code/addons/docs/src/blocks/blocks/Subtitle.stories.tsx b/code/addons/docs/src/blocks/blocks/Subtitle.stories.tsx index ec43b6de3b23..930096ecadad 100644 --- a/code/addons/docs/src/blocks/blocks/Subtitle.stories.tsx +++ b/code/addons/docs/src/blocks/blocks/Subtitle.stories.tsx @@ -3,8 +3,6 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react-vite'; import * as DefaultButtonStories from '../examples/Button.stories'; -import * as ButtonStoriesWithMetaSubtitleAsBoth from '../examples/ButtonWithMetaSubtitleAsBoth.stories'; -import * as ButtonStoriesWithMetaSubtitleAsComponentSubtitle from '../examples/ButtonWithMetaSubtitleAsComponentSubtitle.stories'; import * as ButtonStoriesWithMetaSubtitleAsDocsSubtitle from '../examples/ButtonWithMetaSubtitleAsDocsSubtitle.stories'; import { Subtitle } from './Subtitle'; @@ -26,23 +24,6 @@ export default meta; type Story = StoryObj; -export const OfCSFFileAsBoth: Story = { - args: { - of: ButtonStoriesWithMetaSubtitleAsBoth, - }, - parameters: { - relativeCsfPaths: ['../examples/ButtonWithMetaSubtitleAsBoth.stories'], - }, -}; -export const OfCSFFileAsComponentSubtitle: Story = { - name: 'Of CSF File As parameters.componentSubtitle', - args: { - of: ButtonStoriesWithMetaSubtitleAsComponentSubtitle, - }, - parameters: { - relativeCsfPaths: ['../examples/ButtonWithMetaSubtitleAsComponentSubtitle.stories'], - }, -}; export const OfCSFFileAsDocsSubtitle: Story = { name: 'Of CSF File As parameters.docs.subtitle', args: { @@ -52,23 +33,6 @@ export const OfCSFFileAsDocsSubtitle: Story = { relativeCsfPaths: ['../examples/ButtonWithMetaSubtitleAsDocsSubtitle.stories'], }, }; -export const OfMetaAsBoth: Story = { - args: { - of: ButtonStoriesWithMetaSubtitleAsBoth.default, - }, - parameters: { - relativeCsfPaths: ['../examples/ButtonWithMetaSubtitleAsBoth.stories'], - }, -}; -export const OfMetaAsComponentSubtitle: Story = { - name: 'Of Meta As parameters.componentSubtitle', - args: { - of: ButtonStoriesWithMetaSubtitleAsComponentSubtitle.default, - }, - parameters: { - relativeCsfPaths: ['../examples/ButtonWithMetaSubtitleAsComponentSubtitle.stories'], - }, -}; export const OfMetaAsDocsSubtitle: Story = { name: 'Of Meta As parameters.docs.subtitle', args: { diff --git a/code/addons/docs/src/blocks/blocks/Subtitle.tsx b/code/addons/docs/src/blocks/blocks/Subtitle.tsx index be31a5875e03..17a175a18f43 100644 --- a/code/addons/docs/src/blocks/blocks/Subtitle.tsx +++ b/code/addons/docs/src/blocks/blocks/Subtitle.tsx @@ -1,7 +1,6 @@ import type { FunctionComponent, ReactNode } from 'react'; import React from 'react'; -import { deprecate } from 'storybook/internal/client-logger'; import { InvalidBlockOfPropError } from 'storybook/internal/preview-errors'; import { Subtitle as PureSubtitle } from '../components'; @@ -18,9 +17,6 @@ interface SubtitleProps { of?: Of; } -const DEPRECATION_MIGRATION_LINK = - 'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#subtitle-block-and-parameterscomponentsubtitle'; - const SubtitleImpl: FunctionComponent = (props) => { const { of, children } = props; @@ -38,15 +34,7 @@ const SubtitleImpl: FunctionComponent = (props) => { } } - const { componentSubtitle, docs } = preparedMeta?.parameters || {}; - - if (componentSubtitle) { - deprecate( - `Using 'parameters.componentSubtitle' property to subtitle stories is deprecated. See ${DEPRECATION_MIGRATION_LINK}` - ); - } - - const content = children || docs?.subtitle || componentSubtitle; + const content = children || preparedMeta?.parameters?.docs?.subtitle; return content ? ( {content} diff --git a/code/addons/docs/src/blocks/examples/ButtonWithMetaSubtitleAsBoth.stories.tsx b/code/addons/docs/src/blocks/examples/ButtonWithMetaSubtitleAsBoth.stories.tsx deleted file mode 100644 index 78f6218743ba..000000000000 --- a/code/addons/docs/src/blocks/examples/ButtonWithMetaSubtitleAsBoth.stories.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; - -import { Button } from './Button'; - -const meta = { - title: 'examples/Button with Meta Subtitle in Both', - component: Button, - argTypes: { - backgroundColor: { control: 'color' }, - }, - globals: { sb_theme: 'light' }, - parameters: { - // this is to test the deprecated features of the Subtitle block - componentSubtitle: 'This subtitle is set in parameters.componentSubtitle', - docs: { - subtitle: 'This subtitle is set in parameters.docs.subtitle', - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const WithMetaSubtitleAsBoth: Story = { - args: { - primary: true, - label: 'Button', - }, -}; diff --git a/code/addons/docs/src/blocks/examples/ButtonWithMetaSubtitleAsComponentSubtitle.stories.tsx b/code/addons/docs/src/blocks/examples/ButtonWithMetaSubtitleAsComponentSubtitle.stories.tsx deleted file mode 100644 index f67f7c5aeaef..000000000000 --- a/code/addons/docs/src/blocks/examples/ButtonWithMetaSubtitleAsComponentSubtitle.stories.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; - -import { Button } from './Button'; - -const meta = { - title: 'examples/Button with Meta Subtitle in componentSubtitle', - component: Button, - argTypes: { - backgroundColor: { control: 'color' }, - }, - globals: { sb_theme: 'light' }, - parameters: { - // this is to test the deprecated features of the Subtitle block - componentSubtitle: 'This subtitle is set in parameters.componentSubtitle', - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const WithMetaSubtitleInComponentSubtitle: Story = { - args: { - primary: true, - label: 'Button', - }, -}; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/component-subtitle.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/component-subtitle.test.ts new file mode 100644 index 000000000000..9c5dc6aee415 --- /dev/null +++ b/code/lib/cli-storybook/src/automigrate/fixes/component-subtitle.test.ts @@ -0,0 +1,119 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { formatExistingFile, JsPackageManager } from 'storybook/internal/common'; + +import { fs, vol } from 'memfs'; + +import { checkFix, runFix } from '../helpers/fix-test-utils.ts'; +import { componentSubtitle } from './component-subtitle.ts'; + +vi.mock('node:fs/promises', { spy: true }); +vi.mock('storybook/internal/common', { spy: true }); + +const previewConfigPath = resolve('.storybook/preview.ts'); +const storyPath = resolve('src/Button.stories.ts'); +const options = { + packageManager: vi.mocked(JsPackageManager.prototype), + mainConfig: { stories: [] }, + mainConfigPath: resolve('.storybook/main.ts'), + configDir: resolve('.storybook'), + storybookVersion: '11.0.0', + previewConfigPath, + storiesPaths: [storyPath], +}; + +const migrate = async (files: { preview?: string; story?: string }) => { + vol.fromJSON({ [previewConfigPath]: files.preview ?? '', [storyPath]: files.story ?? '' }); + const failures = await runFix(componentSubtitle, { ...options, result: {} }); + return { + failures, + preview: fs.readFileSync(previewConfigPath, 'utf8'), + story: fs.readFileSync(storyPath, 'utf8'), + }; +}; + +describe('component-subtitle', () => { + beforeEach(() => { + vol.reset(); + vi.mocked(readFile).mockImplementation(fs.promises.readFile as typeof readFile); + vi.mocked(writeFile).mockImplementation(fs.promises.writeFile as typeof writeFile); + vi.mocked(formatExistingFile).mockImplementation(async (_path, source) => source); + }); + + afterEach(() => { + vi.mocked(readFile).mockRestore(); + vi.mocked(writeFile).mockRestore(); + }); + + it('applies only when a file uses componentSubtitle', async () => { + vol.fromJSON({ [previewConfigPath]: 'export default {};', [storyPath]: 'export default {};' }); + expect(await checkFix(componentSubtitle, options)).toBeNull(); + + fs.writeFileSync(storyPath, "export default { parameters: { componentSubtitle: 'A' } };"); + expect(await checkFix(componentSubtitle, options)).toEqual({}); + }); + + it('leaves MDX docs that mention componentSubtitle to the author', async () => { + const docsPath = resolve('src/Intro.mdx'); + vol.fromJSON({ + [previewConfigPath]: 'export default {};', + [docsPath]: "", + }); + + expect(await checkFix(componentSubtitle, { ...options, storiesPaths: [docsPath] })).toBeNull(); + }); + + it('moves componentSubtitle to docs.subtitle in the preview and meta, where the Subtitle block read it', async () => { + const { failures, preview, story } = await migrate({ + preview: "export default { parameters: { componentSubtitle: 'Preview' } };", + story: [ + "export default { parameters: { componentSubtitle: 'Meta', docs: { source: { type: 'code' } } } };", + "export const Primary = { parameters: { componentSubtitle: 'Story' } };", + ].join('\n'), + }); + + expect(failures).toEqual([]); + expect(preview).toMatchInlineSnapshot(` + "export default { parameters: { docs: { + subtitle: 'Preview' + } } };" + `); + expect(story).toMatchInlineSnapshot(` + "export default { parameters: { + docs: { + source: { type: 'code' }, + subtitle: 'Meta' + } + } }; + export const Primary = { parameters: { componentSubtitle: 'Story' } };" + `); + }); + + it('keeps an existing docs.subtitle, which the Subtitle block preferred', async () => { + const { story } = await migrate({ + story: + "export default { parameters: { componentSubtitle: 'Legacy', docs: { subtitle: 'Current' } } };", + }); + + expect(story).toMatchInlineSnapshot(` + "export default { parameters: { + docs: { subtitle: 'Current' } + } };" + `); + }); + + it('skips a file it cannot migrate safely and reports it', async () => { + const story = + "import { shared } from './shared';\nexport default { parameters: { ...shared, componentSubtitle: 'A' } };"; + + const result = await migrate({ story }); + + expect(result.failures).toEqual([ + { file: storyPath, kind: 'story', message: expect.any(String) }, + ]); + expect(result.story).toBe(story); + }); +}); diff --git a/code/lib/cli-storybook/src/automigrate/fixes/component-subtitle.ts b/code/lib/cli-storybook/src/automigrate/fixes/component-subtitle.ts new file mode 100644 index 000000000000..e45de52888d2 --- /dev/null +++ b/code/lib/cli-storybook/src/automigrate/fixes/component-subtitle.ts @@ -0,0 +1,38 @@ +import type { CsfObject } from 'storybook/internal/csf-tools'; + +import picocolors from 'picocolors'; + +import type { Fix } from '../types.ts'; + +const legacyPath = ['parameters', 'componentSubtitle']; +const subtitlePath = ['parameters', 'docs', 'subtitle']; + +// The Subtitle block read the preview and meta parameters only, and preferred `docs.subtitle`, +// so an existing one keeps winning. +const migrate = (object: CsfObject) => { + if (!object.get(legacyPath)) { + return; + } + if (object.get(subtitlePath)) { + object.remove(legacyPath); + } else { + object.move(legacyPath, subtitlePath); + } +}; + +export const componentSubtitle: Fix = { + id: 'component-subtitle', + link: 'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#parameterscomponentsubtitle-removed', + + prompt() { + return `Move deprecated ${picocolors.cyan('parameters.componentSubtitle')} values to ${picocolors.cyan('parameters.docs.subtitle')}`; + }, + + transform: () => [ + { + filter: { kind: ['preview', 'story'], code: 'componentSubtitle' }, + editConfig: migrate, + editCsf: (csf) => csf.objects({ stories: false }).forEach(migrate), + }, + ], +}; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/index.ts b/code/lib/cli-storybook/src/automigrate/fixes/index.ts index 390c13ee1f83..7d9cf4545005 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/index.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/index.ts @@ -3,6 +3,7 @@ import type { CommandFix, Fix } from '../types.ts'; import { angularToAngularVite } from './angular-to-angular-vite.ts'; import { angularViteRemoveCompodoc } from './angular-vite-remove-compodoc.ts'; import { addonMcp } from './addon-mcp.ts'; +import { componentSubtitle } from './component-subtitle.ts'; import { eslintPlugin } from './eslint-plugin.ts'; import { enableExperimentalDocgenServer, @@ -23,6 +24,7 @@ export const allFixes: Fix[] = [ eslintPlugin, upgradeStorybookRelatedDependencies, vitestSetupFile, + componentSubtitle, rnOndeviceAddonsToDeviceAddons, nextjsToNextjsVite, angularToAngularVite, diff --git a/docs/releases/migration-guide.mdx b/docs/releases/migration-guide.mdx index 4d7e022692d6..c9a16d7efebe 100644 --- a/docs/releases/migration-guide.mdx +++ b/docs/releases/migration-guide.mdx @@ -36,6 +36,8 @@ The rest of this guide will help you upgrade successfully, either automatically - Changed defaults - [`storybook dev` no longer opens a browser by default](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#storybook-dev-no-longer-opens-a-browser-by-default) - [Docs Code panel enabled by default](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#docs-code-panel-enabled-by-default) +- Removed APIs + - [`parameters.componentSubtitle` removed](#parameterscomponentsubtitle-removed) - Configuration changes - [Move top-level `setConfig` layout and UI options](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#top-level-setconfig-layout-and-ui-options-removed) - [Vite static asset copying and conflict precedence](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#vite-publicdir-is-handled-by-storybooks-staticdirs) @@ -98,6 +100,21 @@ If you prefer to debug yourself, here are a few useful things you can do to help 1. Try removing all addons that are not in the `@storybook` npm namespace (make sure you don't remove the `storybook` package). Community addons that work well with 10.x might not yet be compatible with 11.x, and this is the fastest way to isolate that possibility. If you find an addon that needs to be upgraded to work with Storybook 11, please post an issue on the addon’s repository, or better yet, a pull request to upgrade it! 2. Another debugging technique is to bisect to older prerelease versions of Storybook to figure out which release broke your Storybook. For example, assuming that the current prerelease of Storybook is `11.0.0-beta.56`, you could set the version to `11.0.0-alpha.0` in your `package.json` and reinstall to verify that it still works (`alpha.0` should be nearly identical to `10.6.x`). If it works, you could then try `11.0.0-beta.0`, then `11.0.0-beta.28` and so forth. Once you’ve isolated the bad release, read through its [CHANGELOG](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.prerelease.md) entry and perhaps there’s a change that jumps out as the culprit. If you find the problem, please submit an issue or pull request to the Storybook monorepo and we’ll do our best to take care of it quickly. +## `parameters.componentSubtitle` removed + +Storybook 11 removes the deprecated `parameters.componentSubtitle` fallback. Move subtitles to `parameters.docs.subtitle` in your preview configuration and story files: + +```diff +export default { + parameters: { +- componentSubtitle: 'Button variants', ++ docs: { subtitle: 'Button variants' }, + }, +}; +``` + +The upgrade command offers the `component-subtitle` automigration to do this for you. See the [full migration notes](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#parameterscomponentsubtitle-removed) for what it cannot migrate and how subtitle precedence changes. + ## Yarn PnP support removed Storybook 11 removes support for Yarn [Plug'n'Play](https://yarnpkg.com/features/pnp) (PnP), which was deprecated in Storybook 10: