Skip to content

Commit

Permalink
Merge pull request #29587 from storybookjs/yann/next-15-support
Browse files Browse the repository at this point in the history
Next.js: Add support for Next 15
  • Loading branch information
shilman authored Nov 12, 2024
2 parents 3b22843 + 4d3c6d9 commit fa138f6
Show file tree
Hide file tree
Showing 45 changed files with 643 additions and 265 deletions.
16 changes: 8 additions & 8 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ jobs:
# if there is a base branch AND a PR number in parameters, benchmark packages against those
# this happens when run against a PR
- when:
condition:
condition:
and:
- << pipeline.parameters.ghBaseBranch >>
- << pipeline.parameters.ghPrNumber >>
Expand Down Expand Up @@ -256,9 +256,9 @@ jobs:
sleep 2
done
yarn bench-packages --upload
- store_artifacts:
- store_artifacts:
path: bench/packages/results.json
- store_artifacts:
- store_artifacts:
path: bench/packages/compare-with-<< pipeline.parameters.ghBaseBranch >>.json
- report-workflow-on-failure
- cancel-workflow-on-failure
Expand Down Expand Up @@ -980,30 +980,30 @@ workflows:
requires:
- build
- create-sandboxes:
parallelism: 38
parallelism: 37
requires:
- build
# - smoke-test-sandboxes: # disabled for now
# requires:
# - create-sandboxes
- build-sandboxes:
parallelism: 38
parallelism: 37
requires:
- create-sandboxes
- chromatic-sandboxes:
parallelism: 35
parallelism: 34
requires:
- build-sandboxes
- e2e-production:
parallelism: 33
parallelism: 32
requires:
- build-sandboxes
- e2e-dev:
parallelism: 2
requires:
- create-sandboxes
- test-runner-production:
parallelism: 33
parallelism: 32
requires:
- build-sandboxes
- vitest-integration:
Expand Down
5 changes: 5 additions & 0 deletions code/e2e-tests/addon-docs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ test.describe('addon-docs', () => {
});

