import * as path from 'path'; import fs from 'fs'; import { Plugin } from 'vite'; import { TypescriptConfig } from '@storybook/core-common'; import viteReact from '@vitejs/plugin-react'; import { allowedEnvPrefix as envPrefix } from './envs'; import { codeGeneratorPlugin } from './code-generator-plugin'; import { injectExportOrderPlugin } from './inject-export-order-plugin'; import { mdxPlugin } from './plugins/mdx-plugin'; import { noFouc } from './plugins/no-fouc'; import { sourceLoaderPlugin } from './source-loader-plugin'; import type { UserConfig } from 'vite'; import type { ExtendedOptions } from './types'; export type PluginConfigType = 'build' | 'development'; export function readPackageJson(): Record | false { const packageJsonPath = path.resolve('package.json'); if (!fs.existsSync(packageJsonPath)) { return false; } const jsonContent = fs.readFileSync(packageJsonPath, 'utf8'); return JSON.parse(jsonContent); } // Vite config that is common to development and production mode export async function commonConfig( options: ExtendedOptions, _type: PluginConfigType ): Promise { const { framework } = options; return { configFile: false, root: path.resolve(options.configDir, '..'), cacheDir: 'node_modules/.vite-storybook', envPrefix, define: {}, resolve: framework === 'vue3' ? { alias: { vue: 'vue/dist/vue.esm-bundler.js', }, } : {}, plugins: await pluginConfig(options, _type), }; } export async function pluginConfig(options: ExtendedOptions, _type: PluginConfigType) { const { framework, presets } = options; const svelteOptions: Record = await presets.apply('svelteOptions', {}, options); const plugins = [ codeGeneratorPlugin(options), sourceLoaderPlugin(options), mdxPlugin(options), noFouc(), injectExportOrderPlugin, // We need the react plugin here to support MDX. viteReact({ // Do not treat story files as HMR boundaries, storybook itself needs to handle them. exclude: [/\.stories\.([tj])sx?$/, /node_modules/].concat(framework === 'react' ? [] : [/\.([tj])sx?$/]), }), { name: 'vite-plugin-storybook-allow', 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 allows anything in the listed directories // add the .storybook directory only if there's an allow list so that we don't end up // disallowing the root directory unless it's already disallowed if (config?.server?.fs?.allow) { config.server.fs.allow.push('.storybook'); } }, }, ] as Plugin[]; if (framework === 'vue' || framework === 'vue3') { try { const vuePlugin = require('@vitejs/plugin-vue'); plugins.push(vuePlugin()); const { vueDocgen } = await import('./plugins/vue-docgen'); plugins.push(vueDocgen()); } catch (err) { if ((err as NodeJS.ErrnoException).code === 'MODULE_NOT_FOUND') { throw new Error( '@storybook/builder-vite requires @vitejs/plugin-vue to be installed ' + 'when using @storybook/vue or @storybook/vue3.' + ' Please install it and start storybook again.' ); } throw err; } } if (framework === 'svelte') { try { const sveltePlugin = require('@sveltejs/vite-plugin-svelte').svelte; // We need to create two separate svelte plugins, one for stories, and one for other svelte files // because stories.svelte files cannot be hot-module-reloaded. // Suggested in: https://github.com/sveltejs/vite-plugin-svelte/issues/321#issuecomment-1113205509 // First, create an array containing user exclude patterns, to combine with ours. const userExclude = Array.isArray(svelteOptions?.exclude) ? svelteOptions?.exclude : svelteOptions?.exclude ? [svelteOptions?.exclude] : []; // These are the svelte stories we need to exclude from HMR const storyPatterns = ['**/*.story.svelte', '**/*.stories.svelte']; // Non-story svelte files // Starting in 1.0.0-next.42, svelte.config.js is included by default. // We disable that, but allow it to be overridden in svelteOptions plugins.push(sveltePlugin({ ...svelteOptions, exclude: [...userExclude, ...storyPatterns] })); // Svelte stories without HMR const storySveltePlugin = sveltePlugin({ ...svelteOptions, exclude: userExclude, include: storyPatterns, hot: false, }); plugins.push({ // Starting in 1.0.0-next.43, the plugin function returns an array of plugins. We only want the first one here. ...(Array.isArray(storySveltePlugin) ? storySveltePlugin[0] : storySveltePlugin), name: 'vite-plugin-svelte-stories', }); } catch (err) { if ((err as NodeJS.ErrnoException).code === 'MODULE_NOT_FOUND') { throw new Error( '@storybook/builder-vite requires @sveltejs/vite-plugin-svelte to be installed' + ' when using @storybook/svelte.' + ' Please install it and start storybook again.' ); } throw err; } const { loadSvelteConfig } = require('@sveltejs/vite-plugin-svelte'); const config = { ...loadSvelteConfig(), ...svelteOptions }; try { const csfPlugin = require('./svelte/csf-plugin').default; plugins.push(csfPlugin(config)); } catch (err) { // Not all projects use `.stories.svelte` for stories, and by default 6.5+ does not auto-install @storybook/addon-svelte-csf. // If it's any other kind of error, re-throw. if ((err as NodeJS.ErrnoException).code !== 'MODULE_NOT_FOUND') { throw err; } } const { svelteDocgen } = await import('./plugins/svelte-docgen'); plugins.push(svelteDocgen(config)); } if (framework === 'preact') { plugins.push(require('@preact/preset-vite').default()); } if (framework === 'react') { const { reactDocgen: reactDocgenOption, reactDocgenTypescriptOptions } = await presets.apply( 'typescript', {} as TypescriptConfig ); if (reactDocgenOption === 'react-docgen-typescript') { plugins.push( require('@joshwooding/vite-plugin-react-docgen-typescript')({ ...reactDocgenTypescriptOptions, // We *need* this set so that RDT returns default values in the same format as react-docgen savePropValueAsString: true, }) ); } // Add react-docgen so long as the option is not false if (typeof reactDocgenOption === 'string') { const { reactDocgen } = await import('./plugins/react-docgen'); // Needs to run before the react plugin, so add to the front plugins.unshift( // If react-docgen is specified, use it for everything, otherwise only use it for non-typescript files reactDocgen({ include: reactDocgenOption === 'react-docgen' ? /\.(mjs|tsx?|jsx?)$/ : /\.(mjs|jsx?)$/ }) ); } } if (framework === 'glimmerx') { const plugin = require('vite-plugin-glimmerx/index.cjs'); plugins.push(plugin.default()); } return plugins; }