diff --git a/CHANGELOG.md b/CHANGELOG.md
index 918bef245067..39e2411abc61 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,16 @@
+## 7.6.16
+
+- Addon Themes: Make type generic less strict - [#26042](https://github.com/storybookjs/storybook/pull/26042), thanks [@yannbf](https://github.com/yannbf)!
+- Interaction: Make sure that adding spies doesn't cause infinite loops with self referencing args [#26019](https://github.com/storybookjs/storybook/pull/26019), thanks @kasperpeulen!
+
+## 7.6.15
+
+This release accidentally didn't contain anything.
+
+## 7.6.14
+
+- Core: Fix boolean `true` args in URL getting ignored - [#25950](https://github.com/storybookjs/storybook/pull/25950), thanks [@JReinhold](https://github.com/JReinhold)!
+
## 7.6.13
- Next.js: Fix frameworkOptions resolution - [#25907](https://github.com/storybookjs/storybook/pull/25907), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)!
diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md
index 620e9307a8a7..e7d6ae22d03b 100644
--- a/CHANGELOG.prerelease.md
+++ b/CHANGELOG.prerelease.md
@@ -1,3 +1,13 @@
+## 8.0.0-beta.3
+
+- Addon-actions: Add spy to action for explicit actions - [#26033](https://github.com/storybookjs/storybook/pull/26033), thanks [@kasperpeulen](https://github.com/kasperpeulen)!
+- Addon-themes: Make type generic less strict - [#26042](https://github.com/storybookjs/storybook/pull/26042), thanks [@yannbf](https://github.com/yannbf)!
+- Addon-docs: Fix pnpm+Vite failing to build with `@storybook/theming` Rollup error - [#26024](https://github.com/storybookjs/storybook/pull/26024), thanks [@JReinhold](https://github.com/JReinhold)!
+- CLI: Refactor to add autoblockers - [#25934](https://github.com/storybookjs/storybook/pull/25934), thanks [@ndelangen](https://github.com/ndelangen)!
+- Codemod: Migrate to test package - [#25958](https://github.com/storybookjs/storybook/pull/25958), thanks [@kasperpeulen](https://github.com/kasperpeulen)!
+- Portable stories: Only provide a play function wrapper if it exists - [#25974](https://github.com/storybookjs/storybook/pull/25974), thanks [@yannbf](https://github.com/yannbf)!
+- Test: Bump user-event to 14.5.2 - [#25889](https://github.com/storybookjs/storybook/pull/25889), thanks [@kasperpeulen](https://github.com/kasperpeulen)!
+
## 8.0.0-beta.2
- Core: Fix boolean `true` args in URL getting ignored - [#25950](https://github.com/storybookjs/storybook/pull/25950), thanks [@JReinhold](https://github.com/JReinhold)!
diff --git a/MIGRATION.md b/MIGRATION.md
index 62a85f2a14c9..77d77356d46e 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -1,12 +1,19 @@
Migration
- [From version 7.x to 8.0.0](#from-version-7x-to-800)
+ - [Type change in `composeStories` API](#type-change-in-composestories-api)
- [Tab addons are now routed to a query parameter](#tab-addons-are-now-routed-to-a-query-parameter)
- [Default keyboard shortcuts changed](#default-keyboard-shortcuts-changed)
- [Manager addons are now rendered with React 18](#manager-addons-are-now-rendered-with-react-18)
- [Removal of `storiesOf`-API](#removal-of-storiesof-api)
- [Removed deprecated shim packages](#removed-deprecated-shim-packages)
- [Framework-specific Vite plugins have to be explicitly added](#framework-specific-vite-plugins-have-to-be-explicitly-added)
+ - [For React:](#for-react)
+ - [For Vue:](#for-vue)
+ - [For Svelte (without Sveltekit):](#for-svelte-without-sveltekit)
+ - [For Preact:](#for-preact)
+ - [For Solid:](#for-solid)
+ - [For Qwik:](#for-qwik)
- [TurboSnap Vite plugin is no longer needed](#turbosnap-vite-plugin-is-no-longer-needed)
- [Implicit actions can not be used during rendering (for example in the play function)](#implicit-actions-can-not-be-used-during-rendering-for-example-in-the-play-function)
- [MDX related changes](#mdx-related-changes)
@@ -120,6 +127,7 @@
- [Story context is prepared before for supporting fine grained updates](#story-context-is-prepared-before-for-supporting-fine-grained-updates)
- [Changed decorator order between preview.js and addons/frameworks](#changed-decorator-order-between-previewjs-and-addonsframeworks)
- [Dark mode detection](#dark-mode-detection)
+ - [`addons.setConfig` should now be imported from `@storybook/manager-api`.](#addonssetconfig-should-now-be-imported-from-storybookmanager-api)
- [7.0 core addons changes](#70-core-addons-changes)
- [Removed auto injection of @storybook/addon-actions decorator](#removed-auto-injection-of-storybookaddon-actions-decorator)
- [Addon-backgrounds: Removed deprecated grid parameter](#addon-backgrounds-removed-deprecated-grid-parameter)
@@ -392,6 +400,23 @@
## From version 7.x to 8.0.0
+### Type change in `composeStories` API
+
+There is a TypeScript type change in the `play` function returned from `composeStories` or `composeStory` in `@storybook/react` or `@storybook/vue3`, where before it was always defined, now it is potentially undefined. This means that you might have to make a small change in your code, such as:
+
+```ts
+const { Primary } = composeStories(stories)
+
+// before
+await Primary.play(...)
+
+// after
+await Primary.play?.(...) // if you don't care whether the play function exists
+await Primary.play!(...) // if you want a runtime error when the play function does not exist
+```
+
+There are plans to make the type of the play function be inferred based on your imported story's play function in a near future, so the types will be 100% accurate.
+
### Tab addons are now routed to a query parameter
The URL of a tab used to be: `http://localhost:6006/?path=/my-addon-tab/my-story`.
@@ -444,23 +469,82 @@ In Storybook 7, these packages existed for backwards compatibility, but were mar
- `@storybook/store` - this package has been merged into `@storybook/preview-api`.
- `@storybook/api` - this package has been replaced with `@storybook/manager-api`.
-This section explains the rationale, and the required changed you might have to make: [New Addons API](#new-addons-api)
+These sections explain the rationale, and the required changes you might have to make:
+
+- [New Addons API](#new-addons-api)
+- [`addons.setConfig` should now be imported from `@storybook/manager-api`.](#addonssetconfig-should-now-be-imported-from-storybookmanager-api)
### Framework-specific Vite plugins have to be explicitly added
In Storybook 7, we would automatically add frameworks-specific Vite plugins, e.g. `@vitejs/plugin-react` if not installed.
In Storybook 8 those plugins have to be added explicitly in the user's `vite.config.ts`:
+#### For React:
+
```ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
-// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
});
```
+#### For Vue:
+
+```ts
+import { defineConfig } from "vite";
+import vue from "@vitejs/plugin-vue";
+
+export default defineConfig({
+ plugins: [vue()],
+});
+```
+
+#### For Svelte (without Sveltekit):
+
+```ts
+import { defineConfig } from "vite";
+import svelte from "@sveltejs/vite-plugin-svelte";
+
+export default defineConfig({
+ plugins: [svelte()],
+});
+```
+
+#### For Preact:
+
+```ts
+import { defineConfig } from "vite";
+import preact from "@preact/preset-vite";
+
+export default defineConfig({
+ plugins: [preact()],
+});
+```
+
+#### For Solid:
+
+```ts
+import { defineConfig } from "vite";
+import solid from "vite-plugin-solid";
+
+export default defineConfig({
+ plugins: [solid()],
+});
+```
+
+#### For Qwik:
+
+```ts
+import { defineConfig } from "vite";
+import qwik from "vite-plugin-qwik";
+
+export default defineConfig({
+ plugins: [qwik()],
+});
+```
+
### TurboSnap Vite plugin is no longer needed
At least in build mode, `builder-vite` now supports the `--webpack-stats-json` flag and will output `preview-stats.json`.
@@ -1949,6 +2033,19 @@ Earlier versions used the light theme by default, so if you don't set a theme an
To learn more about theming, read our [documentation](https://storybook.js.org/docs/react/configure/theming).
+#### `addons.setConfig` should now be imported from `@storybook/manager-api`.
+
+The previous package, `@storybook/addons`, is now deprecated and will be removed in 8.0.
+
+```diff
+- import { addons } from '@storybook/addons';
++ import { addons } from '@storybook/manager-api';
+
+addons.setConfig({
+ // ...
+})
+```
+
### 7.0 core addons changes
#### Removed auto injection of @storybook/addon-actions decorator
diff --git a/code/addons/actions/src/runtime/action.ts b/code/addons/actions/src/runtime/action.ts
index f6779d1a1a64..a647a8eb0d1b 100644
--- a/code/addons/actions/src/runtime/action.ts
+++ b/code/addons/actions/src/runtime/action.ts
@@ -103,6 +103,7 @@ export function action(name: string, options: ActionOptions = {}): HandlerFuncti
channel.emit(EVENT_ID, actionDisplayToEmit);
};
handler.isAction = true;
+ handler.implicit = options.implicit;
return handler;
}
diff --git a/code/addons/docs/src/preset.ts b/code/addons/docs/src/preset.ts
index 45c832b5fe42..68c7efb39f8b 100644
--- a/code/addons/docs/src/preset.ts
+++ b/code/addons/docs/src/preset.ts
@@ -149,8 +149,14 @@ export const viteFinal = async (config: any, options: Options) => {
react,
'react-dom': reactDom,
'@mdx-js/react': mdx,
+ /**
+ * The following aliases are used to ensure a single instance of these packages are used in situations where they are duplicated
+ * The packages will be duplicated by the package manager when the user has react installed with another version than 18.2.0
+ */
+ '@storybook/theming': dirname(require.resolve('@storybook/theming')),
+ '@storybook/components': dirname(require.resolve('@storybook/components')),
+ '@storybook/blocks': dirname(require.resolve('@storybook/blocks')),
},
- dedupe: ['@storybook/theming', '@storybook/components', '@storybook/blocks'],
},
}),
};
diff --git a/code/addons/interactions/src/preview.ts b/code/addons/interactions/src/preview.ts
index d751cbf7bc58..e0cc0aeae87a 100644
--- a/code/addons/interactions/src/preview.ts
+++ b/code/addons/interactions/src/preview.ts
@@ -1,11 +1,11 @@
-/* eslint-disable no-underscore-dangle */
import type {
- Args,
- LoaderFunction,
+ ArgsEnhancer,
PlayFunction,
PlayFunctionContext,
+ Renderer,
StepLabel,
} from '@storybook/types';
+import { fn, isMockFunction } from '@storybook/test';
import { instrument } from '@storybook/instrumenter';
export const { step: runStep } = instrument(
@@ -16,26 +16,47 @@ export const { step: runStep } = instrument(
{ intercept: true }
);
-const instrumentSpies: LoaderFunction = ({ initialArgs }) => {
- const argTypesWithAction = Object.entries(initialArgs).filter(
- ([, value]) =>
- typeof value === 'function' &&
- '_isMockFunction' in value &&
- value._isMockFunction &&
- !value._instrumented
- );
-
- return argTypesWithAction.reduce((acc, [key, value]) => {
- const instrumented = instrument({ [key]: () => value }, { retain: true })[key];
- acc[key] = instrumented();
- // this enhancer is being called multiple times
-
- value._instrumented = true;
- return acc;
- }, {} as Args);
+const traverseArgs = (value: unknown, depth = 0, key?: string): any => {
+ // Make sure to not get in infinite loops with self referencing args
+ if (depth > 5) return value;
+ if (value == null) return value;
+ if (isMockFunction(value)) {
+ // Makes sure we get the arg name in the interactions panel
+ if (key) value.mockName(key);
+ return value;
+ }
+
+ // wrap explicit actions in a spy
+ if (
+ typeof value === 'function' &&
+ 'isAction' in value &&
+ value.isAction &&
+ !('implicit' in value && value.implicit)
+ ) {
+ const mock = fn(value as any);
+ if (key) mock.mockName(key);
+ return mock;
+ }
+
+ if (Array.isArray(value)) {
+ depth++;
+ return value.map((item) => traverseArgs(item, depth));
+ }
+
+ if (typeof value === 'object' && value.constructor === Object) {
+ depth++;
+ // We have to mutate the original object for this to survive HMR.
+ for (const [k, v] of Object.entries(value)) {
+ (value as Record)[k] = traverseArgs(v, depth, k);
+ }
+ return value;
+ }
+ return value;
};
-export const argsEnhancers = [instrumentSpies];
+const wrapActionsInSpyFns: ArgsEnhancer = ({ initialArgs }) => traverseArgs(initialArgs);
+
+export const argsEnhancers = [wrapActionsInSpyFns];
export const parameters = {
throwPlayFunctionExceptions: false,
diff --git a/code/addons/themes/src/decorators/class-name.decorator.tsx b/code/addons/themes/src/decorators/class-name.decorator.tsx
index ccbe4fbf7f31..0306c5ea9912 100644
--- a/code/addons/themes/src/decorators/class-name.decorator.tsx
+++ b/code/addons/themes/src/decorators/class-name.decorator.tsx
@@ -13,7 +13,8 @@ const DEFAULT_ELEMENT_SELECTOR = 'html';
const classStringToArray = (classString: string) => classString.split(' ').filter(Boolean);
-export const withThemeByClassName = ({
+// TODO check with @kasperpeulen: change the types so they can be correctly inferred from context e.g. any>
+export const withThemeByClassName = ({
themes,
defaultTheme,
parentSelector = DEFAULT_ELEMENT_SELECTOR,
diff --git a/code/addons/themes/src/decorators/data-attribute.decorator.tsx b/code/addons/themes/src/decorators/data-attribute.decorator.tsx
index 546885db8d62..4009fd9073a0 100644
--- a/code/addons/themes/src/decorators/data-attribute.decorator.tsx
+++ b/code/addons/themes/src/decorators/data-attribute.decorator.tsx
@@ -12,7 +12,8 @@ export interface DataAttributeStrategyConfiguration {
const DEFAULT_ELEMENT_SELECTOR = 'html';
const DEFAULT_DATA_ATTRIBUTE = 'data-theme';
-export const withThemeByDataAttribute = ({
+// TODO check with @kasperpeulen: change the types so they can be correctly inferred from context e.g. any>
+export const withThemeByDataAttribute = ({
themes,
defaultTheme,
parentSelector = DEFAULT_ELEMENT_SELECTOR,
diff --git a/code/addons/themes/src/decorators/provider.decorator.tsx b/code/addons/themes/src/decorators/provider.decorator.tsx
index 6063034eb859..0466a29e6ac8 100644
--- a/code/addons/themes/src/decorators/provider.decorator.tsx
+++ b/code/addons/themes/src/decorators/provider.decorator.tsx
@@ -16,7 +16,8 @@ export interface ProviderStrategyConfiguration {
const pluckThemeFromKeyPairTuple = ([_, themeConfig]: [string, Theme]): Theme => themeConfig;
-export const withThemeFromJSXProvider = ({
+// TODO check with @kasperpeulen: change the types so they can be correctly inferred from context e.g. any>
+export const withThemeFromJSXProvider = ({
Provider,
GlobalStyles,
defaultTheme,
diff --git a/code/lib/cli/src/autoblock/block-dependencies-versions.ts b/code/lib/cli/src/autoblock/block-dependencies-versions.ts
new file mode 100644
index 000000000000..284562aa9f6d
--- /dev/null
+++ b/code/lib/cli/src/autoblock/block-dependencies-versions.ts
@@ -0,0 +1,92 @@
+import { createBlocker } from './types';
+import { dedent } from 'ts-dedent';
+import { lt } from 'semver';
+
+const minimalVersionsMap = {
+ '@angular/core': '15.0.0',
+ 'react-scripts': '5.0.0',
+ next: '13.5.0',
+ preact: '10.0.0',
+ svelte: '4.0.0',
+ vue: '3.0.0',
+};
+
+type Result = {
+ installedVersion: string | undefined;
+ packageName: keyof typeof minimalVersionsMap;
+ minimumVersion: string;
+};
+const typedKeys = (obj: Record) => Object.keys(obj) as TKey[];
+
+export const blocker = createBlocker({
+ id: 'dependenciesVersions',
+ async check({ packageManager }) {
+ const list = await Promise.all(
+ typedKeys(minimalVersionsMap).map(async (packageName) => ({
+ packageName,
+ installedVersion: await packageManager.getPackageVersion(packageName),
+ minimumVersion: minimalVersionsMap[packageName],
+ }))
+ );
+
+ return list.reduce((acc, { installedVersion, minimumVersion, packageName }) => {
+ if (acc) {
+ return acc;
+ }
+ if (packageName && installedVersion && lt(installedVersion, minimumVersion)) {
+ return {
+ installedVersion,
+ packageName,
+ minimumVersion,
+ };
+ }
+ return acc;
+ }, false);
+ },
+ message(options, data) {
+ return `Found ${data.packageName} version: ${data.installedVersion}, please upgrade to ${data.minimumVersion} or higher.`;
+ },
+ log(options, data) {
+ switch (data.packageName) {
+ case 'react-scripts':
+ return dedent`
+ Support react-script < 5.0.0 has been removed.
+ Please see the migration guide for more information:
+ https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#create-react-app-dropped-cra4-support
+
+ Upgrade to the latest version of react-scripts.
+ `;
+ case 'vue':
+ return dedent`
+ Support for Vue 2 has been removed.
+ Please see the migration guide for more information:
+ https://v3-migration.vuejs.org/
+
+ Please upgrade to the latest version of Vue.
+ `;
+ case '@angular/core':
+ return dedent`
+ Support for Angular < 15 has been removed.
+ Please see the migration guide for more information:
+ https://angular.io/guide/update-to-version-15
+
+ Please upgrade to the latest version of Angular.
+ `;
+ case 'next':
+ return dedent`
+ Support for Next.js < 13.5 has been removed.
+ Please see the migration guide for more information:
+ https://nextjs.org/docs/pages/building-your-application/upgrading/version-13
+
+ Please upgrade to the latest version of Next.js.
+ `;
+ default:
+ return dedent`
+ Support for ${data.packageName} version < ${data.minimumVersion} has been removed.
+ Storybook 8 needs minimum version of ${data.minimumVersion}, but you had version ${data.installedVersion}.
+
+ Please update this dependency.
+ `;
+ }
+ },
+});
diff --git a/code/lib/cli/src/autoblock/block-node-version.ts b/code/lib/cli/src/autoblock/block-node-version.ts
new file mode 100644
index 000000000000..220b29823e4e
--- /dev/null
+++ b/code/lib/cli/src/autoblock/block-node-version.ts
@@ -0,0 +1,25 @@
+import { createBlocker } from './types';
+import { dedent } from 'ts-dedent';
+import { lt } from 'semver';
+
+export const blocker = createBlocker({
+ id: 'minimumNode16',
+ async check() {
+ const nodeVersion = process.versions.node;
+ if (nodeVersion && lt(nodeVersion, '18.0.0')) {
+ return { nodeVersion };
+ }
+ return false;
+ },
+ message(options, data) {
+ return `Please use Node.js v18 or higher.`;
+ },
+ log(options, data) {
+ return dedent`
+ We've detected you're using Node.js v${data.nodeVersion}.
+ Storybook needs Node.js 18 or higher.
+
+ https://nodejs.org/en/download
+ `;
+ },
+});
diff --git a/code/lib/cli/src/autoblock/block-stories-mdx.ts b/code/lib/cli/src/autoblock/block-stories-mdx.ts
new file mode 100644
index 000000000000..b868d913ecd0
--- /dev/null
+++ b/code/lib/cli/src/autoblock/block-stories-mdx.ts
@@ -0,0 +1,33 @@
+import { createBlocker } from './types';
+import { dedent } from 'ts-dedent';
+import { glob } from 'glob';
+
+export const blocker = createBlocker({
+ id: 'storiesMdxUsage',
+ async check() {
+ const files = await glob('**/*.stories.mdx', { cwd: process.cwd() });
+ if (files.length === 0) {
+ return false;
+ }
+ return { files };
+ },
+ message(options, data) {
+ return `Found ${data.files.length} stories.mdx ${
+ data.files.length === 1 ? 'file' : 'files'
+ }, these must be migrated.`;
+ },
+ log() {
+ return dedent`
+ Support for *.stories.mdx files has been removed.
+ Please see the migration guide for more information:
+ https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#dropping-support-for-storiesmdx-csf-in-mdx-format-and-mdx1-support
+
+ Storybook will also require you to use MDX 3.0.0 or later.
+ Check the migration guide for more information:
+ https://mdxjs.com/blog/v3/
+
+ Manually run the migration script to convert your stories.mdx files to CSF format documented here:
+ https://storybook.js.org/docs/migration-guide#storiesmdx-to-mdxcsf
+ `;
+ },
+});
diff --git a/code/lib/cli/src/autoblock/block-storystorev6.ts b/code/lib/cli/src/autoblock/block-storystorev6.ts
new file mode 100644
index 000000000000..40a9f8822ac9
--- /dev/null
+++ b/code/lib/cli/src/autoblock/block-storystorev6.ts
@@ -0,0 +1,40 @@
+import { relative } from 'path';
+import { createBlocker } from './types';
+import { dedent } from 'ts-dedent';
+import type { StorybookConfigRaw } from '@storybook/types';
+
+export const blocker = createBlocker({
+ id: 'storyStoreV7removal',
+ async check({ mainConfig }) {
+ const features = (mainConfig as any as StorybookConfigRaw)?.features;
+ if (features === undefined) {
+ return false;
+ }
+ if (Object.hasOwn(features, 'storyStoreV7')) {
+ return true;
+ }
+ return false;
+ },
+ message(options, data) {
+ const mainConfigPath = relative(process.cwd(), options.mainConfigPath);
+ return `StoryStoreV7 feature must be removed from ${mainConfigPath}`;
+ },
+ log() {
+ return dedent`
+ StoryStoreV7 feature must be removed from your Storybook configuration.
+ This feature was removed in Storybook 8.0.0.
+ Please see the migration guide for more information:
+ https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#storystorev6-and-storiesof-is-deprecated
+
+ In your Storybook configuration file you have this code:
+
+ export default = {
+ features: {
+ storyStoreV7: false, <--- remove this line
+ },
+ };
+
+ You need to remove the storyStoreV7 property.
+ `;
+ },
+});
diff --git a/code/lib/cli/src/autoblock/index.test.ts b/code/lib/cli/src/autoblock/index.test.ts
new file mode 100644
index 000000000000..ce5fa3170411
--- /dev/null
+++ b/code/lib/cli/src/autoblock/index.test.ts
@@ -0,0 +1,109 @@
+import { expect, test, vi } from 'vitest';
+import { autoblock } from './index';
+import { JsPackageManagerFactory } from '@storybook/core-common';
+import { createBlocker } from './types';
+import { writeFile as writeFileRaw } from 'node:fs/promises';
+import { logger } from '@storybook/node-logger';
+
+vi.mock('node:fs/promises', () => ({
+ writeFile: vi.fn(),
+}));
+vi.mock('boxen', () => ({
+ default: vi.fn((x) => x),
+}));
+vi.mock('@storybook/node-logger', () => ({
+ logger: {
+ info: vi.fn(),
+ line: vi.fn(),
+ plain: vi.fn(),
+ },
+}));
+
+const writeFile = vi.mocked(writeFileRaw);
+
+const blockers = {
+ alwaysPass: createBlocker({
+ id: 'alwaysPass',
+ check: async () => false,
+ message: () => 'Always pass',
+ log: () => 'Always pass',
+ }),
+ alwaysFail: createBlocker({
+ id: 'alwaysFail',
+ check: async () => ({ bad: true }),
+ message: () => 'Always fail',
+ log: () => '...',
+ }),
+ alwaysFail2: createBlocker({
+ id: 'alwaysFail2',
+ check: async () => ({ disaster: true }),
+ message: () => 'Always fail 2',
+ log: () => '...',
+ }),
+} as const;
+
+const baseOptions: Parameters[0] = {
+ configDir: '.storybook',
+ mainConfig: {
+ stories: [],
+ },
+ mainConfigPath: '.storybook/main.ts',
+ packageJson: {
+ dependencies: {},
+ devDependencies: {},
+ },
+ packageManager: JsPackageManagerFactory.getPackageManager({ force: 'npm' }),
+};
+
+test('with empty list', async () => {
+ const result = await autoblock({ ...baseOptions }, []);
+ expect(result).toBe(null);
+ expect(logger.plain).not.toHaveBeenCalledWith(expect.stringContaining('No blockers found'));
+});
+
+test('all passing', async () => {
+ const result = await autoblock({ ...baseOptions }, [
+ Promise.resolve({ blocker: blockers.alwaysPass }),
+ Promise.resolve({ blocker: blockers.alwaysPass }),
+ ]);
+ expect(result).toBe(null);
+ expect(logger.plain).toHaveBeenCalledWith(expect.stringContaining('No blockers found'));
+});
+
+test('1 fail', async () => {
+ const result = await autoblock({ ...baseOptions }, [
+ Promise.resolve({ blocker: blockers.alwaysPass }),
+ Promise.resolve({ blocker: blockers.alwaysFail }),
+ ]);
+ expect(writeFile).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.stringContaining('alwaysFail'),
+ expect.any(Object)
+ );
+ expect(result).toBe('alwaysFail');
+ expect(logger.plain).toHaveBeenCalledWith(expect.stringContaining('Oh no..'));
+
+ expect(writeFile.mock.calls[0][1]).toMatchInlineSnapshot(`
+ "(alwaysFail):
+ ..."
+ `);
+});
+
+test('multiple fails', async () => {
+ const result = await autoblock({ ...baseOptions }, [
+ Promise.resolve({ blocker: blockers.alwaysPass }),
+ Promise.resolve({ blocker: blockers.alwaysFail }),
+ Promise.resolve({ blocker: blockers.alwaysFail2 }),
+ ]);
+ expect(writeFile.mock.calls[0][1]).toMatchInlineSnapshot(`
+ "(alwaysFail):
+ ...
+
+ ----
+
+ (alwaysFail2):
+ ..."
+ `);
+
+ expect(result).toBe('alwaysFail');
+});
diff --git a/code/lib/cli/src/autoblock/index.ts b/code/lib/cli/src/autoblock/index.ts
new file mode 100644
index 000000000000..ca8116d890cb
--- /dev/null
+++ b/code/lib/cli/src/autoblock/index.ts
@@ -0,0 +1,85 @@
+import type { AutoblockOptions, Blocker } from './types';
+import { logger } from '@storybook/node-logger';
+import chalk from 'chalk';
+import boxen from 'boxen';
+import { writeFile } from 'node:fs/promises';
+
+const excludesFalse = (x: T | false): x is T => x !== false;
+
+const blockers: () => BlockerModule[] = () => [
+ // add/remove blockers here
+ import('./block-storystorev6'),
+ import('./block-stories-mdx'),
+ import('./block-dependencies-versions'),
+ import('./block-node-version'),
+];
+
+type BlockerModule = Promise<{ blocker: Blocker }>;
+
+export const autoblock = async (
+ options: AutoblockOptions,
+ list: BlockerModule[] = blockers()
+) => {
+ if (list.length === 0) {
+ return null;
+ }
+
+ logger.info('Checking for upgrade blockers...');
+
+ const out = await Promise.all(
+ list.map(async (i) => {
+ const { blocker } = await i;
+ const result = await blocker.check(options);
+ if (result) {
+ return {
+ id: blocker.id,
+ value: true,
+ message: blocker.message(options, result),
+ log: blocker.log(options, result),
+ };
+ } else {
+ return false;
+ }
+ })
+ );
+
+ const faults = out.filter(excludesFalse);
+
+ if (faults.length > 0) {
+ const LOG_FILE_NAME = 'migration-storybook.log';
+
+ const messages = {
+ welcome: `Blocking your upgrade because of the following issues:`,
+ reminder: chalk.yellow('Fix the above issues and try running the upgrade command again.'),
+ logfile: chalk.yellow(`You can find more details in ./${LOG_FILE_NAME}.`),
+ };
+ const borderColor = '#FC521F';
+
+ logger.plain('Oh no..');
+ logger.plain(
+ boxen(
+ [messages.welcome]
+ .concat(faults.map((i) => i.message))
+ .concat([messages.reminder])
+ .concat([messages.logfile])
+ .join('\n\n'),
+ { borderStyle: 'round', padding: 1, borderColor }
+ )
+ );
+
+ await writeFile(
+ LOG_FILE_NAME,
+ faults.map((i) => '(' + i.id + '):\n' + i.log).join('\n\n----\n\n'),
+ {
+ encoding: 'utf-8',
+ }
+ );
+
+ return faults[0].id;
+ }
+
+ logger.plain('No blockers found.');
+ logger.line();
+
+ return null;
+};
diff --git a/code/lib/cli/src/autoblock/types.ts b/code/lib/cli/src/autoblock/types.ts
new file mode 100644
index 000000000000..62be9625c76e
--- /dev/null
+++ b/code/lib/cli/src/autoblock/types.ts
@@ -0,0 +1,42 @@
+import type { JsPackageManager, PackageJson } from '@storybook/core-common';
+import type { StorybookConfig } from '@storybook/types';
+
+export interface AutoblockOptions {
+ packageManager: JsPackageManager;
+ packageJson: PackageJson;
+ mainConfig: StorybookConfig;
+ mainConfigPath: string;
+ configDir: string;
+}
+
+export interface Blocker {
+ /**
+ * A unique string to identify the blocker with.
+ */
+ id: string;
+ /**
+ * Check if the blocker should block.
+ *
+ * @param context
+ * @returns A truthy value to activate the block, return false to proceed.
+ */
+ check: (options: AutoblockOptions) => Promise;
+ /**
+ * Format a message to be printed to the log-file.
+ * @param context
+ * @param data returned from the check method.
+ * @returns The string to print to the terminal.
+ */
+ message: (options: AutoblockOptions, data: T) => string;
+ /**
+ * Format a message to be printed to the log-file.
+ * @param context
+ * @param data returned from the check method.
+ * @returns The string to print to the log-file.
+ */
+ log: (options: AutoblockOptions, data: T) => string;
+}
+
+export function createBlocker(block: Blocker) {
+ return block;
+}
diff --git a/code/lib/cli/src/automigrate/fixes/index.ts b/code/lib/cli/src/automigrate/fixes/index.ts
index 27f8a80b3140..030e31baa6dc 100644
--- a/code/lib/cli/src/automigrate/fixes/index.ts
+++ b/code/lib/cli/src/automigrate/fixes/index.ts
@@ -2,17 +2,18 @@ import type { Fix } from '../types';
import { cra5 } from './cra5';
import { webpack5 } from './webpack5';
+import { vite4 } from './vite4';
import { vue3 } from './vue3';
import { mdxgfm } from './mdx-gfm';
import { eslintPlugin } from './eslint-plugin';
import { builderVite } from './builder-vite';
+import { viteConfigFile } from './vite-config-file';
import { sbScripts } from './sb-scripts';
import { sbBinary } from './sb-binary';
import { newFrameworks } from './new-frameworks';
import { removedGlobalClientAPIs } from './remove-global-client-apis';
import { mdx1to2 } from './mdx-1-to-2';
import { autodocsTrue } from './autodocs-true';
-import { nodeJsRequirement } from './nodejs-requirement';
import { angularBuilders } from './angular-builders';
import { incompatibleAddons } from './incompatible-addons';
import { angularBuildersMultiproject } from './angular-builders-multiproject';
@@ -20,20 +21,23 @@ import { wrapRequire } from './wrap-require';
import { reactDocgen } from './react-docgen';
import { removeReactDependency } from './prompt-remove-react';
import { storyshotsMigration } from './storyshots-migration';
+import { removeJestTestingLibrary } from './remove-jest-testing-library';
export * from '../types';
export const allFixes: Fix[] = [
- nodeJsRequirement,
newFrameworks,
cra5,
webpack5,
vue3,
+ vite4,
+ viteConfigFile,
eslintPlugin,
builderVite,
sbBinary,
sbScripts,
incompatibleAddons,
+ removeJestTestingLibrary,
removedGlobalClientAPIs,
mdx1to2,
mdxgfm,
diff --git a/code/lib/cli/src/automigrate/fixes/nodejs-requirement.test.ts b/code/lib/cli/src/automigrate/fixes/nodejs-requirement.test.ts
deleted file mode 100644
index b3c1f8b311d6..000000000000
--- a/code/lib/cli/src/automigrate/fixes/nodejs-requirement.test.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { describe, afterAll, it, expect, vi } from 'vitest';
-
-import { nodeJsRequirement } from './nodejs-requirement';
-
-vi.mock('fs-extra', async () => import('../../../../../__mocks__/fs-extra'));
-
-const check = async ({ storybookVersion = '7.0.0' }) => {
- return nodeJsRequirement.check({
- storybookVersion,
- packageManager: {} as any,
- mainConfig: {} as any,
- });
-};
-
-const originalNodeVersion = process.version;
-const mockNodeVersion = (version: string) => {
- Object.defineProperties(process, {
- version: {
- value: version,
- },
- });
-};
-
-describe('nodejs-requirement fix', () => {
- afterAll(() => {
- mockNodeVersion(originalNodeVersion);
- vi.restoreAllMocks();
- });
-
- it('skips when sb <= 7.0.0', async () => {
- mockNodeVersion('14.0.0');
- await expect(check({ storybookVersion: '6.3.2' })).resolves.toBeNull();
- });
-
- it('skips when node >= 16.0.0', async () => {
- mockNodeVersion('16.0.0');
- await expect(check({})).resolves.toBeNull();
- });
-
- it('skips when node >= 18.0.0', async () => {
- mockNodeVersion('18.0.0');
- await expect(check({})).resolves.toBeNull();
- });
-
- it('prompts when node <= 16.0.0', async () => {
- mockNodeVersion('14.0.0');
- await expect(check({})).resolves.toEqual({ nodeVersion: '14.0.0' });
- });
-});
diff --git a/code/lib/cli/src/automigrate/fixes/nodejs-requirement.ts b/code/lib/cli/src/automigrate/fixes/nodejs-requirement.ts
deleted file mode 100644
index cf82bceb9bac..000000000000
--- a/code/lib/cli/src/automigrate/fixes/nodejs-requirement.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import chalk from 'chalk';
-import dedent from 'ts-dedent';
-import semver from 'semver';
-import type { Fix } from '../types';
-
-interface NodeJsRequirementOptions {
- nodeVersion: string;
-}
-
-export const nodeJsRequirement: Fix = {
- id: 'nodejs-requirement',
- promptOnly: true,
-
- async check({ storybookVersion }) {
- if (!semver.gte(storybookVersion, '7.0.0')) {
- return null;
- }
-
- const nodeVersion = process.version;
- if (semver.lt(nodeVersion, '16.0.0')) {
- return { nodeVersion };
- }
-
- return null;
- },
- prompt({ nodeVersion }) {
- return dedent`
- ${chalk.bold(
- chalk.red('Attention')
- )}: We could not automatically make this change. You'll need to do it manually.
-
- We've detected that you're using Node ${chalk.bold(
- nodeVersion
- )} but Storybook 7 only supports Node ${chalk.bold(
- 'v16.0.0'
- )} and higher. You will either need to upgrade your Node version or keep using an older version of Storybook.
-
- Please see the migration guide for more information:
- ${chalk.yellow(
- 'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#dropped-support-for-node-15-and-below'
- )}
- `;
- },
-};
diff --git a/code/lib/cli/src/automigrate/fixes/remove-jest-testing-library.test.ts b/code/lib/cli/src/automigrate/fixes/remove-jest-testing-library.test.ts
new file mode 100644
index 000000000000..60a2c2a97a35
--- /dev/null
+++ b/code/lib/cli/src/automigrate/fixes/remove-jest-testing-library.test.ts
@@ -0,0 +1,64 @@
+import { expect, it } from 'vitest';
+
+import type { StorybookConfig } from '@storybook/types';
+import type { JsPackageManager } from '@storybook/core-common';
+import { removeJestTestingLibrary } from './remove-jest-testing-library';
+import ansiRegex from 'ansi-regex';
+
+const check = async ({
+ packageManager,
+ main: mainConfig = {},
+ storybookVersion = '8.0.0',
+}: {
+ packageManager: Partial;
+ main?: Partial & Record;
+ storybookVersion?: string;
+}) => {
+ return removeJestTestingLibrary.check({
+ packageManager: packageManager as any,
+ configDir: '',
+ mainConfig: mainConfig as any,
+ storybookVersion,
+ });
+};
+
+it('should prompt to install the test package and run the codemod', async () => {
+ const options = await check({
+ packageManager: {
+ getAllDependencies: async () => ({
+ '@storybook/jest': '1.0.0',
+ '@storybook/testing-library': '1.0.0',
+ }),
+ },
+ main: { addons: ['@storybook/essentials', '@storybook/addon-info'] },
+ });
+
+ await expect(options).toMatchInlineSnapshot(`
+ {
+ "incompatiblePackages": [
+ "@storybook/jest",
+ "@storybook/testing-library",
+ ],
+ }
+ `);
+
+ expect.addSnapshotSerializer({
+ serialize: (value) => {
+ const stringVal = typeof value === 'string' ? value : value.toString();
+ return stringVal.replace(ansiRegex(), '');
+ },
+ test: () => true,
+ });
+
+ expect(await removeJestTestingLibrary.prompt(options!)).toMatchInlineSnapshot(`
+ Attention: We've detected that you're using the following packages which are known to be incompatible with Storybook 8:
+
+ - @storybook/jest
+ - @storybook/testing-library
+
+ Install the replacement for those packages: @storybook/test
+
+ And run the following codemod:
+ npx storybook migrate migrate-to-test-package --glob="**/*.stories.@(js|jsx|ts|tsx)"
+ `);
+});
diff --git a/code/lib/cli/src/automigrate/fixes/remove-jest-testing-library.ts b/code/lib/cli/src/automigrate/fixes/remove-jest-testing-library.ts
new file mode 100644
index 000000000000..87cf964468b3
--- /dev/null
+++ b/code/lib/cli/src/automigrate/fixes/remove-jest-testing-library.ts
@@ -0,0 +1,32 @@
+import chalk from 'chalk';
+import dedent from 'ts-dedent';
+import type { Fix } from '../types';
+
+export const removeJestTestingLibrary: Fix<{ incompatiblePackages: string[] }> = {
+ id: 'remove-jest-testing-library',
+ promptOnly: true,
+ async check({ mainConfig, packageManager }) {
+ const deps = await packageManager.getAllDependencies();
+
+ const incompatiblePackages = Object.keys(deps).filter(
+ (it) => it === '@storybook/jest' || it === '@storybook/testing-library'
+ );
+ return incompatiblePackages.length ? { incompatiblePackages } : null;
+ },
+ prompt({ incompatiblePackages }) {
+ return dedent`
+ ${chalk.bold(
+ 'Attention'
+ )}: We've detected that you're using the following packages which are known to be incompatible with Storybook 8:
+
+ ${incompatiblePackages.map((name) => `- ${chalk.cyan(`${name}`)}`).join('\n')}
+
+ Install the replacement for those packages: ${chalk.cyan('@storybook/test')}
+
+ And run the following codemod:
+ ${chalk.cyan(
+ 'npx storybook migrate migrate-to-test-package --glob="**/*.stories.@(js|jsx|ts|tsx)"'
+ )}
+ `;
+ },
+};
diff --git a/code/lib/cli/src/automigrate/fixes/vite-config-file.ts b/code/lib/cli/src/automigrate/fixes/vite-config-file.ts
new file mode 100644
index 000000000000..f8047a839af9
--- /dev/null
+++ b/code/lib/cli/src/automigrate/fixes/vite-config-file.ts
@@ -0,0 +1,111 @@
+import { dedent } from 'ts-dedent';
+import type { Fix } from '../types';
+import findUp from 'find-up';
+import { getFrameworkPackageName } from '../helpers/mainConfigFile';
+import { frameworkToRenderer } from '../../helpers';
+import { frameworkPackages } from '@storybook/core-common';
+
+interface Webpack5RunOptions {
+ plugins: string[];
+ existed: boolean;
+}
+
+export const viteConfigFile = {
+ id: 'viteConfigFile',
+
+ async check({ mainConfig, packageManager }) {
+ const viteConfigPath = await findUp([
+ 'vite.config.js',
+ 'vite.config.mjs',
+ 'vite.config.cjs',
+ 'vite.config.ts',
+ ]);
+
+ const rendererToVitePluginMap: Record = {
+ preact: '@preact/preset-vite',
+ qwik: 'vite-plugin-qwik',
+ react: '@vitejs/plugin-react',
+ solid: 'vite-plugin-solid',
+ svelte: '@sveltejs/vite-plugin-svelte',
+ sveltekit: '@sveltejs/kit/vite', // might be pointless?
+ vue: '@vitejs/plugin-vue',
+ };
+
+ const frameworkPackageName = getFrameworkPackageName(mainConfig);
+ if (!frameworkPackageName) {
+ return null;
+ }
+ const frameworkName = frameworkPackages[frameworkPackageName];
+ const isUsingViteBuilder =
+ mainConfig.core?.builder === 'vite' ||
+ frameworkPackageName?.includes('vite') ||
+ frameworkPackageName === 'qwik' ||
+ frameworkPackageName === 'solid' ||
+ frameworkPackageName === 'sveltekit';
+
+ const rendererName = frameworkToRenderer[frameworkName as keyof typeof frameworkToRenderer];
+
+ if (!viteConfigPath && isUsingViteBuilder) {
+ const plugins = [];
+
+ if (rendererToVitePluginMap[rendererName]) {
+ plugins.push(rendererToVitePluginMap[rendererName]);
+ }
+
+ return {
+ plugins,
+ existed: !!viteConfigPath,
+ };
+ }
+
+ const plugin = rendererToVitePluginMap[rendererName];
+
+ if (!plugin) {
+ return null;
+ }
+
+ const pluginVersion = await packageManager.getPackageVersion(plugin);
+
+ if (viteConfigPath && isUsingViteBuilder && !pluginVersion) {
+ const plugins = [];
+
+ if (plugin) {
+ plugins.push(plugin);
+ }
+
+ return {
+ plugins,
+ existed: !viteConfigPath,
+ };
+ }
+
+ return null;
+ },
+
+ prompt({ existed, plugins }) {
+ if (existed) {
+ return dedent`
+ Storybook 8.0.0 no longer ships with a Vite config build-in.
+ We've detected you do have a Vite config, but you may be missing the following plugins in it.
+
+ ${plugins.map((plugin) => ` - ${plugin}`).join('\n')}
+
+ If you do already have these plugins, you can ignore this message.
+
+ You can find more information on how to do this here:
+ https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#framework-specific-vite-plugins-have-to-be-explicitly-added
+
+ This change was necessary to support newer versions of Vite.
+ `;
+ }
+ return dedent`
+ Storybook 8.0.0 no longer ships with a Vite config build-in.
+ Please add a vite.config.js file to your project root.
+
+ You can find more information on how to do this here:
+ https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#framework-specific-vite-plugins-have-to-be-explicitly-added
+
+ This change was necessary to support newer versions of Vite.
+ `;
+ },
+} satisfies Fix;
diff --git a/code/lib/cli/src/automigrate/fixes/vite4.ts b/code/lib/cli/src/automigrate/fixes/vite4.ts
new file mode 100644
index 000000000000..d04c4abd10d7
--- /dev/null
+++ b/code/lib/cli/src/automigrate/fixes/vite4.ts
@@ -0,0 +1,43 @@
+import chalk from 'chalk';
+import { dedent } from 'ts-dedent';
+import semver from 'semver';
+import type { Fix } from '../types';
+
+const logger = console;
+
+interface Webpack5RunOptions {
+ viteVersion: string | null;
+}
+
+export const vite4 = {
+ id: 'vite4',
+
+ async check({ packageManager }) {
+ const viteVersion = await packageManager.getPackageVersion('vite');
+
+ if (!viteVersion || semver.gt(viteVersion, '4.0.0')) {
+ return null;
+ }
+
+ return { viteVersion };
+ },
+
+ prompt({ viteVersion: viteVersion }) {
+ const viteFormatted = chalk.cyan(`${viteVersion}`);
+
+ return dedent`
+ We've detected your version of Vite is outdated (${viteFormatted}).
+
+ Storybook 8.0.0 will require Vite 4.0.0 or later.
+ Do you want us to upgrade Vite for you?
+ `;
+ },
+
+ async run({ packageManager, dryRun }) {
+ const deps = [`vite`];
+ logger.info(`✅ Adding dependencies: ${deps}`);
+ if (!dryRun) {
+ await packageManager.addDependencies({ installAsDevDependencies: true }, deps);
+ }
+ },
+} satisfies Fix;
diff --git a/code/lib/cli/src/automigrate/index.ts b/code/lib/cli/src/automigrate/index.ts
index 3adeff5e0ead..19d4ee8922e9 100644
--- a/code/lib/cli/src/automigrate/index.ts
+++ b/code/lib/cli/src/automigrate/index.ts
@@ -3,20 +3,25 @@ import chalk from 'chalk';
import boxen from 'boxen';
import { createWriteStream, move, remove } from 'fs-extra';
import tempy from 'tempy';
-import dedent from 'ts-dedent';
import { join } from 'path';
import invariant from 'tiny-invariant';
import {
- getStorybookInfo,
- loadMainConfig,
- getCoercedStorybookVersion,
JsPackageManagerFactory,
+ type JsPackageManager,
+ getCoercedStorybookVersion,
+ getStorybookInfo,
} from '@storybook/core-common';
-import type { PackageManagerName } from '@storybook/core-common';
-import type { Fix, FixId, FixOptions, FixSummary } from './fixes';
-import { FixStatus, PreCheckFailure, allFixes } from './fixes';
+import type {
+ Fix,
+ FixId,
+ AutofixOptions,
+ FixSummary,
+ PreCheckFailure,
+ AutofixOptionsFromCLI,
+} from './fixes';
+import { FixStatus, allFixes } from './fixes';
import { cleanLog } from './helpers/cleanLog';
import { getMigrationSummary } from './helpers/getMigrationSummary';
import { getStorybookData } from './helpers/mainConfigFile';
@@ -52,18 +57,47 @@ const logAvailableMigrations = () => {
logger.info(`\nThe following migrations are available: ${availableFixes}`);
};
+export const doAutomigrate = async (options: AutofixOptionsFromCLI) => {
+ const packageManager = JsPackageManagerFactory.getPackageManager({
+ force: options.packageManager,
+ });
+
+ const [packageJson, storybookVersion] = await Promise.all([
+ packageManager.retrievePackageJson(),
+ getCoercedStorybookVersion(packageManager),
+ ]);
+
+ const { configDir: inferredConfigDir, mainConfig: mainConfigPath } = getStorybookInfo(
+ packageJson,
+ options.configDir
+ );
+ const configDir = options.configDir || inferredConfigDir || '.storybook';
+
+ if (!storybookVersion) {
+ throw new Error('Could not determine Storybook version');
+ }
+
+ if (!mainConfigPath) {
+ throw new Error('Could not determine main config path');
+ }
+
+ return automigrate({ ...options, packageManager, storybookVersion, mainConfigPath, configDir });
+};
+
export const automigrate = async ({
fixId,
fixes: inputFixes,
dryRun,
yes,
- packageManager: pkgMgr,
+ packageManager,
list,
- configDir: userSpecifiedConfigDir,
+ configDir,
+ mainConfigPath,
+ storybookVersion,
renderer: rendererPackage,
skipInstall,
hideMigrationSummary = false,
-}: FixOptions = {}): Promise<{
+}: AutofixOptions): Promise<{
fixResults: Record;
preCheckFailure?: PreCheckFailure;
} | null> => {
@@ -87,10 +121,12 @@ export const automigrate = async ({
const { fixResults, fixSummary, preCheckFailure } = await runFixes({
fixes,
- pkgMgr,
- userSpecifiedConfigDir,
+ packageManager,
rendererPackage,
skipInstall,
+ configDir,
+ mainConfigPath,
+ storybookVersion,
dryRun,
yes,
});
@@ -107,7 +143,6 @@ export const automigrate = async ({
}
if (!hideMigrationSummary) {
- const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr });
const installationMetadata = await packageManager.findInstallations([
'@storybook/*',
'storybook',
@@ -129,78 +164,30 @@ export async function runFixes({
fixes,
dryRun,
yes,
- pkgMgr,
- userSpecifiedConfigDir,
rendererPackage,
skipInstall,
+ configDir,
+ packageManager,
+ mainConfigPath,
+ storybookVersion,
}: {
fixes: Fix[];
yes?: boolean;
dryRun?: boolean;
- pkgMgr?: PackageManagerName;
- userSpecifiedConfigDir?: string;
rendererPackage?: string;
skipInstall?: boolean;
+ configDir: string;
+ packageManager: JsPackageManager;
+ mainConfigPath: string;
+ storybookVersion: string;
}): Promise<{
preCheckFailure?: PreCheckFailure;
fixResults: Record;
fixSummary: FixSummary;
}> {
- const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr });
-
const fixResults = {} as Record;
const fixSummary: FixSummary = { succeeded: [], failed: {}, manual: [], skipped: [] };
- const { configDir: inferredConfigDir, mainConfig: mainConfigPath } = getStorybookInfo(
- await packageManager.retrievePackageJson(),
- userSpecifiedConfigDir
- );
-
- const storybookVersion = await getCoercedStorybookVersion(packageManager);
-
- if (!storybookVersion) {
- logger.info(dedent`
- [Storybook automigrate] ❌ Unable to determine storybook version so the automigrations will be skipped.
- 🤔 Are you running automigrate from your project directory? Please specify your Storybook config directory with the --config-dir flag.
- `);
- return {
- fixResults,
- fixSummary,
- preCheckFailure: PreCheckFailure.UNDETECTED_SB_VERSION,
- };
- }
-
- const configDir = userSpecifiedConfigDir || inferredConfigDir || '.storybook';
- try {
- await loadMainConfig({ configDir });
- } catch (err) {
- const errMessage = String(err);
- if (errMessage.includes('No configuration files have been found')) {
- logger.info(
- dedent`[Storybook automigrate] Could not find or evaluate your Storybook main.js config directory at ${chalk.blue(
- configDir
- )} so the automigrations will be skipped. You might be running this command in a monorepo or a non-standard project structure. If that is the case, please rerun this command by specifying the path to your Storybook config directory via the --config-dir option.`
- );
- return {
- fixResults,
- fixSummary,
- preCheckFailure: PreCheckFailure.MAINJS_NOT_FOUND,
- };
- }
- logger.info(
- dedent`[Storybook automigrate] ❌ Failed trying to evaluate ${chalk.blue(
- mainConfigPath
- )} with the following error: ${errMessage}`
- );
- logger.info('Please fix the error and try again.');
-
- return {
- fixResults,
- fixSummary,
- preCheckFailure: PreCheckFailure.MAINJS_EVALUATION,
- };
- }
-
for (let i = 0; i < fixes.length; i += 1) {
const f = fixes[i] as Fix;
let result;
diff --git a/code/lib/cli/src/automigrate/types.ts b/code/lib/cli/src/automigrate/types.ts
index 0fa57c1fdfcc..97d20c09dc45 100644
--- a/code/lib/cli/src/automigrate/types.ts
+++ b/code/lib/cli/src/automigrate/types.ts
@@ -35,14 +35,19 @@ export enum PreCheckFailure {
MAINJS_EVALUATION = 'mainjs_evaluation_error',
}
-export interface FixOptions {
+export interface AutofixOptions extends Omit {
+ packageManager: JsPackageManager;
+ mainConfigPath: string;
+ storybookVersion: string;
+}
+export interface AutofixOptionsFromCLI {
fixId?: FixId;
list?: boolean;
fixes?: Fix[];
yes?: boolean;
- dryRun?: boolean;
packageManager?: PackageManagerName;
- configDir?: string;
+ dryRun?: boolean;
+ configDir: string;
renderer?: string;
skipInstall?: boolean;
hideMigrationSummary?: boolean;
diff --git a/code/lib/cli/src/generate.ts b/code/lib/cli/src/generate.ts
index 71ea9841fc64..250cd200d206 100644
--- a/code/lib/cli/src/generate.ts
+++ b/code/lib/cli/src/generate.ts
@@ -17,7 +17,7 @@ import { migrate } from './migrate';
import { upgrade, type UpgradeOptions } from './upgrade';
import { sandbox } from './sandbox';
import { link } from './link';
-import { automigrate } from './automigrate';
+import { doAutomigrate } from './automigrate';
import { dev } from './dev';
import { build } from './build';
import { doctor } from './doctor';
@@ -80,6 +80,7 @@ command('upgrade')
'Force package manager for installing dependencies'
)
.option('-y --yes', 'Skip prompting the user')
+ .option('-f --force', 'force the upgrade, skipping autoblockers')
.option('-n --dry-run', 'Only check for upgrades, do not install')
.option('-s --skip-check', 'Skip postinstall version and automigration checks')
.option('-c, --config-dir ', 'Directory where to load Storybook configurations from')
@@ -171,7 +172,7 @@ command('automigrate [fixId]')
'The renderer package for the framework Storybook is using.'
)
.action(async (fixId, options) => {
- await automigrate({ fixId, ...options }).catch((e) => {
+ await doAutomigrate({ fixId, ...options }).catch((e) => {
logger.error(e);
process.exit(1);
});
diff --git a/code/lib/cli/src/helpers.ts b/code/lib/cli/src/helpers.ts
index 95c22ee624f1..bbf81816a061 100644
--- a/code/lib/cli/src/helpers.ts
+++ b/code/lib/cli/src/helpers.ts
@@ -130,7 +130,7 @@ type CopyTemplateFilesOptions = {
destination?: string;
};
-const frameworkToRenderer: Record<
+export const frameworkToRenderer: Record<
SupportedFrameworks | SupportedRenderers,
SupportedRenderers | 'vue'
> = {
diff --git a/code/lib/cli/src/migrate.ts b/code/lib/cli/src/migrate.ts
index 5e0093d2480d..5e38507afd61 100644
--- a/code/lib/cli/src/migrate.ts
+++ b/code/lib/cli/src/migrate.ts
@@ -1,7 +1,11 @@
import { listCodemods, runCodemod } from '@storybook/codemod';
import { runFixes } from './automigrate';
import { bareMdxStoriesGlob } from './automigrate/fixes/bare-mdx-stories-glob';
-import { JsPackageManagerFactory } from '@storybook/core-common';
+import {
+ JsPackageManagerFactory,
+ getStorybookInfo,
+ getCoercedStorybookVersion,
+} from '@storybook/core-common';
import { getStorybookVersionSpecifier } from './helpers';
const logger = console;
@@ -11,7 +15,33 @@ export async function migrate(migration: any, { glob, dryRun, list, rename, pars
listCodemods().forEach((key: any) => logger.log(key));
} else if (migration) {
if (migration === 'mdx-to-csf' && !dryRun) {
- await runFixes({ fixes: [bareMdxStoriesGlob] });
+ const packageManager = JsPackageManagerFactory.getPackageManager();
+
+ const [packageJson, storybookVersion] = await Promise.all([
+ //
+ packageManager.retrievePackageJson(),
+ getCoercedStorybookVersion(packageManager),
+ ]);
+ const { configDir: inferredConfigDir, mainConfig: mainConfigPath } =
+ getStorybookInfo(packageJson);
+ const configDir = inferredConfigDir || '.storybook';
+
+ // GUARDS
+ if (!storybookVersion) {
+ throw new Error('Could not determine Storybook version');
+ }
+
+ if (!mainConfigPath) {
+ throw new Error('Could not determine main config path');
+ }
+
+ await runFixes({
+ fixes: [bareMdxStoriesGlob],
+ configDir,
+ mainConfigPath,
+ packageManager,
+ storybookVersion,
+ });
await addStorybookBlocksPackage();
}
await runCodemod(migration, { glob, dryRun, logger, rename, parser });
diff --git a/code/lib/cli/src/upgrade.ts b/code/lib/cli/src/upgrade.ts
index 1d13c5a7f0c2..e9f4a8151b1c 100644
--- a/code/lib/cli/src/upgrade.ts
+++ b/code/lib/cli/src/upgrade.ts
@@ -13,13 +13,16 @@ import dedent from 'ts-dedent';
import boxen from 'boxen';
import type { JsPackageManager, PackageManagerName } from '@storybook/core-common';
import {
- JsPackageManagerFactory,
isCorePackage,
versions,
- commandLog,
+ getStorybookInfo,
+ getCoercedStorybookVersion,
+ loadMainConfig,
+ JsPackageManagerFactory,
} from '@storybook/core-common';
-import { coerceSemver } from './helpers';
-import { automigrate } from './automigrate';
+import { automigrate } from './automigrate/index';
+import { autoblock } from './autoblock/index';
+import { PreCheckFailure } from './automigrate/types';
type Package = {
package: string;
@@ -108,28 +111,32 @@ export const checkVersionConsistency = () => {
export interface UpgradeOptions {
skipCheck: boolean;
- packageManager: PackageManagerName;
+ packageManager?: PackageManagerName;
dryRun: boolean;
yes: boolean;
+ force: boolean;
disableTelemetry: boolean;
configDir?: string;
}
export const doUpgrade = async ({
skipCheck,
- packageManager: pkgMgr,
+ packageManager: packageManagerName,
dryRun,
- configDir,
+ configDir: userSpecifiedConfigDir,
yes,
...options
}: UpgradeOptions) => {
- const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr });
+ const packageManager = JsPackageManagerFactory.getPackageManager({ force: packageManagerName });
// If we can't determine the existing version fallback to v0.0.0 to not block the upgrade
const beforeVersion = (await getInstalledStorybookVersion(packageManager)) ?? '0.0.0';
const currentVersion = versions['@storybook/cli'];
- const isCanary = currentVersion.startsWith('0.0.0');
+ const isCanary =
+ currentVersion.startsWith('0.0.0') ||
+ beforeVersion.startsWith('portal:') ||
+ beforeVersion.startsWith('workspace:');
if (!isCanary && lt(currentVersion, beforeVersion)) {
throw new UpgradeStorybookToLowerVersionError({ beforeVersion, currentVersion });
@@ -138,7 +145,13 @@ export const doUpgrade = async ({
throw new UpgradeStorybookToSameVersionError({ beforeVersion });
}
- const latestVersion = await packageManager.latestVersion('@storybook/cli');
+ const [latestVersion, packageJson, storybookVersion] = await Promise.all([
+ //
+ packageManager.latestVersion('@storybook/cli'),
+ packageManager.retrievePackageJson(),
+ getCoercedStorybookVersion(packageManager),
+ ]);
+
const isOutdated = lt(currentVersion, latestVersion);
const isPrerelease = prerelease(currentVersion) !== null;
@@ -168,36 +181,74 @@ export const doUpgrade = async ({
)
);
- const packageJson = await packageManager.retrievePackageJson();
-
- const toUpgradedDependencies = (deps: Record) => {
- const monorepoDependencies = Object.keys(deps || {}).filter((dependency) => {
- // don't upgrade @storybook/preset-create-react-app if react-scripts is < v5
- if (dependency === '@storybook/preset-create-react-app') {
- const reactScriptsVersion =
- packageJson.dependencies['react-scripts'] ?? packageJson.devDependencies['react-scripts'];
- if (reactScriptsVersion && lt(coerceSemver(reactScriptsVersion), '5.0.0')) {
- return false;
- }
- }
+ let results;
+
+ const { configDir: inferredConfigDir, mainConfig: mainConfigPath } = getStorybookInfo(
+ packageJson,
+ userSpecifiedConfigDir
+ );
+ const configDir = userSpecifiedConfigDir || inferredConfigDir || '.storybook';
+
+ let mainConfigLoadingError = '';
- // only upgrade packages that are in the monorepo
- return dependency in versions;
- }) as Array;
- return monorepoDependencies.map((dependency) => {
- /* add ^ modifier to the version if this is the latest stable or prerelease version
- example outputs: @storybook/react@^8.0.0 */
- const maybeCaret = (!isOutdated || isPrerelease) && !isCanary ? '^' : '';
- return `${dependency}@${maybeCaret}${versions[dependency]}`;
+ const mainConfig = await loadMainConfig({ configDir }).catch((err) => {
+ mainConfigLoadingError = String(err);
+ return false;
+ });
+
+ // GUARDS
+ if (!storybookVersion) {
+ logger.info(missingStorybookVersionMessage());
+ results = { preCheckFailure: PreCheckFailure.UNDETECTED_SB_VERSION };
+ } else if (
+ typeof mainConfigPath === 'undefined' ||
+ mainConfigLoadingError.includes('No configuration files have been found')
+ ) {
+ logger.info(mainjsNotFoundMessage(configDir));
+ results = { preCheckFailure: PreCheckFailure.MAINJS_NOT_FOUND };
+ } else if (typeof mainConfig === 'boolean') {
+ logger.info(mainjsExecutionFailureMessage(mainConfigPath, mainConfigLoadingError));
+ results = { preCheckFailure: PreCheckFailure.MAINJS_EVALUATION };
+ }
+
+ // BLOCKERS
+ if (
+ !results &&
+ typeof mainConfig !== 'boolean' &&
+ typeof mainConfigPath !== 'undefined' &&
+ !options.force
+ ) {
+ const blockResult = await autoblock({
+ packageManager,
+ configDir,
+ packageJson,
+ mainConfig,
+ mainConfigPath,
});
- };
+ if (blockResult) {
+ results = { preCheckFailure: blockResult };
+ }
+ }
+
+ // INSTALL UPDATED DEPENDENCIES
+ if (!dryRun && !results) {
+ const toUpgradedDependencies = (deps: Record) => {
+ const monorepoDependencies = Object.keys(deps || {}).filter((dependency) => {
+ // only upgrade packages that are in the monorepo
+ return dependency in versions;
+ }) as Array;
+ return monorepoDependencies.map((dependency) => {
+ /* add ^ modifier to the version if this is the latest stable or prerelease version
+ example outputs: @storybook/react@^8.0.0 */
+ const maybeCaret = (!isOutdated || isPrerelease) && !isCanary ? '^' : '';
+ return `${dependency}@${maybeCaret}${versions[dependency]}`;
+ });
+ };
- const upgradedDependencies = toUpgradedDependencies(packageJson.dependencies);
- const upgradedDevDependencies = toUpgradedDependencies(packageJson.devDependencies);
+ const upgradedDependencies = toUpgradedDependencies(packageJson.dependencies);
+ const upgradedDevDependencies = toUpgradedDependencies(packageJson.devDependencies);
- if (!dryRun) {
- commandLog(`Updating dependencies in ${chalk.cyan('package.json')}..`);
- logger.plain('');
+ logger.info(`Updating dependencies in ${chalk.cyan('package.json')}..`);
if (upgradedDependencies.length > 0) {
await packageManager.addDependencies(
{ installAsDevDependencies: false, skipInstall: true, packageJson },
@@ -213,26 +264,61 @@ export const doUpgrade = async ({
await packageManager.installDependencies();
}
- let automigrationResults;
- if (!skipCheck) {
+ // AUTOMIGRATIONS
+ if (!skipCheck && !results && mainConfigPath && storybookVersion) {
checkVersionConsistency();
- automigrationResults = await automigrate({ dryRun, yes, packageManager: pkgMgr, configDir });
+ results = await automigrate({
+ dryRun,
+ yes,
+ packageManager,
+ configDir,
+ mainConfigPath,
+ storybookVersion,
+ });
}
+
+ // TELEMETRY
if (!options.disableTelemetry) {
- const afterVersion = await getInstalledStorybookVersion(packageManager);
- const { preCheckFailure, fixResults } = automigrationResults || {};
+ const { preCheckFailure, fixResults } = results || {};
const automigrationTelemetry = {
automigrationResults: preCheckFailure ? null : fixResults,
automigrationPreCheckFailure: preCheckFailure || null,
};
- telemetry('upgrade', {
+
+ await telemetry('upgrade', {
beforeVersion,
- afterVersion,
+ afterVersion: currentVersion,
...automigrationTelemetry,
});
}
};
+function missingStorybookVersionMessage(): string {
+ return dedent`
+ [Storybook automigrate] ❌ Unable to determine Storybook version so that the automigrations will be skipped.
+ 🤔 Are you running automigrate from your project directory? Please specify your Storybook config directory with the --config-dir flag.
+ `;
+}
+
+function mainjsExecutionFailureMessage(
+ mainConfigPath: string,
+ mainConfigLoadingError: string
+): string {
+ return dedent`
+ [Storybook automigrate] ❌ Failed trying to evaluate ${chalk.blue(
+ mainConfigPath
+ )} with the following error: ${mainConfigLoadingError}
+
+ Please fix the error and try again.
+ `;
+}
+
+function mainjsNotFoundMessage(configDir: string): string {
+ return dedent`[Storybook automigrate] Could not find or evaluate your Storybook main.js config directory at ${chalk.blue(
+ configDir
+ )} so the automigrations will be skipped. You might be running this command in a monorepo or a non-standard project structure. If that is the case, please rerun this command by specifying the path to your Storybook config directory via the --config-dir option.`;
+}
+
export async function upgrade(options: UpgradeOptions): Promise {
await withTelemetry('upgrade', { cliOptions: options }, () => doUpgrade(options));
}
diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json
index 04fa46508190..259aae0056a3 100644
--- a/code/lib/codemod/package.json
+++ b/code/lib/codemod/package.json
@@ -31,6 +31,7 @@
"./dist/transforms/csf-hoist-story-annotations.js": "./dist/transforms/csf-hoist-story-annotations.js",
"./dist/transforms/move-builtin-addons.js": "./dist/transforms/move-builtin-addons.js",
"./dist/transforms/mdx-to-csf.js": "./dist/transforms/mdx-to-csf.js",
+ "./dist/transforms/migrate-to-test-package.js": "./dist/transforms/migrate-to-test-package.js",
"./dist/transforms/storiesof-to-csf.js": "./dist/transforms/storiesof-to-csf.js",
"./dist/transforms/update-addon-info.js": "./dist/transforms/update-addon-info.js",
"./dist/transforms/update-organisation-name.js": "./dist/transforms/update-organisation-name.js",
@@ -93,6 +94,7 @@
"./src/transforms/csf-2-to-3.ts",
"./src/transforms/csf-hoist-story-annotations.js",
"./src/transforms/mdx-to-csf.ts",
+ "./src/transforms/migrate-to-test-package.ts",
"./src/transforms/move-builtin-addons.js",
"./src/transforms/storiesof-to-csf.js",
"./src/transforms/update-addon-info.js",
diff --git a/code/lib/codemod/src/transforms/__tests__/migrate-to-test-package.test.ts b/code/lib/codemod/src/transforms/__tests__/migrate-to-test-package.test.ts
new file mode 100644
index 000000000000..e6acb4b60279
--- /dev/null
+++ b/code/lib/codemod/src/transforms/__tests__/migrate-to-test-package.test.ts
@@ -0,0 +1,46 @@
+import { expect, test } from 'vitest';
+import transform from '../migrate-to-test-package';
+import dedent from 'ts-dedent';
+
+expect.addSnapshotSerializer({
+ serialize: (val: any) => (typeof val === 'string' ? val : val.toString()),
+ test: () => true,
+});
+
+const tsTransform = async (source: string) =>
+ (await transform({ source, path: 'Component.stories.tsx' })).trim();
+
+test('replace jest and testing-library with the test package', async () => {
+ const input = dedent`
+ import { expect } from '@storybook/jest';
+ import { within, userEvent } from '@storybook/testing-library';
+ `;
+
+ expect(await tsTransform(input)).toMatchInlineSnapshot(`
+ import { expect } from '@storybook/test';
+ import { within, userEvent } from '@storybook/test';
+ `);
+});
+
+test('Make jest imports namespace imports', async () => {
+ const input = dedent`
+ import { expect, jest } from '@storybook/jest';
+ import { within, userEvent } from '@storybook/testing-library';
+
+ const onFocusMock = jest.fn();
+ const onSearchMock = jest.fn();
+
+ jest.spyOn(window, 'Something');
+ `;
+
+ expect(await tsTransform(input)).toMatchInlineSnapshot(`
+ import { expect } from '@storybook/test';
+ import * as test from '@storybook/test';
+ import { within, userEvent } from '@storybook/test';
+
+ const onFocusMock = test.fn();
+ const onSearchMock = test.fn();
+
+ test.spyOn(window, 'Something');
+ `);
+});
diff --git a/code/lib/codemod/src/transforms/csf-2-to-3.ts b/code/lib/codemod/src/transforms/csf-2-to-3.ts
index 38677f07f702..507c38999c88 100644
--- a/code/lib/codemod/src/transforms/csf-2-to-3.ts
+++ b/code/lib/codemod/src/transforms/csf-2-to-3.ts
@@ -204,16 +204,8 @@ export default async function transform(info: FileInfo, api: API, options: { par
let output = printCsf(csf).code;
try {
- const prettierConfig = (await prettier.resolveConfig(info.path)) ?? {
- printWidth: 100,
- tabWidth: 2,
- bracketSpacing: true,
- trailingComma: 'es5',
- singleQuote: true,
- };
-
output = await prettier.format(output, {
- ...prettierConfig,
+ ...(await prettier.resolveConfig(info.path)),
filepath: info.path,
});
} catch (e) {
diff --git a/code/lib/codemod/src/transforms/mdx-to-csf.ts b/code/lib/codemod/src/transforms/mdx-to-csf.ts
index 53e2162ab19b..9c657c822e04 100644
--- a/code/lib/codemod/src/transforms/mdx-to-csf.ts
+++ b/code/lib/codemod/src/transforms/mdx-to-csf.ts
@@ -291,17 +291,10 @@ export async function transform(info: FileInfo, baseName: string): Promise<[stri
const newMdx = mdxProcessor.stringify(root);
let output = recast.print(file.path.node).code;
- const prettierConfig = (await prettier.resolveConfig(`${info.path}.jsx`)) || {
- printWidth: 100,
- tabWidth: 2,
- bracketSpacing: true,
- trailingComma: 'es5',
- singleQuote: true,
- };
-
+ const path = `${info.path}.jsx`;
output = await prettier.format(output.trim(), {
- ...prettierConfig,
- filepath: `${info.path}.jsx`,
+ ...(await prettier.resolveConfig(path)),
+ filepath: path,
});
return [newMdx, output];
diff --git a/code/lib/codemod/src/transforms/migrate-to-test-package.ts b/code/lib/codemod/src/transforms/migrate-to-test-package.ts
new file mode 100644
index 000000000000..02545aae06af
--- /dev/null
+++ b/code/lib/codemod/src/transforms/migrate-to-test-package.ts
@@ -0,0 +1,63 @@
+/* eslint-disable no-underscore-dangle */
+import type { FileInfo } from 'jscodeshift';
+import { loadCsf, printCsf } from '@storybook/csf-tools';
+import type { BabelFile } from '@babel/core';
+import * as babel from '@babel/core';
+import * as t from '@babel/types';
+import prettier from 'prettier';
+
+export default async function transform(info: FileInfo) {
+ const csf = loadCsf(info.source, { makeTitle: (title) => title });
+ const fileNode = csf._ast;
+ // @ts-expect-error File is not yet exposed, see https://github.com/babel/babel/issues/11350#issuecomment-644118606
+ const file: BabelFile = new babel.File(
+ { filename: info.path },
+ { code: info.source, ast: fileNode }
+ );
+
+ file.path.traverse({
+ ImportDeclaration: (path) => {
+ if (
+ path.node.source.value === '@storybook/jest' ||
+ path.node.source.value === '@storybook/testing-library'
+ ) {
+ if (path.node.source.value === '@storybook/jest') {
+ path.get('specifiers').forEach((specifier) => {
+ if (specifier.isImportSpecifier()) {
+ const imported = specifier.get('imported');
+ if (!imported.isIdentifier()) return;
+ if (imported.node.name === 'jest') {
+ specifier.remove();
+ path.insertAfter(
+ t.importDeclaration(
+ [t.importNamespaceSpecifier(t.identifier('test'))],
+ t.stringLiteral('@storybook/test')
+ )
+ );
+ }
+ }
+ });
+ }
+ path.get('source').replaceWith(t.stringLiteral('@storybook/test'));
+ }
+ },
+ Identifier: (path) => {
+ if (path.node.name === 'jest') {
+ path.replaceWith(t.identifier('test'));
+ }
+ },
+ });
+
+ let output = printCsf(csf).code;
+ try {
+ output = await prettier.format(output, {
+ ...(await prettier.resolveConfig(info.path)),
+ filepath: info.path,
+ });
+ } catch (e) {
+ console.warn(`Failed applying prettier to ${info.path}.`);
+ }
+ return output;
+}
+
+export const parser = 'tsx';
diff --git a/code/lib/codemod/src/transforms/storiesof-to-csf.js b/code/lib/codemod/src/transforms/storiesof-to-csf.js
index 83fc7b058a20..993e9ff8c35b 100644
--- a/code/lib/codemod/src/transforms/storiesof-to-csf.js
+++ b/code/lib/codemod/src/transforms/storiesof-to-csf.js
@@ -265,14 +265,7 @@ export default async function transformer(file, api, options) {
let output = source;
try {
- const prettierConfig = (await prettier.resolveConfig(file.path)) || {
- printWidth: 100,
- tabWidth: 2,
- bracketSpacing: true,
- trailingComma: 'es5',
- singleQuote: true,
- };
-
+ const prettierConfig = await prettier.resolveConfig(file.path);
output = prettier.format(source, {
...prettierConfig,
parser: jscodeshiftToPrettierParser(options.parser),
diff --git a/code/lib/codemod/src/transforms/upgrade-deprecated-types.ts b/code/lib/codemod/src/transforms/upgrade-deprecated-types.ts
index fb4cce571064..5751d139ba97 100644
--- a/code/lib/codemod/src/transforms/upgrade-deprecated-types.ts
+++ b/code/lib/codemod/src/transforms/upgrade-deprecated-types.ts
@@ -36,15 +36,10 @@ export default async function transform(info: FileInfo, api: API, options: { par
let output = printCsf(csf).code;
try {
- const prettierConfig = (await prettier.resolveConfig(info.path)) || {
- printWidth: 100,
- tabWidth: 2,
- bracketSpacing: true,
- trailingComma: 'es5',
- singleQuote: true,
- };
-
- output = await prettier.format(output, { ...prettierConfig, filepath: info.path });
+ output = await prettier.format(output, {
+ ...(await prettier.resolveConfig(info.path)),
+ filepath: info.path,
+ });
} catch (e) {
logger.log(`Failed applying prettier to ${info.path}.`);
}
diff --git a/code/lib/core-common/src/js-package-manager/JsPackageManager.ts b/code/lib/core-common/src/js-package-manager/JsPackageManager.ts
index 923a06409968..8523d7224eda 100644
--- a/code/lib/core-common/src/js-package-manager/JsPackageManager.ts
+++ b/code/lib/core-common/src/js-package-manager/JsPackageManager.ts
@@ -8,7 +8,6 @@ import fs from 'fs';
import dedent from 'ts-dedent';
import { readFile, writeFile, readFileSync } from 'fs-extra';
import invariant from 'tiny-invariant';
-import { commandLog } from '../utils/log';
import type { PackageJson, PackageJsonWithDepsAndDevDeps } from './PackageJson';
import storybookPackagesVersions from '../versions';
import type { InstallationMetadata } from './types';
@@ -55,6 +54,9 @@ export abstract class JsPackageManager {
basePath?: string
): Promise;
+ /**
+ * Get the INSTALLED version of a package from the package.json file
+ */
async getPackageVersion(packageName: string, basePath = this.cwd): Promise {
const packageJSON = await this.getPackageJSON(packageName, basePath);
return packageJSON ? packageJSON.version ?? null : null;
@@ -128,21 +130,13 @@ export abstract class JsPackageManager {
* Install dependencies listed in `package.json`
*/
public async installDependencies() {
- let done = commandLog('Preparing to install dependencies');
- done();
-
- logger.log();
- logger.log();
-
- done = commandLog('Installing dependencies');
-
+ logger.log('Installing dependencies...');
logger.log();
try {
await this.runInstall();
- done();
} catch (e) {
- done('An error occurred while installing dependencies.');
+ logger.error('An error occurred while installing dependencies.');
throw new HandledError(e);
}
}
diff --git a/code/lib/core-common/src/utils/get-storybook-info.ts b/code/lib/core-common/src/utils/get-storybook-info.ts
index dd462a6b0370..acf5aae77b91 100644
--- a/code/lib/core-common/src/utils/get-storybook-info.ts
+++ b/code/lib/core-common/src/utils/get-storybook-info.ts
@@ -14,9 +14,15 @@ export const rendererPackages: Record = {
'@storybook/svelte': 'svelte',
'@storybook/preact': 'preact',
'@storybook/server': 'server',
+
// community (outside of monorepo)
'storybook-framework-qwik': 'qwik',
'storybook-solidjs': 'solid',
+
+ /**
+ * @deprecated This is deprecated.
+ */
+ '@storybook/vue': 'vue',
};
export const frameworkPackages: Record = {
diff --git a/code/lib/instrumenter/src/instrumenter.ts b/code/lib/instrumenter/src/instrumenter.ts
index 71713bc50a6a..3eeb6ea86ed8 100644
--- a/code/lib/instrumenter/src/instrumenter.ts
+++ b/code/lib/instrumenter/src/instrumenter.ts
@@ -432,7 +432,9 @@ export class Instrumenter {
return { __element__: { prefix, localName, id, classNames, innerText } };
}
if (typeof value === 'function') {
- return { __function__: { name: value.name } };
+ return {
+ __function__: { name: 'getMockName' in value ? value.getMockName() : value.name },
+ };
}
if (typeof value === 'symbol') {
return { __symbol__: { description: value.description } };
diff --git a/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts b/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts
index eae92f09aac8..2ad7f7500f5b 100644
--- a/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts
+++ b/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts
@@ -41,7 +41,7 @@ describe('composeStory', () => {
};
const composedStory = composeStory(Story, meta);
- await composedStory.play({ canvasElement: null });
+ await composedStory.play!({ canvasElement: null });
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({
args: {
@@ -52,16 +52,6 @@ describe('composeStory', () => {
);
});
- it('should throw when executing the play function but the story does not have one', async () => {
- const Story = () => {};
- Story.args = {
- primary: true,
- };
-
- const composedStory = composeStory(Story, meta);
- expect(composedStory.play({ canvasElement: null })).rejects.toThrow();
- });
-
it('should throw an error if Story is undefined', () => {
expect(() => {
// @ts-expect-error (invalid input)
diff --git a/code/lib/preview-api/src/modules/store/csf/portable-stories.ts b/code/lib/preview-api/src/modules/store/csf/portable-stories.ts
index 0ebf858e96e8..91e4cdc365e1 100644
--- a/code/lib/preview-api/src/modules/store/csf/portable-stories.ts
+++ b/code/lib/preview-api/src/modules/store/csf/portable-stories.ts
@@ -101,16 +101,13 @@ export function composeStory,
id: story.id,
- play: (async (extraContext: ComposedStoryPlayContext) => {
- if (story.playFunction === undefined) {
- throw new Error('The story does not have a play function. Make sure to add one.');
- }
-
- await story.playFunction({
- ...context,
- ...extraContext,
- });
- }) as unknown as ComposedStoryPlayFn>,
+ play: story.playFunction
+ ? ((async (extraContext: ComposedStoryPlayContext) =>
+ story.playFunction!({
+ ...context,
+ ...extraContext,
+ })) as unknown as ComposedStoryPlayFn>)
+ : undefined,
}
);
diff --git a/code/lib/test/package.json b/code/lib/test/package.json
index e50699b89c0b..1dd68eca5258 100644
--- a/code/lib/test/package.json
+++ b/code/lib/test/package.json
@@ -49,7 +49,7 @@
"@storybook/preview-api": "workspace:*",
"@testing-library/dom": "^9.3.1",
"@testing-library/jest-dom": "^6.4.0",
- "@testing-library/user-event": "14.3.0",
+ "@testing-library/user-event": "^14.5.2",
"@vitest/expect": "1.1.3",
"@vitest/spy": "^1.1.3",
"chai": "^4.3.7",
diff --git a/code/lib/types/src/modules/composedStory.ts b/code/lib/types/src/modules/composedStory.ts
index 379bcb728908..5ce61bc678e8 100644
--- a/code/lib/types/src/modules/composedStory.ts
+++ b/code/lib/types/src/modules/composedStory.ts
@@ -55,7 +55,7 @@ export type ComposedStoryFn<
TRenderer extends Renderer = Renderer,
TArgs = Args,
> = PartialArgsStoryFn & {
- play: ComposedStoryPlayFn;
+ play: ComposedStoryPlayFn | undefined;
args: TArgs;
id: StoryId;
storyName: string;
diff --git a/code/nx.json b/code/nx.json
index ad6c9a817fe7..b072caaa73ab 100644
--- a/code/nx.json
+++ b/code/nx.json
@@ -1,11 +1,5 @@
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
- "implicitDependencies": {
- "package.json": {
- "dependencies": "*",
- "devDependencies": "*"
- }
- },
"pluginsConfig": {
"@nrwl/js": {
"analyzeSourceFiles": false
@@ -47,10 +41,21 @@
"dependencies": true
}
],
- "outputs": ["{projectRoot}/dist"],
+ "outputs": [
+ "{projectRoot}/dist"
+ ],
"cache": true
}
},
"nxCloudAccessToken": "NGVmYTkxMmItYzY3OS00MjkxLTk1ZDktZDFmYTFmNmVlNGY4fHJlYWQ=",
- "parallel": 1
+ "namedInputs": {
+ "default": [
+ "{projectRoot}/**/*",
+ "sharedGlobals"
+ ],
+ "sharedGlobals": [],
+ "production": [
+ "default"
+ ]
+ }
}
diff --git a/code/package.json b/code/package.json
index ae2ef37fd85a..ec5bb2c3de6e 100644
--- a/code/package.json
+++ b/code/package.json
@@ -292,5 +292,6 @@
"Dependency Upgrades"
]
]
- }
+ },
+ "deferredNextVersion": "8.0.0-beta.3"
}
diff --git a/code/renderers/react/src/__test__/portable-stories.test.tsx b/code/renderers/react/src/__test__/portable-stories.test.tsx
index 221404d5ee42..afa0b70142e4 100644
--- a/code/renderers/react/src/__test__/portable-stories.test.tsx
+++ b/code/renderers/react/src/__test__/portable-stories.test.tsx
@@ -99,7 +99,7 @@ describe('CSF3', () => {
const { container } = render();
- await CSF3InputFieldFilled.play({ canvasElement: container });
+ await CSF3InputFieldFilled.play!({ canvasElement: container });
const input = screen.getByTestId('input') as HTMLInputElement;
expect(input.value).toEqual('Hello world!');
diff --git a/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts b/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts
index a56866fd6227..4c541e1c4536 100644
--- a/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts
+++ b/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts
@@ -86,7 +86,7 @@ describe('CSF3', () => {
const { container } = render(CSF3InputFieldFilled());
- await CSF3InputFieldFilled.play({ canvasElement: container as HTMLElement });
+ await CSF3InputFieldFilled.play!({ canvasElement: container as HTMLElement });
const input = screen.getByTestId('input') as HTMLInputElement;
expect(input.value).toEqual('Hello world!');
diff --git a/code/yarn.lock b/code/yarn.lock
index 755a51216c17..eb5c7d503c70 100644
--- a/code/yarn.lock
+++ b/code/yarn.lock
@@ -6647,7 +6647,7 @@ __metadata:
"@storybook/preview-api": "workspace:*"
"@testing-library/dom": "npm:^9.3.1"
"@testing-library/jest-dom": "npm:^6.4.0"
- "@testing-library/user-event": "npm:14.3.0"
+ "@testing-library/user-event": "npm:^14.5.2"
"@vitest/expect": "npm:1.1.3"
"@vitest/spy": "npm:^1.1.3"
chai: "npm:^4.3.7"
@@ -6973,21 +6973,21 @@ __metadata:
languageName: node
linkType: hard
-"@testing-library/user-event@npm:14.3.0":
- version: 14.3.0
- resolution: "@testing-library/user-event@npm:14.3.0"
+"@testing-library/user-event@npm:^14.4.0, @testing-library/user-event@npm:^14.4.3":
+ version: 14.5.1
+ resolution: "@testing-library/user-event@npm:14.5.1"
peerDependencies:
"@testing-library/dom": ">=7.21.4"
- checksum: 8a0e708709f2510287568dff668bc7d6f5c4e7e17407452b7aa0fcf74732dccf511c63fc76ac514d753cb1f0586c1def59ba7f5245a9523715d37a8f198745d3
+ checksum: 1e00d6ead23377885b906db6e46e259161a0efb4138f7527481d7435f3c8f65cb7e3eab2900e2ac1886fa6dd03416e773a3a60dea87a9a2086a7127dee315f6f
languageName: node
linkType: hard
-"@testing-library/user-event@npm:^14.4.0, @testing-library/user-event@npm:^14.4.3":
- version: 14.5.1
- resolution: "@testing-library/user-event@npm:14.5.1"
+"@testing-library/user-event@npm:^14.5.2":
+ version: 14.5.2
+ resolution: "@testing-library/user-event@npm:14.5.2"
peerDependencies:
"@testing-library/dom": ">=7.21.4"
- checksum: 1e00d6ead23377885b906db6e46e259161a0efb4138f7527481d7435f3c8f65cb7e3eab2900e2ac1886fa6dd03416e773a3a60dea87a9a2086a7127dee315f6f
+ checksum: 68a0c2aa28a3c8e6eb05cafee29705438d7d8a9427423ce5064d44f19c29e89b5636de46dd2f28620fb10abba75c67130185bbc3aa23ac1163a227a5f36641e1
languageName: node
linkType: hard
diff --git a/docs/api/doc-block-markdown.md b/docs/api/doc-block-markdown.md
index 8e05ca60d08a..e37ba196ee97 100644
--- a/docs/api/doc-block-markdown.md
+++ b/docs/api/doc-block-markdown.md
@@ -24,6 +24,8 @@ import { Button } from "@storybook/design-system";
```md
+
+
// DON'T do this, will error
import ReadMe from './README.md';
// DO this, will work
diff --git a/docs/versions/next.json b/docs/versions/next.json
index 615ec622d172..b54db583e0d0 100644
--- a/docs/versions/next.json
+++ b/docs/versions/next.json
@@ -1 +1 @@
-{"version":"8.0.0-beta.2","info":{"plain":"- Core: Fix boolean `true` args in URL getting ignored - [#25950](https://github.com/storybookjs/storybook/pull/25950), thanks [@JReinhold](https://github.com/JReinhold)!\n- Core: Move @types packages to dev deps in core-common - [#25387](https://github.com/storybookjs/storybook/pull/25387), thanks [@kyletsang](https://github.com/kyletsang)!\n- Maintenance: Rename testing-utils paths to portable-stories - [#25888](https://github.com/storybookjs/storybook/pull/25888), thanks [@yannbf](https://github.com/yannbf)!\n- Portable stories: Pass story context to the play function of a composed story - [#25943](https://github.com/storybookjs/storybook/pull/25943), thanks [@yannbf](https://github.com/yannbf)!\n- UI: Fix `display=true` warning in console - [#25951](https://github.com/storybookjs/storybook/pull/25951), thanks [@JReinhold](https://github.com/JReinhold)!\n- UI: Update deprecated Icons with the new @storybook/icons in addons - [#25822](https://github.com/storybookjs/storybook/pull/25822), thanks [@cdedreuille](https://github.com/cdedreuille)!\n- Vite: Add a `rollup-plugin-webpack-stats` to allow stats from preview builds - [#25923](https://github.com/storybookjs/storybook/pull/25923), thanks [@tmeasday](https://github.com/tmeasday)!"}}
+{"version":"8.0.0-beta.3","info":{"plain":"- Actions: Add spy to action for explicit actions - [#26033](https://github.com/storybookjs/storybook/pull/26033), thanks [@kasperpeulen](https://github.com/kasperpeulen)!\n- Addon Themes: Make type generic less strict - [#26042](https://github.com/storybookjs/storybook/pull/26042), thanks [@yannbf](https://github.com/yannbf)!\n- Addon-docs: Fix pnpm+Vite failing to build with `@storybook/theming` Rollup error - [#26024](https://github.com/storybookjs/storybook/pull/26024), thanks [@JReinhold](https://github.com/JReinhold)!\n- CLI: Refactor to add autoblockers - [#25934](https://github.com/storybookjs/storybook/pull/25934), thanks [@ndelangen](https://github.com/ndelangen)!\n- Codemod: Migrate to test package - [#25958](https://github.com/storybookjs/storybook/pull/25958), thanks [@kasperpeulen](https://github.com/kasperpeulen)!\n- Portable stories: Only provide a play function wrapper if it exists - [#25974](https://github.com/storybookjs/storybook/pull/25974), thanks [@yannbf](https://github.com/yannbf)!\n- Test: Bump user-event to 14.5.2 - [#25889](https://github.com/storybookjs/storybook/pull/25889), thanks [@kasperpeulen](https://github.com/kasperpeulen)!"}}