From 158e30d7f29873f3bfe383fb42591dbca9d0ef80 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Tue, 12 Nov 2024 17:07:58 +0100 Subject: [PATCH 1/6] wip --- .../experimental-nextjs-vite/package.json | 4 +- .../compatibility/compatibility-map.ts | 46 +++++ .../compatibility/draft-mode.compat.ts | 2 + .../redirect-status-code.compat.ts | 6 + .../compatibility/segment.compat.ts | 8 + .../src/export-mocks/headers/index.ts | 7 +- .../template/stories/Head.stories.tsx | 6 +- .../template/stories/ImageLegacy.stories.tsx | 12 +- .../template/stories/Link.stories.tsx | 6 +- .../template/stories/Navigation.stories.tsx | 2 +- .../template/stories/NextHeader.stories.tsx | 6 +- .../template/stories/NextHeader.tsx | 22 ++- .../template/stories/RSC.jsx | 7 - .../{RSC.stories.jsx => RSC.stories.tsx} | 17 +- .../template/stories/RSC.tsx | 7 + .../stories/ServerActions.stories.tsx | 10 +- .../template/stories/ServerActions.tsx | 6 +- ...dJsx.stories.jsx => StyledJsx.stories.tsx} | 6 +- .../nextjs/src/export-mocks/headers/index.ts | 1 - .../cli-storybook/src/sandbox-templates.ts | 26 ++- code/yarn.lock | 162 +----------------- 21 files changed, 159 insertions(+), 210 deletions(-) create mode 100644 code/frameworks/experimental-nextjs-vite/src/export-mocks/compatibility/compatibility-map.ts create mode 100644 code/frameworks/experimental-nextjs-vite/src/export-mocks/compatibility/draft-mode.compat.ts create mode 100644 code/frameworks/experimental-nextjs-vite/src/export-mocks/compatibility/redirect-status-code.compat.ts create mode 100644 code/frameworks/experimental-nextjs-vite/src/export-mocks/compatibility/segment.compat.ts delete mode 100644 code/frameworks/experimental-nextjs-vite/template/stories/RSC.jsx rename code/frameworks/experimental-nextjs-vite/template/stories/{RSC.stories.jsx => RSC.stories.tsx} (60%) create mode 100644 code/frameworks/experimental-nextjs-vite/template/stories/RSC.tsx rename code/frameworks/experimental-nextjs-vite/template/stories/{StyledJsx.stories.jsx => StyledJsx.stories.tsx} (66%) diff --git a/code/frameworks/experimental-nextjs-vite/package.json b/code/frameworks/experimental-nextjs-vite/package.json index b7ee9fe02f5b..d7437ff0fb90 100644 --- a/code/frameworks/experimental-nextjs-vite/package.json +++ b/code/frameworks/experimental-nextjs-vite/package.json @@ -103,12 +103,12 @@ }, "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:^", diff --git a/code/frameworks/experimental-nextjs-vite/src/export-mocks/compatibility/compatibility-map.ts b/code/frameworks/experimental-nextjs-vite/src/export-mocks/compatibility/compatibility-map.ts new file mode 100644 index 000000000000..4cfb5e92f0b3 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/export-mocks/compatibility/compatibility-map.ts @@ -0,0 +1,46 @@ +import semver from 'semver'; +import type { Configuration as WebpackConfig } from 'webpack'; + +import { addScopedAlias, getNextjsVersion, setAlias } from '../utils'; + +const mapping: Record> = { + '<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', + }, + '<14.0.4': { + // https://github.com/vercel/next.js/blob/v14.0.4/packages/next/src/client/components/redirect-status-code.ts + '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 = () => { + const version = getNextjsVersion(); + const result: Record = {}; + + Object.keys(mapping).forEach((key) => { + if (semver.intersects(version, key)) { + Object.assign(result, mapping[key]); + } + }); + + return result; +}; + +export const configureCompatibilityAliases = (baseConfig: WebpackConfig): void => { + const aliases = getCompatibilityAliases(); + + Object.entries(aliases).forEach(([name, alias]) => { + if (typeof alias === 'string') { + addScopedAlias(baseConfig, name, alias); + } else { + setAlias(baseConfig, name, alias); + } + }); +}; 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 000000000000..0304cb48455a --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/export-mocks/compatibility/draft-mode.compat.ts @@ -0,0 +1,2 @@ +// Compatibility for Next 14 +export { draftMode } from 'next/dist/client/components/headers'; diff --git a/code/frameworks/experimental-nextjs-vite/src/export-mocks/compatibility/redirect-status-code.compat.ts b/code/frameworks/experimental-nextjs-vite/src/export-mocks/compatibility/redirect-status-code.compat.ts new file mode 100644 index 000000000000..cf2adb9e284c --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/export-mocks/compatibility/redirect-status-code.compat.ts @@ -0,0 +1,6 @@ +// Compatibility for Next 13 +export enum RedirectStatusCode { + SeeOther = 303, + TemporaryRedirect = 307, + PermanentRedirect = 308, +} diff --git a/code/frameworks/experimental-nextjs-vite/src/export-mocks/compatibility/segment.compat.ts b/code/frameworks/experimental-nextjs-vite/src/export-mocks/compatibility/segment.compat.ts new file mode 100644 index 000000000000..98c195030f5c --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/export-mocks/compatibility/segment.compat.ts @@ -0,0 +1,8 @@ +// Compatibility for Next 13 +// from https://github.com/vercel/next.js/blob/606f9ff7903b58da51aa043bfe71cd7b6ea306fd/packages/next/src/shared/lib/segment.ts#L4 +export function isGroupSegment(segment: string) { + return segment[0] === '(' && segment.endsWith(')'); +} + +export const PAGE_SEGMENT_KEY = '__PAGE__'; +export const DEFAULT_SEGMENT_KEY = '__DEFAULT__'; 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 1797d4ccaf57..1c903963ba2d 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/template/stories/Head.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/Head.stories.tsx index db1b747bf78d..0d344078868f 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 61e61b916cbe..5e8852c2fb38 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 7c1aa2073ab6..d071539c57a5 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 d50ed5174d25..4b9b49904de0 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 178aea8c3ac8..1d31006f63e7 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 6189f84baa62..eca7197ed79a 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 a5771a6a9202..000000000000 --- 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 60% 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 f5520448bd61..f0db3625b4f8 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; -export const Default = {}; +type Story = StoryObj; -export const DisableRSC = { +export const Default: Story = {}; + +export const DisableRSC: Story = { tags: ['!test'], parameters: { chromatic: { disable: true }, @@ -22,8 +27,8 @@ export const DisableRSC = { }, }; -export const Error = { - tags: ['!test', '!vitest'], +export const Errored: Story = { + tags: ['!test'], 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 000000000000..24655728ff46 --- /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 944bc42d8667..0844293c34f3 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 5e1b3c7227dc..6244f78d2472 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 5a0c586e232c..31adea42456c 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 = () => (