From d7d669f6333c4de17b72c5000f314ffebaf1b81e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:42:12 +0000 Subject: [PATCH 01/16] Revamp Vite dep optimization: add preview annotations as entries, remove INCLUDE_CANDIDATES Co-authored-by: yannbf <1671563+yannbf@users.noreply.github.com> --- code/builders/builder-vite/src/constants.ts | 118 ------------- .../builders/builder-vite/src/optimizeDeps.ts | 30 ---- .../storybook-optimize-deps-plugin.test.ts | 157 ++++++++++++++++++ .../plugins/storybook-optimize-deps-plugin.ts | 31 +++- code/builders/builder-vite/src/vite-server.ts | 8 - 5 files changed, 180 insertions(+), 164 deletions(-) delete mode 100644 code/builders/builder-vite/src/constants.ts delete mode 100644 code/builders/builder-vite/src/optimizeDeps.ts create mode 100644 code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.test.ts diff --git a/code/builders/builder-vite/src/constants.ts b/code/builders/builder-vite/src/constants.ts deleted file mode 100644 index d376399b25e4..000000000000 --- a/code/builders/builder-vite/src/constants.ts +++ /dev/null @@ -1,118 +0,0 @@ -// It ensures that vite converts cjs deps into esm without vite having to find them during startup and then having to log a message about them and restart -// TODO: Many of the deps might be prebundled now though, so probably worth trying to remove and see what happens -export const INCLUDE_CANDIDATES = [ - '@ampproject/remapping', - '@base2/pretty-print-object', - '@emotion/core', - '@emotion/is-prop-valid', - '@emotion/styled', - '@jridgewell/sourcemap-codec', - 'acorn-jsx', - 'acorn-walk', - 'acorn', - 'airbnb-js-shims', - 'ansi-to-html', - 'aria-query', - 'axe-core', - 'axobject-query', - 'chromatic/isChromatic', - 'color-convert', - 'deep-object-diff', - 'doctrine', - 'emotion-theming', - 'escodegen', - 'estraverse', - 'fast-deep-equal', - 'html-tags', - 'isobject', - 'loader-utils', - 'lodash/camelCase.js', - 'lodash/camelCase', - 'lodash/cloneDeep.js', - 'lodash/cloneDeep', - 'lodash/countBy.js', - 'lodash/countBy', - 'lodash/debounce.js', - 'lodash/debounce', - 'lodash/isEqual.js', - 'lodash/isEqual', - 'lodash/isFunction.js', - 'lodash/isFunction', - 'lodash/isPlainObject.js', - 'lodash/isPlainObject', - 'lodash/isString.js', - 'lodash/isString', - 'lodash/kebabCase.js', - 'lodash/kebabCase', - 'lodash/mapKeys.js', - 'lodash/mapKeys', - 'lodash/mapValues.js', - 'lodash/mapValues', - 'lodash/merge.js', - 'lodash/merge', - 'lodash/mergeWith.js', - 'lodash/mergeWith', - 'lodash/pick.js', - 'lodash/pick', - 'lodash/pickBy.js', - 'lodash/pickBy', - 'lodash/startCase.js', - 'lodash/startCase', - 'lodash/throttle.js', - 'lodash/throttle', - 'lodash/uniq.js', - 'lodash/uniq', - 'lodash/upperFirst.js', - 'lodash/upperFirst', - 'memoizerific', - 'mockdate', - 'msw-storybook-addon', - 'overlayscrollbars', - 'polished', - 'prettier/parser-babel', - 'prettier/parser-flow', - 'prettier/parser-typescript', - 'prop-types', - 'qs', - 'react-dom', - 'react-dom/client', - 'react-dom/test-utils', - 'react-fast-compare', - 'react-is', - 'react-textarea-autosize', - 'react', - 'react/jsx-dev-runtime', - 'react/jsx-runtime', - 'refractor/core', - 'refractor/lang/bash.js', - 'refractor/lang/css.js', - 'refractor/lang/graphql.js', - 'refractor/lang/js-extras.js', - 'refractor/lang/json.js', - 'refractor/lang/jsx.js', - 'refractor/lang/markdown.js', - 'refractor/lang/markup.js', - 'refractor/lang/tsx.js', - 'refractor/lang/typescript.js', - 'refractor/lang/yaml.js', - 'regenerator-runtime/runtime.js', - 'sb-original/default-loader', - 'sb-original/image-context', - 'semver', // TODO: Remove once https://github.com/npm/node-semver/issues/712 is fixed - 'slash', - 'store2', - 'storybook/actions', - 'storybook/actions/decorator', - 'storybook/internal/core-events', - 'storybook/internal/csf', - 'storybook/internal/preview/runtime', - 'storybook/preview-api', - 'storybook/viewport', - 'synchronous-promise', - 'telejson', - 'ts-dedent', - 'unfetch', - 'util-deprecate', - 'vue', - 'warning', -]; diff --git a/code/builders/builder-vite/src/optimizeDeps.ts b/code/builders/builder-vite/src/optimizeDeps.ts deleted file mode 100644 index 09c8b51b581e..000000000000 --- a/code/builders/builder-vite/src/optimizeDeps.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { type UserConfig, type InlineConfig as ViteInlineConfig, resolveConfig } from 'vite'; - -import { INCLUDE_CANDIDATES } from './constants'; - -/** - * Helper function which allows us to `filter` with an async predicate. Uses Promise.all for - * performance. - */ -const asyncFilter = async (arr: string[], predicate: (val: string) => Promise) => - Promise.all(arr.map(predicate)).then((results) => arr.filter((_v, index) => results[index])); - -// TODO: This function should be reworked. The code it uses is outdated and we need to investigate -// More info: https://github.com/storybookjs/storybook/issues/32462#issuecomment-3421326557 -export async function getOptimizeDeps(config: ViteInlineConfig) { - // TODO: check if resolveConfig takes a lot of time, possible optimizations here - const resolvedConfig = await resolveConfig(config, 'serve', 'development'); - - // This function converts ids which might include ` > ` to a real path, if it exists on disk. - // See https://github.com/vitejs/vite/blob/67d164392e8e9081dc3f0338c4b4b8eea6c5f7da/packages/vite/src/node/optimizer/index.ts#L182-L199 - const resolve = resolvedConfig.createResolver({ asSrc: false }); - const include = await asyncFilter(INCLUDE_CANDIDATES, async (id) => Boolean(await resolve(id))); - - const optimizeDeps = { - // We need Vite to precompile these dependencies, because they contain non-ESM code that would break - // if we served it directly to the browser. - include: [...include, ...(config.optimizeDeps?.include || [])], - } satisfies UserConfig['optimizeDeps']; - - return optimizeDeps; -} diff --git a/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.test.ts b/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.test.ts new file mode 100644 index 000000000000..78bf925f6a5a --- /dev/null +++ b/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.test.ts @@ -0,0 +1,157 @@ +import { describe, expect, it, vi } from 'vitest'; + +import type { Options, Presets, StoryIndex } from 'storybook/internal/types'; + +import { storybookOptimizeDepsPlugin } from './storybook-optimize-deps-plugin'; + +vi.mock('storybook/internal/common', () => ({ + loadPreviewOrConfigFile: vi.fn(() => undefined), +})); + +const { loadPreviewOrConfigFile } = await import('storybook/internal/common'); +const loadPreviewOrConfigFileMock = vi.mocked(loadPreviewOrConfigFile); + +const storyIndex: StoryIndex = { + v: 5, + entries: { + 'story-one': { + id: 'story-one', + title: 'Story One', + name: 'Default', + importPath: './src/Story.stories.ts', + type: 'story', + subtype: 'story', + }, + 'story-two': { + id: 'story-two', + title: 'Story Two', + name: 'Default', + importPath: './src/Story.stories.ts', // duplicate, should be deduped + type: 'story', + subtype: 'story', + }, + 'story-three': { + id: 'story-three', + title: 'Story Three', + name: 'Default', + importPath: './src/Other.stories.ts', + type: 'story', + subtype: 'story', + }, + }, +}; + +const storyIndexGenerator = { getIndex: vi.fn(async () => storyIndex) }; + +function makeOptions(overrides: Partial> = {}): Options { + return { + configType: 'DEVELOPMENT', + configDir: '/project/.storybook', + packageJson: {}, + presets: { + apply: vi.fn(async (key: string) => { + if (key === 'storyIndexGenerator') return storyIndexGenerator; + if (key === 'optimizeViteDeps') return overrides.extraDeps ?? []; + if (key === 'previewAnnotations') return overrides.previewAnnotations ?? []; + return undefined; + }), + } as unknown as Presets, + presetsList: [], + }; +} + +describe('storybookOptimizeDepsPlugin', () => { + it('returns a plugin with the correct name', () => { + const plugin = storybookOptimizeDepsPlugin(makeOptions()); + expect(plugin.name).toBe('storybook:optimize-deps-plugin'); + }); + + it('returns undefined (no-op) for non-serve commands', async () => { + const plugin = storybookOptimizeDepsPlugin(makeOptions()); + const result = await (plugin.config as Function)({}, { command: 'build' }); + expect(result).toBeUndefined(); + }); + + it('adds story import paths as optimizeDeps entries', async () => { + const plugin = storybookOptimizeDepsPlugin(makeOptions()); + const result = await (plugin.config as Function)({}, { command: 'serve' }); + + expect(result.optimizeDeps.entries).toContain('./src/Story.stories.ts'); + expect(result.optimizeDeps.entries).toContain('./src/Other.stories.ts'); + }); + + it('deduplicates story import paths', async () => { + const plugin = storybookOptimizeDepsPlugin(makeOptions()); + const result = await (plugin.config as Function)({}, { command: 'serve' }); + + const storyEntries = result.optimizeDeps.entries.filter((e: string) => + e.endsWith('Story.stories.ts') + ); + expect(storyEntries).toHaveLength(1); + }); + + it('adds preview annotation files as optimizeDeps entries', async () => { + const previewAnnotations = ['/project/addon/preview.ts', '/project/.storybook/preview.ts']; + const plugin = storybookOptimizeDepsPlugin(makeOptions({ previewAnnotations })); + const result = await (plugin.config as Function)({}, { command: 'serve' }); + + expect(result.optimizeDeps.entries).toContain('/project/addon/preview.ts'); + expect(result.optimizeDeps.entries).toContain('/project/.storybook/preview.ts'); + }); + + it('adds the user preview config file as an entry when it exists', async () => { + loadPreviewOrConfigFileMock.mockReturnValueOnce('/project/.storybook/preview.ts' as any); + const plugin = storybookOptimizeDepsPlugin(makeOptions()); + const result = await (plugin.config as Function)({}, { command: 'serve' }); + + expect(result.optimizeDeps.entries).toContain('/project/.storybook/preview.ts'); + }); + + it('skips undefined preview config file (no user preview)', async () => { + loadPreviewOrConfigFileMock.mockReturnValueOnce(undefined as any); + const plugin = storybookOptimizeDepsPlugin(makeOptions()); + const result = await (plugin.config as Function)({}, { command: 'serve' }); + + expect(result.optimizeDeps.entries).not.toContain(undefined); + }); + + it('merges existing optimizeDeps entries from config', async () => { + const plugin = storybookOptimizeDepsPlugin(makeOptions()); + const result = await (plugin.config as Function)( + { optimizeDeps: { entries: ['/some/custom/entry.ts'] } }, + { command: 'serve' } + ); + + expect(result.optimizeDeps.entries).toContain('/some/custom/entry.ts'); + expect(result.optimizeDeps.entries).toContain('./src/Story.stories.ts'); + }); + + it('handles a string entries value in existing config', async () => { + const plugin = storybookOptimizeDepsPlugin(makeOptions()); + const result = await (plugin.config as Function)( + { optimizeDeps: { entries: '/some/single/entry.ts' } }, + { command: 'serve' } + ); + + expect(result.optimizeDeps.entries).toContain('/some/single/entry.ts'); + }); + + it('includes extra optimizeViteDeps from presets', async () => { + const extraDeps = ['some-cjs-package', 'another-package']; + const plugin = storybookOptimizeDepsPlugin(makeOptions({ extraDeps })); + const result = await (plugin.config as Function)({}, { command: 'serve' }); + + expect(result.optimizeDeps.include).toContain('some-cjs-package'); + expect(result.optimizeDeps.include).toContain('another-package'); + }); + + it('merges existing optimizeDeps include from config', async () => { + const plugin = storybookOptimizeDepsPlugin(makeOptions()); + const result = await (plugin.config as Function)( + { optimizeDeps: { include: ['existing-dep'] } }, + { command: 'serve' } + ); + + expect(result.optimizeDeps.include).toContain('existing-dep'); + }); +}); diff --git a/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts b/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts index ccbd5e4a9a4e..7bee90301998 100644 --- a/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts +++ b/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts @@ -1,8 +1,11 @@ +import { loadPreviewOrConfigFile } from 'storybook/internal/common'; import type { StoryIndexGenerator } from 'storybook/internal/core-server'; -import type { Options, StoryIndex } from 'storybook/internal/types'; +import type { Options, PreviewAnnotation, StoryIndex } from 'storybook/internal/types'; +import { resolve } from 'pathe'; import { type Plugin } from 'vite'; +import { processPreviewAnnotation } from '../utils/process-preview-annotation'; import { getUniqueImportPaths } from '../utils/unique-import-paths'; /** A Vite plugin that configures dependency optimization for Storybook's dev server. */ @@ -15,25 +18,37 @@ export function storybookOptimizeDepsPlugin(options: Options): Plugin { return; } - const [extraOptimizeDeps, storyIndexGenerator] = await Promise.all([ - options.presets.apply('optimizeViteDeps', []), + const projectRoot = resolve(options.configDir, '..'); + + const [extraOptimizeDeps, storyIndexGenerator, previewAnnotations] = await Promise.all([ + options.presets.apply('optimizeViteDeps', []), options.presets.apply('storyIndexGenerator'), + options.presets.apply('previewAnnotations', [], options), ]); const index: StoryIndex = await storyIndexGenerator.getIndex(); + // Include the user's preview file and all addon/framework/renderer preview annotations + // as optimizer entries so Vite can discover all transitive CJS dependencies automatically. + const previewOrConfigFile = loadPreviewOrConfigFile({ configDir: options.configDir }); + const previewAnnotationEntries = [...previewAnnotations, previewOrConfigFile] + .filter((path): path is PreviewAnnotation => path !== undefined) + .map((path) => processPreviewAnnotation(path, projectRoot)); + return { optimizeDeps: { - // Story file paths as entry points for the optimizer + // Story files + preview annotation files as entry points for the dep optimizer. + // Vite will crawl these to discover all transitive CJS dependencies that need + // pre-bundling, removing the need for a hard-coded include list. entries: [ ...(typeof config.optimizeDeps?.entries === 'string' ? [config.optimizeDeps.entries] - : []), + : (config.optimizeDeps?.entries ?? [])), ...getUniqueImportPaths(index), + ...previewAnnotationEntries, ], - // Known CJS dependencies that need to be pre-compiled to ESM, - // plus any extra deps from Storybook presets. - include: [...extraOptimizeDeps, ...(config.optimizeDeps?.include || [])], + // Extra deps explicitly included by Storybook presets (e.g. framework-specific packages). + include: [...extraOptimizeDeps, ...(config.optimizeDeps?.include ?? [])], }, }; }, diff --git a/code/builders/builder-vite/src/vite-server.ts b/code/builders/builder-vite/src/vite-server.ts index 9c694e94e410..a3a671c4c226 100644 --- a/code/builders/builder-vite/src/vite-server.ts +++ b/code/builders/builder-vite/src/vite-server.ts @@ -6,7 +6,6 @@ import { dedent } from 'ts-dedent'; import type { InlineConfig, ServerOptions } from 'vite'; import { createViteLogger } from './logger'; -import { getOptimizeDeps } from './optimizeDeps'; import { commonConfig } from './vite-config'; export async function createViteServer(options: Options, devServer: Server) { @@ -14,15 +13,8 @@ export async function createViteServer(options: Options, devServer: Server) { const commonCfg = await commonConfig(options, 'development'); - const optimizeDeps = await getOptimizeDeps(commonCfg); - const config: InlineConfig & { server: ServerOptions } = { ...commonCfg, - // Set up dev server - optimizeDeps: { - ...commonCfg.optimizeDeps, - include: [...(commonCfg.optimizeDeps?.include || []), ...optimizeDeps.include], - }, server: { middlewareMode: true, hmr: { From 9537a3ee63ce94aae5aab5d4197b7781d8d7c39c Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Thu, 19 Feb 2026 17:24:36 +0100 Subject: [PATCH 02/16] Optimize deps for react and builder-vite --- code/builders/builder-vite/src/preset.ts | 2 ++ code/renderers/react/src/preset.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/code/builders/builder-vite/src/preset.ts b/code/builders/builder-vite/src/preset.ts index 0f33305a8c20..cb4a772559aa 100644 --- a/code/builders/builder-vite/src/preset.ts +++ b/code/builders/builder-vite/src/preset.ts @@ -10,6 +10,8 @@ import { storybookSanitizeEnvs } from './plugins/storybook-runtime-plugin'; import { viteInjectMockerRuntime } from './plugins/vite-inject-mocker/plugin'; import { viteMockPlugin } from './plugins/vite-mock/plugin'; +export const optimizeViteDeps: string[] = ['storybook/internal/preview/runtime']; + /** * Preset that provides the core Storybook Vite plugins shared between `@storybook/builder-vite` and * `@storybook/addon-vitest`. diff --git a/code/renderers/react/src/preset.ts b/code/renderers/react/src/preset.ts index 9e295c6c1056..aadcb550a8fa 100644 --- a/code/renderers/react/src/preset.ts +++ b/code/renderers/react/src/preset.ts @@ -135,3 +135,5 @@ export async function internal_getArgTypesData( return argTypesData; } + +export const optimizeViteDeps: string[] = ['react-dom/test-utils']; From 13849a6be865e1ad77789fcde371d00f57176c8b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:54:39 +0000 Subject: [PATCH 03/16] Add regression tests for virtual module imports and dynamic import optimizeDeps coverage Co-authored-by: yannbf <1671563+yannbf@users.noreply.github.com> --- .../src/codegen-modern-iframe-script.test.ts | 54 +++++++++++++++++++ code/renderers/react/src/preset.test.ts | 13 +++++ 2 files changed, 67 insertions(+) create mode 100644 code/renderers/react/src/preset.test.ts diff --git a/code/builders/builder-vite/src/codegen-modern-iframe-script.test.ts b/code/builders/builder-vite/src/codegen-modern-iframe-script.test.ts index af2dce770d19..f79e6ac4f230 100644 --- a/code/builders/builder-vite/src/codegen-modern-iframe-script.test.ts +++ b/code/builders/builder-vite/src/codegen-modern-iframe-script.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest'; +import { generateAddonSetupCode } from './codegen-set-addon-channel'; import { generateModernIframeScriptCodeFromPreviews } from './codegen-modern-iframe-script'; +import { optimizeViteDeps } from './preset'; describe('generateModernIframeScriptCodeFromPreviews', () => { it('handle one annotation', async () => { @@ -131,3 +133,55 @@ describe('generateModernIframeScriptCodeFromPreviews', () => { `); }); }); + +/** + * Extract bare package import specifiers from a block of generated JavaScript/TypeScript code. + * Captures both `import ... from 'pkg'` and `import 'pkg'` forms, excluding: + * - relative paths (start with `.`) + * - virtual module IDs (start with `virtual:`) + * - absolute paths (start with `/`) + */ +function extractPackageImports(code: string): string[] { + const importRegex = /import\s+(?:[^'"]*\s+from\s+)?['"]([^'"]+)['"]/g; + const specifiers = new Set(); + for (const match of code.matchAll(importRegex)) { + const specifier = match[1]; + if (!specifier.startsWith('.') && !specifier.startsWith('virtual:') && !specifier.startsWith('/')) { + specifiers.add(specifier); + } + } + return [...specifiers]; +} + +describe('optimizeDeps coverage for virtual module imports', () => { + it('every package imported in virtual module code is either in optimizeViteDeps or known to be discovered via entry crawling', async () => { + // Collect all code generated for virtual modules — Vite's dep scanner never sees + // the contents of virtual modules, so any package imported there must be + // pre-bundled explicitly via optimizeViteDeps. + const iframeCode = await generateModernIframeScriptCodeFromPreviews({ frameworkName: 'test' }); + const addonCode = await generateAddonSetupCode(); + const allVirtualModuleCode = [iframeCode, addonCode].join('\n'); + + const packageImports = extractPackageImports(allVirtualModuleCode); + + // These packages are also imported in real source files (preview annotations, renderer + // previews, addon previews) that ARE added as optimizeDeps entries, so Vite discovers + // them via entry crawling. No explicit optimizeViteDeps entry is required. + const discoveredViaEntries = new Set([ + 'storybook/preview-api', // Imported in many renderer/addon preview files + 'storybook/internal/channels', // Imported in addon preview files + ]); + + const notCovered = packageImports.filter( + (pkg) => !discoveredViaEntries.has(pkg) && !optimizeViteDeps.includes(pkg) + ); + + expect( + notCovered, + `The following packages are imported in virtual module code but are NOT covered.\n` + + `They must be added to optimizeViteDeps in builder-vite/src/preset.ts, OR added to\n` + + `the discoveredViaEntries set in this test if they appear in real source entry files.\n` + + `Uncovered: ${notCovered.join(', ')}` + ).toHaveLength(0); + }); +}); diff --git a/code/renderers/react/src/preset.test.ts b/code/renderers/react/src/preset.test.ts new file mode 100644 index 000000000000..b496f641ca03 --- /dev/null +++ b/code/renderers/react/src/preset.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; + +import { optimizeViteDeps } from './preset'; + +describe('optimizeViteDeps', () => { + it('includes react-dom/test-utils', () => { + // react-dom/test-utils is dynamically imported via `await import('react-dom/test-utils')` + // in act-compat.ts. Vite's static analyzer does not pre-bundle dynamic imports, + // so it must be listed explicitly in optimizeViteDeps. + // If this test fails, add 'react-dom/test-utils' back to the optimizeViteDeps array in preset.ts. + expect(optimizeViteDeps).toContain('react-dom/test-utils'); + }); +}); From 0e11b0e36018e24d8678017dca1c231c3fd32f1b Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Fri, 20 Feb 2026 09:38:25 +0100 Subject: [PATCH 04/16] Revert --- .../components/test-fn.stories.tsx | 177 ------------------ .../cli-storybook/src/sandbox-templates.ts | 88 ++------- .../renderers/react/template/stories/csf4.mdx | 8 - .../react/template/stories/csf4.stories.tsx | 13 -- .../template/stories/test-fn.stories.tsx | 92 --------- scripts/tasks/sandbox-parts.ts | 31 ++- 6 files changed, 28 insertions(+), 381 deletions(-) delete mode 100644 code/core/src/component-testing/components/test-fn.stories.tsx delete mode 100644 code/renderers/react/template/stories/csf4.mdx delete mode 100644 code/renderers/react/template/stories/csf4.stories.tsx delete mode 100644 code/renderers/react/template/stories/test-fn.stories.tsx diff --git a/code/core/src/component-testing/components/test-fn.stories.tsx b/code/core/src/component-testing/components/test-fn.stories.tsx deleted file mode 100644 index 6b926a961913..000000000000 --- a/code/core/src/component-testing/components/test-fn.stories.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import React from 'react'; - -import type { StoryContext } from '@storybook/react-vite'; - -import { expect, fn } from 'storybook/test'; - -import preview from '../../../../.storybook/preview'; - -const Button = (args: React.ComponentProps<'button'>) =>