diff --git a/code/addons/vitest/src/preset.ts b/code/addons/vitest/src/preset.ts index c95d534cd96b..fa08fb128d19 100644 --- a/code/addons/vitest/src/preset.ts +++ b/code/addons/vitest/src/preset.ts @@ -1,4 +1,3 @@ -import { readFileSync } from 'node:fs'; import { mkdir } from 'node:fs/promises'; import type { Channel } from 'storybook/internal/channels'; diff --git a/code/addons/vitest/src/vitest-plugin/index.ts b/code/addons/vitest/src/vitest-plugin/index.ts index b07edb2bfd00..f472b46cc97d 100644 --- a/code/addons/vitest/src/vitest-plugin/index.ts +++ b/code/addons/vitest/src/vitest-plugin/index.ts @@ -30,8 +30,9 @@ import path from 'pathe'; import picocolors from 'picocolors'; import sirv from 'sirv'; import { dedent } from 'ts-dedent'; +import type { PluginOption } from 'vite'; -// ! Relative import to prebundle it without needing to depend on the Vite builder +// Shared plugins from builder-vite (relative import to prebundle without adding a package dependency) import { withoutVitePlugins } from '../../../../builders/builder-vite/src/utils/without-vite-plugins'; import type { InternalOptions, UserOptions } from './types'; @@ -194,27 +195,25 @@ export const storybookTest = async (options?: UserOptions): Promise => const stories = await presets.apply('stories', []); - // We can probably add more config here. See code/builders/builder-vite/src/vite-config.ts - // This one is specifically needed for code/builders/builder-vite/src/preset.ts const commonConfig = { root: resolve(finalOptions.configDir, '..') }; const [ + corePlugins, { storiesGlobs }, framework, viteConfigFromStorybook, staticDirs, previewLevelTags, core, - extraOptimizeDeps, features, ] = await Promise.all([ + presets.apply('viteCorePlugins', []), getStoryGlobsAndFiles(presets, directories), presets.apply('framework', undefined), presets.apply<{ plugins?: Plugin[]; root: string }>('viteFinal', commonConfig), presets.apply('staticDirs', []), extractTagsFromPreview(finalOptions.configDir), presets.apply('core'), - presets.apply('optimizeViteDeps', []), presets.apply('features', {}), ]); @@ -230,7 +229,10 @@ export const storybookTest = async (options?: UserOptions): Promise => } // filter out plugins that we know are unnecesary for tests, eg. docgen plugins - const plugins = await withoutVitePlugins(viteConfigFromStorybook.plugins ?? [], pluginsToIgnore); + const plugins: Plugin[] = [ + ...(corePlugins as Plugin[]), + ...(await withoutVitePlugins(viteConfigFromStorybook.plugins ?? [], pluginsToIgnore)), + ]; if (finalOptions.disableAddonDocs) { plugins.push(mdxStubPlugin); @@ -382,26 +384,8 @@ export const storybookTest = async (options?: UserOptions): Promise => }, }, - envPrefix: Array.from( - new Set([...(nonMutableInputConfig.envPrefix || []), 'STORYBOOK_', 'VITE_']) - ), - - resolve: { - conditions: [ - 'storybook', - 'stories', - 'test', - // copying straight from https://github.com/vitejs/vite/blob/main/packages/vite/src/node/constants.ts#L60 - // to avoid having to maintain Vite as a dependency just for this - 'module', - 'browser', - 'development|production', - ], - }, - optimizeDeps: { include: [ - ...extraOptimizeDeps, '@storybook/addon-vitest/internal/setup-file', '@storybook/addon-vitest/internal/global-setup', '@storybook/addon-vitest/internal/test-utils', @@ -419,11 +403,7 @@ export const storybookTest = async (options?: UserOptions): Promise => }, }; - // Merge config from storybook with the plugin config - const config: Omit = mergeConfig( - baseConfig, - viteConfigFromStorybook - ); + const config = mergeConfig(baseConfig, viteConfigFromStorybook); // alert the user of problems if ((nonMutableInputConfig.test?.include?.length ?? 0) > 0) { diff --git a/code/builders/builder-vite/src/build.ts b/code/builders/builder-vite/src/build.ts index f25f14857b61..1f419e038f09 100644 --- a/code/builders/builder-vite/src/build.ts +++ b/code/builders/builder-vite/src/build.ts @@ -4,7 +4,6 @@ import type { Options } from 'storybook/internal/types'; import { dedent } from 'ts-dedent'; import type { InlineConfig } from 'vite'; -import { sanitizeEnvVars } from './envs'; import { createViteLogger } from './logger'; import type { WebpackStatsPlugin } from './plugins'; import { hasVitePlugins } from './utils/has-vite-plugins'; @@ -90,7 +89,7 @@ export async function build(options: Options) { finalConfig.customLogger ??= await createViteLogger(); - await viteBuild(await sanitizeEnvVars(options, finalConfig)); + await viteBuild(finalConfig); const statsPlugin = findPlugin( finalConfig, 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 bac19386fcf9..af2dce770d19 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 @@ -2,15 +2,10 @@ import { describe, expect, it } from 'vitest'; import { generateModernIframeScriptCodeFromPreviews } from './codegen-modern-iframe-script'; -const projectRoot = 'projectRoot'; - describe('generateModernIframeScriptCodeFromPreviews', () => { it('handle one annotation', async () => { const result = await generateModernIframeScriptCodeFromPreviews({ - previewAnnotations: ['/user/.storybook/preview'], - projectRoot, frameworkName: 'frameworkName', - isCsf4: false, }); expect(result).toMatchInlineSnapshot(` "import { setup } from 'storybook/internal/preview/runtime'; @@ -19,18 +14,10 @@ describe('generateModernIframeScriptCodeFromPreviews', () => { setup(); - import { composeConfigs, PreviewWeb } from 'storybook/preview-api'; - import { isPreview } from 'storybook/internal/csf'; + import { PreviewWeb } from 'storybook/preview-api'; import { importFn } from 'virtual:/@storybook/builder-vite/storybook-stories.js'; - - import * as preview_2408 from "/user/.storybook/preview"; - const getProjectAnnotations = (hmrPreviewAnnotationModules = []) => { - const configs = [ - hmrPreviewAnnotationModules[0] ?? preview_2408 - ] - return composeConfigs(configs); - } - + import { getProjectAnnotations } from 'virtual:/@storybook/builder-vite/project-annotations.js'; + window.__STORYBOOK_PREVIEW__ = window.__STORYBOOK_PREVIEW__ || new PreviewWeb(importFn, getProjectAnnotations); window.__STORYBOOK_STORY_STORE__ = window.__STORYBOOK_STORY_STORE__ || window.__STORYBOOK_PREVIEW__.storyStore; @@ -44,21 +31,13 @@ describe('generateModernIframeScriptCodeFromPreviews', () => { // importFn has changed so we need to patch the new one in window.__STORYBOOK_PREVIEW__.onStoriesChanged({ importFn: newModule.importFn }); }); - - import.meta.hot.accept(["/user/.storybook/preview"], (previewAnnotationModules) => { - // getProjectAnnotations has changed so we need to patch the new one in - window.__STORYBOOK_PREVIEW__.onGetProjectAnnotationsChanged({ getProjectAnnotations: () => getProjectAnnotations(previewAnnotationModules) }); - }); };" `); }); it('handle one annotation CSF4', async () => { const result = await generateModernIframeScriptCodeFromPreviews({ - previewAnnotations: ['/user/.storybook/preview'], - projectRoot, frameworkName: 'frameworkName', - isCsf4: true, }); expect(result).toMatchInlineSnapshot(` "import { setup } from 'storybook/internal/preview/runtime'; @@ -67,16 +46,10 @@ describe('generateModernIframeScriptCodeFromPreviews', () => { setup(); - import { composeConfigs, PreviewWeb } from 'storybook/preview-api'; - import { isPreview } from 'storybook/internal/csf'; + import { PreviewWeb } from 'storybook/preview-api'; import { importFn } from 'virtual:/@storybook/builder-vite/storybook-stories.js'; - - import * as preview_2408 from "/user/.storybook/preview"; - const getProjectAnnotations = (hmrPreviewAnnotationModules = []) => { - const preview = hmrPreviewAnnotationModules[0] ?? preview_2408; - return preview.default.composed; - } - + import { getProjectAnnotations } from 'virtual:/@storybook/builder-vite/project-annotations.js'; + window.__STORYBOOK_PREVIEW__ = window.__STORYBOOK_PREVIEW__ || new PreviewWeb(importFn, getProjectAnnotations); window.__STORYBOOK_STORY_STORE__ = window.__STORYBOOK_STORY_STORE__ || window.__STORYBOOK_PREVIEW__.storyStore; @@ -90,21 +63,13 @@ describe('generateModernIframeScriptCodeFromPreviews', () => { // importFn has changed so we need to patch the new one in window.__STORYBOOK_PREVIEW__.onStoriesChanged({ importFn: newModule.importFn }); }); - - import.meta.hot.accept(["/user/.storybook/preview"], (previewAnnotationModules) => { - // getProjectAnnotations has changed so we need to patch the new one in - window.__STORYBOOK_PREVIEW__.onGetProjectAnnotationsChanged({ getProjectAnnotations: () => getProjectAnnotations(previewAnnotationModules) }); - }); };" `); }); it('handle multiple annotations', async () => { const result = await generateModernIframeScriptCodeFromPreviews({ - previewAnnotations: ['/user/previewAnnotations1', '/user/.storybook/preview'], - projectRoot, frameworkName: 'frameworkName', - isCsf4: false, }); expect(result).toMatchInlineSnapshot(` "import { setup } from 'storybook/internal/preview/runtime'; @@ -113,20 +78,10 @@ describe('generateModernIframeScriptCodeFromPreviews', () => { setup(); - import { composeConfigs, PreviewWeb } from 'storybook/preview-api'; - import { isPreview } from 'storybook/internal/csf'; + import { PreviewWeb } from 'storybook/preview-api'; import { importFn } from 'virtual:/@storybook/builder-vite/storybook-stories.js'; - - import * as previewAnnotations1_2526 from "/user/previewAnnotations1"; - import * as preview_2408 from "/user/.storybook/preview"; - const getProjectAnnotations = (hmrPreviewAnnotationModules = []) => { - const configs = [ - hmrPreviewAnnotationModules[0] ?? previewAnnotations1_2526, - hmrPreviewAnnotationModules[1] ?? preview_2408 - ] - return composeConfigs(configs); - } - + import { getProjectAnnotations } from 'virtual:/@storybook/builder-vite/project-annotations.js'; + window.__STORYBOOK_PREVIEW__ = window.__STORYBOOK_PREVIEW__ || new PreviewWeb(importFn, getProjectAnnotations); window.__STORYBOOK_STORY_STORE__ = window.__STORYBOOK_STORY_STORE__ || window.__STORYBOOK_PREVIEW__.storyStore; @@ -140,21 +95,13 @@ describe('generateModernIframeScriptCodeFromPreviews', () => { // importFn has changed so we need to patch the new one in window.__STORYBOOK_PREVIEW__.onStoriesChanged({ importFn: newModule.importFn }); }); - - import.meta.hot.accept(["/user/previewAnnotations1","/user/.storybook/preview"], (previewAnnotationModules) => { - // getProjectAnnotations has changed so we need to patch the new one in - window.__STORYBOOK_PREVIEW__.onGetProjectAnnotationsChanged({ getProjectAnnotations: () => getProjectAnnotations(previewAnnotationModules) }); - }); };" `); }); it('handle multiple annotations CSF4', async () => { const result = await generateModernIframeScriptCodeFromPreviews({ - previewAnnotations: ['/user/previewAnnotations1', '/user/.storybook/preview'], - projectRoot, frameworkName: 'frameworkName', - isCsf4: true, }); expect(result).toMatchInlineSnapshot(` "import { setup } from 'storybook/internal/preview/runtime'; @@ -163,16 +110,10 @@ describe('generateModernIframeScriptCodeFromPreviews', () => { setup(); - import { composeConfigs, PreviewWeb } from 'storybook/preview-api'; - import { isPreview } from 'storybook/internal/csf'; + import { PreviewWeb } from 'storybook/preview-api'; import { importFn } from 'virtual:/@storybook/builder-vite/storybook-stories.js'; - - import * as preview_2408 from "/user/.storybook/preview"; - const getProjectAnnotations = (hmrPreviewAnnotationModules = []) => { - const preview = hmrPreviewAnnotationModules[0] ?? preview_2408; - return preview.default.composed; - } - + import { getProjectAnnotations } from 'virtual:/@storybook/builder-vite/project-annotations.js'; + window.__STORYBOOK_PREVIEW__ = window.__STORYBOOK_PREVIEW__ || new PreviewWeb(importFn, getProjectAnnotations); window.__STORYBOOK_STORY_STORE__ = window.__STORYBOOK_STORY_STORE__ || window.__STORYBOOK_PREVIEW__.storyStore; @@ -186,11 +127,6 @@ describe('generateModernIframeScriptCodeFromPreviews', () => { // importFn has changed so we need to patch the new one in window.__STORYBOOK_PREVIEW__.onStoriesChanged({ importFn: newModule.importFn }); }); - - import.meta.hot.accept(["/user/.storybook/preview"], (previewAnnotationModules) => { - // getProjectAnnotations has changed so we need to patch the new one in - window.__STORYBOOK_PREVIEW__.onGetProjectAnnotationsChanged({ getProjectAnnotations: () => getProjectAnnotations(previewAnnotationModules) }); - }); };" `); }); diff --git a/code/builders/builder-vite/src/codegen-modern-iframe-script.ts b/code/builders/builder-vite/src/codegen-modern-iframe-script.ts index e063c3504c64..e8a615db5b59 100644 --- a/code/builders/builder-vite/src/codegen-modern-iframe-script.ts +++ b/code/builders/builder-vite/src/codegen-modern-iframe-script.ts @@ -1,84 +1,24 @@ -import { getFrameworkName, loadPreviewOrConfigFile } from 'storybook/internal/common'; +import { getFrameworkName } from 'storybook/internal/common'; import { STORY_HOT_UPDATED } from 'storybook/internal/core-events'; -import { isCsfFactoryPreview, readConfig } from 'storybook/internal/csf-tools'; -import type { Options, PreviewAnnotation } from 'storybook/internal/types'; +import type { Options } from 'storybook/internal/types'; -import { genArrayFromRaw, genImport, genSafeVariableName } from 'knitwork'; -import { filename } from 'pathe/utils'; import { dedent } from 'ts-dedent'; -import { processPreviewAnnotation } from './utils/process-preview-annotation'; +import { VIRTUAL_ID as PROJECT_ANNOTATIONS_VIRTUAL_ID } from './plugins/storybook-project-annotations-plugin'; import { SB_VIRTUAL_FILES } from './virtual-file-names'; -export async function generateModernIframeScriptCode(options: Options, projectRoot: string) { - const { presets, configDir } = options; +export async function generateModernIframeScriptCode(options: Options) { const frameworkName = await getFrameworkName(options); - const previewOrConfigFile = loadPreviewOrConfigFile({ configDir }); - const previewConfig = previewOrConfigFile ? await readConfig(previewOrConfigFile) : undefined; - const isCsf4 = previewConfig ? isCsfFactoryPreview(previewConfig) : false; - - const previewAnnotations = await presets.apply( - 'previewAnnotations', - [], - options - ); return generateModernIframeScriptCodeFromPreviews({ - previewAnnotations: [...previewAnnotations, previewOrConfigFile], - projectRoot, frameworkName, - isCsf4, }); } export async function generateModernIframeScriptCodeFromPreviews(options: { - previewAnnotations: (PreviewAnnotation | undefined)[]; - projectRoot: string; frameworkName: string; - isCsf4: boolean; }) { - const { projectRoot, frameworkName } = options; - const previewAnnotationURLs = options.previewAnnotations - .filter((path) => path !== undefined) - .map((path) => processPreviewAnnotation(path, projectRoot)); - - const variables: string[] = []; - const imports: string[] = []; - for (const previewAnnotation of previewAnnotationURLs) { - const variable = - genSafeVariableName(filename(previewAnnotation)).replace(/_(45|46|47)/g, '_') + - '_' + - hash(previewAnnotation); - variables.push(variable); - imports.push(genImport(previewAnnotation, { name: '*', as: variable })); - } - - const previewFileURL = previewAnnotationURLs[previewAnnotationURLs.length - 1]; - const previewFileVariable = variables[variables.length - 1]; - const previewFileImport = imports[imports.length - 1]; - - // This is pulled out to a variable because it is reused in both the initial page load - // and the HMR handler. - // The `hmrPreviewAnnotationModules` parameter is used to pass the updated modules from HMR. - // However, only the changed modules are provided, the rest are null. - const getPreviewAnnotationsFunction = options.isCsf4 - ? dedent` - const getProjectAnnotations = (hmrPreviewAnnotationModules = []) => { - const preview = hmrPreviewAnnotationModules[0] ?? ${previewFileVariable}; - return preview.default.composed; - }` - : dedent` - const getProjectAnnotations = (hmrPreviewAnnotationModules = []) => { - const configs = ${genArrayFromRaw( - variables.map( - (previewAnnotation, index) => - // Prefer the updated module from an HMR update, otherwise the original module - `hmrPreviewAnnotationModules[${index}] ?? ${previewAnnotation}` - ), - ' ' - )} - return composeConfigs(configs); - }`; + const { frameworkName } = options; const generateHMRHandler = (): string => { // Web components are not compatible with HMR, so disable HMR, reload page instead. @@ -99,11 +39,6 @@ export async function generateModernIframeScriptCodeFromPreviews(options: { // importFn has changed so we need to patch the new one in window.__STORYBOOK_PREVIEW__.onStoriesChanged({ importFn: newModule.importFn }); }); - - import.meta.hot.accept(${JSON.stringify(options.isCsf4 ? [previewFileURL] : previewAnnotationURLs)}, (previewAnnotationModules) => { - // getProjectAnnotations has changed so we need to patch the new one in - window.__STORYBOOK_PREVIEW__.onGetProjectAnnotationsChanged({ getProjectAnnotations: () => getProjectAnnotations(previewAnnotationModules) }); - }); }`.trim(); }; @@ -122,13 +57,10 @@ export async function generateModernIframeScriptCodeFromPreviews(options: { setup(); - import { composeConfigs, PreviewWeb } from 'storybook/preview-api'; - import { isPreview } from 'storybook/internal/csf'; + import { PreviewWeb } from 'storybook/preview-api'; import { importFn } from '${SB_VIRTUAL_FILES.VIRTUAL_STORIES_FILE}'; - - ${options.isCsf4 ? previewFileImport : imports.join('\n')} - ${getPreviewAnnotationsFunction} - + import { getProjectAnnotations } from '${PROJECT_ANNOTATIONS_VIRTUAL_ID}'; + window.__STORYBOOK_PREVIEW__ = window.__STORYBOOK_PREVIEW__ || new PreviewWeb(importFn, getProjectAnnotations); window.__STORYBOOK_STORY_STORE__ = window.__STORYBOOK_STORY_STORE__ || window.__STORYBOOK_PREVIEW__.storyStore; @@ -138,6 +70,3 @@ export async function generateModernIframeScriptCodeFromPreviews(options: { `.trim(); return code; } -function hash(value: string) { - return value.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); -} diff --git a/code/builders/builder-vite/src/codegen-project-annotations.ts b/code/builders/builder-vite/src/codegen-project-annotations.ts new file mode 100644 index 000000000000..9d13c9390ec9 --- /dev/null +++ b/code/builders/builder-vite/src/codegen-project-annotations.ts @@ -0,0 +1,110 @@ +import { getFrameworkName, loadPreviewOrConfigFile } from 'storybook/internal/common'; +import { isCsfFactoryPreview, readConfig } from 'storybook/internal/csf-tools'; +import type { Options, PreviewAnnotation } from 'storybook/internal/types'; + +import { genArrayFromRaw, genImport, genSafeVariableName } from 'knitwork'; +import { filename } from 'pathe/utils'; +import { dedent } from 'ts-dedent'; + +import { processPreviewAnnotation } from './utils/process-preview-annotation'; + +/** Generates the code for the `PROJECT_ANNOTATIONS_FILE` virtual module. */ +export async function generateProjectAnnotationsCode(options: Options, projectRoot: string) { + const { presets, configDir } = options; + const frameworkName = await getFrameworkName(options); + + const previewOrConfigFile = loadPreviewOrConfigFile({ configDir }); + const previewConfig = previewOrConfigFile ? await readConfig(previewOrConfigFile) : undefined; + const isCsf4 = previewConfig ? isCsfFactoryPreview(previewConfig) : false; + + const previewAnnotations = await presets.apply( + 'previewAnnotations', + [], + options + ); + + return generateProjectAnnotationsCodeFromPreviews({ + previewAnnotations: [...previewAnnotations, previewOrConfigFile], + projectRoot, + frameworkName, + isCsf4, + }); +} + +export function generateProjectAnnotationsCodeFromPreviews(options: { + previewAnnotations: (PreviewAnnotation | undefined)[]; + projectRoot: string; + frameworkName: string; + isCsf4: boolean; +}) { + const { projectRoot } = options; + const previewAnnotationURLs = options.previewAnnotations + .filter((path) => path !== undefined) + .map((path) => processPreviewAnnotation(path, projectRoot)); + + const variables: string[] = []; + const imports: string[] = []; + for (const previewAnnotation of previewAnnotationURLs) { + const variable = + genSafeVariableName(filename(previewAnnotation)).replace(/_(45|46|47)/g, '_') + + '_' + + hash(previewAnnotation); + variables.push(variable); + imports.push(genImport(previewAnnotation, { name: '*', as: variable })); + } + + const previewFileURL = previewAnnotationURLs[previewAnnotationURLs.length - 1]; + const previewFileVariable = variables[variables.length - 1]; + const previewFileImport = imports[imports.length - 1]; + + if (options.isCsf4) { + return dedent` + ${previewFileImport} + + export function getProjectAnnotations(hmrPreviewAnnotationModules = []) { + const preview = hmrPreviewAnnotationModules[0] ?? ${previewFileVariable}; + return preview.default.composed; + } + + if (import.meta.hot) { + import.meta.hot.accept([${JSON.stringify(previewFileURL)}], (previewAnnotationModules) => { + // getProjectAnnotations has changed so we need to patch the new one in + window?.__STORYBOOK_PREVIEW__?.onGetProjectAnnotationsChanged({ + getProjectAnnotations: () => getProjectAnnotations(previewAnnotationModules), + }); + }); + } + `.trim(); + } + + return dedent` + import { composeConfigs } from 'storybook/preview-api'; + + ${imports.join('\n')} + + export function getProjectAnnotations(hmrPreviewAnnotationModules = []) { + const configs = ${genArrayFromRaw( + variables.map( + (previewAnnotation, index) => + // Prefer the updated module from an HMR update, otherwise the original module + `hmrPreviewAnnotationModules[${index}] ?? ${previewAnnotation}` + ), + ' ' + )}; + return composeConfigs(configs); + } + + if (import.meta.hot) { + import.meta.hot.accept(${JSON.stringify(previewAnnotationURLs)}, (previewAnnotationModules) => { + // getProjectAnnotations has changed so we need to patch the new one in + window?.__STORYBOOK_PREVIEW__?.onGetProjectAnnotationsChanged({ + getProjectAnnotations: () => getProjectAnnotations(previewAnnotationModules), + }); + }); + } + `.trim(); +} + +function hash(value: string) { + return value.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); +} diff --git a/code/builders/builder-vite/src/envs.ts b/code/builders/builder-vite/src/envs.ts index 41716949b81b..bbf0aecef781 100644 --- a/code/builders/builder-vite/src/envs.ts +++ b/code/builders/builder-vite/src/envs.ts @@ -1,5 +1,5 @@ import { stringifyEnvs } from 'storybook/internal/common'; -import type { Builder_EnvsRaw, Options } from 'storybook/internal/types'; +import type { Builder_EnvsRaw } from 'storybook/internal/types'; import type { UserConfig as ViteConfig } from 'vite'; @@ -39,22 +39,3 @@ export function stringifyProcessEnvs(raw: Builder_EnvsRaw, envPrefix: ViteConfig return envs; } - -// Sanitize environment variables if needed -export async function sanitizeEnvVars(options: Options, config: ViteConfig) { - const { presets } = options; - const envsRaw = await presets.apply>('env'); - let { define } = config; - if (Object.keys(envsRaw).length) { - // Stringify env variables after getting `envPrefix` from the config - const envs = stringifyProcessEnvs(envsRaw, config.envPrefix); - define = { - ...define, - ...envs, - }; - } - return { - ...config, - define, - } as ViteConfig; -} diff --git a/code/builders/builder-vite/src/optimizeDeps.ts b/code/builders/builder-vite/src/optimizeDeps.ts index 61e5c1ed5b56..09c8b51b581e 100644 --- a/code/builders/builder-vite/src/optimizeDeps.ts +++ b/code/builders/builder-vite/src/optimizeDeps.ts @@ -1,10 +1,6 @@ -import type { StoryIndexGenerator } from 'storybook/internal/core-server'; -import type { Options, StoryIndex } from 'storybook/internal/types'; - import { type UserConfig, type InlineConfig as ViteInlineConfig, resolveConfig } from 'vite'; import { INCLUDE_CANDIDATES } from './constants'; -import { getUniqueImportPaths } from './utils/unique-import-paths'; /** * Helper function which allows us to `filter` with an async predicate. Uses Promise.all for @@ -15,14 +11,7 @@ const asyncFilter = async (arr: string[], predicate: (val: string) => Promise('storyIndexGenerator'), - ]); - - const index: StoryIndex = await storyIndexGenerator.getIndex(); - +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'); @@ -31,13 +20,11 @@ export async function getOptimizeDeps(config: ViteInlineConfig, options: Options const resolve = resolvedConfig.createResolver({ asSrc: false }); const include = await asyncFilter(INCLUDE_CANDIDATES, async (id) => Boolean(await resolve(id))); - const optimizeDeps: UserConfig['optimizeDeps'] = { - ...config.optimizeDeps, - entries: getUniqueImportPaths(index), + 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, ...extraOptimizeDeps, ...(config.optimizeDeps?.include || [])], - }; + include: [...include, ...(config.optimizeDeps?.include || [])], + } satisfies UserConfig['optimizeDeps']; return optimizeDeps; } diff --git a/code/builders/builder-vite/src/plugins/code-generator-plugin.ts b/code/builders/builder-vite/src/plugins/code-generator-plugin.ts index 2af433d0c90b..50a163e9ff9c 100644 --- a/code/builders/builder-vite/src/plugins/code-generator-plugin.ts +++ b/code/builders/builder-vite/src/plugins/code-generator-plugin.ts @@ -17,10 +17,9 @@ import { getResolvedVirtualModuleId, } from '../virtual-file-names'; -export function codeGeneratorPlugin(options: Options): Plugin { +export function codeGeneratorPlugin(options: Options) { const iframePath = fileURLToPath(importMetaResolve('@storybook/builder-vite/input/iframe.html')); let iframeId: string; - let projectRoot: string; const storyIndexGeneratorPromise: Promise = options.presets.apply('storyIndexGenerator'); @@ -53,7 +52,6 @@ export function codeGeneratorPlugin(options: Options): Plugin { } }, configResolved(config) { - projectRoot = config.root; iframeId = `${config.root}/iframe.html`; }, resolveId(source) { @@ -78,7 +76,7 @@ export function codeGeneratorPlugin(options: Options): Plugin { return generateAddonSetupCode(); } case getResolvedVirtualModuleId(SB_VIRTUAL_FILES.VIRTUAL_APP_FILE): { - return generateModernIframeScriptCode(options, projectRoot); + return generateModernIframeScriptCode(options); } case iframeId: { return readFileSync( @@ -94,5 +92,5 @@ export function codeGeneratorPlugin(options: Options): Plugin { } return transformIframeHtml(html, options); }, - }; + } satisfies Plugin; } diff --git a/code/builders/builder-vite/src/plugins/index.ts b/code/builders/builder-vite/src/plugins/index.ts index bc72dc8755d5..078886a86b4a 100644 --- a/code/builders/builder-vite/src/plugins/index.ts +++ b/code/builders/builder-vite/src/plugins/index.ts @@ -1,6 +1,12 @@ -export * from './inject-export-order-plugin'; -export * from './strip-story-hmr-boundaries'; -export * from './code-generator-plugin'; -export * from './csf-plugin'; -export * from './external-globals-plugin'; -export * from './webpack-stats-plugin'; +// Builder-internal plugins (used by vite-config.ts to assemble the builder's plugin stack) +export { storybookOptimizeDepsPlugin } from './storybook-optimize-deps-plugin'; +export { storybookEntryPlugin } from './storybook-entry-plugin'; +export { pluginWebpackStats } from './webpack-stats-plugin'; +export type { WebpackStatsPlugin } from './webpack-stats-plugin'; + +// Lower-level plugins re-exported for internal use and tests +export { injectExportOrderPlugin } from './inject-export-order-plugin'; +export { stripStoryHMRBoundary } from './strip-story-hmr-boundaries'; +export { codeGeneratorPlugin } from './code-generator-plugin'; +export { csfPlugin } from './csf-plugin'; +export { storybookExternalGlobalsPlugin } from './storybook-external-globals-plugin'; diff --git a/code/builders/builder-vite/src/plugins/inject-export-order-plugin.ts b/code/builders/builder-vite/src/plugins/inject-export-order-plugin.ts index 01b835ff6187..91091a86811b 100644 --- a/code/builders/builder-vite/src/plugins/inject-export-order-plugin.ts +++ b/code/builders/builder-vite/src/plugins/inject-export-order-plugin.ts @@ -1,5 +1,6 @@ import { parse } from 'es-module-lexer'; import MagicString from 'magic-string'; +import type { Plugin } from 'vite'; export async function injectExportOrderPlugin() { const { createFilter } = await import('vite'); @@ -35,5 +36,5 @@ export async function injectExportOrderPlugin() { map: s.generateMap({ hires: true, source: id }), }; }, - }; + } satisfies Plugin; } diff --git a/code/builders/builder-vite/src/plugins/storybook-config-plugin.ts b/code/builders/builder-vite/src/plugins/storybook-config-plugin.ts new file mode 100644 index 000000000000..7152f47f4cf4 --- /dev/null +++ b/code/builders/builder-vite/src/plugins/storybook-config-plugin.ts @@ -0,0 +1,62 @@ +import { isPreservingSymlinks } from 'storybook/internal/common'; + +import { type Plugin } from 'vite'; + +export interface StorybookConfigPluginOptions { + configDir: string; +} + +/** + * A Vite plugin that provides the base Storybook configuration. + * + * This handles: + * + * - Adding Storybook resolve conditions (`storybook`, `stories`, `test`) + * - Setting up environment variable prefixes (`VITE_`, `STORYBOOK_`) + * - Allowing the Storybook config directory in Vite's filesystem restrictions + * - Preserving symlinks when applicable + */ +export function storybookConfigPlugin(options: StorybookConfigPluginOptions): Plugin[] { + return [ + { + name: 'storybook:config-plugin', + enforce: 'pre', + async config(config) { + const { defaultClientConditions = [] } = await import('vite'); + + const existingEnvPrefix = config.envPrefix; + // If an envPrefix is specified in the user's vite config, add STORYBOOK_ to it. + // Otherwise, add both VITE_ and STORYBOOK_ so that Vite doesn't lose its default. + const mergedEnvPrefix = existingEnvPrefix + ? Array.from( + new Set([ + ...(Array.isArray(existingEnvPrefix) ? existingEnvPrefix : [existingEnvPrefix]), + 'STORYBOOK_', + ]) + ) + : ['VITE_', 'STORYBOOK_']; + + return { + resolve: { + conditions: ['storybook', 'stories', 'test', ...defaultClientConditions], + preserveSymlinks: isPreservingSymlinks(), + }, + envPrefix: mergedEnvPrefix, + }; + }, + }, + { + name: 'storybook:allow-storybook-dir', + enforce: 'post', + config(config) { + // If there is NO allow list then Vite allows anything in the root directory. + // If there IS an allow list then Vite only allows the listed directories. + // We add the storybook config directory only if there's already an allow list, + // to avoid disallowing the root unless it's already restricted. + if (config?.server?.fs?.allow) { + config.server.fs.allow.push(options.configDir); + } + }, + }, + ]; +} diff --git a/code/builders/builder-vite/src/plugins/storybook-entry-plugin.ts b/code/builders/builder-vite/src/plugins/storybook-entry-plugin.ts new file mode 100644 index 000000000000..f4c0ba3d38d3 --- /dev/null +++ b/code/builders/builder-vite/src/plugins/storybook-entry-plugin.ts @@ -0,0 +1,22 @@ +import type { Options } from 'storybook/internal/types'; + +import type { Plugin } from 'vite'; + +import { codeGeneratorPlugin } from './code-generator-plugin'; +import { injectExportOrderPlugin } from './inject-export-order-plugin'; +import { stripStoryHMRBoundary } from './strip-story-hmr-boundaries'; + +/** + * A composite Vite plugin that manages the generation and injection of virtual entry points for + * Storybook stories. This is builder-specific and NOT shared with addon-vitest. + */ +export async function storybookEntryPlugin(options: Options): Promise { + return [ + // Pre-enforcement: handles virtual module resolution and loading (must run first) + codeGeneratorPlugin(options), + // Post-enforcement: injects __namedExportsOrder after TypeScript transpilation + await injectExportOrderPlugin(), + // Post-enforcement: removes import.meta.hot.accept() from story files + await stripStoryHMRBoundary(), + ]; +} diff --git a/code/builders/builder-vite/src/plugins/external-globals-plugin.test.ts b/code/builders/builder-vite/src/plugins/storybook-external-globals-plugin.test.ts similarity index 95% rename from code/builders/builder-vite/src/plugins/external-globals-plugin.test.ts rename to code/builders/builder-vite/src/plugins/storybook-external-globals-plugin.test.ts index 67b62690da43..4b5c001f0e3d 100644 --- a/code/builders/builder-vite/src/plugins/external-globals-plugin.test.ts +++ b/code/builders/builder-vite/src/plugins/storybook-external-globals-plugin.test.ts @@ -1,6 +1,6 @@ import { expect, it } from 'vitest'; -import { rewriteImport } from './external-globals-plugin'; +import { rewriteImport } from './storybook-external-globals-plugin'; const packageName = '@storybook/package'; const globals = { [packageName]: '_STORYBOOK_PACKAGE_' }; diff --git a/code/builders/builder-vite/src/plugins/external-globals-plugin.ts b/code/builders/builder-vite/src/plugins/storybook-external-globals-plugin.ts similarity index 89% rename from code/builders/builder-vite/src/plugins/external-globals-plugin.ts rename to code/builders/builder-vite/src/plugins/storybook-external-globals-plugin.ts index 242fb98ff3dc..54bb15a28c96 100644 --- a/code/builders/builder-vite/src/plugins/external-globals-plugin.ts +++ b/code/builders/builder-vite/src/plugins/storybook-external-globals-plugin.ts @@ -2,6 +2,9 @@ import { existsSync } from 'node:fs'; import { mkdir, writeFile } from 'node:fs/promises'; import { dirname, join } from 'node:path'; +import { globalsNameReferenceMap } from 'storybook/internal/preview/globals'; +import type { Options } from 'storybook/internal/types'; + import * as pkg from 'empathic/package'; import { init, parse } from 'es-module-lexer'; import MagicString from 'magic-string'; @@ -38,7 +41,17 @@ const replacementMap = new Map([ * https://github.com/eight04/rollup-plugin-external-globals, but simplified to meet our simple * needs. */ -export async function externalGlobalsPlugin(externals: Record): Promise { + +export async function storybookExternalGlobalsPlugin(options: Options): Promise { + const build = await options.presets.apply('build'); + + const externals: typeof globalsNameReferenceMap & Record = + globalsNameReferenceMap; + + if (build?.test?.disableBlocks) { + externals['@storybook/addon-docs/blocks'] = '__STORYBOOK_BLOCKS_EMPTY_MODULE__'; + } + await init; const { mergeAlias } = await import('vite'); 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 new file mode 100644 index 000000000000..ccbd5e4a9a4e --- /dev/null +++ b/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts @@ -0,0 +1,41 @@ +import type { StoryIndexGenerator } from 'storybook/internal/core-server'; +import type { Options, StoryIndex } from 'storybook/internal/types'; + +import { type Plugin } from 'vite'; + +import { getUniqueImportPaths } from '../utils/unique-import-paths'; + +/** A Vite plugin that configures dependency optimization for Storybook's dev server. */ +export function storybookOptimizeDepsPlugin(options: Options): Plugin { + return { + name: 'storybook:optimize-deps-plugin', + async config(config, { command }) { + // optimizeDeps only applies to the dev server, not production builds + if (command !== 'serve') { + return; + } + + const [extraOptimizeDeps, storyIndexGenerator] = await Promise.all([ + options.presets.apply('optimizeViteDeps', []), + options.presets.apply('storyIndexGenerator'), + ]); + + const index: StoryIndex = await storyIndexGenerator.getIndex(); + + return { + optimizeDeps: { + // Story file paths as entry points for the optimizer + entries: [ + ...(typeof config.optimizeDeps?.entries === 'string' + ? [config.optimizeDeps.entries] + : []), + ...getUniqueImportPaths(index), + ], + // Known CJS dependencies that need to be pre-compiled to ESM, + // plus any extra deps from Storybook presets. + include: [...extraOptimizeDeps, ...(config.optimizeDeps?.include || [])], + }, + }; + }, + }; +} diff --git a/code/builders/builder-vite/src/plugins/storybook-project-annotations-plugin.ts b/code/builders/builder-vite/src/plugins/storybook-project-annotations-plugin.ts new file mode 100644 index 000000000000..beb879493f64 --- /dev/null +++ b/code/builders/builder-vite/src/plugins/storybook-project-annotations-plugin.ts @@ -0,0 +1,40 @@ +import type { Options } from 'storybook/internal/types'; + +import type { Plugin } from 'vite'; + +import { generateProjectAnnotationsCode } from '../codegen-project-annotations'; +import { getResolvedVirtualModuleId } from '../virtual-file-names'; + +export const VIRTUAL_ID = 'virtual:/@storybook/builder-vite/project-annotations.js'; +const RESOLVED_VIRTUAL_ID = getResolvedVirtualModuleId(VIRTUAL_ID); + +/** + * A Vite plugin that serves the project annotations virtual module. + * + * The virtual module can be imported as: + * + * ```ts + * import { getProjectAnnotations } from 'virtual:/@storybook/builder-vite/project-annotations.js'; + * ``` + */ +export function storybookProjectAnnotationsPlugin(options: Options): Plugin { + let projectRoot: string; + + return { + name: 'storybook:project-annotations-plugin', + enforce: 'pre', + configResolved(config) { + projectRoot = config.root; + }, + resolveId(source) { + if (source === VIRTUAL_ID) { + return RESOLVED_VIRTUAL_ID; + } + }, + async load(id) { + if (id === RESOLVED_VIRTUAL_ID) { + return generateProjectAnnotationsCode(options, projectRoot); + } + }, + }; +} diff --git a/code/builders/builder-vite/src/plugins/storybook-runtime-plugin.ts b/code/builders/builder-vite/src/plugins/storybook-runtime-plugin.ts new file mode 100644 index 000000000000..a60d66eace7e --- /dev/null +++ b/code/builders/builder-vite/src/plugins/storybook-runtime-plugin.ts @@ -0,0 +1,31 @@ +import type { Builder_EnvsRaw } from 'storybook/internal/types'; +import type { Options } from 'storybook/internal/types'; + +import type { Plugin } from 'vite'; + +import { stringifyProcessEnvs } from '../envs'; + +export interface StorybookRuntimePluginOptions { + externals: Record; + envs?: Builder_EnvsRaw; +} + +/** A composite Vite plugin that injects environment variables for Storybook's runtime. */ +export async function storybookSanitizeEnvs(options: Options): Promise { + const plugins: Plugin[] = []; + const envs = await options.presets.apply('env'); + + if (envs && Object.keys(envs).length > 0) { + plugins.push({ + name: 'storybook:env-plugin', + config(config) { + const envDefines = stringifyProcessEnvs(envs, config.envPrefix); + return { + define: envDefines, + }; + }, + }); + } + + return plugins; +} diff --git a/code/builders/builder-vite/src/plugins/strip-story-hmr-boundaries.ts b/code/builders/builder-vite/src/plugins/strip-story-hmr-boundaries.ts index 509a7d06adbd..baa433d1afed 100644 --- a/code/builders/builder-vite/src/plugins/strip-story-hmr-boundaries.ts +++ b/code/builders/builder-vite/src/plugins/strip-story-hmr-boundaries.ts @@ -6,7 +6,7 @@ import type { Plugin } from 'vite'; * boundaries, but vite has a bug which causes them to be treated as boundaries * (https://github.com/vitejs/vite/issues/9869). */ -export async function stripStoryHMRBoundary(): Promise { +export async function stripStoryHMRBoundary() { const { createFilter } = await import('vite'); const filter = createFilter(/\.stories\.(tsx?|jsx?|svelte|vue)$/); @@ -26,5 +26,5 @@ export async function stripStoryHMRBoundary(): Promise { map: s.generateMap({ hires: true, source: id }), }; }, - }; + } satisfies Plugin; } diff --git a/code/builders/builder-vite/src/preset.ts b/code/builders/builder-vite/src/preset.ts index b309970c3314..0f33305a8c20 100644 --- a/code/builders/builder-vite/src/preset.ts +++ b/code/builders/builder-vite/src/preset.ts @@ -1,34 +1,39 @@ import { findConfigFile } from 'storybook/internal/common'; import type { Options } from 'storybook/internal/types'; -import type { UserConfig } from 'vite'; +import type { PluginOption } from 'vite'; +import { storybookConfigPlugin } from './plugins/storybook-config-plugin'; +import { storybookOptimizeDepsPlugin } from './plugins/storybook-optimize-deps-plugin'; +import { storybookProjectAnnotationsPlugin } from './plugins/storybook-project-annotations-plugin'; +import { storybookSanitizeEnvs } from './plugins/storybook-runtime-plugin'; import { viteInjectMockerRuntime } from './plugins/vite-inject-mocker/plugin'; import { viteMockPlugin } from './plugins/vite-mock/plugin'; -// This preset defines currently mocking plugins for Vite -// It is defined as a viteFinal preset so that @storybook/addon-vitest can use it as well and that it doesn't have to be duplicated in addon-vitest. -// The main vite configuration is defined in `./vite-config.ts`. -export async function viteFinal(existing: UserConfig, options: Options) { +/** + * Preset that provides the core Storybook Vite plugins shared between `@storybook/builder-vite` and + * `@storybook/addon-vitest`. + */ +export async function viteCorePlugins( + _: PluginOption[], + options: Options +): Promise { const previewConfigPath = findConfigFile('preview', options.configDir); - // If there's no preview file, there's nothing to mock. - if (!previewConfigPath) { - return existing; - } - - const coreOptions = await options.presets.apply('core'); - - return { - ...existing, - plugins: [ - ...(existing.plugins ?? []), - ...(previewConfigPath - ? [ - viteInjectMockerRuntime({ previewConfigPath }), - viteMockPlugin({ previewConfigPath, coreOptions, configDir: options.configDir }), - ] - : []), - ], - }; + return [ + storybookProjectAnnotationsPlugin(options), + ...storybookConfigPlugin({ configDir: options.configDir }), + storybookOptimizeDepsPlugin(options), + ...(await storybookSanitizeEnvs(options)), + ...(previewConfigPath + ? [ + viteInjectMockerRuntime({ previewConfigPath }), + viteMockPlugin({ + previewConfigPath, + coreOptions: await options.presets.apply('core'), + configDir: options.configDir, + }), + ] + : []), + ]; } diff --git a/code/builders/builder-vite/src/vite-config.test.ts b/code/builders/builder-vite/src/vite-config.test.ts index 3097c069431a..0dc8c5b04708 100644 --- a/code/builders/builder-vite/src/vite-config.test.ts +++ b/code/builders/builder-vite/src/vite-config.test.ts @@ -4,6 +4,7 @@ import type { Options, Presets } from 'storybook/internal/types'; import { loadConfigFromFile } from 'vite'; +import { storybookConfigPlugin } from './plugins/storybook-config-plugin'; import { commonConfig } from './vite-config'; vi.mock('vite', async (importOriginal) => ({ @@ -34,7 +35,7 @@ const dummyOptions: Options = { }; describe('commonConfig', () => { - it('should preserve default envPrefix', async () => { + it('should set configFile to false and include plugins', async () => { loadConfigFromFileMock.mockReturnValueOnce( Promise.resolve({ config: {}, @@ -43,30 +44,45 @@ describe('commonConfig', () => { }) ); const config = await commonConfig(dummyOptions, 'development'); - expect(config.envPrefix).toStrictEqual(['VITE_', 'STORYBOOK_']); + expect(config.configFile).toBe(false); + expect(config.plugins).toBeDefined(); }); +}); - it('should preserve custom envPrefix string', async () => { - loadConfigFromFileMock.mockReturnValueOnce( - Promise.resolve({ - config: { envPrefix: 'SECRET_' }, - path: '', - dependencies: [], - }) - ); - const config = await commonConfig(dummyOptions, 'development'); - expect(config.envPrefix).toStrictEqual(['SECRET_', 'STORYBOOK_']); +describe('storybookConfigPlugin', () => { + it('should set default envPrefix when no user envPrefix is set', async () => { + const plugins = storybookConfigPlugin({ configDir: '/test/.storybook' }); + const configPlugin = plugins.find((p) => p.name === 'storybook:config-plugin')!; + + // The config hook receives the current Vite config and returns partial config to merge + const result = await (configPlugin.config as Function)({}, {}); + expect(result.envPrefix).toStrictEqual(['VITE_', 'STORYBOOK_']); }); - it('should preserve custom envPrefix array', async () => { - loadConfigFromFileMock.mockReturnValueOnce( - Promise.resolve({ - config: { envPrefix: ['SECRET_', 'VUE_'] }, - path: '', - dependencies: [], - }) - ); - const config = await commonConfig(dummyOptions, 'development'); - expect(config.envPrefix).toStrictEqual(['SECRET_', 'VUE_', 'STORYBOOK_']); + it('should include storybook resolve conditions', async () => { + const plugins = storybookConfigPlugin({ configDir: '/test/.storybook' }); + const configPlugin = plugins.find((p) => p.name === 'storybook:config-plugin')!; + + const result = await (configPlugin.config as Function)({}, {}); + expect(result.resolve.conditions).toContain('storybook'); + expect(result.resolve.conditions).toContain('stories'); + expect(result.resolve.conditions).toContain('test'); + }); + + it('should not set base when not provided', async () => { + const plugins = storybookConfigPlugin({ configDir: '/test/.storybook' }); + const configPlugin = plugins.find((p) => p.name === 'storybook:config-plugin')!; + + const result = await (configPlugin.config as Function)({}, {}); + expect(result.base).toBeUndefined(); + }); + + it('should allow storybook dir when server fs allow list exists', () => { + const plugins = storybookConfigPlugin({ configDir: '/test/.storybook' }); + const allowPlugin = plugins.find((p) => p.name === 'storybook:allow-storybook-dir')!; + + const config = { server: { fs: { allow: ['/some/path'] } } }; + (allowPlugin.config as Function)(config); + expect(config.server.fs.allow).toContain('/test/.storybook'); }); }); diff --git a/code/builders/builder-vite/src/vite-config.ts b/code/builders/builder-vite/src/vite-config.ts index 9cb4e86042f2..8a8b833962c0 100644 --- a/code/builders/builder-vite/src/vite-config.ts +++ b/code/builders/builder-vite/src/vite-config.ts @@ -1,11 +1,6 @@ import { resolve } from 'node:path'; -import { - getBuilderOptions, - isPreservingSymlinks, - resolvePathInStorybookCache, -} from 'storybook/internal/common'; -import { globalsNameReferenceMap } from 'storybook/internal/preview/globals'; +import { getBuilderOptions, resolvePathInStorybookCache } from 'storybook/internal/common'; import type { Options } from 'storybook/internal/types'; import type { @@ -17,13 +12,12 @@ import type { } from 'vite'; import { - codeGeneratorPlugin, csfPlugin, - externalGlobalsPlugin, - injectExportOrderPlugin, pluginWebpackStats, - stripStoryHMRBoundary, + storybookEntryPlugin, + storybookExternalGlobalsPlugin, } from './plugins'; +import { viteCorePlugins as corePlugins } from './preset'; import type { BuilderOptions } from './types'; export type PluginConfigType = 'build' | 'development'; @@ -46,7 +40,7 @@ export async function commonConfig( _type: PluginConfigType ): Promise { const configEnv = _type === 'development' ? configEnvServe : configEnvBuild; - const { loadConfigFromFile, mergeConfig, defaultClientConditions = [] } = await import('vite'); + const { loadConfigFromFile, mergeConfig } = await import('vite'); const { viteConfigPath } = await getBuilderOptions(options); @@ -58,22 +52,14 @@ export async function commonConfig( const { config: { build: buildProperty = undefined, ...userConfig } = {} } = (await loadConfigFromFile(configEnv, viteConfigPath, projectRoot)) ?? {}; - // This is the main Vite config that is used by Storybook. - // Some shared vite plugins are defined in the `./preset.ts` file so that it can be shared between the @storybook/builder-vite and @storybook/addon-vitest package. + // Storybook's Vite config is assembled from self-contained plugins. + // The config plugin handles base settings (root, cacheDir, resolve conditions, etc.), + // while other plugins handle entry points, docgen, and runtime globals. + // Shared vite plugins for mocking are defined in `./preset.ts` so that they can be + // shared between @storybook/builder-vite and @storybook/addon-vitest. const sbConfig: InlineConfig = { configFile: false, - cacheDir: resolvePathInStorybookCache('sb-vite', options.cacheKey), - root: projectRoot, - // Allow storybook deployed as subfolder. See https://github.com/storybookjs/builder-vite/issues/238 - base: './', plugins: await pluginConfig(options), - resolve: { - conditions: ['storybook', 'stories', 'test', ...defaultClientConditions], - preserveSymlinks: isPreservingSymlinks(), - }, - // If an envPrefix is specified in the vite config, add STORYBOOK_ to it, - // otherwise, add VITE_ and STORYBOOK_ so that vite doesn't lose its default. - envPrefix: userConfig.envPrefix ? ['STORYBOOK_'] : ['VITE_', 'STORYBOOK_'], // Pass build.target option from user's vite config build: { target: buildProperty?.target, @@ -86,33 +72,31 @@ export async function commonConfig( } export async function pluginConfig(options: Options) { - const build = await options.presets.apply('build'); - - const externals: Record = globalsNameReferenceMap; - - if (build?.test?.disableBlocks) { - externals['@storybook/addon-docs/blocks'] = '__STORYBOOK_BLOCKS_EMPTY_MODULE__'; - } + const projectRoot = resolve(options.configDir, '..'); const plugins = [ - codeGeneratorPlugin(options), + // Shared core plugins (resolve conditions, envPrefix, fs.allow, externals, env vars, etc.) + ...(await corePlugins([], options)), + await storybookExternalGlobalsPlugin(options), await csfPlugin(options), - await injectExportOrderPlugin(), - await stripStoryHMRBoundary(), + // Builder-specific: root, base, and cacheDir { - name: 'storybook:allow-storybook-dir', - enforce: 'post', - config(config) { - // if there is NO allow list then Vite allows anything in the root directory - // if there is an allow list then Vite only allows anything in the listed directories - // add storybook specific directories only if there's an allow list so that we don't end up - // disallowing the root unless root is already disallowed - if (config?.server?.fs?.allow) { - config.server.fs.allow.push(options.configDir); - } + name: 'storybook:builder-vite-config', + enforce: 'pre' as const, + config() { + return { + root: projectRoot, + // Allow storybook deployed as subfolder. See https://github.com/storybookjs/builder-vite/issues/238 + base: './', + ...(options.cacheKey + ? { cacheDir: resolvePathInStorybookCache('sb-vite', options.cacheKey) } + : {}), + }; }, }, - await externalGlobalsPlugin(externals), + // Entry plugin: virtual modules for stories, addon setup, and main app entry + ...(await storybookEntryPlugin(options)), + // Builder-specific: webpack-compatible stats for turbosnap/chromatic pluginWebpackStats({ workingDir: process.cwd() }), ] as PluginOption[]; diff --git a/code/builders/builder-vite/src/vite-server.ts b/code/builders/builder-vite/src/vite-server.ts index 30e712c5a0cf..9c694e94e410 100644 --- a/code/builders/builder-vite/src/vite-server.ts +++ b/code/builders/builder-vite/src/vite-server.ts @@ -5,7 +5,6 @@ import type { Server } from 'http'; import { dedent } from 'ts-dedent'; import type { InlineConfig, ServerOptions } from 'vite'; -import { sanitizeEnvVars } from './envs'; import { createViteLogger } from './logger'; import { getOptimizeDeps } from './optimizeDeps'; import { commonConfig } from './vite-config'; @@ -15,9 +14,15 @@ 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: { @@ -29,7 +34,6 @@ export async function createViteServer(options: Options, devServer: Server) { }, }, appType: 'custom' as const, - optimizeDeps: await getOptimizeDeps(commonCfg, options), }; // '0.0.0.0' binds to all interfaces, which is useful for Docker and other containerized environments. @@ -51,5 +55,5 @@ export async function createViteServer(options: Options, devServer: Server) { const { createServer } = await import('vite'); finalConfig.customLogger ??= await createViteLogger(); - return createServer(await sanitizeEnvVars(options, finalConfig)); + return createServer(finalConfig); } diff --git a/scripts/tasks/sandbox-parts.ts b/scripts/tasks/sandbox-parts.ts index 6e26c07b7bac..87bcc1442af1 100644 --- a/scripts/tasks/sandbox-parts.ts +++ b/scripts/tasks/sandbox-parts.ts @@ -497,8 +497,7 @@ export async function setupVitest(details: TemplateDetails, options: PassedOptio if (shouldUseCsf4) { await writeFile( setupFilePath, - dedent`import { beforeAll } from 'vitest' - import { setProjectAnnotations } from '${storybookPackage}' + dedent`import { setProjectAnnotations } from '${storybookPackage}' import projectAnnotations from './preview' // setProjectAnnotations still kept to support non-CSF4 story tests @@ -508,7 +507,7 @@ export async function setupVitest(details: TemplateDetails, options: PassedOptio } else { await writeFile( setupFilePath, - dedent`import { beforeAll } from 'vitest' + dedent` import { setProjectAnnotations } from '${storybookPackage}' import * as rendererDocsAnnotations from '${template.expected.renderer}/entry-preview-docs' import * as addonA11yAnnotations from '@storybook/addon-a11y/preview'