From fe465b24393d6536a787677cdb97188b1e03e759 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Mon, 15 Dec 2025 14:42:31 +0100 Subject: [PATCH 1/4] Merge pull request #33341 from tanujbhaud/fix/nextjs-14-draft-mode-32950 Next.js: Handle v14 compatibility for draftMode import (cherry picked from commit 4033f332355a72e83b95e912c1b13477e0786940) --- .../nextjs/src/export-mocks/headers/index.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/code/frameworks/nextjs/src/export-mocks/headers/index.ts b/code/frameworks/nextjs/src/export-mocks/headers/index.ts index f1e27b49c646..0ae9091dd927 100644 --- a/code/frameworks/nextjs/src/export-mocks/headers/index.ts +++ b/code/frameworks/nextjs/src/export-mocks/headers/index.ts @@ -1,4 +1,3 @@ -import { draftMode as originalDraftMode } from 'next/dist/server/request/draft-mode'; import * as headers from 'next/dist/server/request/headers'; import { fn } from 'storybook/test'; @@ -10,5 +9,18 @@ export { headers } from './headers'; export { cookies } from './cookies'; // passthrough mocks - keep original implementation but allow for spying -const draftMode = fn(originalDraftMode ?? (headers as any).draftMode).mockName('draftMode'); +// In Next.js 14, draftMode is exported from 'next/dist/client/components/headers' +// In Next.js 15+, draftMode is exported from 'next/dist/server/request/draft-mode' +// The webpack alias handles this, but we need to avoid the top-level import +// to prevent build errors when the module doesn't exist +let originalDraftMode: any; +try { + // This will be resolved by webpack alias to the correct location based on Next.js version + originalDraftMode = require('next/dist/server/request/draft-mode').draftMode; +} catch { + // Fallback to the location in the headers module (Next.js 14) + originalDraftMode = (headers as any).draftMode; +} + +const draftMode = fn(originalDraftMode).mockName('draftMode'); export { draftMode }; From b2757b5c9d0cd409ee0c5c41b1a5b52ab659cd40 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Wed, 17 Dec 2025 14:10:44 +0100 Subject: [PATCH 2/4] Merge pull request #33383 from storybookjs/jeppe/fix-env-var-2 Core: Fix `.env`-file parsing (cherry picked from commit 35bd9a262020548382a11a9507d1f55965715019) --- .../src/preview/iframe-webpack.config.ts | 1 + code/core/src/common/utils/envs.ts | 39 +++++++------------ 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts index 0f28b883d413..ab35c51aece8 100644 --- a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts +++ b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts @@ -178,6 +178,7 @@ export default async ( }, }), new DefinePlugin({ + 'process.env': `(${JSON.stringify(envs)})`, ...stringifyProcessEnvs(envs), NODE_ENV: JSON.stringify( features?.developmentModeForBuild && isProd ? 'development' : process.env.NODE_ENV diff --git a/code/core/src/common/utils/envs.ts b/code/core/src/common/utils/envs.ts index e825bd6af2ae..d62eeb6048be 100644 --- a/code/core/src/common/utils/envs.ts +++ b/code/core/src/common/utils/envs.ts @@ -11,7 +11,7 @@ export async function loadEnvs(options: { production?: boolean } = {}): Promise< const { getEnvironment } = await import('lazy-universal-dotenv'); const defaultNodeEnv = options.production ? 'production' : 'development'; - const env: Record = { + const baseEnv: Record = { // eslint-disable-next-line @typescript-eslint/dot-notation NODE_ENV: process.env['NODE_ENV'] || defaultNodeEnv, NODE_PATH: process.env['NODE_PATH'] || '', @@ -23,27 +23,23 @@ export async function loadEnvs(options: { production?: boolean } = {}): Promise< PUBLIC_URL: options.production ? '.' : '', }; - Object.keys(process.env) - .filter((name) => /^STORYBOOK_/.test(name)) - .forEach((name) => { - env[name] = process.env[name]; - }); + const dotenv = getEnvironment({ nodeEnv: baseEnv['NODE_ENV'] }); - const base = Object.entries(env).reduce( - (acc, [k, v]) => Object.assign(acc, { [k]: JSON.stringify(v) }), - {} as Record + const envEntries = Object.fromEntries( + Object.entries({ + // TODO: it seems wrong that dotenv overrides process.env, but that's how it has always worked + ...process.env, + ...dotenv.raw, + }).filter(([name]) => /^STORYBOOK_/.test(name)) ); - const { stringified, raw } = getEnvironment({ nodeEnv: env['NODE_ENV'] }); + const raw: Record = { ...baseEnv, ...envEntries }; + (raw as any).NODE_PATH = nodePathsToArray((raw.NODE_PATH as string) || ''); - const fullRaw = { ...env, ...raw }; - - fullRaw.NODE_PATH = nodePathsToArray(fullRaw.NODE_PATH || ''); - - return { - stringified: { ...base, ...stringified }, - raw: fullRaw, - }; + const stringified = Object.fromEntries( + Object.entries(raw).map(([key, value]) => [key, JSON.stringify(value)]) + ); + return { raw, stringified }; } export const stringifyEnvs = (raw: Record): Record => @@ -57,13 +53,6 @@ export const stringifyProcessEnvs = (raw: Record): Record Date: Wed, 17 Dec 2025 15:09:08 +0100 Subject: [PATCH 3/4] Merge pull request #33354 from storybookjs/kasper/empty-story-factory React: Fix several CSF factory bugs (cherry picked from commit 0a6f8ea9a48c5f293ba29c2e9a61dfff193b150c) --- code/core/src/csf/csf-factories.ts | 4 ++- .../modules/store/csf/processCSFFile.ts | 10 +++--- .../react/src/csf-factories.test.tsx | 19 +++++++++- code/renderers/react/src/preview.tsx | 35 ++++++++++++++----- 4 files changed, 52 insertions(+), 16 deletions(-) diff --git a/code/core/src/csf/csf-factories.ts b/code/core/src/csf/csf-factories.ts index 22af4a85f64c..345892ada527 100644 --- a/code/core/src/csf/csf-factories.ts +++ b/code/core/src/csf/csf-factories.ts @@ -204,7 +204,9 @@ function defineStory< const play = mountDestructured(this.play) || mountDestructured(testFunction) - ? async ({ context }: StoryContext) => { + ? // mount needs to be explicitly destructured + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async ({ mount, context }: StoryContext) => { await this.play?.(context); await testFunction(context); } diff --git a/code/core/src/preview-api/modules/store/csf/processCSFFile.ts b/code/core/src/preview-api/modules/store/csf/processCSFFile.ts index c9b4dcec177a..16c76589757a 100644 --- a/code/core/src/preview-api/modules/store/csf/processCSFFile.ts +++ b/code/core/src/preview-api/modules/store/csf/processCSFFile.ts @@ -49,16 +49,16 @@ export function processCSFFile( ): CSFFile { const { default: defaultExport, __namedExportsOrder, ...namedExports } = moduleExports; - const firstStory = Object.values(namedExports)[0]; - if (isStory(firstStory)) { + const factoryStory = Object.values(namedExports).find((it) => isStory(it)); + if (factoryStory) { const meta: NormalizedComponentAnnotations = - normalizeComponentAnnotations(firstStory.meta.input, title, importPath); + normalizeComponentAnnotations(factoryStory.meta.input, title, importPath); checkDisallowedParameters(meta.parameters); const csfFile: CSFFile = { meta, stories: {}, moduleExports }; Object.keys(namedExports).forEach((key) => { - if (isExportStory(key, meta)) { + if (isExportStory(key, meta) && isStory(namedExports[key])) { const story: Story = namedExports[key]; const storyMeta = normalizeStory(key, story.input as any, meta); @@ -82,7 +82,7 @@ export function processCSFFile( } }); - csfFile.projectAnnotations = firstStory.meta.preview.composed; + csfFile.projectAnnotations = factoryStory.meta.preview.composed; return csfFile; } diff --git a/code/renderers/react/src/csf-factories.test.tsx b/code/renderers/react/src/csf-factories.test.tsx index 03b0de64da0a..e8a354d8e63e 100644 --- a/code/renderers/react/src/csf-factories.test.tsx +++ b/code/renderers/react/src/csf-factories.test.tsx @@ -77,7 +77,7 @@ describe('Args can be provided in multiple ways', () => { args: { label: 'good' }, }); // @ts-expect-error disabled not provided ❌ - const Basic = meta.story({}); + const Basic = meta.story(); } { const meta = preview.meta({ component: Button }); @@ -384,3 +384,20 @@ describe('Composed getters', () => { expect(renderSpy).toHaveBeenCalled(); }); }); + +it('meta.input also contains play', () => { + const meta = preview.meta({ + /** Title, component, etc... */ + play: async ({ canvas }) => { + /** Do some common interactions */ + }, + }); + + const ExtendedInteractionsStory = meta.story({ + play: async ({ canvas, ...rest }) => { + await meta.input.play?.({ canvas, ...rest }); + + /** Do some extra interactions */ + }, + }); +}); diff --git a/code/renderers/react/src/preview.tsx b/code/renderers/react/src/preview.tsx index ebcb0ffb960e..de531ffeee92 100644 --- a/code/renderers/react/src/preview.tsx +++ b/code/renderers/react/src/preview.tsx @@ -38,6 +38,7 @@ export function __definePreview[]>( preview.meta = (_input) => { const meta = defineMeta(_input); const defineStory = meta.story.bind(meta); + // @ts-expect-error internal code that is hard to type meta.story = (__input: any) => { const story = defineStory(__input); // TODO: [test-syntax] Are we sure we want this? the Component construct was for @@ -52,6 +53,12 @@ export function __definePreview[]>( return preview; } +type InferArgs< + TArgs extends Args, + T extends AddonTypes, + Decorators extends DecoratorFunction, +> = Simplify>>>; + /** @ts-expect-error We cannot implement the meta faithfully here, but that is okay. */ export interface ReactPreview extends Preview { meta< @@ -70,13 +77,13 @@ export interface ReactPreview extends Preview ): ReactMeta< - ReactTypes & - T & { - args: Simplify< - TArgs & Simplify>> - >; - }, - { args: Partial extends TMetaArgs ? {} : TMetaArgs } + ReactTypes & T & { args: InferArgs }, + Omit< + ComponentAnnotations }>, + 'args' + > & { + args: Partial extends TMetaArgs ? {} : TMetaArgs; + } >; } @@ -95,7 +102,7 @@ export interface ReactMeta ReactTypes['storyResult']; }), >( - story?: TInput + story: TInput ): ReactStory ReactTypes['storyResult'] ? { render: TInput } : TInput>; story< @@ -108,9 +115,19 @@ export interface ReactMeta >, >( - story?: TInput + story: TInput /** @ts-expect-error hard */ ): ReactStory; + + // This overload matches meta.story(), but only when all args are optional + story( + ..._args: Partial extends SetOptional< + T['args'], + keyof T['args'] & keyof MetaInput['args'] + > + ? [] + : [never] + ): ReactStory; } export interface ReactStory> From 99df3e4f2b213a75807578af31041e4ab8e1df58 Mon Sep 17 00:00:00 2001 From: storybook-bot <32066757+storybook-bot@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:11:52 +0000 Subject: [PATCH 4/4] Write changelog for 10.1.10 [skip ci] --- CHANGELOG.md | 6 ++++++ code/package.json | 3 ++- docs/versions/latest.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6283eba4f47b..e78eccc0eb8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 10.1.10 + +- Core: Fix `.env`-file parsing - [#33383](https://github.com/storybookjs/storybook/pull/33383), thanks @JReinhold! +- Next.js: Handle v14 compatibility for draftMode import - [#33341](https://github.com/storybookjs/storybook/pull/33341), thanks @tanujbhaud! +- React: Fix several CSF factory bugs - [#33354](https://github.com/storybookjs/storybook/pull/33354), thanks @kasperpeulen! + ## 10.1.9 - Telemetry: Remove instance of check for sub-error handling - [#33356](https://github.com/storybookjs/storybook/pull/33356), thanks @valentinpalkovic! diff --git a/code/package.json b/code/package.json index 06fc300e9b6d..394c227f5e71 100644 --- a/code/package.json +++ b/code/package.json @@ -286,5 +286,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "10.1.10" } diff --git a/docs/versions/latest.json b/docs/versions/latest.json index 1b1e4a906e8a..072db8fea44d 100644 --- a/docs/versions/latest.json +++ b/docs/versions/latest.json @@ -1 +1 @@ -{"version":"10.1.9","info":{"plain":"- Telemetry: Remove instance of check for sub-error handling - [#33356](https://github.com/storybookjs/storybook/pull/33356), thanks @valentinpalkovic!"}} \ No newline at end of file +{"version":"10.1.10","info":{"plain":"- Core: Fix `.env`-file parsing - [#33383](https://github.com/storybookjs/storybook/pull/33383), thanks @JReinhold!\n- Next.js: Handle v14 compatibility for draftMode import - [#33341](https://github.com/storybookjs/storybook/pull/33341), thanks @tanujbhaud!\n- React: Fix several CSF factory bugs - [#33354](https://github.com/storybookjs/storybook/pull/33354), thanks @kasperpeulen!"}} \ No newline at end of file