Skip to content

Commit

Permalink
Merge pull request #29640 from storybookjs/yann/next-15-support-vite
Browse files Browse the repository at this point in the history
Nextjs-Vite: Add Next.js 15 support
(cherry picked from commit 9794e35)
  • Loading branch information
yannbf authored and storybook-bot committed Nov 20, 2024
1 parent 5096486 commit 4019947
Show file tree
Hide file tree
Showing 21 changed files with 117 additions and 231 deletions.
9 changes: 5 additions & 4 deletions code/frameworks/experimental-nextjs-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,16 +99,16 @@
"@storybook/react": "workspace:*",
"@storybook/test": "workspace:*",
"styled-jsx": "5.1.6",
"vite-plugin-storybook-nextjs": "^1.0.11"
"vite-plugin-storybook-nextjs": "^1.1.0"
},
"devDependencies": {
"@types/node": "^18.0.0",
"next": "^14.2.5",
"next": "^15.0.3",
"typescript": "^5.3.2"
},
"peerDependencies": {
"@storybook/test": "workspace:*",
"next": "^14.1.0",
"next": "^14.1.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 @@ -141,7 +141,8 @@
"./src/images/decorator.tsx"
],
"externals": [
"sb-original/image-context"
"sb-original/image-context",
"sb-original/default-loader"
],
"platform": "node"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// @ts-expect-error Compatibility for Next 14
export { draftMode } from 'next/dist/client/components/headers';
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { fn } from '@storybook/test';

import * as originalHeaders from 'next/dist/client/components/headers';
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 };
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
Expand Up @@ -33,8 +33,8 @@ export const Default: Story = {
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"]') as any).content).toEqual(
'My new title'
);
await expect(
(document.querySelector('meta[property="og:title"]') as HTMLMetaElement)?.content
).toEqual('My new title');
},
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from 'react';

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

import Image from 'next/legacy/image';

import Accessibility from '../../assets/accessibility.svg';
Expand All @@ -10,17 +12,19 @@ export default {
src: Accessibility,
alt: 'Accessibility',
},
};
} as Meta<typeof Image>;

type Story = StoryObj<typeof Image>;

export const Default = {};
export const Default: Story = {};

