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

Nextjs-Vite: Add Next.js 15 support #29640

Merged
merged 6 commits into from
Nov 20, 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
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');
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 type assertion for headers.draftMode to avoid using any

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 }) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: potential race condition between getting cookies and rendering - cookies().getAll() is called on each render

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}</>;
valentinpalkovic marked this conversation as resolved.
Show resolved Hide resolved

export const Nested = async ({ children }: any) => <>Nested {children}</>;
Copy link
Contributor

Choose a reason for hiding this comment

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

style: children prop uses 'any' type which loses type safety. Consider using PropsWithChildren or ReactNode type instead

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');
Copy link
Contributor

Choose a reason for hiding this comment

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

style: check that cookie exists before attempting to delete it

revalidatePath('/');
redirect('/');
}

export async function login() {
cookies().set('user', 'storybookjs');
(await cookies()).set('user', 'storybookjs');
Copy link
Contributor

Choose a reason for hiding this comment

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

style: verify cookie is set successfully before proceeding with redirect

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:
Comment on lines +211 to 213
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: template name 'Next.js Latest' is misleading since this is specifically for Next.js 14

'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
Loading