diff --git a/code/frameworks/experimental-nextjs-vite/package.json b/code/frameworks/experimental-nextjs-vite/package.json index 8853f392f4d..3bbe5e17cb0 100644 --- a/code/frameworks/experimental-nextjs-vite/package.json +++ b/code/frameworks/experimental-nextjs-vite/package.json @@ -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:^", @@ -141,7 +141,8 @@ "./src/images/decorator.tsx" ], "externals": [ - "sb-original/image-context" + "sb-original/image-context", + "sb-original/default-loader" ], "platform": "node" }, diff --git a/code/frameworks/experimental-nextjs-vite/src/export-mocks/compatibility/draft-mode.compat.ts b/code/frameworks/experimental-nextjs-vite/src/export-mocks/compatibility/draft-mode.compat.ts new file mode 100644 index 00000000000..cdcaf63bbf1 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/export-mocks/compatibility/draft-mode.compat.ts @@ -0,0 +1,2 @@ +// @ts-expect-error Compatibility for Next 14 +export { draftMode } from 'next/dist/client/components/headers'; diff --git a/code/frameworks/experimental-nextjs-vite/src/export-mocks/headers/index.ts b/code/frameworks/experimental-nextjs-vite/src/export-mocks/headers/index.ts index 1797d4ccaf5..1c903963ba2 100644 --- a/code/frameworks/experimental-nextjs-vite/src/export-mocks/headers/index.ts +++ b/code/frameworks/experimental-nextjs-vite/src/export-mocks/headers/index.ts @@ -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 }; diff --git a/code/frameworks/experimental-nextjs-vite/src/routing/app-router-provider.tsx b/code/frameworks/experimental-nextjs-vite/src/routing/app-router-provider.tsx index 99269e56907..68e01381948 100644 --- a/code/frameworks/experimental-nextjs-vite/src/routing/app-router-provider.tsx +++ b/code/frameworks/experimental-nextjs-vite/src/routing/app-router-provider.tsx @@ -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 | undefined>; + type AppRouterProviderProps = { routeParams: RouteParams; }; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/Head.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/Head.stories.tsx index db1b747bf78..0d344078868 100644 --- a/code/frameworks/experimental-nextjs-vite/template/stories/Head.stories.tsx +++ b/code/frameworks/experimental-nextjs-vite/template/stories/Head.stories.tsx @@ -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'); }, }; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/ImageLegacy.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/ImageLegacy.stories.tsx index 61e61b916cb..5e8852c2fb3 100644 --- a/code/frameworks/experimental-nextjs-vite/template/stories/ImageLegacy.stories.tsx +++ b/code/frameworks/experimental-nextjs-vite/template/stories/ImageLegacy.stories.tsx @@ -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'; @@ -10,17 +12,19 @@ export default { src: Accessibility, alt: 'Accessibility', }, -}; +} as Meta; + +type Story = StoryObj; -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, diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/Link.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/Link.stories.tsx index 7c1aa2073ab..d071539c57a 100644 --- a/code/frameworks/experimental-nextjs-vite/template/stories/Link.stories.tsx +++ b/code/frameworks/experimental-nextjs-vite/template/stories/Link.stories.tsx @@ -76,9 +76,11 @@ export default { component: Component, } as Meta; -export const Default: StoryObj = {}; +type Story = StoryObj; -export const InAppDir: StoryObj = { +export const Default: Story = {}; + +export const InAppDir: Story = { parameters: { nextjs: { appDirectory: true, diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/Navigation.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/Navigation.stories.tsx index d50ed5174d2..4b9b49904de 100644 --- a/code/frameworks/experimental-nextjs-vite/template/stories/Navigation.stories.tsx +++ b/code/frameworks/experimental-nextjs-vite/template/stories/Navigation.stories.tsx @@ -109,7 +109,7 @@ export default { }, } as Meta; -export const Default: StoryObj = { +export const Default: Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); const routerMock = getRouter(); diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/NextHeader.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/NextHeader.stories.tsx index 178aea8c3ac..1d31006f63e 100644 --- a/code/frameworks/experimental-nextjs-vite/template/stories/NextHeader.stories.tsx +++ b/code/frameworks/experimental-nextjs-vite/template/stories/NextHeader.stories.tsx @@ -8,7 +8,11 @@ import NextHeader from './NextHeader'; export default { component: NextHeader, - parameters: { react: { rsc: true } }, + parameters: { + react: { + rsc: true, + }, + }, } as Meta; type Story = StoryObj; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/NextHeader.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/NextHeader.tsx index 6189f84baa6..eca7197ed79 100644 --- a/code/frameworks/experimental-nextjs-vite/template/stories/NextHeader.tsx +++ b/code/frameworks/experimental-nextjs-vite/template/stories/NextHeader.tsx @@ -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 ( <>

