Skip to content
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## 10.2.4

- CSF-Factories: Fix codemod for preview files without exports - [#33673](https://github.com/storybookjs/storybook/pull/33673), thanks @kasperpeulen!
- CSF: Fix false positive detection of Zod v4 .meta() as CSF Factory - [#33666](https://github.com/storybookjs/storybook/pull/33666), thanks @kasperpeulen!
- CSFFactories: Add non-interactive mode and --glob flag - [#33648](https://github.com/storybookjs/storybook/pull/33648), thanks @kasperpeulen!
- CSFFactories: Preserve leading comments when adding imports - [#33645](https://github.com/storybookjs/storybook/pull/33645), thanks @kasperpeulen!
- Codemod: Fix csf-2-to-3 failing due to quoted filenames - [#33646](https://github.com/storybookjs/storybook/pull/33646), thanks @kasperpeulen!
- Codemod: Fix glob pattern handling on Windows - [#33714](https://github.com/storybookjs/storybook/pull/33714), thanks @kasperpeulen!
- Manager: Remove deprecated `active` prop warning in ZoomButton - [#33697](https://github.com/storybookjs/storybook/pull/33697), thanks @yatishgoel!
- Next.js: Alias AppRouterContext to shared runtime to fix Link navigation - [#33419](https://github.com/storybookjs/storybook/pull/33419), thanks @pallaprolus!

## 10.2.3

- Addon-Vitest: Normalize Windows paths in addon-vitest automigration - [#33340](https://github.com/storybookjs/storybook/pull/33340), thanks @tanujbhaud!
Expand Down
61 changes: 59 additions & 2 deletions code/core/src/csf-tools/CsfFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2970,11 +2970,13 @@ describe('CsfFile', () => {
});

it('bad preview import', () => {
// Only throws when the variable is named "preview" to avoid false positives
// from libraries like Zod that have their own .meta() methods
expect(() =>
parse(
dedent`
import { config } from '#.storybook/bad-preview'
const meta = config.meta({ component: 'foo' });
import { preview } from '#.storybook/bad-preview'
const meta = preview.meta({ component: 'foo' });
export const A = meta.story({})
`
)
Expand Down Expand Up @@ -3047,6 +3049,61 @@ describe('CsfFile', () => {
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export]
`);
});

it('ignores unrelated .meta() calls on imported variables (e.g., Zod v4)', () => {
// This should NOT throw - mySchema.meta() is not a CSF Factory call
// See: https://github.com/storybookjs/storybook/issues/33654
const parsed = loadCsf(
dedent`
import { mySchema } from './schemas';

const validatedSchema = mySchema.meta({ description: 'Value' });

export default {
title: 'Example',
component: () => null,
};

export const Default = {};
`,
{ makeTitle }
).parse();

expect(parsed._meta).toMatchInlineSnapshot(`
title: Example
component: () => null
`);
expect(Object.keys(parsed._stories)).toEqual(['Default']);
});

it('ignores chained .meta() calls from libraries like Zod', () => {
// More complex Zod-like patterns should also work
const parsed = loadCsf(
dedent`
import { z } from 'zod';
import { mySchema } from './schemas';

const workingSchema = z.object({
name: z.string().meta({ description: 'Name' }),
});

const failingSchema = z.object({
value: mySchema.meta({ description: 'Value' }),
});

export default {
title: 'Example',
component: () => null,
};

export const Default = {};
`,
{ makeTitle }
).parse();

expect(parsed._meta?.title).toBe('Example');
expect(Object.keys(parsed._stories)).toEqual(['Default']);
});
});
});
});
Expand Down
5 changes: 4 additions & 1 deletion code/core/src/csf-tools/CsfFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,10 @@ export class CsfFile {
: callee.property.name;
const metaNode = node.arguments[0] as t.ObjectExpression;
self._parseMeta(metaNode, self._ast.program);
} else {
} else if (rootObject.name === 'preview') {
// Only throw if the variable is named "preview" - this indicates
// the user is trying to use CSF Factories but with a wrong import path.
// Other .meta() calls (e.g., Zod v4's .meta()) are silently ignored.
throw new BadMetaError(
'meta() factory must be imported from .storybook/preview configuration',
configParent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import preview from '../../../../../../.storybook/preview';
import { Zoom } from './zoom';

const openDialog = async (context: StoryContext<typeof Zoom>) => {
const zoom = await context.canvas.findByRole('button', { name: 'Change zoom level' });
const zoom = await context.canvas.findByRole('switch', { name: 'Change zoom level' });
await context.userEvent.click(zoom);
return screen.findByRole('dialog');
};
Expand Down
6 changes: 3 additions & 3 deletions code/core/src/manager/components/preview/tools/zoom.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { PropsWithChildren } from 'react';
import React, { Component, createContext, memo, useCallback, useEffect, useRef } from 'react';

import { ActionList, Button, PopoverProvider } from 'storybook/internal/components';
import { ActionList, PopoverProvider, ToggleButton } from 'storybook/internal/components';
import type { Addon_BaseType } from 'storybook/internal/types';

import { UndoIcon, ZoomIcon } from '@storybook/icons';
Expand All @@ -15,7 +15,7 @@ import { NumericInput } from '../NumericInput';
const ZOOM_LEVELS = [0.25, 0.5, 0.75, 0.9, 1, 1.1, 1.25, 1.5, 2, 3, 4, 8] as const;
const INITIAL_ZOOM_LEVEL = 1;

const ZoomButton = styled(Button)({
const ZoomButton = styled(ToggleButton)({
minWidth: 48,
});

Expand Down Expand Up @@ -148,7 +148,7 @@ export const Zoom = memo<{
padding="small"
variant="ghost"
ariaLabel="Change zoom level"
active={value !== INITIAL_ZOOM_LEVEL}
pressed={value !== INITIAL_ZOOM_LEVEL}
>
{Math.round(value * 100)}%
</ZoomButton>
Expand Down
4 changes: 4 additions & 0 deletions code/frameworks/nextjs/src/aliases/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export const configureAliases = (baseConfig: WebpackConfig): void => {
...(baseConfig.resolve?.alias ?? {}),
'@opentelemetry/api': 'next/dist/compiled/@opentelemetry/api',
next: resolvePackageDir('next'),
'next/dist/shared/lib/app-router-context.shared-runtime':
'next/dist/shared/lib/app-router-context.shared-runtime',
'next/dist/shared/lib/app-router-context':
'next/dist/shared/lib/app-router-context.shared-runtime',
},
};

Expand Down
4 changes: 4 additions & 0 deletions code/lib/cli-storybook/src/automigrate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export const automigrate = async ({
isLatest,
storiesPaths,
hasCsfFactoryPreview,
glob,
}: AutofixOptions): Promise<{
fixResults: Record<string, FixStatus>;
preCheckFailure?: PreCheckFailure;
Expand All @@ -146,6 +147,8 @@ export const automigrate = async ({
result: null,
storybookVersion,
storiesPaths,
yes,
glob,
});

return null;
Expand Down Expand Up @@ -380,6 +383,7 @@ export async function runFixes({
skipInstall,
storybookVersion,
storiesPaths,
yes,
});
logger.log(`✅ ran ${picocolors.cyan(f.id)} migration`);

Expand Down
3 changes: 2 additions & 1 deletion code/lib/cli-storybook/src/automigrate/multi-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ export async function runAutomigrationsForProjects(
selectedAutomigrations: AutomigrationCheckResult[],
options: MultiProjectRunAutomigrationOptions
): Promise<Record<ConfigDir, AutomigrationResult>> {
const { dryRun, skipInstall, automigrations } = options;
const { dryRun, skipInstall, automigrations, yes } = options;
const projectResults: Record<ConfigDir, AutomigrationResult> = {};

const applicableAutomigrations = selectedAutomigrations.filter((am) =>
Expand Down Expand Up @@ -378,6 +378,7 @@ export async function runAutomigrationsForProjects(
skipInstall,
storybookVersion: project.storybookVersion,
storiesPaths: project.storiesPaths,
yes,
};

await fix.run(runOptions);
Expand Down
6 changes: 6 additions & 0 deletions code/lib/cli-storybook/src/automigrate/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export interface RunOptions<ResultType> {
skipInstall?: boolean;
storybookVersion: string;
storiesPaths: string[];
/** Skip prompts and use defaults (from --yes flag) */
yes?: boolean;
/** Glob pattern for story files (for csf-factories codemod) */
glob?: string;
}

/**
Expand Down Expand Up @@ -97,6 +101,8 @@ export interface AutofixOptionsFromCLI {
skipInstall?: boolean;
hideMigrationSummary?: boolean;
skipDoctor?: boolean;
/** Glob pattern for story files (for csf-factories codemod) */
glob?: string;
}

export enum FixStatus {
Expand Down
1 change: 1 addition & 0 deletions code/lib/cli-storybook/src/bin/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ command('automigrate [fixId]')
'The renderer package for the framework Storybook is using.'
)
.option('--skip-doctor', 'Skip doctor check')
.option('--glob <pattern>', 'Glob pattern for story files (for csf-factories codemod)')
.action(async (fixId, options) => {
withTelemetry('automigrate', { cliOptions: options }, async () => {
logger.intro(fixId ? `Running ${fixId} automigration` : 'Running automigrations');
Expand Down
36 changes: 28 additions & 8 deletions code/lib/cli-storybook/src/codemod/csf-factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,22 @@ async function runStoriesCodemod(options: {
packageManager: JsPackageManager;
useSubPathImports: boolean;
previewConfigPath: string;
yes: boolean | undefined;
glob: string | undefined;
}) {
const { dryRun, packageManager, ...codemodOptions } = options;
const { dryRun, packageManager, yes, glob, ...codemodOptions } = options;
try {
let globString = '{stories,src}/**/{Button,Header,Page,button,header,page}.stories.*';
if (!optionalEnvToBoolean(process.env.IN_STORYBOOK_SANDBOX)) {
const inSandbox = optionalEnvToBoolean(process.env.IN_STORYBOOK_SANDBOX) ?? false;
let globString = glob ?? '**/*.{stories,story}.{js,jsx,ts,tsx,mjs,mjsx,mts,mtsx}';

if (!glob && inSandbox) {
// Sandbox uses limited glob for faster testing (unless glob explicitly provided)
globString = '{stories,src}/**/{Button,Header,Page,button,header,page}.stories.*';
} else if (!glob && !yes) {
logger.log('Please enter the glob for your stories to migrate');
globString = await prompt.text({
message: 'glob',
initialValue: '**/*.{stories,story}.{js,jsx,ts,tsx,mjs,mjsx,mts,mtsx}',
initialValue: globString,
});
}

Expand All @@ -52,10 +59,21 @@ async function runStoriesCodemod(options: {
export const csfFactories: CommandFix = {
id: 'csf-factories',
promptType: 'command',
async run({ dryRun, mainConfig, mainConfigPath, previewConfigPath, packageManager, configDir }) {
let useSubPathImports = true;

if (!optionalEnvToBoolean(process.env.IN_STORYBOOK_SANDBOX)) {
async run({
dryRun,
mainConfig,
mainConfigPath,
previewConfigPath,
packageManager,
configDir,
yes,
glob,
}) {
const inSandbox = optionalEnvToBoolean(process.env.IN_STORYBOOK_SANDBOX) ?? false;
// Defaults to false for users and true in sandbox
let useSubPathImports = inSandbox;

if (!yes && !inSandbox) {
// prompt whether the user wants to use imports map
logger.logBox(dedent`
The CSF Factories format can benefit from using absolute imports of your ${picocolors.cyan(previewConfigPath)} file. We can configure that for you, using subpath imports (a node standard), by adjusting the imports property of your package.json.
Expand Down Expand Up @@ -96,6 +114,8 @@ export const csfFactories: CommandFix = {
packageManager,
useSubPathImports,
previewConfigPath: previewConfigPath!,
yes,
glob,
});

logger.step('Applying codemod on your main config...');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,28 @@ describe('main/preview codemod: general parsing functionality', () => {
});
`);
});

it('should preserve leading comments when adding import', async () => {
await expect(
transform(dedent`
// @ts-check
/** @license MIT */
export default {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
framework: '@storybook/react-vite',
};
`)
).resolves.toMatchInlineSnapshot(`
// @ts-check
/** @license MIT */
import { defineMain } from '@storybook/react-vite/node';

export default defineMain({
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
framework: '@storybook/react-vite',
});
`);
});
it('should wrap defineMain call from const declared default export with different type annotations', async () => {
const typedVariants = [
'export default config;',
Expand Down Expand Up @@ -325,7 +347,7 @@ describe('preview specific functionality', () => {
import { type Preview } from '@storybook/react-vite';
export const decorators = []
const preview = {

parameters: {
options: {}
}
Expand All @@ -346,4 +368,42 @@ describe('preview specific functionality', () => {
});
`);
});

it('should add default export when preview only has side-effect imports', async () => {
await expect(
transform(dedent`
import './preview.scss'
`)
).resolves.toMatchInlineSnapshot(`
import { definePreview } from '@storybook/react-vite';

import './preview.scss';

export default definePreview({});
`);
});

it('should add default export when preview file is empty', async () => {
await expect(transform('')).resolves.toMatchInlineSnapshot(`
import { definePreview } from '@storybook/react-vite';

export default definePreview({});
`);
});

it('should add default export when preview only has multiple side-effect imports', async () => {
await expect(
transform(dedent`
import './preview.scss'
import './global.css'
`)
).resolves.toMatchInlineSnapshot(`
import { definePreview } from '@storybook/react-vite';

import './global.css';
import './preview.scss';

export default definePreview({});
`);
});
});
Loading