From 074d5daca1c94729455e05f7f7ebc9abdf21b80c Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Mon, 12 Feb 2024 15:06:05 +0100 Subject: [PATCH 1/7] Add remove argtypes-regex automigration --- code/lib/cli/package.json | 1 + code/lib/cli/src/automigrate/fixes/index.ts | 2 + .../fixes/remove-argtypes-regex.ts | 78 +++++++++++++++++++ code/lib/codemod/package.json | 2 + .../src/transforms/find-implicit-spies.ts | 50 ++++++++++++ code/yarn.lock | 1 + 6 files changed, 134 insertions(+) create mode 100644 code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts create mode 100644 code/lib/codemod/src/transforms/find-implicit-spies.ts diff --git a/code/lib/cli/package.json b/code/lib/cli/package.json index 2ea8e20d58b0..d691e0323f7f 100644 --- a/code/lib/cli/package.json +++ b/code/lib/cli/package.json @@ -56,6 +56,7 @@ "prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/bundle.ts" }, "dependencies": { + "@babel/core": "^7.23.0", "@babel/types": "^7.23.0", "@ndelangen/get-tarball": "^3.0.7", "@storybook/codemod": "workspace:*", diff --git a/code/lib/cli/src/automigrate/fixes/index.ts b/code/lib/cli/src/automigrate/fixes/index.ts index 27f8a80b3140..ff060341a7c8 100644 --- a/code/lib/cli/src/automigrate/fixes/index.ts +++ b/code/lib/cli/src/automigrate/fixes/index.ts @@ -20,6 +20,7 @@ import { wrapRequire } from './wrap-require'; import { reactDocgen } from './react-docgen'; import { removeReactDependency } from './prompt-remove-react'; import { storyshotsMigration } from './storyshots-migration'; +import { removeArgtypesRegex } from './remove-argtypes-regex'; export * from '../types'; @@ -34,6 +35,7 @@ export const allFixes: Fix[] = [ sbBinary, sbScripts, incompatibleAddons, + removeArgtypesRegex, removedGlobalClientAPIs, mdx1to2, mdxgfm, diff --git a/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts b/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts new file mode 100644 index 000000000000..5bdc46071881 --- /dev/null +++ b/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts @@ -0,0 +1,78 @@ +import type { Fix } from '../types'; +import * as fs from 'node:fs/promises'; +import * as babel from '@babel/core'; +import { BabelFile, NodePath } from '@babel/core'; +import { babelParse } from '@storybook/csf-tools'; +import dedent from 'ts-dedent'; +import chalk from 'chalk'; + +export const removeArgtypesRegex: Fix<{ argTypesRegex: NodePath; previewConfigPath: string }> = { + id: 'remove-argtypes-regex', + promptOnly: true, + async check({ previewConfigPath }) { + if (!previewConfigPath) return null; + + const previewFile = await fs.readFile(previewConfigPath, { encoding: 'utf-8' }); + + // @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: previewConfigPath }, + { code: previewFile, ast: babelParse(previewFile) } + ); + + let argTypesRegex; + + file.path.traverse({ + Identifier: (path) => { + if (path.node.name === 'argTypesRegex') { + argTypesRegex = path; + } + }, + }); + + return argTypesRegex ? { argTypesRegex, previewConfigPath } : null; + }, + prompt({ argTypesRegex, previewConfigPath }) { + const snippet = dedent` + import { fn } from '@storybook/test'; + export default { + args: { onClick: fn() }, // will log to the action panel when clicked + };`; + + // @ts-expect-error File is not yet exposed, see https://github.com/babel/babel/issues/11350#issuecomment-644118606 + const file: BabelFile = new babel.File( + { file: 'story.tsx' }, + { code: snippet, ast: babelParse(snippet) } + ); + + let formattedSnippet; + file.path.traverse({ + Identifier: (path) => { + if (path.node.name === 'fn') { + formattedSnippet = path.buildCodeFrameError(``).message; + } + }, + }); + + return dedent` + ${chalk.bold('Attention')}: We've detected that you're using argTypesRegex: + + ${argTypesRegex.buildCodeFrameError(`${previewConfigPath}`).message} + + In Storybook 8, we recommend removing this regex, and assigning explicit spies with the ${chalk.cyan( + 'fn' + )} function instead: + ${formattedSnippet} + + Even if you keep using the argTypesRegex, you will need to above pattern if you want to use spies in your play function. + Implicit spies (based on a combination of argTypesRegex and docgen) is not supported anymore in Storybook 8. + + Use the following command to check for spy usages in your play functions: + ${chalk.cyan( + 'npx storybook migrate find-implicit-spies --glob="**/*.stories.@(js|jsx|ts|tsx)"' + )} + + Make sure to assign an explicit ${chalk.cyan('fn')} to your args for those usages. + `; + }, +}; diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json index 04fa46508190..303ab36f49de 100644 --- a/code/lib/codemod/package.json +++ b/code/lib/codemod/package.json @@ -29,6 +29,7 @@ "./dist/transforms/add-component-parameters.js": "./dist/transforms/add-component-parameters.js", "./dist/transforms/csf-2-to-3.js": "./dist/transforms/csf-2-to-3.js", "./dist/transforms/csf-hoist-story-annotations.js": "./dist/transforms/csf-hoist-story-annotations.js", + "./dist/transforms/find-implicit-spies.js": "./dist/transforms/find-implicit-spies.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/storiesof-to-csf.js": "./dist/transforms/storiesof-to-csf.js", @@ -92,6 +93,7 @@ "./src/transforms/add-component-parameters.js", "./src/transforms/csf-2-to-3.ts", "./src/transforms/csf-hoist-story-annotations.js", + "./src/transforms/find-implicit-spies.ts", "./src/transforms/mdx-to-csf.ts", "./src/transforms/move-builtin-addons.js", "./src/transforms/storiesof-to-csf.js", diff --git a/code/lib/codemod/src/transforms/find-implicit-spies.ts b/code/lib/codemod/src/transforms/find-implicit-spies.ts new file mode 100644 index 000000000000..594bb1cfc1c8 --- /dev/null +++ b/code/lib/codemod/src/transforms/find-implicit-spies.ts @@ -0,0 +1,50 @@ +/* eslint-disable no-underscore-dangle */ +import type { FileInfo } from 'jscodeshift'; +import { loadCsf } from '@storybook/csf-tools'; +import type { BabelFile } from '@babel/core'; +import * as babel from '@babel/core'; + +function findImplicitSpies(path: babel.NodePath, file: string) { + path.traverse({ + Identifier: (identifier) => { + if (/^on[A-Z].*/.test(identifier.node.name)) { + console.warn(identifier.buildCodeFrameError(`${file} Possible implicit spy found`).message); + } + }, + }); +} + +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({ + // CSF2 play function Story.play = + AssignmentExpression: (path) => { + const left = path.get('left'); + if (!left.isMemberExpression()) return; + + const property = left.get('property'); + if (property.isIdentifier() && property.node.name === 'play') { + findImplicitSpies(path, info.path); + } + }, + + // CSF3 play function: const Story = {play: () => {} }; + ObjectProperty: (path) => { + const key = path.get('key'); + if (key.isIdentifier() && key.node.name === 'play') { + findImplicitSpies(path, info.path); + } + }, + }); + + return; +} + +export const parser = 'tsx'; diff --git a/code/yarn.lock b/code/yarn.lock index 755a51216c17..d8200e4a3cd4 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -5354,6 +5354,7 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/cli@workspace:lib/cli" dependencies: + "@babel/core": "npm:^7.23.0" "@babel/types": "npm:^7.23.0" "@ndelangen/get-tarball": "npm:^3.0.7" "@storybook/codemod": "workspace:*" From ce77d9f97e220af288d151e510f0383afb136522 Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Mon, 12 Feb 2024 15:27:28 +0100 Subject: [PATCH 2/7] Add a test --- .../__tests__/find-implicit-spies.test.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 code/lib/codemod/src/transforms/__tests__/find-implicit-spies.test.ts diff --git a/code/lib/codemod/src/transforms/__tests__/find-implicit-spies.test.ts b/code/lib/codemod/src/transforms/__tests__/find-implicit-spies.test.ts new file mode 100644 index 000000000000..0d4cbbe3cee6 --- /dev/null +++ b/code/lib/codemod/src/transforms/__tests__/find-implicit-spies.test.ts @@ -0,0 +1,43 @@ +import { beforeEach, expect, test, vi } from 'vitest'; +import transform from '../find-implicit-spies'; +import dedent from 'ts-dedent'; +import ansiRegex from 'ansi-regex'; + +expect.addSnapshotSerializer({ + print: (val, print) => print((val as string).replace(ansiRegex(), '')), + test: (value) => typeof value === 'string' && ansiRegex().test(value), +}); + +const tsTransform = async (source: string) => transform({ source, path: 'Component.stories.tsx' }); + +const warn = vi.spyOn(console, 'warn'); + +beforeEach(() => { + warn.mockImplementation(() => {}); +}); + +test('replace jest and testing-library with the test package', async () => { + const input = dedent` + Interactions.play = async ({ args }) => { + await userEvent.click(screen.getByRole("button")); + await expect(args.onButtonIconClick).toHaveBeenCalled(); + }; + + `; + + await tsTransform(input); + + expect(warn.mock.calls).toMatchInlineSnapshot(` + [ + [ + "Component.stories.tsx Possible implicit spy found + 1 | Interactions.play = async ({ args }) => { + 2 | await userEvent.click(screen.getByRole("button")); + > 3 | await expect(args.onButtonIconClick).toHaveBeenCalled(); + | ^^^^^^^^^^^^^^^^^ + 4 | }; + 5 |", + ], + ] + `); +}); From dcb87ef8fbb929fb4134bcc9dbf09d0cef6e9c4c Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Mon, 12 Feb 2024 15:43:47 +0100 Subject: [PATCH 3/7] Fix eslint --- code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts b/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts index 5bdc46071881..153d988630c9 100644 --- a/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts +++ b/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts @@ -1,7 +1,7 @@ import type { Fix } from '../types'; import * as fs from 'node:fs/promises'; import * as babel from '@babel/core'; -import { BabelFile, NodePath } from '@babel/core'; +import type { BabelFile, NodePath } from '@babel/core'; import { babelParse } from '@storybook/csf-tools'; import dedent from 'ts-dedent'; import chalk from 'chalk'; From 5b988d0f35072a4da18d9eec65db110a3b4a8b96 Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Fri, 16 Feb 2024 14:53:16 +0100 Subject: [PATCH 4/7] Don't warn if explicit arg or argType is found --- .../__tests__/find-implicit-spies.test.ts | 48 +++++-- .../src/transforms/find-implicit-spies.ts | 132 +++++++++++++++--- 2 files changed, 151 insertions(+), 29 deletions(-) diff --git a/code/lib/codemod/src/transforms/__tests__/find-implicit-spies.test.ts b/code/lib/codemod/src/transforms/__tests__/find-implicit-spies.test.ts index 0d4cbbe3cee6..c3885605e8ed 100644 --- a/code/lib/codemod/src/transforms/__tests__/find-implicit-spies.test.ts +++ b/code/lib/codemod/src/transforms/__tests__/find-implicit-spies.test.ts @@ -16,13 +16,30 @@ beforeEach(() => { warn.mockImplementation(() => {}); }); -test('replace jest and testing-library with the test package', async () => { +test('Warn for possible implicit actions', async () => { const input = dedent` - Interactions.play = async ({ args }) => { + export default { title: 'foo/bar', args: {onClick: fn() }, argTypes: { onHover: {action: true} } }; + const Template = (args) => { }; + export const A = Template.bind({}); + A.args = { onBla: fn() }; + A.play = async ({ args }) => { await userEvent.click(screen.getByRole("button")); - await expect(args.onButtonIconClick).toHaveBeenCalled(); + await expect(args.onImplicit).toHaveBeenCalled(); + await expect(args.onClick).toHaveBeenCalled(); + await expect(args.onHover).toHaveBeenCalled(); + await expect(args.onBla).toHaveBeenCalled(); + }; + + export const B = { + args: {onBla: fn() }, + play: async ({ args }) => { + await userEvent.click(screen.getByRole("button")); + await expect(args.onImplicit).toHaveBeenCalled(); + await expect(args.onClick).toHaveBeenCalled(); + await expect(args.onHover).toHaveBeenCalled(); + await expect(args.onBla).toHaveBeenCalled(); + } }; - `; await tsTransform(input); @@ -31,12 +48,23 @@ test('replace jest and testing-library with the test package', async () => { [ [ "Component.stories.tsx Possible implicit spy found - 1 | Interactions.play = async ({ args }) => { - 2 | await userEvent.click(screen.getByRole("button")); - > 3 | await expect(args.onButtonIconClick).toHaveBeenCalled(); - | ^^^^^^^^^^^^^^^^^ - 4 | }; - 5 |", + 5 | A.play = async ({ args }) => { + 6 | await userEvent.click(screen.getByRole("button")); + > 7 | await expect(args.onImplicit).toHaveBeenCalled(); + | ^^^^^^^^^^ + 8 | await expect(args.onClick).toHaveBeenCalled(); + 9 | await expect(args.onHover).toHaveBeenCalled(); + 10 | await expect(args.onBla).toHaveBeenCalled();", + ], + [ + "Component.stories.tsx Possible implicit spy found + 15 | play: async ({ args }) => { + 16 | await userEvent.click(screen.getByRole("button")); + > 17 | await expect(args.onImplicit).toHaveBeenCalled(); + | ^^^^^^^^^^ + 18 | await expect(args.onClick).toHaveBeenCalled(); + 19 | await expect(args.onHover).toHaveBeenCalled(); + 20 | await expect(args.onBla).toHaveBeenCalled();", ], ] `); diff --git a/code/lib/codemod/src/transforms/find-implicit-spies.ts b/code/lib/codemod/src/transforms/find-implicit-spies.ts index 594bb1cfc1c8..558cde383aa7 100644 --- a/code/lib/codemod/src/transforms/find-implicit-spies.ts +++ b/code/lib/codemod/src/transforms/find-implicit-spies.ts @@ -3,47 +3,141 @@ import type { FileInfo } from 'jscodeshift'; import { loadCsf } from '@storybook/csf-tools'; import type { BabelFile } from '@babel/core'; import * as babel from '@babel/core'; +import { isIdentifier, isObjectExpression, isObjectProperty } from '@babel/types'; -function findImplicitSpies(path: babel.NodePath, file: string) { +function findImplicitSpies(path: babel.NodePath, file: string, keys: string[]) { path.traverse({ Identifier: (identifier) => { - if (/^on[A-Z].*/.test(identifier.node.name)) { + if (!keys.includes(identifier.node.name) && /^on[A-Z].*/.test(identifier.node.name)) { console.warn(identifier.buildCodeFrameError(`${file} Possible implicit spy found`).message); } }, }); } -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 } - ); +function getAnnotationKeys(file: BabelFile, storyName: string, annotationName: string) { + const argKeys: string[] = []; file.path.traverse({ - // CSF2 play function Story.play = + // CSF2 play function Story.args = AssignmentExpression: (path) => { const left = path.get('left'); if (!left.isMemberExpression()) return; + const object = left.get('object'); + + if (!(object.isIdentifier() && object.node.name === storyName)) return; const property = left.get('property'); - if (property.isIdentifier() && property.node.name === 'play') { - findImplicitSpies(path, info.path); + const right = path.get('right'); + if ( + property.isIdentifier() && + property.node.name === annotationName && + right.isObjectExpression() + ) { + argKeys.push( + ...right.node.properties.flatMap((value) => + isObjectProperty(value) && isIdentifier(value.key) ? [value.key.name] : [] + ) + ); } }, + // CSF3 const Story = {args: () => {} }; + VariableDeclarator: (path) => { + const id = path.get('id'); + const init = path.get('init'); + if (!(id.isIdentifier() && id.node.name === storyName) || !init.isObjectExpression()) return; - // CSF3 play function: const Story = {play: () => {} }; - ObjectProperty: (path) => { - const key = path.get('key'); - if (key.isIdentifier() && key.node.name === 'play') { - findImplicitSpies(path, info.path); - } + const args = init + .get('properties') + .flatMap((it) => (it.isObjectProperty() ? [it] : [])) + .find((it) => { + const argKey = it.get('key'); + return argKey.isIdentifier() && argKey.node.name === annotationName; + }); + + if (!args) return; + const argsValue = args.get('value'); + + if (!argsValue || !argsValue.isObjectExpression()) return; + argKeys.push( + ...argsValue.node.properties.flatMap((value) => + isObjectProperty(value) && isIdentifier(value.key) ? [value.key.name] : [] + ) + ); }, }); + return argKeys; +} + +const getObjectExpressionKeys = (node: babel.Node | undefined) => { + return isObjectExpression(node) + ? node.properties.flatMap((value) => + isObjectProperty(value) && isIdentifier(value.key) ? [value.key.name] : [] + ) + : []; +}; + +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 } + ); + + csf.parse(); + + const metaKeys = [ + ...getObjectExpressionKeys(csf._metaAnnotations.args), + ...getObjectExpressionKeys(csf._metaAnnotations.argTypes), + ]; + + Object.entries(csf.stories).forEach(([key, { name }]) => { + if (!name) return; + const allKeys = [ + ...metaKeys, + ...getAnnotationKeys(file, name, 'args'), + ...getAnnotationKeys(file, name, 'argTypes'), + ]; + + file.path.traverse({ + // CSF2 play function Story.play = + AssignmentExpression: (path) => { + const left = path.get('left'); + if (!left.isMemberExpression()) return; + const object = left.get('object'); + + if (!(object.isIdentifier() && object.node.name === name)) return; + + const property = left.get('property'); + if (property.isIdentifier() && property.node.name === 'play') { + findImplicitSpies(path, info.path, allKeys); + } + }, + + // CSF3 play function: const Story = {play: () => {} }; + VariableDeclarator: (path) => { + const id = path.get('id'); + const init = path.get('init'); + if (!(id.isIdentifier() && id.node.name === name) || !init.isObjectExpression()) return; + + const play = init + .get('properties') + .flatMap((it) => (it.isObjectProperty() ? [it] : [])) + .find((it) => { + const argKey = it.get('key'); + return argKey.isIdentifier() && argKey.node.name === 'play'; + }); + + if (play) { + findImplicitSpies(play, info.path, allKeys); + } + }, + }); + }); + return; } From d47fe18e54b1bc8dbf227ddb5e856be9f5f79089 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Mon, 19 Feb 2024 12:28:27 +0100 Subject: [PATCH 5/7] Update promptType to 'manual' in removeArgtypesRegex fix --- code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts b/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts index 153d988630c9..6e5d26583453 100644 --- a/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts +++ b/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts @@ -8,7 +8,7 @@ import chalk from 'chalk'; export const removeArgtypesRegex: Fix<{ argTypesRegex: NodePath; previewConfigPath: string }> = { id: 'remove-argtypes-regex', - promptOnly: true, + promptType: 'manual', async check({ previewConfigPath }) { if (!previewConfigPath) return null; From 88cea6fc73e9556a7a2dbbd417574b82ca6423b3 Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Mon, 19 Feb 2024 14:36:46 +0100 Subject: [PATCH 6/7] Update code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts Co-authored-by: Valentin Palkovic --- code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts b/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts index 6e5d26583453..0402773df5ea 100644 --- a/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts +++ b/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts @@ -73,6 +73,8 @@ export const removeArgtypesRegex: Fix<{ argTypesRegex: NodePath; previewConfigPa )} Make sure to assign an explicit ${chalk.cyan('fn')} to your args for those usages. + + For more information please visit our migration guide: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#implicit-actions-can-not-be-used-during-rendering-for-example-in-the-play-function `; }, }; From e20a8c2c0e7fba8f86324d587ab48d34bc7661a6 Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Mon, 19 Feb 2024 16:19:49 +0100 Subject: [PATCH 7/7] Avoid long lines, as wrapping lines is buggy in boxen --- .../src/automigrate/fixes/remove-argtypes-regex.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts b/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts index 153d988630c9..273c0c648e78 100644 --- a/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts +++ b/code/lib/cli/src/automigrate/fixes/remove-argtypes-regex.ts @@ -59,13 +59,14 @@ export const removeArgtypesRegex: Fix<{ argTypesRegex: NodePath; previewConfigPa ${argTypesRegex.buildCodeFrameError(`${previewConfigPath}`).message} - In Storybook 8, we recommend removing this regex, and assigning explicit spies with the ${chalk.cyan( - 'fn' - )} function instead: + In Storybook 8, we recommend removing this regex. + Assign explicit spies with the ${chalk.cyan('fn')} function instead: ${formattedSnippet} - Even if you keep using the argTypesRegex, you will need to above pattern if you want to use spies in your play function. - Implicit spies (based on a combination of argTypesRegex and docgen) is not supported anymore in Storybook 8. + The above pattern is needed when using spies in the play function, ${chalk.bold( + 'even' + )} if you keep using argTypesRegex. + Implicit spies (based on a combination of argTypesRegex and docgen) is not supported in Storybook 8. Use the following command to check for spy usages in your play functions: ${chalk.cyan(