Cookies:

- {cookies() - .getAll() - .map(({ name, value }) => { - return ( -

- Name: {name} - Value: {value} -

- ); - })} + {(await cookies()).getAll().map(({ name, value }) => { + return ( +

+ Name: {name} + Value: {value} +

+ ); + })}

Headers:

- {Array.from(headers().entries()).map(([name, value]: [string, string]) => { + {Array.from((await headers()).entries()).map(([name, value]: [string, string]) => { return (

Name: {name} diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/RSC.jsx b/code/frameworks/experimental-nextjs-vite/template/stories/RSC.jsx deleted file mode 100644 index a5771a6a920..00000000000 --- a/code/frameworks/experimental-nextjs-vite/template/stories/RSC.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -import 'server-only'; - -export const RSC = async ({ label }) => <>RSC {label}; - -export const Nested = async ({ children }) => <>Nested {children}; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/RSC.stories.jsx b/code/frameworks/experimental-nextjs-vite/template/stories/RSC.stories.tsx similarity index 62% rename from code/frameworks/experimental-nextjs-vite/template/stories/RSC.stories.jsx rename to code/frameworks/experimental-nextjs-vite/template/stories/RSC.stories.tsx index f5520448bd6..655a5f1a93e 100644 --- a/code/frameworks/experimental-nextjs-vite/template/stories/RSC.stories.jsx +++ b/code/frameworks/experimental-nextjs-vite/template/stories/RSC.stories.tsx @@ -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 { @@ -10,11 +13,13 @@ export default { rsc: true, }, }, -}; +} as Meta; + +type Story = StoryObj; -export const Default = {}; +export const Default: Story = {}; -export const DisableRSC = { +export const DisableRSC: Story = { tags: ['!test'], parameters: { chromatic: { disable: true }, @@ -22,7 +27,7 @@ export const DisableRSC = { }, }; -export const Error = { +export const Errored: Story = { tags: ['!test', '!vitest'], parameters: { chromatic: { disable: true }, @@ -32,7 +37,7 @@ export const Error = { }, }; -export const NestedRSC = { +export const NestedRSC: Story = { render: (args) => ( diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/RSC.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/RSC.tsx new file mode 100644 index 00000000000..24655728ff4 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/RSC.tsx @@ -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}; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/ServerActions.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/ServerActions.stories.tsx index 944bc42d866..0844293c34f 100644 --- a/code/frameworks/experimental-nextjs-vite/template/stories/ServerActions.stories.tsx +++ b/code/frameworks/experimental-nextjs-vite/template/stories/ServerActions.stories.tsx @@ -59,7 +59,9 @@ export default { }, } as Meta; -export const ProtectedWhileLoggedOut: StoryObj = { +type Story = StoryObj; + +export const ProtectedWhileLoggedOut: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); await userEvent.click(canvas.getByText('Access protected route')); @@ -71,7 +73,7 @@ export const ProtectedWhileLoggedOut: StoryObj = { }, }; -export const ProtectedWhileLoggedIn: StoryObj = { +export const ProtectedWhileLoggedIn: Story = { beforeEach() { cookies().set('user', 'storybookjs'); }, @@ -87,7 +89,7 @@ export const ProtectedWhileLoggedIn: StoryObj = { }, }; -export const Logout: StoryObj = { +export const Logout: Story = { beforeEach() { cookies().set('user', 'storybookjs'); }, @@ -103,7 +105,7 @@ export const Logout: StoryObj = { }, }; -export const Login: StoryObj = { +export const Login: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); await userEvent.click(canvas.getByText('Login')); diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/ServerActions.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/ServerActions.tsx index 5e1b3c7227d..6244f78d247 100644 --- a/code/frameworks/experimental-nextjs-vite/template/stories/ServerActions.tsx +++ b/code/frameworks/experimental-nextjs-vite/template/stories/ServerActions.tsx @@ -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('/'); @@ -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('/'); } diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/StyledJsx.stories.jsx b/code/frameworks/experimental-nextjs-vite/template/stories/StyledJsx.stories.tsx similarity index 66% rename from code/frameworks/experimental-nextjs-vite/template/stories/StyledJsx.stories.jsx rename to code/frameworks/experimental-nextjs-vite/template/stories/StyledJsx.stories.tsx index 5a0c586e232..31adea42456 100644 --- a/code/frameworks/experimental-nextjs-vite/template/stories/StyledJsx.stories.jsx +++ b/code/frameworks/experimental-nextjs-vite/template/stories/StyledJsx.stories.tsx @@ -1,5 +1,7 @@ import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + const Component = () => (