Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Next.js: Add support for Next 15 #29587

Merged
merged 9 commits into from
Nov 12, 2024
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
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'),
];
Comment on lines +21 to +24
Copy link
Contributor

Choose a reason for hiding this comment

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

style: ignoring warnings about missing draftMode export could mask real issues if the export path changes again in future Next.js versions

};
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);
}
});
Comment on lines 39 to 45
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Consider adding error handling for unexpected alias types

};
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
Loading