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)!"}}