test('should resolve react to the correct version', async ({ page }) => {
test.skip(
templateName?.includes('nextjs'),
'TODO: remove this once sandboxes are synced (SOON!!)'
);
// Arrange - Navigate to MDX docs
const sbPage = new SbPage(page, expect);
await sbPage.navigateToStory('addons/docs/docs2/resolvedreact', 'mdx', 'docs');
Expand All @@ -201,6 +205,7 @@ test.describe('addon-docs', () => {
} else if (templateName.includes('react16')) {
expectedReactVersionRange = /^16/;
} else if (
templateName.includes('nextjs/default-ts') ||
templateName.includes('nextjs/prerelease') ||
templateName.includes('react-vite/prerelease') ||
templateName.includes('react-webpack/prerelease')
Expand Down
7 changes: 2 additions & 5 deletions code/e2e-tests/framework-nextjs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,7 @@ test.describe('Next.js', () => {
test.beforeEach(async ({ page }) => {
sbPage = new SbPage(page, expect);

await sbPage.navigateToStory(
'stories/frameworks/nextjs-nextjs-default-ts/Navigation',
'default'
);
await sbPage.navigateToStory('stories/frameworks/nextjs/Navigation', 'default');
root = sbPage.previewRoot();
});

Expand Down Expand Up @@ -88,7 +85,7 @@ test.describe('Next.js', () => {
test.beforeEach(async ({ page }) => {
sbPage = new SbPage(page, expect);

await sbPage.navigateToStory('stories/frameworks/nextjs-nextjs-default-ts/Router', 'default');
await sbPage.navigateToStory('stories/frameworks/nextjs/Router', 'default');
root = sbPage.previewRoot();
});

Expand Down
10 changes: 8 additions & 2 deletions code/frameworks/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
"import": "./dist/compatibility/redirect-status-code.compat.mjs",
"require": "./dist/compatibility/redirect-status-code.compat.js"
},
"./dist/compatibility/draft-mode.compat": {
"types": "./dist/compatibility/draft-mode.compat.d.ts",
"import": "./dist/compatibility/draft-mode.compat.mjs",
"require": "./dist/compatibility/draft-mode.compat.js"
},
"./export-mocks": {
"types": "./dist/export-mocks/index.d.ts",
"import": "./dist/export-mocks/index.mjs",
Expand Down Expand Up @@ -171,12 +176,12 @@
"@types/babel__preset-env": "^7",
"@types/loader-utils": "^2.0.5",
"@types/react-refresh": "^0",
"next": "^14.1.0",
"next": "^15.0.3",
"typescript": "^5.3.2",
"webpack": "^5.65.0"
},
"peerDependencies": {
"next": "^13.5.0 || ^14.0.0",
"next": "^13.5.0 || ^14.0.0 || ^15.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
"storybook": "workspace:^",
Expand Down Expand Up @@ -212,6 +217,7 @@
"./src/export-mocks/navigation/index.ts",
"./src/compatibility/segment.compat.ts",
"./src/compatibility/redirect-status-code.compat.ts",
"./src/compatibility/draft-mode.compat.ts",
"./src/next-image-loader-stub.ts",
"./src/images/decorator.tsx",
"./src/images/next-legacy-image.tsx",
Expand Down
14 changes: 14 additions & 0 deletions code/frameworks/nextjs/src/aliases/webpack.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import type { Configuration as WebpackConfig } from 'webpack';

import { configureCompatibilityAliases } from '../compatibility/compatibility-map';
import { configureNextExportMocks } from '../export-mocks/webpack';

export const configureAliases = (baseConfig: WebpackConfig): void => {
configureNextExportMocks(baseConfig);
configureCompatibilityAliases(baseConfig);

baseConfig.resolve = {
...(baseConfig.resolve ?? {}),
alias: {
...(baseConfig.resolve?.alias ?? {}),
'@opentelemetry/api': 'next/dist/compiled/@opentelemetry/api',
},
};

// remove warnings regarding compatibility paths
baseConfig.ignoreWarnings = [
...(baseConfig.ignoreWarnings ?? []),
(warning) =>
warning.message.includes("export 'draftMode'") &&
warning.message.includes('next/dist/server/request/headers'),
];
};
15 changes: 12 additions & 3 deletions code/frameworks/nextjs/src/compatibility/compatibility-map.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import semver from 'semver';
import type { Configuration as WebpackConfig } from 'webpack';

import { addScopedAlias, getNextjsVersion } from '../utils';
import { addScopedAlias, getNextjsVersion, setAlias } from '../utils';

const mapping: Record<string, Record<string, string>> = {
const mapping: Record<string, Record<string, string | boolean>> = {
'<14.1.0': {
// https://github.com/vercel/next.js/blob/v14.1.0/packages/next/src/shared/lib/segment.ts
'next/dist/shared/lib/segment': '@storybook/nextjs/dist/compatibility/segment.compat',
Expand All @@ -13,6 +13,11 @@ const mapping: Record<string, Record<string, string>> = {
'next/dist/client/components/redirect-status-code':
'@storybook/nextjs/dist/compatibility/redirect-status-code.compat',
},
'<15.0.0': {
'next/dist/server/request/headers': 'next/dist/client/components/headers',
// this path only exists from Next 15 onwards
'next/dist/server/request/draft-mode': '@storybook/nextjs/dist/compatibility/draft-mode.compat',
},
};

export const getCompatibilityAliases = () => {
Expand All @@ -32,6 +37,10 @@ export const configureCompatibilityAliases = (baseConfig: WebpackConfig): void =
const aliases = getCompatibilityAliases();

Object.entries(aliases).forEach(([name, alias]) => {
addScopedAlias(baseConfig, name, alias);
if (typeof alias === 'string') {
addScopedAlias(baseConfig, name, alias);
} else {
setAlias(baseConfig, name, alias);
}
});
};
2 changes: 2 additions & 0 deletions code/frameworks/nextjs/src/compatibility/draft-mode.compat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Compatibility for Next 14
export { draftMode } from 'next/dist/client/components/headers';
8 changes: 5 additions & 3 deletions code/frameworks/nextjs/src/export-mocks/headers/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { fn } from '@storybook/test';

import * as originalHeaders from 'next/dist/client/components/headers';
// This export won't exist in Next.js 14 but it's safe because we ignore it in Webpack when applicable
import { draftMode as originalDraftMode } from 'next/dist/server/request/draft-mode';
import * as headers from 'next/dist/server/request/headers';

// re-exports of the actual module
export * from 'next/dist/client/components/headers';
export * from 'next/dist/server/request/headers';

// mock utilities/overrides (as of Next v14.2.0)
export { headers } from './headers';
export { cookies } from './cookies';

// passthrough mocks - keep original implementation but allow for spying
const draftMode = fn(originalHeaders.draftMode).mockName('draftMode');
const draftMode = fn(originalDraftMode ?? (headers as any).draftMode).mockName('draftMode');
export { draftMode };
4 changes: 2 additions & 2 deletions code/frameworks/nextjs/src/export-mocks/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ export const getPackageAliases = ({ useESM = false }: { useESM?: boolean } = {})
const packageLocation = dirname(require.resolve('@storybook/nextjs/package.json'));

const getFullPath = (path: string) =>
join(packageLocation, path.replace('@storybook/nextjs', ''));
path.startsWith('next') ? path : join(packageLocation, path.replace('@storybook/nextjs', ''));

const aliases = Object.fromEntries(
Object.entries(mapping).map(([originalPath, aliasedPath]) => [
originalPath,
// Use paths for both next/xyz and @storybook/nextjs/xyz imports
// to make sure they all serve the MJS/CJS version of the file
getFullPath(`${aliasedPath}.${extension}`),
typeof aliasedPath === 'string' ? getFullPath(`${aliasedPath}.${extension}`) : aliasedPath,
])
);

Expand Down
2 changes: 0 additions & 2 deletions code/frameworks/nextjs/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,6 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig,
configureStyledJsx(baseConfig);
configureNodePolyfills(baseConfig);
configureAliases(baseConfig);
configureCompatibilityAliases(baseConfig);
configureNextExportMocks(baseConfig);

if (isDevelopment) {
configureFastRefresh(baseConfig);
Expand Down
5 changes: 4 additions & 1 deletion code/frameworks/nextjs/src/routing/app-router-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ import {
PathnameContext,
SearchParamsContext,
} from 'next/dist/shared/lib/hooks-client-context.shared-runtime';
import { type Params } from 'next/dist/shared/lib/router/utils/route-matcher';
import { PAGE_SEGMENT_KEY } from 'next/dist/shared/lib/segment';

import type { RouteParams } from './types';

// Using an inline type so we can support Next 14 and lower
// from https://github.com/vercel/next.js/blob/v15.0.3/packages/next/src/server/request/params.ts#L25
type Params = Record<string, string | Array<string> | undefined>;

type AppRouterProviderProps = {
routeParams: RouteParams;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { Suspense } from 'react';

import type { Meta, StoryObj } from '@storybook/react';

import dynamic from 'next/dynamic';

const DynamicComponent = dynamic(() => import('./dynamic-component'), {
Expand All @@ -16,6 +18,6 @@ function Component() {

export default {
component: Component,
};
} as Meta<typeof Component>;

export const Default = {};
export const Default: StoryObj<typeof Component> = {};
27 changes: 27 additions & 0 deletions code/frameworks/nextjs/template/stories/Font.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Meta, StoryObj } from '@storybook/react';

import Font from './Font';

export default {
component: Font,
} as Meta<typeof Font>;

type Story = StoryObj<typeof Font>;

export const WithClassName: Story = {
args: {
variant: 'className',
},
};

export const WithStyle: Story = {
args: {
variant: 'style',
},
};

export const WithVariable: Story = {
args: {
variant: 'variable',
},
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable react/prop-types */
import React from 'react';

import { Rubik_Puddles } from 'next/font/google';
Expand All @@ -15,7 +14,7 @@ export const localRubikStorm = localFont({
variable: '--font-rubik-storm',
});

export default function Font({ variant }) {
export default function Font({ variant }: { variant: 'className' | 'style' | 'variable' }) {
switch (variant) {
case 'className':
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React from 'react';

import type { Meta, StoryObj } from '@storybook/react';

import { getImageProps } from 'next/image';

import Accessibility from '../../assets/accessibility.svg';
import Testing from '../../assets/testing.png';

// referenced from https://nextjs.org/docs/pages/api-reference/components/image#theme-detection-picture
const Component = (props) => {
const Component = (props: any) => {
const {
props: { srcSet: dark },
} = getImageProps({ src: Accessibility, ...props });
Expand All @@ -29,6 +31,6 @@ export default {
args: {
alt: 'getImageProps Example',
},
};
} as Meta<typeof Component>;

export const Default = {};
export const Default: StoryObj<typeof Component> = {};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';

import type { Meta, StoryObj } from '@storybook/react';
import { expect, waitFor } from '@storybook/test';

import Head from 'next/head';
Expand All @@ -21,14 +22,14 @@ function Component() {

export default {
component: Component,
};
} as Meta<typeof Component>;

export const Default = {
export const Default: StoryObj<typeof Component> = {
play: async () => {
await waitFor(() => expect(document.title).toEqual('Next.js Head Title'));
await expect(document.querySelectorAll('meta[property="og:title"]')).toHaveLength(1);
await expect(document.querySelector('meta[property="og:title"]').content).toEqual(
'My new title'
);
await expect(
(document.querySelector('meta[property="og:title"]') as HTMLMetaElement)?.content
).toEqual('My new title');
},
};
Loading

0 comments on commit fa138f6

Please sign in to comment.