diff --git a/code/frameworks/nextjs-vite/build-config.ts b/code/frameworks/nextjs-vite/build-config.ts index 1b213e76d805..c950ab2c894b 100644 --- a/code/frameworks/nextjs-vite/build-config.ts +++ b/code/frameworks/nextjs-vite/build-config.ts @@ -11,6 +11,11 @@ const config: BuildEntries = { exportEntries: ['./preview'], entryPoint: './src/preview.tsx', }, + { + exportEntries: ['./config/preview'], + entryPoint: './src/config/preview.ts', + dts: false, + }, { exportEntries: ['./cache.mock'], entryPoint: './src/export-mocks/cache/index.ts', diff --git a/code/frameworks/nextjs-vite/package.json b/code/frameworks/nextjs-vite/package.json index acb9d6e8d30e..2fc5f3939112 100644 --- a/code/frameworks/nextjs-vite/package.json +++ b/code/frameworks/nextjs-vite/package.json @@ -35,6 +35,7 @@ "types": "./dist/export-mocks/cache/index.d.ts", "default": "./dist/export-mocks/cache/index.js" }, + "./config/preview": "./dist/config/preview.js", "./headers.mock": { "types": "./dist/export-mocks/headers/index.d.ts", "default": "./dist/export-mocks/headers/index.js" @@ -88,6 +89,7 @@ "@types/node": "^22.0.0", "next": "^15.2.3", "postcss-load-config": "^6.0.1", + "semver": "^7.3.5", "typescript": "^5.8.3" }, "peerDependencies": { diff --git a/code/frameworks/nextjs-vite/src/preset.ts b/code/frameworks/nextjs-vite/src/preset.ts index eb02256953ee..b34294dd7977 100644 --- a/code/frameworks/nextjs-vite/src/preset.ts +++ b/code/frameworks/nextjs-vite/src/preset.ts @@ -11,8 +11,10 @@ import type { StorybookConfigVite } from '@storybook/builder-vite'; import { viteFinal as reactViteFinal } from '@storybook/react-vite/preset'; import postCssLoadConfig from 'postcss-load-config'; +import semver from 'semver'; import type { FrameworkOptions } from './types'; +import { getNextjsVersion } from './utils'; const require = createRequire(import.meta.url); @@ -35,8 +37,20 @@ export const core: PresetProperty<'core'> = async (config, options) => { }; export const previewAnnotations: PresetProperty<'previewAnnotations'> = (entry = []) => { - const result = [...entry, fileURLToPath(import.meta.resolve('@storybook/nextjs-vite/preview'))]; - return result; + const annotations = [ + ...entry, + fileURLToPath(import.meta.resolve('@storybook/nextjs-vite/preview')), + ]; + + const nextjsVersion = getNextjsVersion(); + const isNext16orNewer = semver.gte(nextjsVersion, '16.0.0'); + + // TODO: Remove this once we only support Next.js v16 and above + if (!isNext16orNewer) { + annotations.push(fileURLToPath(import.meta.resolve('@storybook/nextjs-vite/config/preview'))); + } + + return annotations; }; export const optimizeViteDeps = [ diff --git a/code/frameworks/nextjs-vite/src/preview.tsx b/code/frameworks/nextjs-vite/src/preview.tsx index e5eeaeb53672..6be65e13db4a 100644 --- a/code/frameworks/nextjs-vite/src/preview.tsx +++ b/code/frameworks/nextjs-vite/src/preview.tsx @@ -2,7 +2,7 @@ import type * as React from 'react'; import type { Addon_DecoratorFunction, LoaderFunction } from 'storybook/internal/types'; -import type { ReactRenderer, StoryFn } from '@storybook/react'; +import type { ReactRenderer } from '@storybook/react'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore we must ignore types here as during compilation they are not generated yet @@ -13,7 +13,6 @@ import { createRouter } from '@storybook/nextjs-vite/router.mock'; import { isNextRouterError } from 'next/dist/client/components/is-next-router-error'; -import './config/preview'; import { HeadManagerDecorator } from './head-manager/decorator'; import { ImageDecorator } from './images/decorator'; import { RouterDecorator } from './routing/decorator'; diff --git a/code/frameworks/nextjs-vite/src/utils.ts b/code/frameworks/nextjs-vite/src/utils.ts new file mode 100644 index 000000000000..5a02840b80b6 --- /dev/null +++ b/code/frameworks/nextjs-vite/src/utils.ts @@ -0,0 +1,7 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { resolvePackageDir } from '../../../core/src/shared/utils/module'; + +export const getNextjsVersion = (): string => + JSON.parse(readFileSync(join(resolvePackageDir('next'), 'package.json'), 'utf8')).version; diff --git a/code/frameworks/nextjs/build-config.ts b/code/frameworks/nextjs/build-config.ts index 85accd9abcae..e7c8f55e43c4 100644 --- a/code/frameworks/nextjs/build-config.ts +++ b/code/frameworks/nextjs/build-config.ts @@ -11,6 +11,11 @@ const config: BuildEntries = { exportEntries: ['./preview'], entryPoint: './src/preview.tsx', }, + { + exportEntries: ['./config/preview'], + entryPoint: './src/config/preview.ts', + dts: false, + }, { exportEntries: ['./cache.mock'], entryPoint: './src/export-mocks/cache/index.ts', diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json index c9de551e215a..2ebbb4e6ff95 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -35,6 +35,7 @@ "default": "./dist/export-mocks/cache/index.js" }, "./compatibility/draft-mode.compat": "./dist/compatibility/draft-mode.compat.js", + "./config/preview": "./dist/config/preview.js", "./export-mocks": "./dist/export-mocks/index.js", "./headers.mock": { "types": "./dist/export-mocks/headers/index.d.ts", diff --git a/code/frameworks/nextjs/src/config/webpack.ts b/code/frameworks/nextjs/src/config/webpack.ts index fc5a73678606..369f885557bc 100644 --- a/code/frameworks/nextjs/src/config/webpack.ts +++ b/code/frameworks/nextjs/src/config/webpack.ts @@ -1,9 +1,13 @@ import { fileURLToPath } from 'node:url'; import type { NextConfig } from 'next'; +import semver from 'semver'; import type { Configuration as WebpackConfig } from 'webpack'; -import { addScopedAlias, resolveNextConfig } from '../utils'; +import { addScopedAlias, getNextjsVersion, resolveNextConfig } from '../utils'; + +const nextjsVersion = getNextjsVersion(); +const isNext16orNewer = semver.gte(nextjsVersion, '16.0.0'); const tryResolve = (path: string) => { try { @@ -22,7 +26,10 @@ export const configureConfig = async ({ }): Promise => { const nextConfig = await resolveNextConfig({ nextConfigPath }); - addScopedAlias(baseConfig, 'next/config'); + // TODO: Remove this once we only support Next.js 16 and above + if (!isNext16orNewer) { + addScopedAlias(baseConfig, 'next/config'); + } // @ts-expect-error We know that alias is an object if (baseConfig.resolve?.alias?.['react-dom']) { @@ -58,14 +65,17 @@ const setupRuntimeConfig = async ( baseConfig: WebpackConfig, nextConfig: NextConfig ): Promise => { - const definePluginConfig: Record = { + const definePluginConfig: Record = {}; + + // TODO: Remove this once we only support Next.js 16 and above + if (!isNext16orNewer) { // this mimics what nextjs does client side // https://github.com/vercel/next.js/blob/57702cb2a9a9dba4b552e0007c16449cf36cfb44/packages/next/client/index.tsx#L101 - 'process.env.__NEXT_RUNTIME_CONFIG': JSON.stringify({ + definePluginConfig['process.env.__NEXT_RUNTIME_CONFIG'] = JSON.stringify({ serverRuntimeConfig: {}, publicRuntimeConfig: nextConfig.publicRuntimeConfig, - }), - }; + }); + } const newNextLinkBehavior = (nextConfig.experimental as any)?.newNextLinkBehavior; diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index 6acb68e17e18..27b5c5ed1a18 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -15,6 +15,7 @@ import nextBabelPreset from './babel/preset'; import { configureConfig } from './config/webpack'; import TransformFontImports from './font/babel'; import type { FrameworkOptions, StorybookConfig } from './types'; +import { getNextjsVersion } from './utils'; export const addons: PresetProperty<'addons'> = [ fileURLToPath(import.meta.resolve('@storybook/preset-react-webpack')), @@ -48,8 +49,17 @@ export const core: PresetProperty<'core'> = async (config, options) => { }; export const previewAnnotations: PresetProperty<'previewAnnotations'> = (entry = []) => { - const result = [...entry, fileURLToPath(import.meta.resolve('@storybook/nextjs/preview'))]; - return result; + const annotations = [...entry, fileURLToPath(import.meta.resolve('@storybook/nextjs/preview'))]; + + const nextjsVersion = getNextjsVersion(); + const isNext16orNewer = semver.gte(nextjsVersion, '16.0.0'); + + // TODO: Remove this once we only support Next.js v16 and above + if (!isNext16orNewer) { + annotations.push(fileURLToPath(import.meta.resolve('@storybook/nextjs/config/preview'))); + } + + return annotations; }; export const babel: PresetProperty<'babel'> = async (baseConfig: TransformOptions) => { diff --git a/code/frameworks/nextjs/src/preview.tsx b/code/frameworks/nextjs/src/preview.tsx index 9c204f4dce32..b13dfd70ba53 100644 --- a/code/frameworks/nextjs/src/preview.tsx +++ b/code/frameworks/nextjs/src/preview.tsx @@ -14,7 +14,6 @@ import { createRouter } from '@storybook/nextjs/router.mock'; import { isNextRouterError } from 'next/dist/client/components/is-next-router-error'; -import './config/preview'; import { HeadManagerDecorator } from './head-manager/decorator'; import { ImageDecorator } from './images/decorator'; import { RouterDecorator } from './routing/decorator'; diff --git a/code/yarn.lock b/code/yarn.lock index 882fc3cde4de..570f12def78e 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6566,6 +6566,7 @@ __metadata: "@types/node": "npm:^22.0.0" next: "npm:^15.2.3" postcss-load-config: "npm:^6.0.1" + semver: "npm:^7.3.5" styled-jsx: "npm:5.1.6" typescript: "npm:^5.8.3" vite-plugin-storybook-nextjs: "npm:^2.0.7"