Skip to content
Closed
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 14 additions & 25 deletions code/core/src/common/utils/envs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | undefined> = {
const baseEnv: Record<string, string> = {
// eslint-disable-next-line @typescript-eslint/dot-notation
NODE_ENV: process.env['NODE_ENV'] || defaultNodeEnv,
NODE_PATH: process.env['NODE_PATH'] || '',
Expand All @@ -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<string, string>
const envEntries = Object.fromEntries<string>(
Object.entries<string>({
// 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<string, string> = { ...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<string, string>): Record<string, string> =>
Expand All @@ -57,13 +53,6 @@ export const stringifyProcessEnvs = (raw: Record<string, string>): Record<string
acc[`process.env.${key}`] = JSON.stringify(value);
return acc;
}, {});
// FIXME: something like this is necessary to support destructuring like:
//
// const { foo } = process.env;
//
// However, it also means that process.env.foo = 'bar' will fail, so removing this:
//
// envs['process.env'] = JSON.stringify(raw);
return envs;
};

Expand Down
4 changes: 3 additions & 1 deletion code/core/src/csf/csf-factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,9 @@ function defineStory<

const play =
mountDestructured(this.play) || mountDestructured(testFunction)
? async ({ context }: StoryContext<TRenderer>) => {
? // mount needs to be explicitly destructured
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async ({ mount, context }: StoryContext<TRenderer>) => {
await this.play?.(context);
await testFunction(context);
}
Expand Down
10 changes: 5 additions & 5 deletions code/core/src/preview-api/modules/store/csf/processCSFFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,16 @@ export function processCSFFile<TRenderer extends Renderer>(
): CSFFile<TRenderer> {
const { default: defaultExport, __namedExportsOrder, ...namedExports } = moduleExports;

const firstStory = Object.values(namedExports)[0];
if (isStory<TRenderer>(firstStory)) {
const factoryStory = Object.values(namedExports).find((it) => isStory<TRenderer>(it));
if (factoryStory) {
const meta: NormalizedComponentAnnotations<TRenderer> =
normalizeComponentAnnotations<TRenderer>(firstStory.meta.input, title, importPath);
normalizeComponentAnnotations<TRenderer>(factoryStory.meta.input, title, importPath);
checkDisallowedParameters(meta.parameters);

const csfFile: CSFFile<TRenderer> = { meta, stories: {}, moduleExports };

Object.keys(namedExports).forEach((key) => {
if (isExportStory(key, meta)) {
if (isExportStory(key, meta) && isStory<TRenderer>(namedExports[key])) {
const story: Story<TRenderer> = namedExports[key];

const storyMeta = normalizeStory(key, story.input as any, meta);
Expand All @@ -82,7 +82,7 @@ export function processCSFFile<TRenderer extends Renderer>(
}
});

csfFile.projectAnnotations = firstStory.meta.preview.composed;
csfFile.projectAnnotations = factoryStory.meta.preview.composed;

return csfFile;
}
Expand Down
16 changes: 14 additions & 2 deletions code/frameworks/nextjs/src/export-mocks/headers/index.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 };
3 changes: 2 additions & 1 deletion code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -286,5 +286,6 @@
"Dependency Upgrades"
]
]
}
},
"deferredNextVersion": "10.1.10"
}
19 changes: 18 additions & 1 deletion code/renderers/react/src/csf-factories.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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 */
},
});
});
35 changes: 26 additions & 9 deletions code/renderers/react/src/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function __definePreview<Addons extends PreviewAddon<never>[]>(
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
Expand All @@ -52,6 +53,12 @@ export function __definePreview<Addons extends PreviewAddon<never>[]>(
return preview;
}

type InferArgs<
TArgs extends Args,
T extends AddonTypes,
Decorators extends DecoratorFunction<ReactTypes & T, any>,
> = Simplify<TArgs & Simplify<RemoveIndexSignature<DecoratorsArgs<ReactTypes & T, Decorators>>>>;

/** @ts-expect-error We cannot implement the meta faithfully here, but that is okay. */
export interface ReactPreview<T extends AddonTypes> extends Preview<ReactTypes & T> {
meta<
Expand All @@ -70,13 +77,13 @@ export interface ReactPreview<T extends AddonTypes> extends Preview<ReactTypes &
'decorators' | 'component' | 'args' | 'render'
>
): ReactMeta<
ReactTypes &
T & {
args: Simplify<
TArgs & Simplify<RemoveIndexSignature<DecoratorsArgs<ReactTypes & T, Decorators>>>
>;
},
{ args: Partial<TArgs> extends TMetaArgs ? {} : TMetaArgs }
ReactTypes & T & { args: InferArgs<TArgs, T, Decorators> },
Omit<
ComponentAnnotations<ReactTypes & T & { args: InferArgs<TArgs, T, Decorators> }>,
'args'
> & {
args: Partial<TArgs> extends TMetaArgs ? {} : TMetaArgs;
}
>;
}

Expand All @@ -95,7 +102,7 @@ export interface ReactMeta<T extends ReactTypes, MetaInput extends ComponentAnno
render: () => ReactTypes['storyResult'];
}),
>(
story?: TInput
story: TInput
): ReactStory<T, TInput extends () => ReactTypes['storyResult'] ? { render: TInput } : TInput>;

story<
Expand All @@ -108,9 +115,19 @@ export interface ReactMeta<T extends ReactTypes, MetaInput extends ComponentAnno
>
>,
>(
story?: TInput
story: TInput
/** @ts-expect-error hard */
): ReactStory<T, TInput>;

// This overload matches meta.story(), but only when all args are optional
story(
..._args: Partial<T['args']> extends SetOptional<
T['args'],
keyof T['args'] & keyof MetaInput['args']
>
? []
: [never]
): ReactStory<T, {}>;
}

export interface ReactStory<T extends ReactTypes, TInput extends StoryAnnotations<T, T['args']>>
Expand Down
2 changes: 1 addition & 1 deletion docs/versions/latest.json
Original file line number Diff line number Diff line change
@@ -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!"}}
{"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!"}}