From deebf2f32ca80e774bd262442c5658287d00ef9a Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Tue, 20 Oct 2026 15:15:39 +0200 Subject: [PATCH] fix(builder-vite): print the found Vite config path with forward slashes The discovery-miss warning embedded the path from empathic's find.any verbatim. On Windows that is a backslash path, so the suggested snippet `viteConfigPath: 'C:\project\vite.config.mjs'` is not a valid path once pasted into a JS string (`\v` is a vertical tab), and the nested-configDir test failed on the daily Windows unit job because it expected the posix form. Normalise the path with pathe before printing it, compute the expected string the same way in the test, and add a test that feeds a Windows path and expects forward slashes. Co-Authored-By: Claude Fable 5.1 --- .../builder-vite/src/vite-config.test.ts | 27 ++++++++++++++++--- code/builders/builder-vite/src/vite-config.ts | 9 +++++-- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/code/builders/builder-vite/src/vite-config.test.ts b/code/builders/builder-vite/src/vite-config.test.ts index 88c09db09d87..50f1f00d29d4 100644 --- a/code/builders/builder-vite/src/vite-config.test.ts +++ b/code/builders/builder-vite/src/vite-config.test.ts @@ -9,6 +9,7 @@ import { getProjectRoot } from 'storybook/internal/common'; import type { Options, Presets } from 'storybook/internal/types'; import * as find from 'empathic/find'; +import { normalize } from 'pathe'; import { loadConfigFromFile } from 'vite'; import { storybookConfigPlugin } from './plugins/storybook-config-plugin.ts'; @@ -208,8 +209,11 @@ describe('resolveVitePublicDir', () => { describe('discovery miss reporting', () => { it('warns when configDir is nested and the Vite config sits at the project root', async () => { + // `resolve` gives the drive-letter, backslash form on Windows, as `find.any` does there. + const found = resolve('/project/vite.config.mjs'); + const printed = normalize(found); vi.mocked(getProjectRoot).mockReturnValue('/project'); - vi.mocked(find.any).mockReturnValueOnce(resolve('/project/vite.config.mjs')); + vi.mocked(find.any).mockReturnValueOnce(found); loadConfigFromFileMock.mockResolvedValueOnce(null); await commonConfig( @@ -222,13 +226,30 @@ describe('discovery miss reporting', () => { last: '/project', }); expect(loggerWarnMock).toHaveBeenCalledTimes(1); - expect(loggerWarnMock.mock.calls[0][0]).toContain('/project/vite.config.mjs'); + expect(loggerWarnMock.mock.calls[0][0]).toContain(`found at "${printed}"`); expect(loggerWarnMock.mock.calls[0][0]).toContain( - "core: { builder: { options: { viteConfigPath: '/project/vite.config.mjs' } } }" + `core: { builder: { options: { viteConfigPath: '${printed}' } } }` ); expect(loggerWarnMock.mock.calls[0][0]).toContain('viteConfigPath: null'); }); + it('prints a Windows config path with forward slashes so the snippet stays pasteable', async () => { + vi.mocked(getProjectRoot).mockReturnValue('/project'); + vi.mocked(find.any).mockReturnValueOnce('C:\\project\\vite.config.mjs'); + loadConfigFromFileMock.mockResolvedValueOnce(null); + + await commonConfig( + { ...dummyOptions, configDir: '/project/configuration/storybook' }, + 'development' + ); + + expect(loggerWarnMock).toHaveBeenCalledTimes(1); + expect(loggerWarnMock.mock.calls[0][0]).toContain('found at "C:/project/vite.config.mjs"'); + expect(loggerWarnMock.mock.calls[0][0]).toContain( + "core: { builder: { options: { viteConfigPath: 'C:/project/vite.config.mjs' } } }" + ); + }); + it('stays quiet when no Vite config exists in the project at all', async () => { vi.mocked(getProjectRoot).mockReturnValue('/project'); vi.mocked(find.any).mockReturnValueOnce(undefined); diff --git a/code/builders/builder-vite/src/vite-config.ts b/code/builders/builder-vite/src/vite-config.ts index 729e0820a964..912c1b6147ef 100644 --- a/code/builders/builder-vite/src/vite-config.ts +++ b/code/builders/builder-vite/src/vite-config.ts @@ -10,6 +10,7 @@ import { once } from 'storybook/internal/node-logger'; import type { Options } from 'storybook/internal/types'; import * as find from 'empathic/find'; +import { normalize } from 'pathe'; import { dedent } from 'ts-dedent'; import type { ConfigEnv, @@ -64,15 +65,19 @@ function reportUnloadedUserViteConfig(configDir: string, projectRoot: string): v return; } - const unloadedConfig = find.any(DEFAULT_VITE_CONFIG_FILES, { + const found = find.any(DEFAULT_VITE_CONFIG_FILES, { cwd: projectRoot, last: root, }); - if (!unloadedConfig) { + if (!found) { return; } + // Forward slashes keep the pasteable snippet valid on Windows, where a backslash path inside + // a JS string literal reads as escapes (`'C:\vite.config.ts'` holds a vertical tab). + const unloadedConfig = normalize(found); + warnOnce( dedent` No user Vite config was loaded: Storybook auto-discovers a Vite config file in "${projectRoot}" (the directory above your Storybook config dir "${configDir}"), and none was found there.