export const BlurredPlaceholder = {
export const BlurredPlaceholder: Story = {
args: {
placeholder: 'blur',
},
};

export const BlurredAbsolutePlaceholder = {
export const BlurredAbsolutePlaceholder: Story = {
args: {
src: 'https://storybook.js.org/images/placeholders/50x50.png',
width: 50,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,11 @@ export default {
component: Component,
} as Meta<typeof Component>;

export const Default: StoryObj<typeof Component> = {};
type Story = StoryObj<typeof Component>;

export const InAppDir: StoryObj<typeof Component> = {
export const Default: Story = {};

export const InAppDir: Story = {
parameters: {
nextjs: {
appDirectory: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export default {
},
} as Meta<typeof Component>;

export const Default: StoryObj<typeof Component> = {
export const Default: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
const routerMock = getRouter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import NextHeader from './NextHeader';

export default {
component: NextHeader,
parameters: { react: { rsc: true } },
parameters: {
react: {
rsc: true,
},
},
} as Meta<typeof NextHeader>;

type Story = StoryObj<typeof NextHeader>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,23 @@ import { cookies, headers } from 'next/headers';
export default async function Component() {
async function handleClick() {
'use server';
cookies().set('user-id', 'encrypted-id');
(await cookies()).set('user-id', 'encrypted-id');
}

return (
<>
<h3>Cookies:</h3>
{cookies()
.getAll()
.map(({ name, value }) => {
return (
<p key={name} style={{ display: 'flex', flexDirection: 'row', gap: 8 }}>
<strong>Name:</strong> <span>{name}</span>
<strong>Value:</strong> <span>{value}</span>
</p>
);
})}
{(await cookies()).getAll().map(({ name, value }) => {
return (
<p key={name} style={{ display: 'flex', flexDirection: 'row', gap: 8 }}>
<strong>Name:</strong> <span>{name}</span>
<strong>Value:</strong> <span>{value}</span>
</p>
);
})}

<h3>Headers:</h3>
{Array.from(headers().entries()).map(([name, value]: [string, string]) => {
{Array.from((await headers()).entries()).map(([name, value]: [string, string]) => {
return (
<p key={name} style={{ display: 'flex', flexDirection: 'row', gap: 8 }}>
<strong>Name:</strong> <span>{name}</span>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/* eslint-disable local-rules/no-uncategorized-errors */
import React from 'react';

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

import { Nested, RSC } from './RSC';

export default {
Expand All @@ -10,19 +13,21 @@ export default {
rsc: true,
},
},
};
} as Meta<typeof RSC>;

type Story = StoryObj<typeof RSC>;

export const Default = {};
export const Default: Story = {};

export const DisableRSC = {
export const DisableRSC: Story = {
tags: ['!test'],
parameters: {
chromatic: { disable: true },
nextjs: { rsc: false },
},
};

export const Error = {
export const Errored: Story = {
tags: ['!test', '!vitest'],
parameters: {
chromatic: { disable: true },
Expand All @@ -32,7 +37,7 @@ export const Error = {
},
};

export const NestedRSC = {
export const NestedRSC: Story = {
render: (args) => (
<Nested>
<RSC {...args} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react';

import 'server-only';

export const RSC = async ({ label }: { label: string }) => <>RSC {label}</>;

export const Nested = async ({ children }: any) => <>Nested {children}</>;
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ export default {
},
} as Meta<typeof Component>;

export const ProtectedWhileLoggedOut: StoryObj<typeof Component> = {
type Story = StoryObj<typeof Component>;

export const ProtectedWhileLoggedOut: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByText('Access protected route'));
Expand All @@ -71,7 +73,7 @@ export const ProtectedWhileLoggedOut: StoryObj<typeof Component> = {
},
};

export const ProtectedWhileLoggedIn: StoryObj<typeof Component> = {
export const ProtectedWhileLoggedIn: Story = {
beforeEach() {
cookies().set('user', 'storybookjs');
},
Expand All @@ -87,7 +89,7 @@ export const ProtectedWhileLoggedIn: StoryObj<typeof Component> = {
},
};

export const Logout: StoryObj<typeof Component> = {
export const Logout: Story = {
beforeEach() {
cookies().set('user', 'storybookjs');
},
Expand All @@ -103,7 +105,7 @@ export const Logout: StoryObj<typeof Component> = {
},
};

export const Login: StoryObj<typeof Component> = {
export const Login: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByText('Login'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

export async function accessRoute() {
const user = cookies().get('user');
const user = (await cookies()).get('user');

if (!user) {
redirect('/');
Expand All @@ -16,13 +16,13 @@ export async function accessRoute() {
}

export async function logout() {
cookies().delete('user');
(await cookies()).delete('user');
revalidatePath('/');
redirect('/');
}

export async function login() {
cookies().set('user', 'storybookjs');
(await cookies()).set('user', 'storybookjs');
revalidatePath('/');
redirect('/');
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from 'react';

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

const Component = () => (
<div>
<style jsx>{`
Expand All @@ -15,6 +17,6 @@ const Component = () => (

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

export const Default = {};
export const Default: StoryObj<typeof Component> = {};
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Compatibility for Next 14
// @ts-expect-error Compatibility for Next 14
export { draftMode } from 'next/dist/client/components/headers';
1 change: 0 additions & 1 deletion code/frameworks/nextjs/src/export-mocks/headers/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { fn } from '@storybook/test';

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

Expand Down
2 changes: 1 addition & 1 deletion code/frameworks/nextjs/template/stories/RSC.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const DisableRSC: Story = {
};

export const Errored: Story = {
tags: ['!test'],
tags: ['!test', '!vitest'],
parameters: {
chromatic: { disable: true },
},
Expand Down
26 changes: 25 additions & 1 deletion code/lib/cli-storybook/src/sandbox-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ const baseTemplates = {
},
skipTasks: ['e2e-tests-dev', 'bench', 'vitest-integration'],
},
'experimental-nextjs-vite/default-ts': {
'experimental-nextjs-vite/14-ts': {
name: 'Next.js Latest (Vite | TypeScript)',
script:
'npx create-next-app@^14 {{beforeDir}} --eslint --tailwind --app --import-alias="@/*" --src-dir',
Expand All @@ -229,6 +229,30 @@ const baseTemplates = {
'prop-types',
],
},
inDevelopment: true,
skipTasks: ['e2e-tests-dev', 'bench'],
},
'experimental-nextjs-vite/default-ts': {
name: 'Next.js Latest (Vite | TypeScript)',
script:
'npx create-next-app {{beforeDir}} --eslint --tailwind --app --import-alias="@/*" --src-dir',
expected: {
framework: '@storybook/experimental-nextjs-vite',
renderer: '@storybook/react',
builder: '@storybook/builder-vite',
},
modifications: {
mainConfig: {
framework: '@storybook/experimental-nextjs-vite',
features: { experimentalRSC: true },
},
extraDependencies: [
'server-only',
'@storybook/experimental-nextjs-vite',
'vite',
'prop-types',
],
},
skipTasks: ['e2e-tests-dev', 'bench'],
},
'react-vite/default-js': {
Expand Down
Loading

0 comments on commit 4019947

Please sign in to comment.