diff --git a/CHANGELOG.md b/CHANGELOG.md index 13751f6c92f8..a46a8d76c007 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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! diff --git a/code/builders/builder-vite/src/build.ts b/code/builders/builder-vite/src/build.ts index db5eb1eb17c0..f25f14857b61 100644 --- a/code/builders/builder-vite/src/build.ts +++ b/code/builders/builder-vite/src/build.ts @@ -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', 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, 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)}% diff --git a/code/frameworks/angular/build-schema.json b/code/frameworks/angular/build-schema.json index d3963edc7215..aa8da2b6f738 100644 --- a/code/frameworks/angular/build-schema.json +++ b/code/frameworks/angular/build-schema.json @@ -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": { diff --git a/code/frameworks/angular/start-schema.json b/code/frameworks/angular/start-schema.json index ac81c01d6ab0..7b67578ac332 100644 --- a/code/frameworks/angular/start-schema.json +++ b/code/frameworks/angular/start-schema.json @@ -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": { 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', }, }; 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...'); 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..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 @@ -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;', @@ -325,7 +347,7 @@ describe('preview specific functionality', () => { import { type Preview } from '@storybook/react-vite'; export const decorators = [] const preview = { - + parameters: { options: {} } @@ -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({}); + `); + }); }); 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..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 @@ -7,6 +7,7 @@ import picocolors from 'picocolors'; import type { FileInfo } from '../../automigrate/codemod'; import { + addImportToTop, cleanupTypeImports, getConfigProperties, removeExportDeclarations, @@ -165,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( @@ -197,7 +213,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); 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 0455f7aeb5f7..22e453c26e75 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(','); @@ -86,7 +88,7 @@ export async function runCodemod( '-t', `${TRANSFORM_DIR}/${codemod}.js`, ...parserArgs, - ...files.map((file) => `"${file}"`), + ...files, ], { stdio: 'inherit', diff --git a/code/package.json b/code/package.json index c13ea3a972ee..a6192ffe4559 100644 --- a/code/package.json +++ b/code/package.json @@ -220,5 +220,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "10.2.4" } diff --git a/docs/api/cli-options.mdx b/docs/api/cli-options.mdx index f33b476e0f38..ccbd8fa769b4 100644 --- a/docs/api/cli-options.mdx +++ b/docs/api/cli-options.mdx @@ -42,7 +42,7 @@ Options include: | `--exact-port` | Attempts to run Storybook on the exact port number specified.
If the port is already in use, Storybook will exit with an error message.
`storybook dev -p 9009 --exact-port` | | `-h`, `--host [string]` | Host to run Storybook.
`storybook dev -h my-host.com` | | `-c`, `--config-dir [dir-name]` | Storybook configuration directory.
`storybook dev -c .storybook` | -| `--loglevel [level]` | Controls level of logging during build.
Available options: `silly`, `verbose`, `info` (default), `warn`, `error`, `silent`
`storybook dev --loglevel warn` | +| `--loglevel [level]` | Controls level of logging during build.
Available options: `trace`, `debug`, `info` (default), `warn`, `error`, `silent`
`storybook dev --loglevel warn` | | `--https` | Serve Storybook over HTTPS. Note: You must provide your own certificate information.
`storybook dev --https` | | `--ssl-ca` | Provide an SSL certificate authority. (Optional with --https, required if using a self-signed certificate)
`storybook dev --ssl-ca my-certificate` | | `--ssl-cert` | Provide an SSL certificate. (Required with --https)
`storybook dev --ssl-cert my-ssl-certificate` | @@ -85,7 +85,7 @@ Options include: | `-V`, `--version` | Output the version number.
`storybook build -V` | | `-o`, `--output-dir [dir-name]` | Directory where to store built files.
`storybook build -o /my-deployed-storybook` | | `-c`, `--config-dir [dir-name]` | Storybook configuration directory.
`storybook build -c .storybook` | -| `--loglevel [level]` | Controls level of logging during build.
Available options: `silly`, `verbose`, `info` (default), `warn`, `error`, `silent`.
`storybook build --loglevel warn` | +| `--loglevel [level]` | Controls level of logging during build.
Available options: `trace`, `debug`, `info` (default), `warn`, `error`, `silent`.
`storybook build --loglevel warn` | | `--quiet` | Suppress verbose build output.
`storybook build --quiet` | | `--debug` | Outputs more logs in the CLI to assist debugging.
`storybook build --debug` | | `--debug-webpack` | Display final webpack configurations for debugging purposes.
`storybook build --debug-webpack` | diff --git a/docs/get-started/frameworks/angular.mdx b/docs/get-started/frameworks/angular.mdx index e1f2b411128f..dc525a26b613 100644 --- a/docs/get-started/frameworks/angular.mdx +++ b/docs/get-started/frameworks/angular.mdx @@ -289,7 +289,7 @@ These are common options you may need for the Angular builder: | `"initialPath"` | URL path to be appended when visiting Storybook for the first time.
`"initialPath": "docs/configure-your-project--docs"` | | `"webpackStatsJson"` | Write Webpack Stats JSON to disk.
`"webpackStatsJson": true` | | `"previewUrl"` | Disables the default storybook preview and lets you use your own.
`"previewUrl": "iframe.html"` | -| `"loglevel"` | Controls level of logging during build. Can be one of: [silly, verbose, info (default), warn, error, silent].
`"loglevel": "info"` | +| `"loglevel"` | Controls level of logging during build. Can be one of: [trace, debug, info (default), warn, error, silent].
`"loglevel": "info"` | | `"sourceMap"` | Configure [sourcemaps](https://angular.dev/reference/configs/workspace-config#source-map-configuration.).
`"sourceMap": true` | | `"experimentalZoneless"` | Configure [zoneless change detection](https://angular.dev/guide/zoneless).
`"experimentalZoneless": true` | diff --git a/docs/versions/latest.json b/docs/versions/latest.json index 1ee94b9180e2..6e721caffc3b 100644 --- a/docs/versions/latest.json +++ b/docs/versions/latest.json @@ -1 +1 @@ -{"version":"10.2.3","info":{"plain":"- Addon-Vitest: Normalize Windows paths in addon-vitest automigration - [#33340](https://github.com/storybookjs/storybook/pull/33340), thanks @tanujbhaud!\n- Core: Fix `previewHref` when current path does not end with a slash - [#33647](https://github.com/storybookjs/storybook/pull/33647), thanks @ghengeveld!"}} \ No newline at end of file +{"version":"10.2.4","info":{"plain":"- Angular: fix --loglevel options in docs and descriptions - [#33726](https://github.com/storybookjs/storybook/pull/33726), thanks @theRuslan!\n- Builder-Vite: Add plugin to enforce Storybook's output directory in Vite build configuration - [#33740](https://github.com/storybookjs/storybook/pull/33740), thanks @valentinpalkovic!\n- CSF-Factories: Fix codemod for preview files without exports - [#33673](https://github.com/storybookjs/storybook/pull/33673), thanks @kasperpeulen!\n- CSF-Factories: Preserve leading comments when adding imports - [#33645](https://github.com/storybookjs/storybook/pull/33645), 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- 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- Manager: Remove deprecated `active` prop warning in ZoomButton - [#33697](https://github.com/storybookjs/storybook/pull/33697), thanks @yatishgoel!\n- Next.js: Alias AppRouterContext to shared runtime to fix Link navigation - [#33419](https://github.com/storybookjs/storybook/pull/33419), thanks @pallaprolus!"}} \ No newline at end of file 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 `