Skip to content

Commit 28156d3

Browse files
Merge pull request storybookjs#34080 from holvi-sebastian/next
Builder-Vite: Support configLoader via builder options
2 parents db04b12 + c8c1840 commit 28156d3

12 files changed

Lines changed: 241 additions & 3 deletions

File tree

code/addons/vitest/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
},
8282
"devDependencies": {
8383
"@storybook/addon-a11y": "workspace:*",
84+
"@storybook/builder-vite": "workspace:*",
8485
"@types/istanbul-lib-report": "^3.0.3",
8586
"@types/micromatch": "^4.0.0",
8687
"@types/node": "^22.19.1",

code/addons/vitest/src/node/boot-test-runner.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
} from 'storybook/internal/core-server';
1010
import type { EventInfo, Options } from 'storybook/internal/types';
1111

12+
import type { BuilderOptions } from '@storybook/builder-vite';
13+
1214
import { normalize } from 'pathe';
1315

1416
import { importMetaResolve } from '../../../../core/src/shared/utils/module.ts';
@@ -49,10 +51,12 @@ const bootTestRunner = async ({
4951
channel,
5052
store,
5153
options,
54+
configLoader,
5255
}: {
5356
channel: Channel;
5457
store: Store;
5558
options: Options;
59+
configLoader?: BuilderOptions['configLoader'];
5660
}) => {
5761
let stderr: string[] = [];
5862
const killChild = () => {
@@ -86,6 +90,7 @@ const bootTestRunner = async ({
8690
VITEST_CHILD_PROCESS: 'true',
8791
NODE_ENV: process.env.NODE_ENV ?? 'test',
8892
STORYBOOK_CONFIG_DIR: normalize(options.configDir),
93+
STORYBOOK_CONFIG_LOADER: configLoader,
8994
},
9095
extendEnv: true,
9196
},
@@ -159,19 +164,21 @@ export const runTestRunner = async ({
159164
initEvent,
160165
initArgs,
161166
options,
167+
configLoader,
162168
}: {
163169
channel: Channel;
164170
store: Store;
165171
initEvent?: string;
166172
initArgs?: any[];
167173
options: Options;
174+
configLoader?: BuilderOptions['configLoader'];
168175
}) => {
169176
if (!ready && initEvent) {
170177
eventQueue.push({ type: initEvent, args: initArgs });
171178
}
172179
if (!child) {
173180
ready = false;
174-
await bootTestRunner({ channel, store, options });
181+
await bootTestRunner({ channel, store, options, configLoader });
175182
ready = true;
176183
}
177184
};

code/addons/vitest/src/node/test-manager.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import type {
88
TestProviderStoreById,
99
} from 'storybook/internal/types';
1010

11+
import type { BuilderOptions } from '@storybook/builder-vite';
12+
1113
import { throttle } from 'es-toolkit/function';
1214
import type { Report } from 'storybook/preview-api';
1315

@@ -26,6 +28,7 @@ import { VitestManager } from './vitest-manager.ts';
2628

2729
export type TestManagerOptions = {
2830
storybookOptions: Options;
31+
configLoader?: BuilderOptions['configLoader'];
2932
store: experimental_UniversalStore<StoreState, StoreEvent>;
3033
componentTestStatusStore: StatusStoreByTypeId;
3134
a11yStatusStore: StatusStoreByTypeId;
@@ -57,6 +60,8 @@ export class TestManager {
5760

5861
public storybookOptions: Options;
5962

63+
public readonly configLoader?: TestManagerOptions['configLoader'];
64+
6065
private batchedTestCaseResults: {
6166
storyId: string;
6267
testResult: TestResult;
@@ -70,6 +75,7 @@ export class TestManager {
7075
this.testProviderStore = options.testProviderStore;
7176
this.onReady = options.onReady;
7277
this.storybookOptions = options.storybookOptions;
78+
this.configLoader = options.configLoader;
7379

7480
this.vitestManager = new VitestManager(this);
7581

code/addons/vitest/src/node/vitest-manager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export class VitestManager {
127127
try {
128128
this.vitest = await createVitest('test', {
129129
root: vitestWorkspaceConfig ?? vitestConfigFallbackLocation,
130+
configLoader: this.testManager.configLoader,
130131
watch: true,
131132
passWithNoTests: false,
132133
project: [projectName],

code/addons/vitest/src/node/vitest.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
experimental_getTestProviderStore,
88
} from 'storybook/internal/core-server';
99

10+
import type { BuilderOptions } from '@storybook/builder-vite';
11+
1012
import {
1113
ADDON_ID,
1214
STATUS_TYPE_ID_A11Y,
@@ -48,6 +50,7 @@ new TestManager({
4850
storybookOptions: {
4951
configDir: process.env.STORYBOOK_CONFIG_DIR || '',
5052
} as any,
53+
configLoader: process.env.STORYBOOK_CONFIG_LOADER as BuilderOptions['configLoader'],
5154
});
5255

5356
const exit = (code = 0) => {

code/addons/vitest/src/preset.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import type {
2121
StoryId,
2222
} from 'storybook/internal/types';
2323

24+
import type { BuilderOptions } from '@storybook/builder-vite';
25+
2426
import { isEqual } from 'es-toolkit/predicate';
2527
import picocolors from 'picocolors';
2628
import { dedent } from 'ts-dedent';
@@ -82,6 +84,10 @@ export const experimental_serverChannel = async (channel: Channel, options: Opti
8284
return channel;
8385
}
8486

87+
const configLoader =
88+
typeof core.builder !== 'string' &&
89+
(core.builder?.options?.configLoader as BuilderOptions['configLoader']);
90+
8591
const storyIndexGenerator =
8692
await options.presets.apply<Promise<StoryIndexGenerator>>('storyIndexGenerator');
8793

@@ -136,6 +142,7 @@ export const experimental_serverChannel = async (channel: Channel, options: Opti
136142
initEvent: STORE_CHANNEL_EVENT_NAME,
137143
initArgs: [{ event, eventInfo }],
138144
options,
145+
configLoader: configLoader || undefined,
139146
});
140147
});
141148
store.subscribe('TOGGLE_WATCHING', (event, eventInfo) => {
@@ -157,6 +164,7 @@ export const experimental_serverChannel = async (channel: Channel, options: Opti
157164
initEvent: STORE_CHANNEL_EVENT_NAME,
158165
initArgs: [{ event, eventInfo }],
159166
options,
167+
configLoader: configLoader || undefined,
160168
});
161169
}
162170
});

code/builders/builder-vite/src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,11 @@ export type StorybookConfigVite = {
2121
export type BuilderOptions = {
2222
/** Path to `vite.config` file, relative to `process.cwd()`. */
2323
viteConfigPath?: string;
24+
/**
25+
* How Vite loads the config file. Equivalent to Vite's `--configLoader` CLI flag and the
26+
* `configLoader` option of `loadConfigFromFile`.
27+
*
28+
* Requires Vite 6.1.0 or higher. On older Vite versions this option is silently ignored.
29+
*/
30+
configLoader?: 'bundle' | 'runner' | 'native';
2431
};

code/builders/builder-vite/src/vite-config.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,50 @@ describe('commonConfig', () => {
4949
expect(config.configFile).toBe(false);
5050
expect(config.plugins).toBeDefined();
5151
});
52+
53+
it('should pass configLoader option to loadConfigFromFile', async () => {
54+
const optionsWithConfigLoader: Options = {
55+
...dummyOptions,
56+
presets: {
57+
apply: async (key: string) =>
58+
({
59+
framework: { name: '' },
60+
addons: [],
61+
core: {
62+
builder: {
63+
name: '@storybook/builder-vite',
64+
options: {
65+
configLoader: 'native',
66+
},
67+
},
68+
},
69+
options: {},
70+
})[key],
71+
} as Presets,
72+
};
73+
74+
// Inline mock: this test asserts a specific call signature, so it needs its
75+
// own one-shot return value distinct from the shared default mock.
76+
loadConfigFromFileMock.mockReturnValueOnce(
77+
Promise.resolve({
78+
config: {},
79+
path: '',
80+
dependencies: [],
81+
})
82+
);
83+
84+
await commonConfig(optionsWithConfigLoader, 'development');
85+
86+
// Verify loadConfigFromFile was called with configLoader as the 6th argument
87+
expect(loadConfigFromFileMock).toHaveBeenCalledWith(
88+
expect.objectContaining({ command: 'serve' }),
89+
undefined,
90+
expect.any(String),
91+
undefined,
92+
undefined,
93+
'native'
94+
);
95+
});
5296
});
5397

5498
describe('storybookConfigPlugin', () => {

code/builders/builder-vite/src/vite-config.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,22 @@ export async function commonConfig(
4242
const configEnv = _type === 'development' ? configEnvServe : configEnvBuild;
4343
const { loadConfigFromFile, mergeConfig } = await import('vite');
4444

45-
const { viteConfigPath } = await getBuilderOptions<BuilderOptions>(options);
45+
const { viteConfigPath, configLoader } = await getBuilderOptions<BuilderOptions>(options);
4646

4747
const projectRoot = resolve(options.configDir, '..');
4848

4949
// I destructure away the `build` property from the user's config object
5050
// I do this because I can contain config that breaks storybook, such as we had in a lit project.
5151
// If the user needs to configure the `build` they need to do so in the viteFinal function in main.js.
5252
const { config: { build: buildProperty = undefined, ...userConfig } = {} } =
53-
(await loadConfigFromFile(configEnv, viteConfigPath, projectRoot)) ?? {};
53+
(await loadConfigFromFile(
54+
configEnv,
55+
viteConfigPath,
56+
projectRoot,
57+
undefined,
58+
undefined,
59+
configLoader
60+
)) ?? {};
5461

5562
// Storybook's Vite config is assembled from self-contained plugins.
5663
// The config plugin handles base settings (root, cacheDir, resolve conditions, etc.),
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
```ts filename=".storybook/main.ts" renderer="common" language="ts" tabTitle="CSF 3"
2+
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
3+
import type { StorybookConfig } from '@storybook/your-framework';
4+
5+
const config: StorybookConfig = {
6+
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
7+
core: {
8+
builder: {
9+
name: '@storybook/builder-vite',
10+
options: {
11+
configLoader: 'runner',
12+
},
13+
},
14+
},
15+
};
16+
17+
export default config;
18+
```
19+
20+
```js filename=".storybook/main.js" renderer="common" language="js" tabTitle="CSF 3"
21+
export default {
22+
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
23+
core: {
24+
builder: {
25+
name: '@storybook/builder-vite',
26+
options: {
27+
configLoader: 'runner',
28+
},
29+
},
30+
},
31+
};
32+
```
33+
34+
```ts filename=".storybook/main.ts" renderer="react" language="ts" tabTitle="CSF Next 🧪"
35+
// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
36+
import { defineMain } from '@storybook/your-framework/node';
37+
38+
export default defineMain({
39+
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
40+
core: {
41+
builder: {
42+
name: '@storybook/builder-vite',
43+
options: {
44+
configLoader: 'runner',
45+
},
46+
},
47+
},
48+
});
49+
```
50+
51+
<!-- JS snippets still needed while providing both CSF 3 & Next -->
52+
53+
```js filename=".storybook/main.js" renderer="react" language="js" tabTitle="CSF Next 🧪"
54+
// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
55+
import { defineMain } from '@storybook/your-framework/node';
56+
57+
export default defineMain({
58+
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
59+
60+
core: {
61+
builder: {
62+
name: '@storybook/builder-vite',
63+
options: {
64+
configLoader: 'runner',
65+
},
66+
},
67+
},
68+
});
69+
```
70+
71+
```ts filename=".storybook/main.ts" renderer="vue" language="ts" tabTitle="CSF Next 🧪"
72+
import { defineMain } from '@storybook/vue3-vite/node';
73+
74+
export default defineMain({
75+
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
76+
77+
core: {
78+
builder: {
79+
name: '@storybook/builder-vite',
80+
options: {
81+
configLoader: 'runner',
82+
},
83+
},
84+
},
85+
});
86+
```
87+
88+
<!-- JS snippets still needed while providing both CSF 3 & Next -->
89+
90+
```js filename=".storybook/main.js" renderer="vue" language="js" tabTitle="CSF Next 🧪"
91+
import { defineMain } from '@storybook/vue3-vite/node';
92+
93+
export default defineMain({
94+
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
95+
96+
core: {
97+
builder: {
98+
name: '@storybook/builder-vite',
99+
options: {
100+
configLoader: 'runner',
101+
},
102+
},
103+
},
104+
});
105+
```
106+
107+
```ts filename=".storybook/main.ts" renderer="web-components" language="ts" tabTitle="CSF Next 🧪"
108+
import { defineMain } from '@storybook/web-components-vite/node';
109+
110+
export default defineMain({
111+
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
112+
113+
core: {
114+
builder: {
115+
name: '@storybook/builder-vite',
116+
options: {
117+
configLoader: 'runner',
118+
},
119+
},
120+
},
121+
});
122+
```
123+
124+
<!-- JS snippets still needed while providing both CSF 3 & Next -->
125+
126+
```js filename=".storybook/main.js" renderer="web-components" language="js" tabTitle="CSF Next 🧪"
127+
import { defineMain } from '@storybook/web-components-vite/node';
128+
129+
export default defineMain({
130+
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
131+
132+
core: {
133+
builder: {
134+
name: '@storybook/builder-vite',
135+
options: {
136+
configLoader: 'runner',
137+
},
138+
},
139+
},
140+
});
141+
```

0 commit comments

Comments
 (0)