Vite: Allow specifying path to vite.config file (#20681)

Issue: #20552

## What I did

I added a new builder option for the Vite builder named `viteConfigPath`, which is a path to a custom config relative to the cwd.  If the path does not resolve, a clear error will be thrown with the absolute path being checked.

I also added a new utility, `getBuilderOptions()`, which is helpful because these options can be specified two different ways, with `core.builder.options` or `framework.options.builder`.  The utility gives an easy way to check both.  It gives preference to ~`core.builder.options`~ `framework.options.builder`, since that's the newer way to define the options.

## How to test

1. `yarn task --task sandbox --start-from auto --template react-vite/default-ts`
2. Move the `vite.config.ts` file into `.storybook`
3. Add an alias, like:
```ts
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      stories: path.resolve(__dirname, '../src/stories'),
    },
  },
});
```
4. Change one of the stories to use this alias, such as the Button story:
```ts
import { Button } from 'stories/Button';
```
5. Start Storybook, it will fail
6. Add `viteConfigPath: ".storybook/vite.config.ts"` to the builder options in `.storybook/main.ts`
7. Restart storybook, and it should work.


## Checklist

<!-- Please check (put an "x" inside the "[ ]") the applicable items below to make sure your PR is ready to be reviewed. -->

- [ ] Make sure your changes are tested (stories and/or unit, integration, or end-to-end tests)
- [X] Make sure to add/update documentation regarding your changes
- [ ] If you are deprecating/removing a feature, make sure to update
      [MIGRATION.MD](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md)

Is there a way to create an automated test for this?  I couldn't think of a good way to do it.

#### Maintainers

- [ ] If this PR should be tested against many or all sandboxes,
      make sure to add the `ci:merged` or `ci:daily` GH label to it.
- [X] Make sure this PR contains **one** of the labels below.

`["cleanup", "BREAKING CHANGE", "feature request", "bug", "documentation", "maintenance", "dependencies", "other"]`

<!--

Everybody: Please submit all PRs to the `next` branch unless they are specific to the current release. Storybook maintainers cherry-pick bug and documentation fixes into the `main` branch as part of the release process, so you shouldn't need to worry about this. For additional guidance: https://storybook.js.org/docs/react/contribute/how-to-contribute

-->
This commit is contained in:
Ian VanSchooten 2023-01-20 09:17:29 -05:00 committed by GitHub
commit 994a26df8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 53 additions and 4 deletions

View File

@ -66,8 +66,26 @@ For other details about the differences between vite and webpack projects, be su
### Customize Vite config
The builder _will_ read your `vite.config.js` file, though it may change some of the options in order to work correctly.
It looks for the Vite config in the CWD. If your config is located elsewhere, specify the path using the `viteConfigPath` builder option:
In `.storybook/main.js` (or whatever your Storybook config file is named), you can override the merged Vite config:
```javascript
// .storybook/main.mjs
const config = {
framework: {
name: '@storybook/react-vite', // Your framework name here.
options: {
builder: {
viteConfigPath: '.storybook/customViteConfig.js',
},
},
},
};
export default config;
```
You can also override the merged Vite config:
```javascript
// use `mergeConfig` to recursively merge Vite options

View File

@ -17,4 +17,9 @@ export type StorybookConfigVite = {
viteFinal?: ViteFinal;
};
export type BuilderOptions = {};
export type BuilderOptions = {
/**
* Path to vite.config file, relative to CWD.
*/
viteConfigPath?: string;
};

View File

@ -8,7 +8,7 @@ import type {
InlineConfig,
} from 'vite';
import { viteExternalsPlugin } from 'vite-plugin-externals';
import { isPreservingSymlinks, getFrameworkName } from '@storybook/core-common';
import { isPreservingSymlinks, getFrameworkName, getBuilderOptions } from '@storybook/core-common';
import { globals } from '@storybook/preview/globals';
import type { Options } from '@storybook/types';
import {
@ -18,6 +18,7 @@ import {
mdxPlugin,
stripStoryHMRBoundary,
} from './plugins';
import type { BuilderOptions } from './types';
export type PluginConfigType = 'build' | 'development';
@ -39,12 +40,13 @@ export async function commonConfig(
_type: PluginConfigType
): Promise<ViteInlineConfig> {
const configEnv = _type === 'development' ? configEnvServe : configEnvBuild;
const { viteConfigPath } = await getBuilderOptions<BuilderOptions>(options);
// I destructure away the `build` property from the user's config object
// I do this because I can contain config that breaks storybook, such as we had in a lit project.
// If the user needs to configure the `build` they need to do so in the viteFinal function in main.js.
const { config: { build: buildProperty = undefined, ...userConfig } = {} } =
(await loadConfigFromFile(configEnv)) ?? {};
(await loadConfigFromFile(configEnv, viteConfigPath)) ?? {};
const sbConfig: InlineConfig = {
configFile: false,

View File

@ -6,6 +6,7 @@ export * from './utils/cache';
export * from './utils/check-addon-order';
export * from './utils/envs';
export * from './utils/findDistEsm';
export * from './utils/get-builder-options';
export * from './utils/get-framework-name';
export * from './utils/get-renderer-name';
export * from './utils/get-storybook-configuration';

View File

@ -0,0 +1,23 @@
import type { Options } from '@storybook/types';
/**
* Builder options can be specified in `core.builder.options` or `framework.options.builder`.
* Preference is given here to `framework.options.builder` if both are specified.
*/
export async function getBuilderOptions<T extends Record<string, any>>(
options: Options
): Promise<T | Record<string, never>> {
const framework = await options.presets.apply('framework', {}, options);
if (typeof framework !== 'string' && framework?.options?.builder) {
return framework.options.builder;
}
const { builder } = await options.presets.apply('core', {}, options);
if (typeof builder !== 'string' && builder?.options) {
return builder.options as T;
}
return {};
}