From 70ac26366dfe1e55ab8f197950c579671d729bb4 Mon Sep 17 00:00:00 2001 From: storybook-bot <32066757+storybook-bot@users.noreply.github.com> Date: Fri, 30 Jan 2026 13:11:34 +0000 Subject: [PATCH 01/11] Update ./docs/versions/next.json for v10.3.0-alpha.2 --- docs/versions/next.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/versions/next.json b/docs/versions/next.json index 3bf2ff42bd48..0303d19f6a1b 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"10.3.0-alpha.1","info":{"plain":"- Builder-Webpack5: Fix @vitest/mocker resolution issue - [#33315](https://github.com/storybookjs/storybook/pull/33315), thanks @valentinpalkovic!\n- CLI: Add init telemetry for CLI integrations - [#33603](https://github.com/storybookjs/storybook/pull/33603), thanks @shilman!\n- Core: Fix `previewHref` when current path does not end with a slash - [#33647](https://github.com/storybookjs/storybook/pull/33647), thanks @ghengeveld!\n- Core: Fix rendering of View Transitions in Firefox - [#33651](https://github.com/storybookjs/storybook/pull/33651), thanks @ghengeveld!\n- Manifest: Add docs entries to debugger - [#33607](https://github.com/storybookjs/storybook/pull/33607), thanks @JReinhold!\n- Theming: Export interface declaration for `ThemesGlobals` - [#33343](https://github.com/storybookjs/storybook/pull/33343), thanks @icopp!\n- UI: Avoid large animation for reduced motion users - [#33530](https://github.com/storybookjs/storybook/pull/33530), thanks @Sidnioulz!"}} \ No newline at end of file +{"version":"10.3.0-alpha.2","info":{"plain":"- Addon Vitest: Support simple vite.config without defineConfig helper - [#33694](https://github.com/storybookjs/storybook/pull/33694), thanks @valentinpalkovic!\n- Addon-Docs: Add support for `sourceState: 'none'` to canvas block parameters - [#33627](https://github.com/storybookjs/storybook/pull/33627), thanks @quisido!\n- Addon-Vitest: Append Storybook project to existing test.projects array without double nesting - [#33708](https://github.com/storybookjs/storybook/pull/33708), thanks @valentinpalkovic!\n- Addon-Vitest: Normalize Windows paths in addon-vitest automigration - [#33340](https://github.com/storybookjs/storybook/pull/33340), thanks @tanujbhaud!\n- Addon-Vitest: Skip postinstall setup when configured - [#33712](https://github.com/storybookjs/storybook/pull/33712), thanks @valentinpalkovic!\n- Addon-Vitest: Update Vitest plugin configuration to disable requireAssertions for expect - [#33693](https://github.com/storybookjs/storybook/pull/33693), thanks @valentinpalkovic!\n- CSF-Factories: Fix codemod for preview files without exports - [#33673](https://github.com/storybookjs/storybook/pull/33673), thanks @kasperpeulen!\n- CSF: Fix false positive detection of Zod v4 .meta() as CSF Factory - [#33666](https://github.com/storybookjs/storybook/pull/33666), thanks @kasperpeulen!\n- CSFFactories: Add non-interactive mode and --glob flag - [#33648](https://github.com/storybookjs/storybook/pull/33648), thanks @kasperpeulen!\n- CSFFactories: Preserve leading comments when adding imports - [#33645](https://github.com/storybookjs/storybook/pull/33645), thanks @kasperpeulen!\n- Cli: Use npm for registry URL in PNPMProxy to avoid workspace errors - [#33571](https://github.com/storybookjs/storybook/pull/33571), thanks @ia319!\n- Codemod: Fix csf-2-to-3 failing due to quoted filenames - [#33646](https://github.com/storybookjs/storybook/pull/33646), thanks @kasperpeulen!\n- Codemod: Fix glob pattern handling on Windows - [#33714](https://github.com/storybookjs/storybook/pull/33714), thanks @kasperpeulen!\n- Composition: Handle 401 responses with loginUrl from Chromatic - [#33705](https://github.com/storybookjs/storybook/pull/33705), thanks @kasperpeulen!\n- Core: Fix false-positive CJS warning when 'exports' appears in strings or comments - [#33572](https://github.com/storybookjs/storybook/pull/33572), thanks @reeseo3o!\n- Telemetry: Add agent detection - [#33675](https://github.com/storybookjs/storybook/pull/33675), thanks @valentinpalkovic!"}} \ No newline at end of file From 8ead8f89f3ea0fce50489a330a414291fae0f427 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Fri, 30 Jan 2026 11:09:59 +0100 Subject: [PATCH 02/11] Merge pull request #33714 from storybookjs/kasper/fix-codemod-windows Codemod: Fix glob pattern handling on Windows (cherry picked from commit 4d07f578f4bc7981ad6ca4786a84e8be175ee207) --- code/lib/codemod/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/code/lib/codemod/src/index.ts b/code/lib/codemod/src/index.ts index 0455f7aeb5f7..f20f770bb267 100644 --- a/code/lib/codemod/src/index.ts +++ b/code/lib/codemod/src/index.ts @@ -6,6 +6,7 @@ import { extname, join } from 'node:path'; import { resolvePackageDir } from 'storybook/internal/common'; import { sync as spawnSync } from 'cross-spawn'; +import { normalize } from 'pathe'; import { glob as tinyglobby } from 'tinyglobby'; import { jscodeshiftToPrettierParser } from './lib/utils'; @@ -60,7 +61,8 @@ export async function runCodemod( } } - const files = await tinyglobby([glob, '!**/node_modules', '!**/dist']); + // Normalize the glob pattern to use forward slashes (required for glob patterns on Windows) + const files = await tinyglobby([normalize(glob), '!**/node_modules', '!**/dist']); const extensions = new Set(files.map((file) => extname(file).slice(1))); const commaSeparatedExtensions = Array.from(extensions).join(','); From fe31daf5c98d0d1b8f59346828ce0080ae47d368 Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Wed, 28 Jan 2026 16:10:02 +0700 Subject: [PATCH 03/11] Merge pull request #33648 from storybookjs/kasper/csf-factories-non-interactive-mode CSFFactories: Add non-interactive mode and --glob flag (cherry picked from commit e9c8a87d221154f5681af330f4d28171cadd11d5) --- .../cli-storybook/src/automigrate/index.ts | 4 +++ .../src/automigrate/multi-project.ts | 3 +- .../cli-storybook/src/automigrate/types.ts | 6 ++++ code/lib/cli-storybook/src/bin/run.ts | 1 + .../src/codemod/csf-factories.ts | 36 ++++++++++++++----- 5 files changed, 41 insertions(+), 9 deletions(-) diff --git a/code/lib/cli-storybook/src/automigrate/index.ts b/code/lib/cli-storybook/src/automigrate/index.ts index 040f427ba5ec..c7a2a5b21a12 100644 --- a/code/lib/cli-storybook/src/automigrate/index.ts +++ b/code/lib/cli-storybook/src/automigrate/index.ts @@ -122,6 +122,7 @@ export const automigrate = async ({ isLatest, storiesPaths, hasCsfFactoryPreview, + glob, }: AutofixOptions): Promise<{ fixResults: Record; preCheckFailure?: PreCheckFailure; @@ -146,6 +147,8 @@ export const automigrate = async ({ result: null, storybookVersion, storiesPaths, + yes, + glob, }); return null; @@ -380,6 +383,7 @@ export async function runFixes({ skipInstall, storybookVersion, storiesPaths, + yes, }); logger.log(`βœ… ran ${picocolors.cyan(f.id)} migration`); diff --git a/code/lib/cli-storybook/src/automigrate/multi-project.ts b/code/lib/cli-storybook/src/automigrate/multi-project.ts index 59543089404c..7c331348c63d 100644 --- a/code/lib/cli-storybook/src/automigrate/multi-project.ts +++ b/code/lib/cli-storybook/src/automigrate/multi-project.ts @@ -264,7 +264,7 @@ export async function runAutomigrationsForProjects( selectedAutomigrations: AutomigrationCheckResult[], options: MultiProjectRunAutomigrationOptions ): Promise> { - const { dryRun, skipInstall, automigrations } = options; + const { dryRun, skipInstall, automigrations, yes } = options; const projectResults: Record = {}; const applicableAutomigrations = selectedAutomigrations.filter((am) => @@ -378,6 +378,7 @@ export async function runAutomigrationsForProjects( skipInstall, storybookVersion: project.storybookVersion, storiesPaths: project.storiesPaths, + yes, }; await fix.run(runOptions); diff --git a/code/lib/cli-storybook/src/automigrate/types.ts b/code/lib/cli-storybook/src/automigrate/types.ts index 1856e53c4211..28e4dcc1d183 100644 --- a/code/lib/cli-storybook/src/automigrate/types.ts +++ b/code/lib/cli-storybook/src/automigrate/types.ts @@ -24,6 +24,10 @@ export interface RunOptions { 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; } /** @@ -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 { diff --git a/code/lib/cli-storybook/src/bin/run.ts b/code/lib/cli-storybook/src/bin/run.ts index db04e21eb2b1..031c540a27c6 100644 --- a/code/lib/cli-storybook/src/bin/run.ts +++ b/code/lib/cli-storybook/src/bin/run.ts @@ -281,6 +281,7 @@ command('automigrate [fixId]') 'The renderer package for the framework Storybook is using.' ) .option('--skip-doctor', 'Skip doctor check') + .option('--glob ', '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'); diff --git a/code/lib/cli-storybook/src/codemod/csf-factories.ts b/code/lib/cli-storybook/src/codemod/csf-factories.ts index 35e0e1a4ecea..0aad19f84efa 100644 --- a/code/lib/cli-storybook/src/codemod/csf-factories.ts +++ b/code/lib/cli-storybook/src/codemod/csf-factories.ts @@ -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, }); } @@ -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. @@ -96,6 +114,8 @@ export const csfFactories: CommandFix = { packageManager, useSubPathImports, previewConfigPath: previewConfigPath!, + yes, + glob, }); logger.step('Applying codemod on your main config...'); From c40089391e544766cc4292002c4890cace1b67c1 Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Wed, 28 Jan 2026 16:08:12 +0700 Subject: [PATCH 04/11] Merge pull request #33666 from storybookjs/fix-zod-meta-false-positive CSF: Fix false positive detection of Zod v4 .meta() as CSF Factory (cherry picked from commit 32ee24ef707508db4f40057281adf73c35fa8aa5) --- code/core/src/csf-tools/CsfFile.test.ts | 61 ++++++++++++++++++++++++- code/core/src/csf-tools/CsfFile.ts | 5 +- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/code/core/src/csf-tools/CsfFile.test.ts b/code/core/src/csf-tools/CsfFile.test.ts index b6fec897d9b9..ab2ea4236bc2 100644 --- a/code/core/src/csf-tools/CsfFile.test.ts +++ b/code/core/src/csf-tools/CsfFile.test.ts @@ -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({}) ` ) @@ -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']); + }); }); }); }); diff --git a/code/core/src/csf-tools/CsfFile.ts b/code/core/src/csf-tools/CsfFile.ts index dbff965d8070..4ecbdbd72c35 100644 --- a/code/core/src/csf-tools/CsfFile.ts +++ b/code/core/src/csf-tools/CsfFile.ts @@ -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, From 92ce460bf8d92631bd5de4da443f98ea952e3f89 Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Wed, 28 Jan 2026 16:10:12 +0700 Subject: [PATCH 05/11] Merge pull request #33645 from storybookjs/fix-csf-import-comments CSFFactories: Preserve leading comments when adding imports (cherry picked from commit 0cfa3a8d6c1523a4321d9ed44237ed6c64c41935) --- .../helpers/config-to-csf-factory.test.ts | 22 ++++++++++++++++ .../codemod/helpers/config-to-csf-factory.ts | 3 ++- .../codemod/helpers/csf-factories-utils.ts | 26 +++++++++++++++++++ .../helpers/story-to-csf-factory.test.ts | 22 ++++++++++++++++ .../codemod/helpers/story-to-csf-factory.ts | 4 +-- 5 files changed, 74 insertions(+), 3 deletions(-) diff --git a/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.test.ts b/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.test.ts index 48aa77ccff9b..eb20715486f5 100644 --- a/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.test.ts +++ b/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.test.ts @@ -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;', diff --git a/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.ts b/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.ts index 42b6c1787e51..631e7677abfb 100644 --- a/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.ts +++ b/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.ts @@ -7,6 +7,7 @@ import picocolors from 'picocolors'; import type { FileInfo } from '../../automigrate/codemod'; import { + addImportToTop, cleanupTypeImports, getConfigProperties, removeExportDeclarations, @@ -197,7 +198,7 @@ export async function configToCsfFactory( } } else { // if not, add import { defineMain } from '@storybook/framework' - programNode.body.unshift(configImport); + addImportToTop(programNode, configImport); } // Remove type imports – now inferred – from @storybook/* packages diff --git a/code/lib/cli-storybook/src/codemod/helpers/csf-factories-utils.ts b/code/lib/cli-storybook/src/codemod/helpers/csf-factories-utils.ts index 0b23e670d09e..46864f464091 100644 --- a/code/lib/cli-storybook/src/codemod/helpers/csf-factories-utils.ts +++ b/code/lib/cli-storybook/src/codemod/helpers/csf-factories-utils.ts @@ -119,3 +119,29 @@ export function getConfigProperties( // error TS4058: Return type of exported function has or is using name 'ObjectProperty' from external module "/tmp/storybook/code/core/dist/babel/index" but cannot be named. return properties as any; } + +/** + * Adds an import declaration to the beginning of the program while preserving any leading comments + * (like license headers or @ts-check directives). + * + * When using `programNode.body.unshift()`, the import would be placed before any leading comments + * attached to the first node. This function transfers those comments to the new import so they + * remain at the top of the file. + * + * Note: We use the `comments` property (used by recast for printing) rather than `leadingComments` + * (used by babel internally) to ensure proper output formatting. + */ +export function addImportToTop(programNode: t.Program, importDecl: t.ImportDeclaration): void { + const firstNode = programNode.body[0] as t.Node & { comments?: t.Comment[] }; + + if (firstNode && firstNode.leadingComments && firstNode.leadingComments.length > 0) { + // Transfer leading comments from the first node to the import using 'comments' property + // which is what recast uses for printing (not 'leadingComments') + (importDecl as t.Node & { comments?: t.Comment[] }).comments = firstNode.leadingComments; + // Clear comments from the original first node to avoid duplication + firstNode.leadingComments = []; + firstNode.comments = []; + } + + programNode.body.unshift(importDecl); +} diff --git a/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.test.ts b/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.test.ts index 814d69522eb6..a621f15f6d34 100644 --- a/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.test.ts +++ b/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.test.ts @@ -45,6 +45,28 @@ describe('stories codemod', () => { `); }); + it('should preserve leading comments when adding import', async () => { + await expect( + transform(dedent` + // @ts-check + /** + * @license MIT + * Copyright 2024 + */ + const meta = { title: 'Component' }; + export default meta; + export const A = {}; + `) + ).resolves.toMatchInlineSnapshot(` + // @ts-check + /** @license MIT Copyright 2024 */ + import preview from '#.storybook/preview'; + + const meta = preview.meta({ title: 'Component' }); + export const A = meta.story(); + `); + }); + it('should transform and wrap inline default exported meta', async () => { await expect( transform(dedent` diff --git a/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.ts b/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.ts index 07d9f27ab195..8e4a23c3d44a 100644 --- a/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.ts +++ b/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.ts @@ -5,7 +5,7 @@ import { logger } from 'storybook/internal/node-logger'; import path from 'path'; import type { FileInfo } from '../../automigrate/codemod'; -import { cleanupTypeImports } from './csf-factories-utils'; +import { addImportToTop, cleanupTypeImports } from './csf-factories-utils'; import { removeUnusedTypes } from './remove-unused-types'; const typesDisallowList = [ @@ -324,7 +324,7 @@ export async function storyToCsfFactory( [t.importDefaultSpecifier(t.identifier(sbConfigImportName))], t.stringLiteral(previewPath) ); - programNode.body.unshift(configImport); + addImportToTop(programNode, configImport); } removeUnusedTypes(programNode, csf._ast); From ee7ef005ab6b494817f056ffb9414785194da283 Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Wed, 28 Jan 2026 16:10:26 +0700 Subject: [PATCH 06/11] Merge pull request #33646 from storybookjs/kasper/fix-csf-factories-codemod-33639 Codemod: Fix csf-2-to-3 failing due to quoted filenames (cherry picked from commit acf98b324d0667ca500b0350082b75f5acdad432) --- code/lib/codemod/src/index.test.ts | 119 +++++++++++++++++++++++++++++ code/lib/codemod/src/index.ts | 2 +- 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 code/lib/codemod/src/index.test.ts diff --git a/code/lib/codemod/src/index.test.ts b/code/lib/codemod/src/index.test.ts new file mode 100644 index 000000000000..482bc947b963 --- /dev/null +++ b/code/lib/codemod/src/index.test.ts @@ -0,0 +1,119 @@ +import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { runCodemod } from './index'; + +const logger = { + step: vi.fn(), + log: vi.fn(), +}; + +const csf2Source = `import { Meta, Story } from '@storybook/react-vite' + +import { Chart, ChartProps } from './chart' + +export default { + component: Chart, + title: 'Chart', +} as Meta + +const Template: Story = (args) => + +export const SimpleBar = Template.bind({}) +SimpleBar.args = { name: 'test' } +`; + +describe('runCodemod', () => { + const tempDir = join(tmpdir(), 'storybook-codemod-test'); + + beforeEach(() => { + mkdirSync(tempDir, { recursive: true }); + vi.clearAllMocks(); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + /** + * https://github.com/storybookjs/storybook/issues/33639 + * + * Reproduces the bug where csf-2-to-3 silently fails because filenames are incorrectly quoted + * when passed to spawnSync. + * + * Test file from: + * https://github.com/seb-oss/green/blob/b634df24d2dae157300f73b711e569d1755eb138/libs/react-charts/src/lib/chart.stories.tsx + */ + it('should transform CSF2 Template.bind({}) files with csf-2-to-3', async () => { + const storyFile = join(tempDir, 'chart.stories.tsx'); + writeFileSync(storyFile, csf2Source); + + await runCodemod('csf-2-to-3', { + glob: storyFile, + logger, + }); + + const result = readFileSync(storyFile, 'utf-8'); + + // csf-2-to-3 should transform Template.bind({}) to CSF3 object + expect(result).not.toContain('Template.bind({})'); + expect(result).toContain('export const SimpleBar = {'); + }); + + it('should handle filenames with spaces', async () => { + const storyFile = join(tempDir, 'my component.stories.tsx'); + writeFileSync(storyFile, csf2Source); + + await runCodemod('csf-2-to-3', { + glob: storyFile, + logger, + }); + + const result = readFileSync(storyFile, 'utf-8'); + + expect(result).not.toContain('Template.bind({})'); + expect(result).toContain('export const SimpleBar = {'); + }); + + it('should handle glob patterns', async () => { + const storyFile1 = join(tempDir, 'chart.stories.tsx'); + const storyFile2 = join(tempDir, 'button.stories.tsx'); + writeFileSync(storyFile1, csf2Source); + writeFileSync(storyFile2, csf2Source); + + await runCodemod('csf-2-to-3', { + glob: join(tempDir, '*.stories.tsx'), + logger, + }); + + const result1 = readFileSync(storyFile1, 'utf-8'); + const result2 = readFileSync(storyFile2, 'utf-8'); + + expect(result1).not.toContain('Template.bind({})'); + expect(result2).not.toContain('Template.bind({})'); + }); + + it('should handle recursive glob patterns', async () => { + const nestedDir = join(tempDir, 'components', 'charts'); + mkdirSync(nestedDir, { recursive: true }); + + const storyFile1 = join(tempDir, 'root.stories.tsx'); + const storyFile2 = join(nestedDir, 'nested.stories.tsx'); + writeFileSync(storyFile1, csf2Source); + writeFileSync(storyFile2, csf2Source); + + await runCodemod('csf-2-to-3', { + glob: join(tempDir, '**/*.stories.tsx'), + logger, + }); + + const result1 = readFileSync(storyFile1, 'utf-8'); + const result2 = readFileSync(storyFile2, 'utf-8'); + + expect(result1).not.toContain('Template.bind({})'); + expect(result2).not.toContain('Template.bind({})'); + }); +}); diff --git a/code/lib/codemod/src/index.ts b/code/lib/codemod/src/index.ts index f20f770bb267..22e453c26e75 100644 --- a/code/lib/codemod/src/index.ts +++ b/code/lib/codemod/src/index.ts @@ -88,7 +88,7 @@ export async function runCodemod( '-t', `${TRANSFORM_DIR}/${codemod}.js`, ...parserArgs, - ...files.map((file) => `"${file}"`), + ...files, ], { stdio: 'inherit', From 1f194b1cc2af8dc2693e985a560c57ab8aac19c0 Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Wed, 28 Jan 2026 20:53:34 +0700 Subject: [PATCH 07/11] Merge pull request #33673 from storybookjs/fix-csf-factory-preview-no-exports CSF-Factories: Fix codemod for preview files without exports (cherry picked from commit 0d867e479591e3b7f73ced4be9dbe15e82ea5864) --- .../helpers/config-to-csf-factory.test.ts | 40 ++++++++++++++++++- .../codemod/helpers/config-to-csf-factory.ts | 15 +++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.test.ts b/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.test.ts index eb20715486f5..8c22d4fbba96 100644 --- a/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.test.ts +++ b/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.test.ts @@ -347,7 +347,7 @@ describe('preview specific functionality', () => { import { type Preview } from '@storybook/react-vite'; export const decorators = [] const preview = { - + parameters: { options: {} } @@ -368,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({}); + `); + }); }); diff --git a/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.ts b/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.ts index 631e7677abfb..c289c247ff51 100644 --- a/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.ts +++ b/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.ts @@ -166,6 +166,21 @@ export async function configToCsfFactory( // Add the new export default declaration programNode.body.push(t.exportDefaultDeclaration(defineConfigCall)); + } else if (configType === 'preview') { + /** + * Scenario 4: No exports (empty file or only side-effect imports) + * + * ``` + * import './preview.scss'; + * ``` + * + * Transform into: `import './preview.scss'; export default definePreview({})` + * + * This is needed because story files using CSF factories import from preview, so the preview + * file must have a default export. + */ + const defineConfigCall = t.callExpression(t.identifier(methodName), [t.objectExpression([])]); + programNode.body.push(t.exportDefaultDeclaration(defineConfigCall)); } const configImport = t.importDeclaration( From 8d21731e5d42e638cff4b1f888abb01deec1b8e5 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Fri, 30 Jan 2026 15:23:25 +0100 Subject: [PATCH 08/11] Merge pull request #33419 from pallaprolus/fix/30390-nextjs-link-context Next.js: Alias AppRouterContext to shared runtime to fix Link navigation (cherry picked from commit 045142734288ce2bd0fe9c66d4db725a6cc6e0bb) --- code/frameworks/nextjs/src/aliases/webpack.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/code/frameworks/nextjs/src/aliases/webpack.ts b/code/frameworks/nextjs/src/aliases/webpack.ts index 879b28e0d483..a9453814b7be 100644 --- a/code/frameworks/nextjs/src/aliases/webpack.ts +++ b/code/frameworks/nextjs/src/aliases/webpack.ts @@ -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', }, }; From c947cc86e20bab66fb5ce315b8de789533ff1bb0 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Fri, 30 Jan 2026 15:22:03 +0100 Subject: [PATCH 09/11] Merge pull request #33697 from yatishgoel/fix/33689-zoom-button-active-prop Manager: Remove deprecated `active` prop warning in ZoomButton (cherry picked from commit faab9239628e112b1f57b08d26492c998345d6cc) --- .../src/manager/components/preview/tools/zoom.stories.tsx | 2 +- code/core/src/manager/components/preview/tools/zoom.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/code/core/src/manager/components/preview/tools/zoom.stories.tsx b/code/core/src/manager/components/preview/tools/zoom.stories.tsx index 4c4309a6ab68..75a5eec411ca 100644 --- a/code/core/src/manager/components/preview/tools/zoom.stories.tsx +++ b/code/core/src/manager/components/preview/tools/zoom.stories.tsx @@ -8,7 +8,7 @@ import preview from '../../../../../../.storybook/preview'; import { Zoom } from './zoom'; const openDialog = async (context: StoryContext) => { - 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'); }; diff --git a/code/core/src/manager/components/preview/tools/zoom.tsx b/code/core/src/manager/components/preview/tools/zoom.tsx index d6dfc5307929..8ae440d7f092 100644 --- a/code/core/src/manager/components/preview/tools/zoom.tsx +++ b/code/core/src/manager/components/preview/tools/zoom.tsx @@ -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'; @@ -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, }); @@ -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)}% From 9d9bceb488d6dce9d13978f2dd969d9b71e38ea2 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Mon, 2 Feb 2026 14:14:21 +0100 Subject: [PATCH 10/11] Merge pull request #33687 from storybookjs/kasper/docs-csf-next-generic-vue Docs: Add FAQ about generic Vue components in CSF Next (cherry picked from commit 72d12b40ca07d0a7553d73be8f3cd59f5dbafcbc) --- docs/writing-stories/typescript.mdx | 65 ++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/docs/writing-stories/typescript.mdx b/docs/writing-stories/typescript.mdx index 220e985c6b20..a78660f8b960 100644 --- a/docs/writing-stories/typescript.mdx +++ b/docs/writing-stories/typescript.mdx @@ -69,32 +69,57 @@ Sometimes stories need to define args that aren’t included in the component's {/* prettier-ignore-end */} - ## Vue specific tips - Vue has excellent support for TypeScript, and we have done our utmost to take advantage of that in the stories files. For example, consider the following strongly typed Vue 3 single file component (SFC): +## Vue specific tips - ```html - +```html + + + +``` + +You can type check SFC files with `vue-tsc` and get editor support in VSCode by installing the official [Vue extension](https://marketplace.visualstudio.com/items?itemName=Vue.volar). + +This setup will add type support for `*.vue` imports to your `*.stories.ts` files, providing the same type safety and autocomplete features. + +[CSF Next](../api/csf/csf-next.mdx) adds support for [Generic Vue components](https://vuejs.org/api/sfc-script-setup.html#generics) (using `