diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md
index d4950d935198..8aff1d0b5641 100644
--- a/CHANGELOG.prerelease.md
+++ b/CHANGELOG.prerelease.md
@@ -1,3 +1,9 @@
+## 10.0.0-rc.0
+
+- A11Y: Bugfix missing `manager.js` entry-file - [#32780](https://github.com/storybookjs/storybook/pull/32780), thanks @ndelangen!
+- Addon Vitest: Support modifying mergeConfig on addon setup - [#32753](https://github.com/storybookjs/storybook/pull/32753), thanks @yannbf!
+- CLI: Change message in downgrade-blocker - [#32745](https://github.com/storybookjs/storybook/pull/32745), thanks @ndelangen!
+
## 10.0.0-beta.13
- CLI: CSF factories codemod - support annotations in npx context - [#32741](https://github.com/storybookjs/storybook/pull/32741), thanks @yannbf!
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1d8db751182a..5f0f32191b85 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,4 +1,6 @@
# Contributors Guide
+> Tip: If you want to make a fast contribution, check the “good first issue” label in the Issues tab for small frontend and docs tasks that are easy to fix directly on GitHub.
+
We welcome contributions of any type and skill level. As an open-source project, we believe in the power of community and welcome any contributions that help us improve Storybook. Whether you are a developer, designer, writer, or someone who wants to help, we'd love to have you on board. If you are interested in contributing, please read the following guidelines.
@@ -224,4 +226,4 @@ By executing this command, you can see which untracked or ignored files and dire
## Contributing to Storybook
-For further advice on contributing, please refer to our [NEW contributing guide on the Storybook website](https://storybook.js.org/docs/contribute).
\ No newline at end of file
+For further advice on contributing, please refer to our [NEW contributing guide on the Storybook website](https://storybook.js.org/docs/contribute).
diff --git a/README.md b/README.md
index 1bf638d98ba1..9eebc6f6b6f0 100644
--- a/README.md
+++ b/README.md
@@ -124,7 +124,7 @@ For additional help, share your issue in [the repo's GitHub Discussions](https:/
| [events](https://github.com/storybookjs/addon-events) | Interactively fire events to components that respond to EventEmitter |
| [google-analytics](https://github.com/storybookjs/addon-google-analytics) | Reports google analytics on stories |
| [graphql](https://github.com/storybookjs/addon-graphql) | Query a GraphQL server within Storybook stories |
-| [jest](code/addons/jest/) | View the results of components' unit tests in Storybook |
+| [jest](https://github.com/storybookjs/addon-jest) | View the results of components' unit tests in Storybook |
| [links](code/addons/links/) | Create links between stories |
| [measure](code/core/src/measure/) | Visually inspect the layout and box model within the Storybook UI |
| [outline](code/core/src/outline/) | Visually debug the CSS layout and alignment within the Storybook UI |
diff --git a/code/addons/a11y/manager.js b/code/addons/a11y/manager.js
new file mode 100644
index 000000000000..bb3432a1730f
--- /dev/null
+++ b/code/addons/a11y/manager.js
@@ -0,0 +1 @@
+export * from './dist/manager.js';
diff --git a/code/addons/vitest/src/updateVitestFile.test.ts b/code/addons/vitest/src/updateVitestFile.test.ts
index cabb4b36312c..3396e1d4dec6 100644
--- a/code/addons/vitest/src/updateVitestFile.test.ts
+++ b/code/addons/vitest/src/updateVitestFile.test.ts
@@ -4,6 +4,7 @@ import { describe, expect, it, vi } from 'vitest';
import * as babel from 'storybook/internal/babel';
+import { getDiff } from '../../../core/src/core-server/utils/save-story/getDiff';
import { loadTemplate, updateConfigFile, updateWorkspaceFile } from './updateVitestFile';
vi.mock('storybook/internal/node-logger', () => ({
@@ -15,7 +16,7 @@ vi.mock('storybook/internal/node-logger', () => ({
}));
vi.mock('../../../core/src/shared/utils/module', () => ({
- resolvePackageDir: vi.fn().mockImplementation((a) => join(__dirname, '..')),
+ resolvePackageDir: vi.fn().mockImplementation(() => join(__dirname, '..')),
}));
describe('updateConfigFile', () => {
@@ -42,44 +43,55 @@ describe('updateConfigFile', () => {
})
`);
+ const before = babel.generate(target).code;
const updated = updateConfigFile(source, target);
expect(updated).toBe(true);
- const { code } = babel.generate(target);
- expect(code).toMatchInlineSnapshot(`
- "///
- import { defineConfig } from 'vite';
- import react from '@vitejs/plugin-react';
-
- // https://vite.dev/config/
- import path from 'node:path';
- import { fileURLToPath } from 'node:url';
- import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
- const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
-
- // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
- export default defineConfig({
- plugins: [react()],
- test: {
- globals: true,
- workspace: ['packages/*', {
- extends: true,
- plugins: [
- // The plugin will run tests for the stories defined in your Storybook config
- // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
- storybookTest({
- configDir: path.join(dirname, '.storybook')
- })],
- test: {
- name: 'storybook',
- browser: {
- provider: 'playwright'
- },
- setupFiles: ['../.storybook/vitest.setup.ts']
- }
- }]
- }
- });"
+ const after = babel.generate(target).code;
+
+ // check if the code was updated at all
+ expect(after).not.toBe(before);
+
+ // check if the code was updated correctly
+ expect(getDiff(before, after)).toMatchInlineSnapshot(`
+ " ...
+ import react from '@vitejs/plugin-react';
+
+ // https://vite.dev/config/
+
+ + import path from 'node:path';
+ + import { fileURLToPath } from 'node:url';
+ + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
+ + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
+ +
+ + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
+ +
+ export default defineConfig({
+ plugins: [react()],
+ test: {
+ globals: true,
+
+ - workspace: ['packages/*']
+ -
+ + workspace: ['packages/*', {
+ + extends: true,
+ + plugins: [
+ + // The plugin will run tests for the stories defined in your Storybook config
+ + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
+ + storybookTest({
+ + configDir: path.join(dirname, '.storybook')
+ + })],
+ + test: {
+ + name: 'storybook',
+ + browser: {
+ + provider: 'playwright'
+ + },
+ + setupFiles: ['../.storybook/vitest.setup.ts']
+ + }
+ + }]
+ +
+ }
+ });"
`);
});
@@ -105,44 +117,56 @@ describe('updateConfigFile', () => {
}
`);
+ const before = babel.generate(target).code;
const updated = updateConfigFile(source, target);
expect(updated).toBe(true);
- const { code } = babel.generate(target);
- expect(code).toMatchInlineSnapshot(`
- "///
- import react from '@vitejs/plugin-react';
-
- // https://vite.dev/config/
- import path from 'node:path';
- import { fileURLToPath } from 'node:url';
- import { defineConfig } from 'vitest/config';
- import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
- const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
-
- // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
- export default {
- plugins: [react()],
- test: {
- globals: true,
- workspace: ['packages/*', {
- extends: true,
- plugins: [
- // The plugin will run tests for the stories defined in your Storybook config
- // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
- storybookTest({
- configDir: path.join(dirname, '.storybook')
- })],
- test: {
- name: 'storybook',
- browser: {
- provider: 'playwright'
- },
- setupFiles: ['../.storybook/vitest.setup.ts']
- }
- }]
- }
- };"
+ const after = babel.generate(target).code;
+
+ // check if the code was updated at all
+ expect(after).not.toBe(before);
+
+ // check if the code was updated correctly
+ expect(getDiff(before, after)).toMatchInlineSnapshot(`
+ " ...
+ import react from '@vitejs/plugin-react';
+
+ // https://vite.dev/config/
+
+ + import path from 'node:path';
+ + import { fileURLToPath } from 'node:url';
+ + import { defineConfig } from 'vitest/config';
+ + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
+ + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
+ +
+ + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
+ +
+ export default {
+ plugins: [react()],
+ test: {
+ globals: true,
+
+ - workspace: ['packages/*']
+ -
+ + workspace: ['packages/*', {
+ + extends: true,
+ + plugins: [
+ + // The plugin will run tests for the stories defined in your Storybook config
+ + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
+ + storybookTest({
+ + configDir: path.join(dirname, '.storybook')
+ + })],
+ + test: {
+ + name: 'storybook',
+ + browser: {
+ + provider: 'playwright'
+ + },
+ + setupFiles: ['../.storybook/vitest.setup.ts']
+ + }
+ + }]
+ +
+ }
+ };"
`);
});
@@ -168,8 +192,14 @@ describe('updateConfigFile', () => {
}))
`);
+ const before = babel.generate(target).code;
const updated = updateConfigFile(source, target);
expect(updated).toBe(false);
+
+ const after = babel.generate(target).code;
+
+ // check if the code was NOT updated
+ expect(after).toBe(before);
});
it('adds projects property to test config', async () => {
@@ -194,44 +224,55 @@ describe('updateConfigFile', () => {
})
`);
+ const before = babel.generate(target).code;
const updated = updateConfigFile(source, target);
expect(updated).toBe(true);
- const { code } = babel.generate(target);
- expect(code).toMatchInlineSnapshot(`
- "///
- import { defineConfig } from 'vite';
- import react from '@vitejs/plugin-react';
-
- // https://vite.dev/config/
- import path from 'node:path';
- import { fileURLToPath } from 'node:url';
- import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
- const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
-
- // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
- export default defineConfig({
- plugins: [react()],
- test: {
- globals: true,
- projects: [{
- extends: true,
- plugins: [
- // The plugin will run tests for the stories defined in your Storybook config
- // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
- storybookTest({
- configDir: path.join(dirname, '.storybook')
- })],
- test: {
- name: 'storybook',
- browser: {
- provider: 'playwright'
- },
- setupFiles: ['../.storybook/vitest.setup.ts']
- }
- }]
- }
- });"
+ const after = babel.generate(target).code;
+
+ // check if the code was updated at all
+ expect(after).not.toBe(before);
+
+ // check if the code was updated correctly
+ expect(getDiff(before, after)).toMatchInlineSnapshot(`
+ " ...
+ import react from '@vitejs/plugin-react';
+
+ // https://vite.dev/config/
+
+ + import path from 'node:path';
+ + import { fileURLToPath } from 'node:url';
+ + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
+ + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
+ +
+ + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
+ +
+ export default defineConfig({
+ plugins: [react()],
+ test: {
+
+ - globals: true
+ -
+ + globals: true,
+ + projects: [{
+ + extends: true,
+ + plugins: [
+ + // The plugin will run tests for the stories defined in your Storybook config
+ + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
+ + storybookTest({
+ + configDir: path.join(dirname, '.storybook')
+ + })],
+ + test: {
+ + name: 'storybook',
+ + browser: {
+ + provider: 'playwright'
+ + },
+ + setupFiles: ['../.storybook/vitest.setup.ts']
+ + }
+ + }]
+ +
+ }
+ });"
`);
});
@@ -258,46 +299,55 @@ describe('updateConfigFile', () => {
})
`);
+ const before = babel.generate(target).code;
const updated = updateConfigFile(source, target);
expect(updated).toBe(true);
- const { code } = babel.generate(target);
- expect(code).toMatchInlineSnapshot(`
- "///
- import { defineConfig } from 'vite';
- import react from '@vitejs/plugin-react';
-
- // https://vite.dev/config/
- import path from 'node:path';
- import { fileURLToPath } from 'node:url';
- import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
- const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
-
- // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
- export default defineConfig({
- plugins: [react()],
- test: {
- globals: true,
- projects: ['packages/*', {
- some: 'config'
- }, {
- extends: true,
- plugins: [
- // The plugin will run tests for the stories defined in your Storybook config
- // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
- storybookTest({
- configDir: path.join(dirname, '.storybook')
- })],
- test: {
- name: 'storybook',
- browser: {
- provider: 'playwright'
- },
- setupFiles: ['../.storybook/vitest.setup.ts']
- }
- }]
- }
- });"
+ const after = babel.generate(target).code;
+
+ // check if the code was updated at all
+ expect(after).not.toBe(before);
+
+ // check if the code was updated correctly
+ expect(getDiff(before, after)).toMatchInlineSnapshot(`
+ " ...
+ import react from '@vitejs/plugin-react';
+
+ // https://vite.dev/config/
+
+ + import path from 'node:path';
+ + import { fileURLToPath } from 'node:url';
+ + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
+ + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
+ +
+ + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
+ +
+ export default defineConfig({
+ plugins: [react()],
+ test: {
+ globals: true,
+ projects: ['packages/*', {
+ some: 'config'
+
+ + }, {
+ + extends: true,
+ + plugins: [
+ + // The plugin will run tests for the stories defined in your Storybook config
+ + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
+ + storybookTest({
+ + configDir: path.join(dirname, '.storybook')
+ + })],
+ + test: {
+ + name: 'storybook',
+ + browser: {
+ + provider: 'playwright'
+ + },
+ + setupFiles: ['../.storybook/vitest.setup.ts']
+ + }
+ +
+ }]
+ }
+ });"
`);
});
@@ -323,44 +373,55 @@ describe('updateConfigFile', () => {
})
`);
+ const before = babel.generate(target).code;
const updated = updateConfigFile(source, target);
expect(updated).toBe(true);
- const { code } = babel.generate(target);
- expect(code).toMatchInlineSnapshot(`
- "///
- import { defineConfig } from 'vite';
- import react from '@vitejs/plugin-react';
-
- // https://vite.dev/config/
- import path from 'node:path';
- import { fileURLToPath } from 'node:url';
- import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
- const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
-
- // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
- export default defineConfig({
- plugins: [react()],
- test: {
- globals: true,
- workspace: [{
- extends: true,
- plugins: [
- // The plugin will run tests for the stories defined in your Storybook config
- // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
- storybookTest({
- configDir: path.join(dirname, '.storybook')
- })],
- test: {
- name: 'storybook',
- browser: {
- provider: 'playwright'
- },
- setupFiles: ['../.storybook/vitest.setup.ts']
- }
- }]
- }
- });"
+ const after = babel.generate(target).code;
+
+ // check if the code was updated at all
+ expect(after).not.toBe(before);
+
+ // check if the code was updated correctly
+ expect(getDiff(before, after)).toMatchInlineSnapshot(`
+ " ...
+ import react from '@vitejs/plugin-react';
+
+ // https://vite.dev/config/
+
+ + import path from 'node:path';
+ + import { fileURLToPath } from 'node:url';
+ + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
+ + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
+ +
+ + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
+ +
+ export default defineConfig({
+ plugins: [react()],
+ test: {
+
+ - globals: true
+ -
+ + globals: true,
+ + workspace: [{
+ + extends: true,
+ + plugins: [
+ + // The plugin will run tests for the stories defined in your Storybook config
+ + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
+ + storybookTest({
+ + configDir: path.join(dirname, '.storybook')
+ + })],
+ + test: {
+ + name: 'storybook',
+ + browser: {
+ + provider: 'playwright'
+ + },
+ + setupFiles: ['../.storybook/vitest.setup.ts']
+ + }
+ + }]
+ +
+ }
+ });"
`);
});
@@ -383,43 +444,286 @@ describe('updateConfigFile', () => {
})
`);
+ const before = babel.generate(target).code;
const updated = updateConfigFile(source, target);
expect(updated).toBe(true);
- const { code } = babel.generate(target);
- expect(code).toMatchInlineSnapshot(`
- "///
- import { defineConfig } from 'vite';
- import react from '@vitejs/plugin-react';
+ const after = babel.generate(target).code;
+
+ // check if the code was updated at all
+ expect(after).not.toBe(before);
+
+ // check if the code was updated correctly
+ expect(getDiff(before, after)).toMatchInlineSnapshot(`
+ " ...
+ import react from '@vitejs/plugin-react';
+
+ // https://vite.dev/config/
+
+ + import path from 'node:path';
+ + import { fileURLToPath } from 'node:url';
+ + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
+ + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
+ +
+ + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
+ +
+ export default defineConfig({
+
+ - plugins: [react()]
+ -
+ + plugins: [react()],
+ + test: {
+ + workspace: [{
+ + extends: true,
+ + plugins: [
+ + // The plugin will run tests for the stories defined in your Storybook config
+ + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
+ + storybookTest({
+ + configDir: path.join(dirname, '.storybook')
+ + })],
+ + test: {
+ + name: 'storybook',
+ + browser: {
+ + provider: 'playwright'
+ + },
+ + setupFiles: ['../.storybook/vitest.setup.ts']
+ + }
+ + }]
+ + }
+ +
+ });"
+ `);
+ });
+
+ it('supports mergeConfig with multiple defineConfig calls, finding the one with test', async () => {
+ const source = babel.babelParse(
+ await loadTemplate('vitest.config.template.ts', {
+ CONFIG_DIR: '.storybook',
+ BROWSER_CONFIG: "{ provider: 'playwright' }",
+ SETUP_FILE: '../.storybook/vitest.setup.ts',
+ })
+ );
+ const target = babel.babelParse(`
+ import { mergeConfig } from 'vite'
+ import { defineConfig } from 'vitest/config'
+ import viteConfig from './vite.config'
+
+ export default mergeConfig(
+ viteConfig,
+ defineConfig({
+ plugins: [react()],
+ }),
+ defineConfig({
+ test: {
+ environment: 'jsdom',
+ }
+ })
+ )
+ `);
+
+ const before = babel.generate(target).code;
+ const updated = updateConfigFile(source, target);
+ expect(updated).toBe(true);
+
+ const after = babel.generate(target).code;
+
+ // check if the code was updated at all
+ expect(after).not.toBe(before);
+
+ // check if the code was updated correctly
+ expect(getDiff(before, after)).toMatchInlineSnapshot(`
+ " import { mergeConfig } from 'vite';
+ import { defineConfig } from 'vitest/config';
+ import viteConfig from './vite.config';
+
+ + import path from 'node:path';
+ + import { fileURLToPath } from 'node:url';
+ + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
+ + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
+ +
+ + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
+ +
+ export default mergeConfig(viteConfig, defineConfig({
+ plugins: [react()]
+ }), defineConfig({
+ test: {
+
+ - environment: 'jsdom'
+ -
+ + workspace: [{
+ + extends: true,
+ + test: {
+ + environment: 'jsdom'
+ + }
+ + }, {
+ + extends: true,
+ + plugins: [
+ + // The plugin will run tests for the stories defined in your Storybook config
+ + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
+ + storybookTest({
+ + configDir: path.join(dirname, '.storybook')
+ + })],
+ + test: {
+ + name: 'storybook',
+ + browser: {
+ + provider: 'playwright'
+ + },
+ + setupFiles: ['../.storybook/vitest.setup.ts']
+ + }
+ + }]
+ +
+ }
+ }));"
+ `);
+ });
+
+ it('supports mergeConfig without config containing test property', async () => {
+ const source = babel.babelParse(
+ await loadTemplate('vitest.config.template.ts', {
+ CONFIG_DIR: '.storybook',
+ BROWSER_CONFIG: "{ provider: 'playwright' }",
+ SETUP_FILE: '../.storybook/vitest.setup.ts',
+ })
+ );
+ const target = babel.babelParse(`
+ import { mergeConfig } from 'vite'
+ import { defineConfig } from 'vitest/config'
+ import viteConfig from './vite.config'
+
+ export default mergeConfig(
+ viteConfig,
+ defineConfig({
+ plugins: [react()],
+ })
+ )
+ `);
+
+ const before = babel.generate(target).code;
+ const updated = updateConfigFile(source, target);
+ expect(updated).toBe(true);
+
+ const after = babel.generate(target).code;
+
+ // check if the code was updated at all
+ expect(after).not.toBe(before);
+
+ // check if the code was updated correctly
+ expect(getDiff(before, after)).toMatchInlineSnapshot(`
+ " import { mergeConfig } from 'vite';
+ import { defineConfig } from 'vitest/config';
+ import viteConfig from './vite.config';
+
+ + import path from 'node:path';
+ + import { fileURLToPath } from 'node:url';
+ + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
+ + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
+ +
+ + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
+ +
+ export default mergeConfig(viteConfig, defineConfig({
+
+ - plugins: [react()]
+ -
+ + plugins: [react()],
+ + test: {
+ + workspace: [{
+ + extends: true,
+ + plugins: [
+ + // The plugin will run tests for the stories defined in your Storybook config
+ + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
+ + storybookTest({
+ + configDir: path.join(dirname, '.storybook')
+ + })],
+ + test: {
+ + name: 'storybook',
+ + browser: {
+ + provider: 'playwright'
+ + },
+ + setupFiles: ['../.storybook/vitest.setup.ts']
+ + }
+ + }]
+ + }
+ +
+ }));"
+ `);
+ });
+
+ it('supports mergeConfig with defineConfig pattern using projects (Vitest 3.2+)', async () => {
+ const source = babel.babelParse(
+ await loadTemplate('vitest.config.3.2.template.ts', {
+ CONFIG_DIR: '.storybook',
+ BROWSER_CONFIG: "{ provider: 'playwright' }",
+ SETUP_FILE: '../.storybook/vitest.setup.ts',
+ })
+ );
+ const target = babel.babelParse(`
+ ///
+ import { mergeConfig, defineConfig } from 'vitest/config'
+ import viteConfig from './vite.config'
// https://vite.dev/config/
- import path from 'node:path';
- import { fileURLToPath } from 'node:url';
- import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
- const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
+ export default mergeConfig(
+ viteConfig,
+ defineConfig({
+ test: {
+ globals: true,
+ },
+ })
+ )
+ `);
- // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
- export default defineConfig({
- plugins: [react()],
- test: {
- workspace: [{
- extends: true,
- plugins: [
- // The plugin will run tests for the stories defined in your Storybook config
- // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
- storybookTest({
- configDir: path.join(dirname, '.storybook')
- })],
- test: {
- name: 'storybook',
- browser: {
- provider: 'playwright'
- },
- setupFiles: ['../.storybook/vitest.setup.ts']
- }
- }]
- }
- });"
+ const before = babel.generate(target).code;
+ const updated = updateConfigFile(source, target);
+ expect(updated).toBe(true);
+
+ const after = babel.generate(target).code;
+
+ // check if the code was updated at all
+ expect(after).not.toBe(before);
+
+ // check if the code was updated correctly
+ expect(getDiff(before, after)).toMatchInlineSnapshot(`
+ " ...
+ import viteConfig from './vite.config';
+
+ // https://vite.dev/config/
+
+ + import path from 'node:path';
+ + import { fileURLToPath } from 'node:url';
+ + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
+ + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
+ +
+ + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
+ +
+ export default mergeConfig(viteConfig, defineConfig({
+ test: {
+
+ - globals: true
+ -
+ + projects: [{
+ + extends: true,
+ + test: {
+ + globals: true
+ + }
+ + }, {
+ + extends: true,
+ + plugins: [
+ + // The plugin will run tests for the stories defined in your Storybook config
+ + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
+ + storybookTest({
+ + configDir: path.join(dirname, '.storybook')
+ + })],
+ + test: {
+ + name: 'storybook',
+ + browser: {
+ + provider: 'playwright'
+ + },
+ + setupFiles: ['../.storybook/vitest.setup.ts']
+ + }
+ + }]
+ +
+ }
+ }));"
`);
});
});
@@ -438,34 +742,41 @@ describe('updateWorkspaceFile', () => {
export default ['packages/*']
`);
+ const before = babel.generate(target).code;
const updated = updateWorkspaceFile(source, target);
expect(updated).toBe(true);
- const { code } = babel.generate(target);
- expect(code).toMatchInlineSnapshot(`
- "import path from 'node:path';
- import { fileURLToPath } from 'node:url';
- import { defineWorkspace } from 'vitest/config';
- import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
- const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
-
- // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
- export default ['packages/*', 'ROOT_CONFIG', {
- extends: '',
- plugins: [
- // The plugin will run tests for the stories defined in your Storybook config
- // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
- storybookTest({
- configDir: path.join(dirname, '.storybook')
- })],
- test: {
- name: 'storybook',
- browser: {
- provider: 'playwright'
- },
- setupFiles: ['../.storybook/vitest.setup.ts']
- }
- }];"
+ const after = babel.generate(target).code;
+
+ // check if the code was updated at all
+ expect(after).not.toBe(before);
+
+ // check if the code was updated correctly
+ expect(getDiff(before, after)).toMatchInlineSnapshot(`
+ "- export default ['packages/*'];
+ + import path from 'node:path';
+ + import { fileURLToPath } from 'node:url';
+ + import { defineWorkspace } from 'vitest/config';
+ + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
+ + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
+ +
+ + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
+ + export default ['packages/*', 'ROOT_CONFIG', {
+ + extends: '',
+ + plugins: [
+ + // The plugin will run tests for the stories defined in your Storybook config
+ + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
+ + storybookTest({
+ + configDir: path.join(dirname, '.storybook')
+ + })],
+ + test: {
+ + name: 'storybook',
+ + browser: {
+ + provider: 'playwright'
+ + },
+ + setupFiles: ['../.storybook/vitest.setup.ts']
+ + }
+ + }];"
`);
});
@@ -484,34 +795,42 @@ describe('updateWorkspaceFile', () => {
export default defineWorkspace(['packages/*'])
`);
+ const before = babel.generate(target).code;
const updated = updateWorkspaceFile(source, target);
expect(updated).toBe(true);
- const { code } = babel.generate(target);
- expect(code).toMatchInlineSnapshot(`
- "import { defineWorkspace } from 'vitest/config';
- import path from 'node:path';
- import { fileURLToPath } from 'node:url';
- import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
- const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
-
- // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
- export default defineWorkspace(['packages/*', 'ROOT_CONFIG', {
- extends: '',
- plugins: [
- // The plugin will run tests for the stories defined in your Storybook config
- // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
- storybookTest({
- configDir: path.join(dirname, '.storybook')
- })],
- test: {
- name: 'storybook',
- browser: {
- provider: 'playwright'
- },
- setupFiles: ['../.storybook/vitest.setup.ts']
- }
- }]);"
+ const after = babel.generate(target).code;
+
+ // check if the code was updated at all
+ expect(after).not.toBe(before);
+
+ // check if the code was updated correctly
+ expect(getDiff(before, after)).toMatchInlineSnapshot(`
+ " import { defineWorkspace } from 'vitest/config';
+
+ - export default defineWorkspace(['packages/*']);
+ + import path from 'node:path';
+ + import { fileURLToPath } from 'node:url';
+ + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
+ + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
+ +
+ + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
+ + export default defineWorkspace(['packages/*', 'ROOT_CONFIG', {
+ + extends: '',
+ + plugins: [
+ + // The plugin will run tests for the stories defined in your Storybook config
+ + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
+ + storybookTest({
+ + configDir: path.join(dirname, '.storybook')
+ + })],
+ + test: {
+ + name: 'storybook',
+ + browser: {
+ + provider: 'playwright'
+ + },
+ + setupFiles: ['../.storybook/vitest.setup.ts']
+ + }
+ + }]);"
`);
});
});
diff --git a/code/addons/vitest/src/updateVitestFile.ts b/code/addons/vitest/src/updateVitestFile.ts
index 362095645108..04af8916ac28 100644
--- a/code/addons/vitest/src/updateVitestFile.ts
+++ b/code/addons/vitest/src/updateVitestFile.ts
@@ -78,6 +78,66 @@ const mergeProperties = (
*/
export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['ast']) => {
let updated = false;
+
+ // First, check if we can actually modify the configuration
+ const sourceExportDefault = source.program.body.find(
+ (n) => n.type === 'ExportDefaultDeclaration'
+ );
+ if (!sourceExportDefault || sourceExportDefault.declaration.type !== 'CallExpression') {
+ return false;
+ }
+
+ const targetExportDefault = target.program.body.find(
+ (n) => n.type === 'ExportDefaultDeclaration'
+ );
+ if (!targetExportDefault) {
+ return false;
+ }
+
+ // Check if this is a function notation that we don't support
+ if (
+ targetExportDefault.declaration.type === 'CallExpression' &&
+ targetExportDefault.declaration.callee.type === 'Identifier' &&
+ targetExportDefault.declaration.callee.name === 'defineConfig' &&
+ targetExportDefault.declaration.arguments.length > 0 &&
+ targetExportDefault.declaration.arguments[0].type === 'ArrowFunctionExpression'
+ ) {
+ // This is function notation that we don't support
+ return false;
+ }
+
+ // Check if we can handle mergeConfig patterns
+ let canHandleConfig = false;
+ if (targetExportDefault.declaration.type === 'ObjectExpression') {
+ canHandleConfig = true;
+ } else if (
+ targetExportDefault.declaration.type === 'CallExpression' &&
+ targetExportDefault.declaration.callee.type === 'Identifier' &&
+ targetExportDefault.declaration.callee.name === 'defineConfig' &&
+ targetExportDefault.declaration.arguments[0]?.type === 'ObjectExpression'
+ ) {
+ canHandleConfig = true;
+ } else if (
+ targetExportDefault.declaration.type === 'CallExpression' &&
+ targetExportDefault.declaration.callee.type === 'Identifier' &&
+ targetExportDefault.declaration.callee.name === 'mergeConfig' &&
+ targetExportDefault.declaration.arguments.length >= 2
+ ) {
+ const defineConfigNodes = targetExportDefault.declaration.arguments.filter(
+ (arg): arg is t.CallExpression =>
+ arg?.type === 'CallExpression' &&
+ arg.callee.type === 'Identifier' &&
+ arg.callee.name === 'defineConfig' &&
+ arg.arguments[0]?.type === 'ObjectExpression'
+ );
+ canHandleConfig = defineConfigNodes.length > 0;
+ }
+
+ if (!canHandleConfig) {
+ return false;
+ }
+
+ // If we get here, we can modify the configuration, so add imports and variables first
for (const sourceNode of source.program.body) {
if (sourceNode.type === 'ImportDeclaration') {
// Insert imports that don't already exist (according to their local specifier name)
@@ -128,6 +188,111 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as
) {
mergeProperties(properties, exportDefault.declaration.arguments[0].properties);
updated = true;
+ } else if (
+ exportDefault.declaration.type === 'CallExpression' &&
+ exportDefault.declaration.callee.type === 'Identifier' &&
+ exportDefault.declaration.callee.name === 'mergeConfig' &&
+ exportDefault.declaration.arguments.length >= 2
+ ) {
+ const defineConfigNodes = exportDefault.declaration.arguments.filter(
+ (arg): arg is t.CallExpression =>
+ arg?.type === 'CallExpression' &&
+ arg.callee.type === 'Identifier' &&
+ arg.callee.name === 'defineConfig' &&
+ arg.arguments[0]?.type === 'ObjectExpression'
+ );
+
+ const defineConfigNodeWithTest = defineConfigNodes.find(
+ (node) =>
+ node.arguments[0].type === 'ObjectExpression' &&
+ node.arguments[0].properties.some(
+ (p) =>
+ p.type === 'ObjectProperty' &&
+ p.key.type === 'Identifier' &&
+ p.key.name === 'test'
+ )
+ );
+
+ // Give precedence for the defineConfig expression which contains a test config property
+ // As with mergeConfig you never know where the test could be e.g. mergeConfig(viteConfig, defineConfig({}), defineConfig({ test: {...} }))
+ const defineConfigNode = defineConfigNodeWithTest || defineConfigNodes[0];
+
+ if (!defineConfigNode) {
+ return false;
+ }
+
+ const defineConfigProps = defineConfigNode.arguments[0] as t.ObjectExpression;
+
+ // Check if there's already a test property in the defineConfig
+ const existingTestProp = defineConfigProps.properties.find(
+ (p) =>
+ p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'test'
+ ) as t.ObjectProperty | undefined;
+
+ if (existingTestProp && existingTestProp.value.type === 'ObjectExpression') {
+ // Find the test property from the template (either workspace or projects)
+ const templateTestProp = properties.find(
+ (p) =>
+ p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'test'
+ ) as t.ObjectProperty | undefined;
+
+ if (templateTestProp && templateTestProp.value.type === 'ObjectExpression') {
+ // Find the workspace/projects array in the template
+ const workspaceOrProjectsProp = templateTestProp.value.properties.find(
+ (p) =>
+ p.type === 'ObjectProperty' &&
+ p.key.type === 'Identifier' &&
+ (p.key.name === 'workspace' || p.key.name === 'projects')
+ ) as t.ObjectProperty | undefined;
+
+ if (
+ workspaceOrProjectsProp &&
+ workspaceOrProjectsProp.value.type === 'ArrayExpression'
+ ) {
+ // Create the existing test project
+ const existingTestProject: t.ObjectExpression = {
+ type: 'ObjectExpression',
+ properties: [
+ {
+ type: 'ObjectProperty',
+ key: { type: 'Identifier', name: 'extends' },
+ value: { type: 'BooleanLiteral', value: true },
+ computed: false,
+ shorthand: false,
+ },
+ {
+ type: 'ObjectProperty',
+ key: { type: 'Identifier', name: 'test' },
+ value: existingTestProp.value,
+ computed: false,
+ shorthand: false,
+ },
+ ],
+ };
+
+ // Add the existing test project to the template's array
+ workspaceOrProjectsProp.value.elements.unshift(existingTestProject);
+
+ // Remove the existing test property from defineConfig since we're moving it to the array
+ defineConfigProps.properties = defineConfigProps.properties.filter(
+ (p) => p !== existingTestProp
+ );
+
+ // Merge the template properties (which now include our existing test project in the array)
+ mergeProperties(properties, defineConfigProps.properties);
+ } else {
+ // Fallback to original behavior if template structure is unexpected
+ mergeProperties(properties, defineConfigProps.properties);
+ }
+ } else {
+ // Fallback to original behavior if template doesn't have expected structure
+ mergeProperties(properties, defineConfigProps.properties);
+ }
+ } else {
+ // No existing test config, just merge normally
+ mergeProperties(properties, defineConfigProps.properties);
+ }
+ updated = true;
}
}
}
diff --git a/code/lib/cli-storybook/src/autoblock/block-major-version.ts b/code/lib/cli-storybook/src/autoblock/block-major-version.ts
index 03c390321fa3..8b98fe38ad3c 100644
--- a/code/lib/cli-storybook/src/autoblock/block-major-version.ts
+++ b/code/lib/cli-storybook/src/autoblock/block-major-version.ts
@@ -76,10 +76,9 @@ export const blocker = createBlocker({
return {
title: 'Downgrade Not Supported',
message: dedent`
- Your Storybook version (v${data.currentVersion}) is newer than the target release (v${versions.storybook}).Downgrading is not supported.
- Please follow the 8.0 migration guide to upgrade to v8.0 first.
+ Your Storybook version (v${data.currentVersion}) is newer than the target release (v${versions.storybook}). Downgrading is not supported.
`,
- link: 'https://storybook.js.org/docs/8/migration-guide?ref=upgrade',
+ link: 'https://storybook.js.org/docs/releases/migration-guide?ref=upgrade',
};
}
diff --git a/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.test.ts b/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.test.ts
index b9d250686cec..9ee937954270 100644
--- a/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.test.ts
+++ b/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.test.ts
@@ -13,6 +13,33 @@ const fileMocks = {
import { defineConfig } from 'vitest/config'
export default defineConfig({})
`,
+ 'vitest.merge-config.ts': `
+ import { mergeConfig, defineConfig } from 'vitest/config'
+ import viteConfig from './vite.config'
+
+ export default mergeConfig(
+ viteConfig,
+ defineConfig({
+ test: {
+ environment: 'jsdom',
+ },
+ }),
+ )
+ `,
+ 'vitest.merge-config-multi-args.ts': `
+ import { mergeConfig, defineConfig } from 'vitest/config'
+ import viteConfig from './vite.config'
+
+ export default mergeConfig(
+ viteConfig,
+ {},
+ defineConfig({
+ test: {
+ environment: 'jsdom',
+ },
+ }),
+ )
+ `,
'invalidConfig.ts': `
import { defineConfig } from 'vitest/config'
export default defineConfig(['packages/*'])
@@ -159,6 +186,22 @@ describe('vitestConfigFiles', () => {
});
});
+ it('should allow existing test config option with mergeConfig', async () => {
+ vi.mocked(find.any).mockImplementation(coerce('config', 'vitest.merge-config.ts'));
+ const result = await vitestConfigFiles.condition(mockContext, state);
+ expect(result).toEqual({
+ type: 'compatible',
+ });
+ });
+
+ it('should allow existing test config option with mergeConfig and defineConfig as argument in a different position', async () => {
+ vi.mocked(find.any).mockImplementation(coerce('config', 'vitest.merge-config-multi-args.ts'));
+ const result = await vitestConfigFiles.condition(mockContext, state);
+ expect(result).toEqual({
+ type: 'compatible',
+ });
+ });
+
it('should disallow invalid test config option', async () => {
vi.mocked(find.any).mockImplementation(coerce('config', 'testConfig-invalid.ts'));
const result = await vitestConfigFiles.condition(mockContext, state);
diff --git a/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.tsx b/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.tsx
index 56c3870c58f6..613517eb3a35 100644
--- a/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.tsx
+++ b/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.tsx
@@ -55,7 +55,12 @@ const isDefineConfigExpression = (path: Declaration) =>
path.callee.name === 'defineConfig' &&
isObjectExpression(path.arguments[0]);
+const isMergeConfigExpression = (path: Declaration) =>
+ isCallExpression(path) && path.callee.name === 'mergeConfig';
+
const isSafeToExtendWorkspace = (path: CallExpression) =>
+ isCallExpression(path) &&
+ path.arguments.length > 0 &&
isObjectExpression(path.arguments[0]) &&
path.arguments[0]?.properties.every(
(p) =>
@@ -118,11 +123,14 @@ export const vitestConfigFiles: Check = {
const parsedConfig = babel.babelParse(configContent);
babel.traverse(parsedConfig, {
ExportDefaultDeclaration(path) {
- if (
- isDefineConfigExpression(path.node.declaration) &&
- isSafeToExtendWorkspace(path.node.declaration as CallExpression)
- ) {
- isValidVitestConfig = true;
+ if (isDefineConfigExpression(path.node.declaration)) {
+ isValidVitestConfig = isSafeToExtendWorkspace(path.node.declaration as CallExpression);
+ } else if (isMergeConfigExpression(path.node.declaration)) {
+ // the config could be anywhere in the mergeConfig call, so we need to check each argument
+ const mergeCall = path.node.declaration as CallExpression;
+ isValidVitestConfig = mergeCall.arguments.some((arg) =>
+ isSafeToExtendWorkspace(arg as CallExpression)
+ );
}
},
});
diff --git a/code/package.json b/code/package.json
index bd13404d6841..3e823c38c279 100644
--- a/code/package.json
+++ b/code/package.json
@@ -282,5 +282,6 @@
"Dependency Upgrades"
]
]
- }
+ },
+ "deferredNextVersion": "10.0.0-rc.0"
}
diff --git a/docs/addons/addon-migration-guide.mdx b/docs/addons/addon-migration-guide.mdx
index 0f04e848ff35..2e9c50dfbca5 100644
--- a/docs/addons/addon-migration-guide.mdx
+++ b/docs/addons/addon-migration-guide.mdx
@@ -1,48 +1,36 @@
---
-title: Addon migration guide for Storybook 9.0
+title: Addon migration guide for Storybook 10.0
sidebar:
order: 9
- title: Migrate addons to 9.0
+ title: Migrate addons to 10.0
---
-We sincerely appreciate the dedication and effort addon creators put into keeping the Storybook ecosystem vibrant and up-to-date. As Storybook evolves to version 9.0, bringing new features and improvements, this guide is here to assist you in migrating your addons from 8.x to 9.0. If you need to migrate your addon from an earlier version of Storybook, please first refer to the [Addon migration guide for Storybook 8.0](../../../release-8-6/docs/addons/addon-migration-guide.mdx).
+We sincerely appreciate the dedication and effort addon creators put into keeping the Storybook ecosystem vibrant and up-to-date. As Storybook evolves to version 10.0, bringing new features and improvements, this guide is here to assist you in migrating your addons from 9.x to 10.0. If you need to migrate your addon from an earlier version of Storybook, please first refer to the [Addon migration guide for Storybook 9.0](../../../release-9-1/docs/addons/addon-migration-guide.mdx).
- As we gather feedback from the community, we'll update this page. We also have a general [Storybook migration guide](../releases/migration-guide.mdx) if you're looking for that.
+ We also have a general [Storybook migration guide](../releases/migration-guide.mdx) that covers updates to your Storybook instance rather than your addon code.
-## Replacing dependencies
+## Dependency updates
-Many previously-published packages have [moved to be part of Storybook's core](../releases/migration-guide.mdx#package-structure-changes). If your addon depends on any of these packages, you should remove them from your `package.json` and update your addon to import from the new location. If your addon does not already depend on the `storybook` package, you should add it to your `package.json` as a dependency.
-
-```diff title="package.json"
-{
- "devDependencies": {
- "storybook": "next" // or "latest", or "^9.0.0"
- }
-}
-```
-
-### Dependency Management
-
-With Storybook 9.0, most Storybook packages have been consolidated into the main `storybook` package. This means you no longer need to reference individual Storybook packages as dependencies. Instead, define `storybook` as a peer dependency in your addon's `package.json`:
+You will need to update your Storybook dependencies. Peer dependencies must point to `^10.0.0` to ensure broad compatibility for your end users. Development dependencies can be set to `^10.0.0`, or to `next` if you want to try the latest prerelease all year round.
```jsonc title="package.json"
{
- "name": "your-storybook-addon",
- "peerDependencies": {
- "storybook": "^9.0.0"
- },
"devDependencies": {
- "storybook": ">=9.0.0-0 <10.0.0-0" // For local development
+ "@storybook/addon-docs": "next",
+ "@storybook/react-vite": "next",
+ "storybook": "next"
+ },
+ "peerDependencies": {
+ "storybook": "^10.0.0"
}
}
```
-This approach ensures that:
-1. Your addon uses the same version of Storybook APIs as the host project
-2. You avoid duplicate Storybook packages in the final bundle
-3. Your addon's package size is minimized
+If you still have `@storybook/addon-essentials`, `@storybook/addon-interactions`, `@storybook/addon-links`, or `@storybook/blocks` in your dependencies, you will need to remove them. These packages are empty since Storybook 9 and will no longer be published going forward.
+
+### Supporting earlier versions
If your addon supports multiple major versions of Storybook, you can specify a wider version range in your peer dependencies:
@@ -50,10 +38,10 @@ If your addon supports multiple major versions of Storybook, you can specify a w
{
"name": "your-storybook-addon",
"peerDependencies": {
- "storybook": "^8.0.0 || ^9.0.0"
+ "storybook": "^9.0.0 || ^10.0.0"
},
"devDependencies": {
- "storybook": ">=9.0.0-0 <10.0.0-0" // For local development
+ "storybook": ">=10.0.0-0 <11.0.0-0" // For local development
}
}
```
@@ -65,92 +53,313 @@ However, we recommend releasing a new major version of your addon alongside new
## Key changes for addons
-Here are the essential changes in version 9.0 that impact addon development.
+Here are the changes in version 10.0 that impact addon development.
+
+### ESM-only builds
+
+Storybook 10 requires all addons to be built as ESM-only. This change simplifies the build process and reduces maintenance overhead. You'll need to make many changes to `tsup.config.ts`, so it can be easier to copy the reference file in the [`addon-kit` repository](https://github.com/storybookjs/addon-kit/blob/esm-only/tsup.config.ts).
+
+This update brings the following changes:
+* The Node target moves from Node 20.0 to Node 20.19
+* You no longer need to build CJS files
+* You no longer need to pass `globalManagerPackages` and `globalPreviewPackages`
+* The `bundler` config in `package.json` no longer needs to be manually typed
+* We recommend you stop using `exportEntries` and switch exported entries to `previewEntries` and `managerEntries` instead based on where they are consumed by your users
+
+```diff title="tsup.config.ts"
+- import { readFile } from "node:fs/promises";
+
+import { defineConfig, type Options } from "tsup";
+
+- import { globalPackages as globalManagerPackages } from "storybook/internal/manager/globals";
+- import { globalPackages as globalPreviewPackages } from "storybook/internal/preview/globals";
+
+- const NODE_TARGET: Options["target"] = "node20";
++ const NODE_TARGET = "node20.19"; // Minimum Node version supported by Storybook 10
+
+- type BundlerConfig = {
+- bundler?: {
+- exportEntries?: string[];
+- nodeEntries?: string[];
+- managerEntries?: string[];
+- previewEntries?: string[];
+- };
+- };
+
+export default defineConfig(async (options) => {
+ // reading the three types of entries from package.json, which has the following structure:
+ // {
+ // ...
+ // "bundler": {
+- // "exportEntries": ["./src/index.ts"],
+ // "managerEntries": ["./src/manager.ts"],
+- // "previewEntries": ["./src/preview.ts"],
++ // "previewEntries": ["./src/preview.ts", "./src/index.ts"],
+ // "nodeEntries": ["./src/preset.ts"]
+ // }
+ // }
+- const packageJson = (await readFile("./package.json", "utf8").then(
+- JSON.parse,
+- )) as BundlerConfig;
++ const packageJson = (
++ await import("./package.json", { with: { type: "json" } })
++ ).default;
++
+ const {
+ bundler: {
+- exportEntries = [],
+ managerEntries = [],
+ previewEntries = [],
+ nodeEntries = [],
+- } = {},
++ },
+ } = packageJson;
+
+ const commonConfig: Options = {
+- splitting: false,
++ splitting: true,
++ format: ["esm"],
+- minify: !options.watch,
+ treeshake: true,
+- sourcemap: true,
+ // keep this line commented until https://github.com/egoist/tsup/issues/1270 is resolved
+ // clean: options.watch ? false : true,
+ clean: false,
++ // The following packages are provided by Storybook and should always be externalized
++ // Meaning they shouldn't be bundled with the addon, and they shouldn't be regular dependencies either
++ external: ["react", "react-dom", "@storybook/icons"],
+ };
+
+ const configs: Options[] = [];
+-
+- // export entries are entries meant to be manually imported by the user
+- // they are not meant to be loaded by the manager or preview
+- // they'll be usable in both node and browser environments, depending on which features and modules they depend on
+- if (exportEntries.length) {
+- configs.push({
+- ...commonConfig,
+- entry: exportEntries,
+- dts: {
+- resolve: true,
+- },
+- format: ["esm", "cjs"],
+- platform: "neutral",
+- target: NODE_TARGET,
+- external: [...globalManagerPackages, ...globalPreviewPackages],
+- });
+- }
+
+ // manager entries are entries meant to be loaded into the manager UI
+ // they'll have manager-specific packages externalized and they won't be usable in node
+ // they won't have types generated for them as they're usually loaded automatically by Storybook
+ if (managerEntries.length) {
+ configs.push({
+ ...commonConfig,
+ entry: managerEntries,
+- format: ["esm"],
+ platform: "browser",
+- target: BROWSER_TARGETS,
++ target: "esnext", // we can use esnext for manager entries since Storybook will bundle the addon's manager entries again anyway
+- external: globalManagerPackages,
+ });
+ }
+
+ // preview entries are entries meant to be loaded into the preview iframe
+ // they'll have preview-specific packages externalized and they won't be usable in node
+ // they'll have types generated for them so they can be imported when setting up Portable Stories
+ if (previewEntries.length) {
+ configs.push({
+ ...commonConfig,
+ entry: previewEntries,
+- dts: {
+- resolve: true,
+- },
+- format: ["esm", "cjs"],
+ platform: "browser",
+- target: BROWSER_TARGETS,
++ target: "esnext", // we can use esnext for preview entries since Storybook will bundle the addon's preview entries again anyway
+- external: globalPreviewPackages,
++ dts: true,
+ });
+ }
+
+ // node entries are entries meant to be used in node-only
+ // this is useful for presets, which are loaded by Storybook when setting up configurations
+ // they won't have types generated for them as they're usually loaded automatically by Storybook
+ if (nodeEntries.length) {
+ configs.push({
+ ...commonConfig,
+ entry: nodeEntries,
+- format: ["cjs"],
+ platform: "node",
+ target: NODE_TARGET,
+ });
+```
+
+Next, update the `exports` field in your `package.json` to remove mentions of CJS files.
+
+```diff title="package.json"
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+- "import": "./dist/index.js",
+- "require": "./dist/index.cjs"
++ "default": "./dist/index.js"
+ },
+ "./preview": {
+- "types": "./dist/index.d.ts",
+- "import": "./dist/preview.js",
+- "require": "./dist/preview.cjs"
++ "types": "./dist/preview.d.ts",
++ "default": "./dist/preview.js"
+ },
+- "./preset": "./dist/preset.cjs",
++ "./preset": "./dist/preset.js",
+ "./manager": "./dist/manager.js",
+ "./package.json": "./package.json"
+ },
+```
+
+Update `tsconfig.json` to use `esnext` modules.
+
+```diff title="tsconfig.json"
+{
+ "compilerOptions": {
+ // ...
+- "module": "preserve",
++ "module": "esnext",
+ // ...
+- "lib": ["es2023", "dom", "dom.iterable"],
++ "lib": ["esnext", "dom", "dom.iterable"],
+ },
+- "include": ["src/**/*"]
++ "include": ["src/**/*", "tsup.config.ts"]
+ }
+```
-### Package Consolidation
+Finally, change the `preset.js` file at the top-level of your addon to be ESM. This file used to be CJS because the Storybook Node app only supported CJS.
-Several packages have been consolidated into the main `storybook` package. Update your imports to use the new paths:
+```diff title="preset.js"
+-// this file is slightly misleading. It needs to be CJS, and thus in this "type": "module" package it should be named preset.cjs
+-// but Storybook won't pick that filename up so we have to name it preset.js instead
+-
+-module.exports = require('./dist/preset.cjs');
++export * from './dist/preset.js';
+```
-| Old Package | New Path |
-| ------------------------------- | ----------------------- |
-| `@storybook/manager-api` | `storybook/manager-api` |
-| `@storybook/preview-api` | `storybook/preview-api` |
-| `@storybook/theming` | `storybook/theming` |
-| `@storybook/test` | `storybook/test` |
-| `@storybook/addon-actions` | `storybook/actions` |
-| `@storybook/addon-backgrounds` | N/A |
-| `@storybook/addon-controls` | N/A |
-| `@storybook/addon-highlight` | `storybook/highlight` |
-| `@storybook/addon-interactions` | N/A |
-| `@storybook/addon-measure` | N/A |
-| `@storybook/addon-outline` | N/A |
-| `@storybook/addon-toolbars` | N/A |
-| `@storybook/addon-viewport` | `storybook/viewport` |
+### Local addon loading
-Additionally, several internal packages have been moved under the `/internal` sub-path.
-These paths are not part of our public API, so they may change without following semver. While you can use them for a quick upgrade, we strongly encourage finding replacements as they could break in future minor versions:
+Because addons are now ESM-only, you must change how you load your own addon in your development Storybook instance. Imports and exports must now follow ESM syntax, and relative paths must use `import.meta.resolve`.
-| Old Package | New Path |
-| ---------------------------- | ------------------------------------ |
-| `@storybook/channels` | `storybook/internal/channels` |
-| `@storybook/client-logger` | `storybook/internal/client-logger` |
-| `@storybook/core-events` | `storybook/internal/core-events` |
-| `@storybook/types` | `storybook/internal/types` |
-| `@storybook/components` | `storybook/internal/components` |
-Please visit the [full list of consolidated packages](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#dropped-support-for-legacy-packages) in our `Migration.md` file.
+Remove `.storybook/local-preset.cjs` and create `.storybook/local-preset.ts` with the following content:
-### Icon System Updates
+```ts title=".storybook/local-preset.ts"
+import { fileURLToPath } from "node:url";
-The icon system has been updated to use `@storybook/icons`. Several icon-related exports have been removed:
+export function previewAnnotations(entry = []) {
+ return [...entry, fileURLToPath(import.meta.resolve("../dist/preview.js"))];
+}
+
+export function managerEntries(entry = []) {
+ return [...entry, fileURLToPath(import.meta.resolve("../dist/manager.js"))];
+}
-```diff
-- import { Icons, IconButtonSkeleton } from '@storybook/components';
-+ import { ZoomIcon } from '@storybook/icons';
+export * from "../dist/preset.js";
```
-### Manager Builder Changes
+Next, update your `main.ts` to reference the new preset file:
-The manager builder no longer aliases `util`, `assert`, and `process`. If your addon depends on these packages, you'll need to:
+```diff title=".storybook/main.ts"
+- addons: ["@storybook/addon-docs", "./local-preset.cjs"],
++ addons: ["@storybook/addon-docs", import.meta.resolve("./local-preset.ts")],
+```
+
+## Optional changes
-1. Implement the alias at compile time in your addon
-2. Update your bundling configuration to ensure correct dependencies are used
+The following changes are not strictly required yet, but we recommend making them to improve your users' experience.
-### Node.js 18 support dropped
+### CSF Factories support
+To support CSF Factories annotations in your addon, you will need to update your `src/index.ts` file to use the new `definePreviewAddon`. This feature will be part of [CSF Next](../api/csf/csf-next.mdx). This change is highly recommended, as it will help your own users reap the benefits of CSF Factories.
-Please upgrade your addon to Node.js 20, as support for Node.js 18 has ended.
+With CSF Factories, users can chain their preview configuration and benefit from better typing and more flexibility. Addons must export annotations to be compatible with this new syntax. CSF Factories will be the default way to write stories in Storybook 11.
-### TypeScript Requirements
+```diff title="src/index.ts"
+- // make it work with --isolatedModules
+- export default {};
++ import { definePreviewAddon } from "storybook/internal/csf";
++ import addonAnnotations from "./preview";
++
++ export default () => definePreviewAddon(addonAnnotations);
+```
-Storybook now requires TypeScript 4.9 or later. Ensure your addon is compatible with this version.
+### Removal of exportEntries
-### Sidebar Component Changes
+The `exportEntries` property in `package.json`'s `bundler` was used to produce the `index.js` build output from `src/index.ts`. It was compatible with Node.js, rather than strictly with browsers. This build configuration could cause subtle bugs when addon authors exported code in `index.js` for use in the Storybook preview or manager.
-1. The 'extra' prop has been removed from the Sidebar's Heading component
-2. Experimental sidebar features have been removed:
- - `experimental_SIDEBAR_BOTTOM`
- - `experimental_SIDEBAR_TOP`
+In the Storybook 10 [addon-kit](https://github.com/storybookjs/addon-kit), we removed `exportEntries` from the `bundler` config, and we moved `src/index.ts` to be part of `previewEntries` instead. This way, any code exported from `src/index.ts` is bundled for browsers and usable with CSF Next. If you need to export additional code to run in the preview (such as optional decorators), you can add them to `src/index.ts`.
-### Type System Updates
+If you need to export code for the manager (such as a `renderLabel` function for the sidebar), you can create a new `src/manager-helpers.ts` file and add it to `managerEntries`, like so:
-The following types have been removed:
-- `Addon_SidebarBottomType`
-- `Addon_SidebarTopType`
-- `DeprecatedState`
+```diff title="package.json"
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "./preview": {
+ "types": "./dist/preview.d.ts",
+ "default": "./dist/preview.js"
+ },
+ "./preset": "./dist/preset.js",
+ "./manager": "./dist/manager.js",
++ "./manager-helpers": "./dist/manager-helpers.js",
+ "./package.json": "./package.json"
+ },
+ "bundler": {
+ "managerEntries": [
++ "src/manager-helpers.ts",
+ "src/manager.tsx"
+ ]
+ }
+```
+
+### Build file cleanup
+
+We recommend removing your old build files as you build. This will avoid your `dist` folder growing with expired JS chunks. You may add the following to your `package.json` scripts:
+
+```diff title="package.json"
+{
+ "scripts": {
++ "prebuild": "node -e \"fs.rmSync('./dist', { recursive: true, force: true })\"",
+ }
+}
+```
+
+### Package keyword changes
+
+We've updated default keywords for addons in the Storybook `addon-kit` template.
+
+```diff title="package.json"
+ "keywords": [
+- "storybook-addons",
++ "storybook-addon",
+ ],
+```
-## 9.0.0 Full migration guide
+## 10.0.0 full migration guide
-For a full list of changes, please visit the [Migration.md](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#from-version-8x-to-900) file
+For a full list of changes, please visit the [Migration.md](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#from-version-9x-to-1000) file
-## Migration Example
+## Migration example
-For a complete example of an addon updated to support Storybook 9.0, refer to the [Addon Kit migration PR](https://github.com/storybookjs/addon-kit/pull/75).
-Once merged, it will demonstrate all the necessary changes for modern addon development.
+For a complete example of an addon updated to support Storybook 10.0, refer to the [Addon Kit migration PR](https://github.com/storybookjs/addon-kit/pull/82).
+Once merged, it will demonstrate all the necessary and recommended changes for Storybook 10.
## Releasing
-To support Storybook 9.0, we encourage you to release a new major version of your addon. For experimental features or testing, use the `next` tag. This allows you to gather feedback before releasing a stable version.
+To support Storybook 10.0, we encourage you to release a new major version of your addon. For experimental features or testing, use the `next` tag. This allows you to gather feedback before releasing a stable version.
## Support
-If you're having issues with your addon after following this guide, please open a [new discussion](https://github.com/storybookjs/storybook/discussions/new?category=migrations) in our GitHub repository.
+If you're having issues with your addon after following this guide, please open a [new discussion](https://github.com/storybookjs/storybook/discussions/new?category=migrations) in our GitHub repository or come talk to us in our [dedicated addon developer channel, `#addons`](https://discord.gg/KKXFQy9sFc) on Discord.
diff --git a/docs/configure/integration/frameworks-feature-support.mdx b/docs/configure/integration/frameworks-feature-support.mdx
index 7cb43797d5ca..1d0644497b2b 100644
--- a/docs/configure/integration/frameworks-feature-support.mdx
+++ b/docs/configure/integration/frameworks-feature-support.mdx
@@ -34,7 +34,7 @@ Core frameworks have dedicated maintainers or contributors who are responsible f
| [Events](https://github.com/storybookjs/addon-events) | ✅ | ✅ | ✅ | ✅ |
| [Google analytics](https://github.com/storybookjs/addon-google-analytics) | ✅ | ✅ | ✅ | ✅ |
| [GraphQL](https://github.com/storybookjs/addon-graphql) | ✅ | | ✅ | |
-| [Jest](https://github.com/storybookjs/storybook/tree/next/code/addons/jest) | ✅ | ✅ | ✅ | ✅ |
+| [Jest](https://github.com/storybookjs/addon-jest) | ✅ | ✅ | ✅ | ✅ |
| [Links](https://github.com/storybookjs/storybook/tree/next/code/addons/links) | ✅ | ✅ | ✅ | ✅ |
| [Queryparams](https://github.com/storybookjs/addon-queryparams) | ✅ | ✅ | ✅ | ✅ |
| **Docs** | | | | |
@@ -83,7 +83,7 @@ Community frameworks have fewer contributors which means they may not be as up t
| [Events](https://github.com/storybookjs/addon-events) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Google analytics](https://github.com/storybookjs/addon-google-analytics) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [GraphQL](https://github.com/storybookjs/addon-graphql) | | | | | | |
-| [Jest](https://github.com/storybookjs/storybook/tree/next/code/addons/jest) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
+| [Jest](https://github.com/storybookjs/addon-jest) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Links](https://github.com/storybookjs/storybook/tree/next/code/addons/links) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Queryparams](https://github.com/storybookjs/addon-queryparams) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Docs** | | | | | | |
diff --git a/docs/versions/next.json b/docs/versions/next.json
index f9deff565439..4cb7c3383fae 100644
--- a/docs/versions/next.json
+++ b/docs/versions/next.json
@@ -1 +1 @@
-{"version":"10.0.0-beta.13","info":{"plain":"- CLI: CSF factories codemod - support annotations in npx context - [#32741](https://github.com/storybookjs/storybook/pull/32741), thanks @yannbf!\n- Move: Addon jest into it's own repository - [#32646](https://github.com/storybookjs/storybook/pull/32646), thanks @ndelangen!\n- Upgrade: Enhance ESM compatibility checks and banner generation - [#32694](https://github.com/storybookjs/storybook/pull/32694), thanks @ndelangen!"}}
\ No newline at end of file
+{"version":"10.0.0-rc.0","info":{"plain":"- A11Y: Bugfix missing `manager.js` entry-file - [#32780](https://github.com/storybookjs/storybook/pull/32780), thanks @ndelangen!\n- Addon Vitest: Support modifying mergeConfig on addon setup - [#32753](https://github.com/storybookjs/storybook/pull/32753), thanks @yannbf!\n- CLI: Change message in downgrade-blocker - [#32745](https://github.com/storybookjs/storybook/pull/32745), thanks @ndelangen!"}}
\ No newline at end of file
diff --git a/scripts/tasks/sandbox-parts.ts b/scripts/tasks/sandbox-parts.ts
index c9312ae02b1a..9527f4fa3942 100644
--- a/scripts/tasks/sandbox-parts.ts
+++ b/scripts/tasks/sandbox-parts.ts
@@ -541,7 +541,7 @@ export async function addExtraDependencies({
debug: boolean;
extraDeps?: string[];
}) {
- const extraDevDeps = ['@storybook/test-runner@0.23.1--canary.d0c3175.0'];
+ const extraDevDeps = ['@storybook/test-runner@next'];
if (debug) {
logger.log('\uD83C\uDF81 Adding extra dev deps', extraDevDeps);