Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vite: Automatically use vite.config.js #19026

Merged
merged 11 commits into from
Sep 1, 2022
64 changes: 1 addition & 63 deletions code/frameworks/svelte-vite/src/preset.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from 'path';
import fs from 'fs';
import type { StorybookConfig } from '@storybook/builder-vite';
import { svelteDocgen } from './plugins/svelte-docgen';

export const addons: StorybookConfig['addons'] = ['@storybook/svelte'];

Expand All @@ -20,70 +21,7 @@ export function readPackageJson(): Record<string, any> | false {

export const viteFinal: StorybookConfig['viteFinal'] = async (config, { presets }) => {
const { plugins = [] } = config;
const svelteOptions = await presets.apply<Record<string, any>>('frameworkOptions');
try {
// eslint-disable-next-line 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];
}

// 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;
}

// eslint-disable-next-line global-require
const { loadSvelteConfig } = require('@sveltejs/vite-plugin-svelte');
const csfConfig = { ...loadSvelteConfig(), ...svelteOptions };

try {
// eslint-disable-next-line global-require
const csfPlugin = require('./plugins/csf-plugin').default;
plugins.push(csfPlugin(csfConfig));
} 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));

return {
Expand Down
18 changes: 2 additions & 16 deletions code/frameworks/vue3-vite/src/preset.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from 'path';
import fs from 'fs';
import type { StorybookConfig } from '@storybook/builder-vite';
import { vueDocgen } from './plugins/vue-docgen';

export const addons: StorybookConfig['addons'] = ['@storybook/vue3'];

Expand All @@ -21,22 +22,7 @@ export function readPackageJson(): Record<string, any> | false {
export const viteFinal: StorybookConfig['viteFinal'] = async (config, { presets }) => {
const { plugins = [] } = config;

try {
// eslint-disable-next-line global-require
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;
}
plugins.push(vueDocgen());

const updated = {
...config,
Expand Down
25 changes: 6 additions & 19 deletions code/lib/builder-vite/src/build.ts
Original file line number Diff line number Diff line change
@@ -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<Promise<EnvsRaw>>('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);
}
3 changes: 0 additions & 3 deletions code/lib/builder-vite/src/envs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 5 additions & 6 deletions code/lib/builder-vite/src/optimizeDeps.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -101,13 +102,11 @@ const INCLUDE_CANDIDATES = [
const asyncFilter = async (arr: string[], predicate: (val: string) => Promise<boolean>) =>
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.
Expand Down
27 changes: 27 additions & 0 deletions code/lib/builder-vite/src/plugins/strip-story-hmr-boundaries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Plugin } from 'vite';
import { createFilter } from 'vite';
import MagicString from 'magic-string';

/**
* This plugin removes HMR `accept` calls in story files. Stories should not be treated
* as hmr boundaries, but vite has a bug which causes them to be treated as boundaries
* (https://github.com/vitejs/vite/issues/9869).
*/
export function stripStoryHMRBoundary(): Plugin {
const filter = createFilter(/\.stories\.([tj])sx?$/);
return {
name: 'storybook:strip-hmr-boundary',
enforce: 'post',
async transform(src: string, id: string) {
if (!filter(id)) return undefined;

const s = new MagicString(src);
s.replace(/import\.meta\.hot\.accept\(\);/, '');

return {
code: s.toString(),
map: s.generateMap({ hires: true, source: id }),
};
},
};
}
89 changes: 57 additions & 32 deletions code/lib/builder-vite/src/vite-config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
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 { isPreservingSymlinks, getFrameworkName } from '@storybook/core-common';
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 { stripStoryHMRBoundary } from './plugins/strip-story-hmr-boundaries';
import type { ExtendedOptions, EnvsRaw } from './types';

export type PluginConfigType = 'build' | 'development';

Expand All @@ -23,23 +29,56 @@ export function readPackageJson(): Record<string, any> | 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<UserConfig & { configFile: false; root: string }> {
return {
): Promise<ViteInlineConfig> {
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: {},
root: path.resolve(options.configDir, '..'),
plugins: await pluginConfig(options),
resolve: { preserveSymlinks: isPreservingSymlinks() },
plugins: await pluginConfig(options, _type),
// 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<Promise<EnvsRaw>>('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) {
export async function pluginConfig(options: ExtendedOptions) {
const frameworkName = await getFrameworkName(options);

const plugins = [
Expand All @@ -48,27 +87,13 @@ 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(
frameworkName === '@storybook/react-vite' ? [] : [/\.([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[];
stripStoryHMRBoundary(),
] as PluginOption[];

// We need the react plugin here to support MDX in non-react projects.
if (frameworkName !== '@storybook/react-vite') {
plugins.push(viteReact({ exclude: [/\.stories\.([tj])sx?$/, /node_modules/, /\.([tj])sx?$/] }));
}

// TODO: framework doesn't exist, should move into framework when/if built
if (frameworkName === '@storybook/preact-vite') {
Expand Down
46 changes: 18 additions & 28 deletions code/lib/builder-vite/src/vite-server.ts
Original file line number Diff line number Diff line change
@@ -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<Promise<EnvsRaw>>('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);
}