From 8d1692733d9bee3d953bcd54a73f4235f5ac3c32 Mon Sep 17 00:00:00 2001 From: Ian VanSchooten Date: Fri, 26 Aug 2022 00:18:40 -0400 Subject: [PATCH] Vite: use vite.config.js as config base --- code/lib/builder-vite/src/build.ts | 25 +--- code/lib/builder-vite/src/envs.ts | 3 - code/lib/builder-vite/src/optimizeDeps.ts | 11 +- code/lib/builder-vite/src/vite-config.ts | 140 ++++++++++------------ code/lib/builder-vite/src/vite-server.ts | 46 +++---- 5 files changed, 89 insertions(+), 136 deletions(-) diff --git a/code/lib/builder-vite/src/build.ts b/code/lib/builder-vite/src/build.ts index d0a36401fc7f..5333c56f7b2c 100644 --- a/code/lib/builder-vite/src/build.ts +++ b/code/lib/builder-vite/src/build.ts @@ -1,32 +1,19 @@ import { build as viteBuild } from 'vite'; -import { stringifyProcessEnvs } from './envs'; import { commonConfig } from './vite-config'; -import type { EnvsRaw, ExtendedOptions } from './types'; +import type { ExtendedOptions } from './types'; export async function build(options: ExtendedOptions) { const { presets } = options; - const baseConfig = await commonConfig(options, 'build'); - const config = { - ...baseConfig, - build: { - outDir: options.outputDir, - emptyOutDir: false, // do not clean before running Vite build - Storybook has already added assets in there! - sourcemap: true, - }, + const config = await commonConfig(options, 'build'); + config.build = { + outDir: options.outputDir, + emptyOutDir: false, // do not clean before running Vite build - Storybook has already added assets in there! + sourcemap: true, }; const finalConfig = await presets.apply('viteFinal', config, options); - const envsRaw = await presets.apply>('env'); - // Stringify env variables after getting `envPrefix` from the final config - const envs = stringifyProcessEnvs(envsRaw, finalConfig.envPrefix); - // Update `define` - finalConfig.define = { - ...finalConfig.define, - ...envs, - }; - await viteBuild(finalConfig); } diff --git a/code/lib/builder-vite/src/envs.ts b/code/lib/builder-vite/src/envs.ts index 107391642627..7ebead6aab45 100644 --- a/code/lib/builder-vite/src/envs.ts +++ b/code/lib/builder-vite/src/envs.ts @@ -15,9 +15,6 @@ const allowedEnvVariables = [ 'SSR', ]; -// Env variables starts with env prefix will be exposed to your client source code via `import.meta.env` -export const allowedEnvPrefix = ['VITE_', 'STORYBOOK_']; - /** * Customized version of stringifyProcessEnvs from @storybook/core-common which * uses import.meta.env instead of process.env and checks for allowed variables. diff --git a/code/lib/builder-vite/src/optimizeDeps.ts b/code/lib/builder-vite/src/optimizeDeps.ts index fbe560720ed4..09df4ba81c8c 100644 --- a/code/lib/builder-vite/src/optimizeDeps.ts +++ b/code/lib/builder-vite/src/optimizeDeps.ts @@ -1,5 +1,6 @@ import * as path from 'path'; -import { normalizePath, resolveConfig, UserConfig } from 'vite'; +import { normalizePath, resolveConfig } from 'vite'; +import type { InlineConfig as ViteInlineConfig } from 'vite'; import { listStories } from './list-stories'; import type { ExtendedOptions } from './types'; @@ -101,13 +102,11 @@ const INCLUDE_CANDIDATES = [ const asyncFilter = async (arr: string[], predicate: (val: string) => Promise) => Promise.all(arr.map(predicate)).then((results) => arr.filter((_v, index) => results[index])); -export async function getOptimizeDeps( - config: UserConfig & { configFile: false; root: string }, - options: ExtendedOptions -) { - const { root } = config; +export async function getOptimizeDeps(config: ViteInlineConfig, options: ExtendedOptions) { + const { root = process.cwd() } = config; const absoluteStories = await listStories(options); const stories = absoluteStories.map((storyPath) => normalizePath(path.relative(root, storyPath))); + // 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. diff --git a/code/lib/builder-vite/src/vite-config.ts b/code/lib/builder-vite/src/vite-config.ts index 19ca4d59db55..f959989e91e6 100644 --- a/code/lib/builder-vite/src/vite-config.ts +++ b/code/lib/builder-vite/src/vite-config.ts @@ -1,14 +1,19 @@ import * as path from 'path'; import fs from 'fs'; -import { Plugin } from 'vite'; +import { loadConfigFromFile, mergeConfig } from 'vite'; +import type { + ConfigEnv, + InlineConfig as ViteInlineConfig, + PluginOption, + UserConfig as ViteConfig, +} from 'vite'; import viteReact from '@vitejs/plugin-react'; -import type { UserConfig } from 'vite'; -import { allowedEnvPrefix as envPrefix } from './envs'; import { codeGeneratorPlugin } from './code-generator-plugin'; +import { stringifyProcessEnvs } from './envs'; import { injectExportOrderPlugin } from './inject-export-order-plugin'; import { mdxPlugin } from './plugins/mdx-plugin'; import { noFouc } from './plugins/no-fouc'; -import type { ExtendedOptions } from './types'; +import type { ExtendedOptions, EnvsRaw } from './types'; export type PluginConfigType = 'build' | 'development'; @@ -22,23 +27,58 @@ export function readPackageJson(): Record | false { return JSON.parse(jsonContent); } +const configEnvServe: ConfigEnv = { + mode: 'development', + command: 'serve', + ssrBuild: false, +}; + +const configEnvBuild: ConfigEnv = { + mode: 'production', + command: 'build', + ssrBuild: false, +}; + // Vite config that is common to development and production mode export async function commonConfig( options: ExtendedOptions, _type: PluginConfigType -): Promise { - return { +): Promise { + const { presets } = options; + const configEnv = _type === 'development' ? configEnvServe : configEnvBuild; + + const { config: userConfig = {} } = (await loadConfigFromFile(configEnv)) ?? {}; + + const sbConfig = { configFile: false, - root: path.resolve(options.configDir, '..'), cacheDir: 'node_modules/.vite-storybook', - envPrefix, - define: {}, - plugins: await pluginConfig(options, _type), + root: path.resolve(options.configDir, '..'), + plugins: await pluginConfig(options), + // 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_'], }; + + const config: ViteConfig = mergeConfig(userConfig, sbConfig); + + // Sanitize environment variables if needed + const envsRaw = await presets.apply>('env'); + if (Object.keys(envsRaw).length) { + // Stringify env variables after getting `envPrefix` from the config + const envs = stringifyProcessEnvs(envsRaw, config.envPrefix); + config.define = { + ...config.define, + ...envs, + }; + } + + return config; } -export async function pluginConfig(options: ExtendedOptions, _type: PluginConfigType) { - const { framework, presets } = options; +export async function pluginConfig(options: ExtendedOptions) { + const { presets } = options; + const framework = await presets.apply('framework', '', options); + const frameworkName: string = typeof framework === 'object' ? framework.name : framework; const svelteOptions: Record = await presets.apply('svelteOptions', {}, options); const plugins = [ @@ -47,74 +87,14 @@ export async function pluginConfig(options: ExtendedOptions, _type: PluginConfig 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 === 'svelte') { - try { - // eslint-disable-next-line import/no-extraneous-dependencies, global-require - 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. - - let userExclude = []; - if (Array.isArray(svelteOptions?.exclude)) { - userExclude = svelteOptions?.exclude; - } else if (svelteOptions?.exclude) { - userExclude = [svelteOptions?.exclude]; - } + ] as PluginOption[]; - // 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; - } + // We need the react plugin here to support MDX in non-react projects. + if (frameworkName !== '@storybook/react-vite') { + plugins.push(viteReact()); + } + if (frameworkName === 'svelte') { // eslint-disable-next-line import/no-extraneous-dependencies, global-require const { loadSvelteConfig } = require('@sveltejs/vite-plugin-svelte'); const config = { ...loadSvelteConfig(), ...svelteOptions }; @@ -135,12 +115,12 @@ export async function pluginConfig(options: ExtendedOptions, _type: PluginConfig plugins.push(svelteDocgen(config)); } - if (framework === 'preact') { + if (frameworkName === 'preact') { // eslint-disable-next-line global-require plugins.push(require('@preact/preset-vite').default()); } - if (framework === 'glimmerx') { + if (frameworkName === 'glimmerx') { // eslint-disable-next-line global-require, import/extensions const plugin = require('vite-plugin-glimmerx/index.cjs'); plugins.push(plugin.default()); diff --git a/code/lib/builder-vite/src/vite-server.ts b/code/lib/builder-vite/src/vite-server.ts index 26ecbc4d51ad..a8a1b35b1d46 100644 --- a/code/lib/builder-vite/src/vite-server.ts +++ b/code/lib/builder-vite/src/vite-server.ts @@ -1,40 +1,30 @@ import type { Server } from 'http'; import { createServer } from 'vite'; -import { stringifyProcessEnvs } from './envs'; -import { getOptimizeDeps } from './optimizeDeps'; import { commonConfig } from './vite-config'; -import type { EnvsRaw, ExtendedOptions } from './types'; +import type { ExtendedOptions } from './types'; +import { getOptimizeDeps } from './optimizeDeps'; export async function createViteServer(options: ExtendedOptions, devServer: Server) { - const { port, presets } = options; + const { presets } = options; + + const config = await commonConfig(options, 'development'); - const baseConfig = await commonConfig(options, 'development'); - const defaultConfig = { - ...baseConfig, - server: { - middlewareMode: true, - hmr: { - port, - server: devServer, - }, - fs: { - strict: true, - }, + // Set up dev server + config.server = { + middlewareMode: true, + hmr: { + port: options.port, + server: devServer, + }, + fs: { + strict: true, }, - appType: 'custom' as const, - optimizeDeps: await getOptimizeDeps(baseConfig, options), }; + config.appType = 'custom'; - const finalConfig = await presets.apply('viteFinal', defaultConfig, options); - - const envsRaw = await presets.apply>('env'); - // Stringify env variables after getting `envPrefix` from the final config - const envs = stringifyProcessEnvs(envsRaw, finalConfig.envPrefix); - // Update `define` - finalConfig.define = { - ...finalConfig.define, - ...envs, - }; + // TODO: find a way to avoid having to do this in a separate step. + config.optimizeDeps = await getOptimizeDeps(config, options); + const finalConfig = await presets.apply('viteFinal', config, options); return createServer(finalConfig); }