Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions code/frameworks/nextjs-vite/build-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions code/frameworks/nextjs-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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": {
Expand Down
18 changes: 16 additions & 2 deletions code/frameworks/nextjs-vite/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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')));
}
Comment on lines +40 to +51
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Treat 16 prereleases (canary/rc) as >=16 when gating behavior.

semver.gte('16.0.0-canary.1', '16.0.0') is false. You likely want canaries/RCs to opt into the v16 path.

Apply this diff:

-  const nextjsVersion = getNextjsVersion();
-  const isNext16orNewer = semver.gte(nextjsVersion, '16.0.0');
+  const nextjsVersion = getNextjsVersion();
+  const coerced = semver.coerce(nextjsVersion)?.version ?? nextjsVersion;
+  const isNext16orNewer = semver.gte(coerced, '16.0.0');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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')));
}
const annotations = [
...entry,
fileURLToPath(import.meta.resolve('@storybook/nextjs-vite/preview')),
];
const nextjsVersion = getNextjsVersion();
const coerced = semver.coerce(nextjsVersion)?.version ?? nextjsVersion;
const isNext16orNewer = semver.gte(coerced, '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')));
}
🤖 Prompt for AI Agents
In code/frameworks/nextjs-vite/src/preset.ts around lines 40 to 51, the semver
check treats prerelease Next.js versions (canary/rc) as older than 16.0.0;
change the gating to treat prereleases as >=16 by calling
semver.gte(nextjsVersion, '16.0.0', { includePrerelease: true }) when computing
isNext16orNewer so canary/rc versions opt into the v16 path.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


return annotations;
};

export const optimizeViteDeps = [
Expand Down
3 changes: 1 addition & 2 deletions code/frameworks/nextjs-vite/src/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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';
Expand Down
7 changes: 7 additions & 0 deletions code/frameworks/nextjs-vite/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 5 additions & 0 deletions code/frameworks/nextjs/build-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions code/frameworks/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 16 additions & 6 deletions code/frameworks/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
@@ -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');
Comment on lines +4 to +10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid import-time Next version resolution; include canary prereleases in the check

Reading the installed Next version at module import time can throw in environments where Next isn’t present (e.g., build/test of this package in isolation). Compute this lazily and be tolerant to absence. Also treat 16.0.0 prereleases (e.g., canary) as “>=16”.

Apply this diff to make the check resilient and prerelease-aware:

-const nextjsVersion = getNextjsVersion();
-const isNext16orNewer = semver.gte(nextjsVersion, '16.0.0');
+let isNext16orNewer = false;
+try {
+  const nextjsVersion = getNextjsVersion();
+  // Include prereleases like 16.0.0-canary.X as >= 16
+  isNext16orNewer = semver.satisfies(nextjsVersion, '>=16.0.0-0');
+} catch {
+  // If Next isn't resolvable (e.g., during isolated builds/tests), default to legacy behavior (<16)
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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');
import semver from 'semver';
import type { Configuration as WebpackConfig } from 'webpack';
import { addScopedAlias, getNextjsVersion, resolveNextConfig } from '../utils';
let isNext16orNewer = false;
try {
const nextjsVersion = getNextjsVersion();
// Include prereleases like 16.0.0-canary.X as >= 16
isNext16orNewer = semver.satisfies(nextjsVersion, '>=16.0.0-0');
} catch {
// If Next isn't resolvable (e.g., during isolated builds/tests), default to legacy behavior (<16)
}
🤖 Prompt for AI Agents
In code/frameworks/nextjs/src/config/webpack.ts around lines 4 to 10, avoid
resolving the installed Next.js version at module import time and make the >=16
check tolerant to missing Next and prereleases: replace the top-level
nextjsVersion/isNext16orNewer constants with a small lazy helper (e.g., function
isNext16orNewer()) that calls getNextjsVersion() inside a try/catch, treats
missing or errored resolution as a safe low version (e.g., "0.0.0"), and uses
semver.gte(version, '16.0.0', { includePrerelease: true }) (or equivalent semver
API) to treat canary/prerelease versions as >=16; update any call sites to
invoke the helper instead of using the import-time boolean.


const tryResolve = (path: string) => {
try {
Expand All @@ -22,7 +26,10 @@ export const configureConfig = async ({
}): Promise<NextConfig> => {
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');
}
Comment on lines +29 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Version‑gate the next/config alias (LGTM)

Conditionally aliasing next/config only for Next < 16 aligns with the upstream removal.

Run this to ensure no unconditional imports of next/config remain elsewhere:


🏁 Script executed:

#!/bin/bash
# Find any imports/requires of next/config and show context
rg -nP -C2 '(?:from\s+["'\'']next/config["'\'']|require\(\s*["'\'']next/config["'\'']\s*\))' \
  --type=ts --type=tsx --type=js --type=jsx -g '!**/node_modules/**' code

Length of output: 27


🏁 Script executed:

#!/bin/bash
set -euo pipefail

pattern="(?:from\s+['\"]next/config['\"]|require\\(\\s*['\"]next/config['\"]\\s*\\)|import\\(\\s*['\"]next/config['\"]\\s*\\))"

rg -nP -C2 "$pattern" \
  -g 'code/**/*.ts' -g 'code/**/*.tsx' -g 'code/**/*.js' -g 'code/**/*.jsx' -g '!code/**/node_modules/**' -S || true

Length of output: 475


Unconditional imports of next/config remain — fix required

Direct imports found at:

  • code/frameworks/nextjs/src/config/preview.ts
  • code/frameworks/nextjs-vite/src/config/preview.ts

Gate these imports/use behind the Next <16 check (or convert to a dynamic require used only when Next <16) so runtime/builds on Next ≥16 don't reference the removed module, or make the webpack alias unconditional.


// @ts-expect-error We know that alias is an object
if (baseConfig.resolve?.alias?.['react-dom']) {
Expand Down Expand Up @@ -58,14 +65,17 @@ const setupRuntimeConfig = async (
baseConfig: WebpackConfig,
nextConfig: NextConfig
): Promise<void> => {
const definePluginConfig: Record<string, any> = {
const definePluginConfig: Record<string, any> = {};

// 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;

Expand Down
14 changes: 12 additions & 2 deletions code/frameworks/nextjs/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')),
Expand Down Expand Up @@ -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) => {
Expand Down
1 change: 0 additions & 1 deletion code/frameworks/nextjs/src/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions code/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down