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

- Angular: fix --loglevel options in docs and descriptions - [#33726](https://github.com/storybookjs/storybook/pull/33726), thanks @theRuslan!
- Builder-Vite: Add plugin to enforce Storybook's output directory in Vite build configuration - [#33740](https://github.com/storybookjs/storybook/pull/33740), thanks @valentinpalkovic!
- CSF-Factories: Fix codemod for preview files without exports - [#33673](https://github.com/storybookjs/storybook/pull/33673), thanks @kasperpeulen!
- CSF-Factories: Preserve leading comments when adding imports - [#33645](https://github.com/storybookjs/storybook/pull/33645), 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!
- 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
23 changes: 23 additions & 0 deletions code/builders/builder-vite/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,29 @@ export async function build(options: Options) {

const finalConfig = (await presets.apply('viteFinal', config, options)) as InlineConfig;

// Add a plugin to enforce Storybook's outDir after all other plugins.
// This prevents frameworks like Nitro from redirecting
// build output to their own directories (e.g., .output/public/).
// The 'enforce: post' ensures this runs after all other config hooks.
finalConfig.plugins?.push({
name: 'storybook:enforce-output-dir',
enforce: 'post',
config: (config) => ({
...config,
build: {
outDir: options.outputDir,
},
}),
// configEnvironment is a new method in Vite 6
// It is used to configure configs based on the environment
// E.g. Nitro uses this method to set the output directory to .output/public/
configEnvironment: () => ({
build: {
outDir: options.outputDir,
},
}),
});

if (options.features?.developmentModeForBuild) {
finalConfig.plugins?.push({
name: 'storybook:define-env',
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
2 changes: 1 addition & 1 deletion code/frameworks/angular/build-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
},
"loglevel": {
"type": "string",
"description": "Controls level of logging during build. Can be one of: [silly, verbose, info (default), warn, error, silent].",
"description": "Controls level of logging during build. Can be one of: [trace, debug, info (default), warn, error, silent].",
"pattern": "(trace|debug|info|warn|error|silent)"
},
"logfile": {
Expand Down
2 changes: 1 addition & 1 deletion code/frameworks/angular/start-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@
},
"loglevel": {
"type": "string",
"description": "Controls level of logging during build. Can be one of: [silly, verbose, info (default), warn, error, silent].",
"description": "Controls level of logging during build. Can be one of: [trace, debug, info (default), warn, error, silent].",
"pattern": "(trace|debug|info|warn|error|silent)"
},
"logfile": {
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
Loading