diff --git a/code/lib/vite-plugin-storybook-nextjs/src/index.test.ts b/code/lib/vite-plugin-storybook-nextjs/src/index.test.ts index c122c485f68b..580117af3852 100644 --- a/code/lib/vite-plugin-storybook-nextjs/src/index.test.ts +++ b/code/lib/vite-plugin-storybook-nextjs/src/index.test.ts @@ -145,5 +145,11 @@ describe('VitePlugin', () => { }, }); expect(config?.resolve).not.toHaveProperty('alias'); + expect(config?.test?.alias).toEqual( + expect.objectContaining({ + 'next/dist/compiled/react': '/resolved/next/dist/compiled', + 'next/dist/compiled/react-dom': '/resolved/next/dist/compiled', + }) + ); }); }); diff --git a/code/lib/vite-plugin-storybook-nextjs/src/index.ts b/code/lib/vite-plugin-storybook-nextjs/src/index.ts index d24919ea0fa4..4580378b7fcd 100644 --- a/code/lib/vite-plugin-storybook-nextjs/src/index.ts +++ b/code/lib/vite-plugin-storybook-nextjs/src/index.ts @@ -1,4 +1,4 @@ -import { resolve } from 'pathe'; +import { dirname, resolve } from 'pathe'; import { createRequire } from 'node:module'; import type { NextConfigComplete } from 'next/dist/server/config-shared.js'; @@ -28,6 +28,8 @@ import { import { loadNextConfig } from './utils/next-config.ts'; const require = createRequire(import.meta.url); +const compiledReactDir = dirname(require.resolve('next/dist/compiled/react')); +const compiledReactDomDir = dirname(require.resolve('next/dist/compiled/react-dom')); export type PluginOptions = { /** @@ -167,6 +169,8 @@ function VitePlugin({ dir = process.cwd(), image }: PluginOptions = {}): PluginO }, test: { alias: { + 'next/dist/compiled/react': compiledReactDir, + 'next/dist/compiled/react-dom': compiledReactDomDir, 'react/jsx-dev-runtime': require.resolve('next/dist/compiled/react/jsx-dev-runtime.js'), 'react/jsx-runtime': require.resolve('next/dist/compiled/react/jsx-runtime.js'), diff --git a/code/lib/vite-plugin-storybook-nextjs/src/utils/next-config.test.ts b/code/lib/vite-plugin-storybook-nextjs/src/utils/next-config.test.ts index 117ed24d9497..687f0cb08ff7 100644 --- a/code/lib/vite-plugin-storybook-nextjs/src/utils/next-config.test.ts +++ b/code/lib/vite-plugin-storybook-nextjs/src/utils/next-config.test.ts @@ -22,7 +22,7 @@ describe('loadNextConfig', () => { }); it('uses Next.js config loading unchanged by default', async () => { - const config = { distDir: '.next' }; + const config = { distDir: '.next', experimental: {}, configFileName: 'next.config.js' }; loadConfigMock.mockResolvedValue(config); await expect(loadNextConfig(phase, dir)).resolves.toBe(config); @@ -80,4 +80,57 @@ describe('loadNextConfig', () => { typedEnv: true, }); }); + + it('normalizes a cached raw config module that is missing Next.js defaults', async () => { + const cachedRawConfig = { distDir: '.next' }; + const normalizedConfig = { + distDir: '.next', + experimental: { + turbopackRustReactCompiler: true, + typedEnv: true, + }, + }; + const resolvedConfig = { + distDir: '.next', + experimental: { typedEnv: true }, + }; + + loadConfigMock.mockResolvedValueOnce(cachedRawConfig).mockResolvedValueOnce(resolvedConfig); + normalizeConfigMock.mockResolvedValue(normalizedConfig); + + await expect(loadNextConfig(phase, dir)).resolves.toBe(resolvedConfig); + expect(loadConfigMock).toHaveBeenNthCalledWith(1, phase, dir); + expect(normalizeConfigMock).toHaveBeenCalledWith(phase, cachedRawConfig); + expect(loadConfigMock).toHaveBeenNthCalledWith(2, phase, dir, { + customConfig: { + distDir: '.next', + experimental: { typedEnv: true }, + }, + }); + }); + + it('normalizes a cached raw config that already has a user experimental option', async () => { + const cachedRawConfig = { experimental: { typedEnv: true } }; + const normalizedConfig = { + experimental: { + turbopackRustReactCompiler: true, + typedEnv: true, + }, + }; + const resolvedConfig = { + experimental: { typedEnv: true }, + configFileName: 'next.config.js', + }; + + loadConfigMock.mockResolvedValueOnce(cachedRawConfig).mockResolvedValueOnce(resolvedConfig); + normalizeConfigMock.mockResolvedValue(normalizedConfig); + + await expect(loadNextConfig(phase, dir)).resolves.toBe(resolvedConfig); + expect(normalizeConfigMock).toHaveBeenCalledWith(phase, cachedRawConfig); + expect(loadConfigMock).toHaveBeenNthCalledWith(2, phase, dir, { + customConfig: { + experimental: { typedEnv: true }, + }, + }); + }); }); diff --git a/code/lib/vite-plugin-storybook-nextjs/src/utils/next-config.ts b/code/lib/vite-plugin-storybook-nextjs/src/utils/next-config.ts index abb535f54e86..0d586d2f76b5 100644 --- a/code/lib/vite-plugin-storybook-nextjs/src/utils/next-config.ts +++ b/code/lib/vite-plugin-storybook-nextjs/src/utils/next-config.ts @@ -12,15 +12,32 @@ export async function loadNextConfig( phase: Parameters[0], dir: string ): Promise { + let nextConfig: NextConfigComplete; + try { - return await loadConfig(phase, dir); + nextConfig = await loadConfig(phase, dir); } catch (error) { if (!isTurbopackRustReactCompilerError(error)) { throw error; } + + return loadNormalizedNextConfig(phase, dir); + } + + if (isResolvedNextConfig(nextConfig)) { + return nextConfig; } - const rawConfigModule = await loadConfig(phase, dir, { rawConfig: true }); + return loadNormalizedNextConfig(phase, dir, nextConfig); +} + +async function loadNormalizedNextConfig( + phase: Parameters[0], + dir: string, + cachedRawConfigModule?: unknown +): Promise { + const rawConfigModule = + cachedRawConfigModule ?? (await loadConfig(phase, dir, { rawConfig: true })); const rawConfig = interopDefault(rawConfigModule); const normalizedConfig = await normalizeConfig(phase, rawConfig); const { turbopackRustReactCompiler: _turbopackRustReactCompiler, ...experimental } = @@ -34,10 +51,18 @@ export async function loadNextConfig( }); } +function isResolvedNextConfig(config: NextConfigComplete): boolean { + return typeof config.configFileName === 'string'; +} + function isTurbopackRustReactCompilerError(error: unknown): boolean { return error instanceof Error && error.message.startsWith(TURBOPACK_RUST_REACT_COMPILER_ERROR); } -function interopDefault(module: T | { default: T }): T { - return (module as { default?: T }).default ?? (module as T); +function interopDefault(module: unknown): unknown { + if (typeof module === 'object' && module !== null && 'default' in module) { + return (module as { default: unknown }).default ?? module; + } + + return module; }