diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 0578ee43fed7..34bb31fb7726 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,11 @@ +## 10.0.0-beta.12 + +- CLI: Avoid mixed CSF in files with unconventional stories - [#32716](https://github.com/storybookjs/storybook/pull/32716), thanks @yannbf! +- CLI: Fix CSF factories addon syncing in storybook add command - [#32728](https://github.com/storybookjs/storybook/pull/32728), thanks @yannbf! +- CLI: Make relative imports default in csf-factories codemod - [#32610](https://github.com/storybookjs/storybook/pull/32610), thanks @copilot-swe-agent! +- Core: Prevent navigating to hidden (filtered) item - [#32715](https://github.com/storybookjs/storybook/pull/32715), thanks @ghengeveld! +- Fix: Allow proceeding without selecting automigrations in upgrade command - [#32597](https://github.com/storybookjs/storybook/pull/32597), thanks @copilot-swe-agent! + ## 10.0.0-beta.11 - Automigration: Improve the viewport/backgrounds automigration - [#32619](https://github.com/storybookjs/storybook/pull/32619), thanks @valentinpalkovic! diff --git a/code/core/src/common/utils/load-main-config.ts b/code/core/src/common/utils/load-main-config.ts index 579680e8da8f..b02ac8d6a42c 100644 --- a/code/core/src/common/utils/load-main-config.ts +++ b/code/core/src/common/utils/load-main-config.ts @@ -14,16 +14,18 @@ import { validateConfigurationFiles } from './validate-configuration-files'; export async function loadMainConfig({ configDir = '.storybook', cwd, + skipCache, }: { configDir: string; cwd?: string; + skipCache?: boolean; }): Promise { await validateConfigurationFiles(configDir, cwd); const mainPath = getInterpretedFile(resolve(configDir, 'main')) as string; try { - const out = await importModule(mainPath); + const out = await importModule(mainPath, { skipCache }); return out; } catch (e) { if (!(e instanceof Error)) { diff --git a/code/core/src/manager-api/modules/stories.ts b/code/core/src/manager-api/modules/stories.ts index 4c3b6fd1bd42..4af228dcf048 100644 --- a/code/core/src/manager-api/modules/stories.ts +++ b/code/core/src/manager-api/modules/stories.ts @@ -368,7 +368,7 @@ export const init: ModuleFn = ({ return parameters || undefined; }, jumpToComponent: (direction) => { - const { index, storyId, refs, refId } = store.getState(); + const { filteredIndex, storyId, refs, refId } = store.getState(); const story = api.getData(storyId, refId); // cannot navigate when there's no current selection @@ -376,7 +376,7 @@ export const init: ModuleFn = ({ return; } - const hash = refId ? refs[refId].index || {} : index; + const hash = refId ? refs[refId].filteredIndex || {} : filteredIndex; if (!hash) { return; @@ -389,7 +389,7 @@ export const init: ModuleFn = ({ } }, jumpToStory: (direction) => { - const { index, storyId, refs, refId } = store.getState(); + const { filteredIndex, storyId, refs, refId } = store.getState(); const story = api.getData(storyId, refId); // cannot navigate when there's no current selection @@ -397,7 +397,7 @@ export const init: ModuleFn = ({ return; } - const hash = story.refId ? refs[story.refId].index : index; + const hash = story.refId ? refs[story.refId].filteredIndex : filteredIndex; if (!hash) { return; @@ -425,49 +425,50 @@ export const init: ModuleFn = ({ }, selectStory: (titleOrId = undefined, name = undefined, options = {}) => { const { ref } = options; - const { storyId, index, refs } = store.getState(); + const { storyId, index, filteredIndex, refs, settings } = store.getState(); - const hash = ref ? refs[ref].index : index; + const gotoStory = (entry?: API_HashEntry) => { + if (entry?.type === 'docs' || entry?.type === 'story') { + store.setState({ settings: { ...settings, lastTrackedStoryId: entry.id } }); + navigate(`/${entry.type}/${entry.refId ? `${entry.refId}_${entry.id}` : entry.id}`); + return true; + } + return false; + }; const kindSlug = storyId?.split('--', 2)[0]; - - if (!hash) { + const hash = ref ? refs[ref].index : index; + const filteredHash = ref ? refs[ref].filteredIndex : filteredIndex; + if (!hash || !filteredHash) { return; } if (!name) { - // Find the entry (group, component or story) that is referred to + // Find the entry (group, component, story or docs) that is referred to const entry = titleOrId ? hash[titleOrId] || hash[sanitize(titleOrId)] : hash[kindSlug]; if (!entry) { throw new Error(`Unknown id or title: '${titleOrId}'`); } - store.setState({ - settings: { ...store.getState().settings, lastTrackedStoryId: entry.id }, - }); - - // We want to navigate to the first ancestor entry that is a leaf - const leafEntry = api.findLeafEntry(hash, entry.id); - const fullId = leafEntry.refId ? `${leafEntry.refId}_${leafEntry.id}` : leafEntry.id; - navigate(`/${leafEntry.type}/${fullId}`); + if (!gotoStory(entry)) { + // If the entry is not a story or docs, find the first descendant entry that is + gotoStory(api.findLeafEntry(filteredHash, entry.id)); + } } else if (!titleOrId) { + // Navigate to a named story/docs within the current component (i.e. "kind") // This is a slugified version of the kind, but that's OK, our toId function is idempotent - const id = toId(kindSlug, name); - - api.selectStory(id, undefined, options); + gotoStory(hash[toId(kindSlug, name)]); } else { const id = ref ? `${ref}_${toId(titleOrId, name)}` : toId(titleOrId, name); if (hash[id]) { - api.selectStory(id, undefined, options); + gotoStory(hash[id]); } else { // Support legacy API with component permalinks, where kind is `x/y` but permalink is 'z' const entry = hash[sanitize(titleOrId)]; if (entry?.type === 'component') { - const foundId = entry.children.find((childId: any) => hash[childId].name === name); - if (foundId) { - api.selectStory(foundId, undefined, options); - } + const foundId = entry.children.find((childId) => hash[childId].name === name); + gotoStory(foundId ? hash[foundId] : undefined); } } } @@ -478,7 +479,7 @@ export const init: ModuleFn = ({ return entry; } - const childStoryId = entry.children[0]; + const childStoryId = entry.children.find((childId) => index[childId]) || entry.children[0]; return api.findLeafEntry(index, childStoryId); }, findLeafStoryId(index, storyId) { diff --git a/code/core/src/manager-api/tests/stories.test.ts b/code/core/src/manager-api/tests/stories.test.ts index 521f6de75105..ea2e43e79b9f 100644 --- a/code/core/src/manager-api/tests/stories.test.ts +++ b/code/core/src/manager-api/tests/stories.test.ts @@ -1106,6 +1106,45 @@ describe('stories API', () => { api.selectStory('a--1'); expect(store.getState().settings.lastTrackedStoryId).toBe('a--1'); }); + it('selects first visible child when component is clicked with filtered index', () => { + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate, store } = moduleArgs; + + // Set index with stories + api.setIndex({ v: 5, entries: navigationEntries }); + + // Set up filtered index where first child (a--1) is hidden + const filteredIndex = { + a: { + id: 'a', + type: 'component' as const, + name: 'a', + depth: 0, + tags: [], + children: ['a--1', 'a--2'], + importPath: './a.ts', + }, + 'a--2': { + ...navigationEntries['a--2'], + type: 'story' as const, + subtype: 'story' as const, + parent: 'a', + depth: 1, + tags: [], + prepared: false, + exportName: '2', + }, + // Note: 'a--1' is missing from filtered index (hidden) + }; + + store.setState({ filteredIndex }); + + // When selecting the component, it should select the first visible child (a--2) + api.selectStory('a'); + expect(navigate).toHaveBeenCalledWith('/story/a--2'); + }); describe('deprecated api', () => { it('allows navigating to a combination of title + name', () => { const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; diff --git a/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx b/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx index 187ea6456a21..1fe947674eb7 100644 --- a/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx @@ -213,7 +213,7 @@ export const TagsFilterPanel = ({ title: 'Learn how to add tags', icon: , right: , - href: api.getDocsUrl({ subpath: 'writing-stories/tags#filtering-by-custom-tags' }), + href: api.getDocsUrl({ subpath: 'writing-stories/tags#custom-tags' }), }, ]); } diff --git a/code/core/src/node-logger/prompts/prompt-provider-clack.ts b/code/core/src/node-logger/prompts/prompt-provider-clack.ts index 2b6de0749c5c..4a527fe48ce1 100644 --- a/code/core/src/node-logger/prompts/prompt-provider-clack.ts +++ b/code/core/src/node-logger/prompts/prompt-provider-clack.ts @@ -53,7 +53,10 @@ export class ClackPromptProvider extends PromptProvider { options: MultiSelectPromptOptions, promptOptions?: PromptOptions ): Promise { - const result = await clack.multiselect(options); + const result = await clack.multiselect({ + ...options, + required: options.required, + }); this.handleCancel(result, promptOptions); logTracker.addLog('prompt', options.message, { choice: result }); return result as T[]; diff --git a/code/core/src/shared/utils/module.ts b/code/core/src/shared/utils/module.ts index 10eb9812ccf7..e4aa51f84e35 100644 --- a/code/core/src/shared/utils/module.ts +++ b/code/core/src/shared/utils/module.ts @@ -51,7 +51,10 @@ let isTypescriptLoaderRegistered = false; * // Returns the default export or the entire module * ``` */ -export async function importModule(path: string) { +export async function importModule( + path: string, + { skipCache = false }: { skipCache?: boolean } = {} +) { if (!isTypescriptLoaderRegistered) { const typescriptLoaderUrl = importMetaResolve('storybook/internal/bin/loader'); register(typescriptLoaderUrl, import.meta.url); @@ -61,12 +64,17 @@ export async function importModule(path: string) { let mod; try { const resolvedPath = win32.isAbsolute(path) ? pathToFileURL(path).href : path; - mod = await import(resolvedPath); + // When applicable, add a hash to the import URL to bypass cache + const importUrl = skipCache ? `${resolvedPath}?${Date.now()}` : resolvedPath; + mod = await import(importUrl); } catch (importError) { try { // fallback to require to support older behavior // this is relevant for presets that are only available with the "require" condition in a package's export map const require = createRequire(import.meta.url); + if (skipCache) { + delete require.cache[require.resolve(path)]; + } mod = require(path); } catch (requireError) { /* diff --git a/code/lib/cli-storybook/src/add.ts b/code/lib/cli-storybook/src/add.ts index 3d6fb548a7f2..95ca4722a0bd 100644 --- a/code/lib/cli-storybook/src/add.ts +++ b/code/lib/cli-storybook/src/add.ts @@ -1,8 +1,6 @@ -import { isAbsolute, join } from 'node:path'; - import { type PackageManagerName, - serverRequire, + loadMainConfig, syncStorybookAddons, versions, } from 'storybook/internal/common'; @@ -182,7 +180,8 @@ export async function add( // TODO: remove try/catch once CSF factories is shipped, for now gracefully handle any error try { - await syncStorybookAddons(mainConfig, previewConfigPath!, configDir); + const newMainConfig = await loadMainConfig({ configDir, skipCache: true }); + await syncStorybookAddons(newMainConfig, previewConfigPath!, configDir); } catch (e) { // } diff --git a/code/lib/cli-storybook/src/automigrate/multi-project.test.ts b/code/lib/cli-storybook/src/automigrate/multi-project.test.ts index 4ffc78668d9a..cd3e731dae7f 100644 --- a/code/lib/cli-storybook/src/automigrate/multi-project.test.ts +++ b/code/lib/cli-storybook/src/automigrate/multi-project.test.ts @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { type ProjectAutomigrationData, collectAutomigrationsAcrossProjects, + promptForAutomigrations, } from './multi-project'; import type { Fix } from './types'; @@ -120,4 +121,131 @@ describe('multi-project automigrations', () => { expect(results[0].reports.every((report) => report.status === 'check_failed')).toBe(true); }); }); + + describe('promptForAutomigrations', () => { + it('should call multiselect with required: false', async () => { + const { prompt } = await import('storybook/internal/node-logger'); + const multiselectMock = vi.mocked(prompt.multiselect); + multiselectMock.mockResolvedValue(['fix1']); + + const fix1 = createMockFix('fix1', { needsFix: true }); + const project1 = createMockProject('/project1/.storybook'); + + const automigrations = [ + { + fix: fix1, + reports: [ + { + result: { needsFix: true }, + status: 'check_succeeded' as const, + project: project1, + }, + ], + }, + ]; + + await promptForAutomigrations(automigrations, { dryRun: false, yes: false }); + + expect(multiselectMock).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Select automigrations to run', + required: false, + }) + ); + }); + + it('should return empty array when user selects nothing', async () => { + const { prompt } = await import('storybook/internal/node-logger'); + const multiselectMock = vi.mocked(prompt.multiselect); + multiselectMock.mockResolvedValue([]); + + const fix1 = createMockFix('fix1', { needsFix: true }); + const project1 = createMockProject('/project1/.storybook'); + + const automigrations = [ + { + fix: fix1, + reports: [ + { + result: { needsFix: true }, + status: 'check_succeeded' as const, + project: project1, + }, + ], + }, + ]; + + const result = await promptForAutomigrations(automigrations, { + dryRun: false, + yes: false, + }); + + expect(result).toEqual([]); + }); + + it('should return all automigrations when yes option is true', async () => { + const { logger } = await import('storybook/internal/node-logger'); + const logSpy = vi.spyOn(logger, 'log'); + + const fix1 = createMockFix('fix1', { needsFix: true }); + const fix2 = createMockFix('fix2', { needsFix: true }); + const project1 = createMockProject('/project1/.storybook'); + + const automigrations = [ + { + fix: fix1, + reports: [ + { + result: { needsFix: true }, + status: 'check_succeeded' as const, + project: project1, + }, + ], + }, + { + fix: fix2, + reports: [ + { + result: { needsFix: true }, + status: 'check_succeeded' as const, + project: project1, + }, + ], + }, + ]; + + const result = await promptForAutomigrations(automigrations, { dryRun: false, yes: true }); + + expect(result).toEqual(automigrations); + expect(logSpy).toHaveBeenCalledWith('Running all detected automigrations:'); + }); + + it('should return empty array when dryRun is true', async () => { + const { logger } = await import('storybook/internal/node-logger'); + const logSpy = vi.spyOn(logger, 'log'); + + const fix1 = createMockFix('fix1', { needsFix: true }); + const project1 = createMockProject('/project1/.storybook'); + + const automigrations = [ + { + fix: fix1, + reports: [ + { + result: { needsFix: true }, + status: 'check_succeeded' as const, + project: project1, + }, + ], + }, + ]; + + const result = await promptForAutomigrations(automigrations, { dryRun: true, yes: false }); + + expect(result).toEqual([]); + expect(logSpy).toHaveBeenCalledWith( + 'Detected automigrations (dry run - no changes will be made):' + ); + }); + }); }); diff --git a/code/lib/cli-storybook/src/automigrate/multi-project.ts b/code/lib/cli-storybook/src/automigrate/multi-project.ts index d3d9522f9ca4..a6256e84771d 100644 --- a/code/lib/cli-storybook/src/automigrate/multi-project.ts +++ b/code/lib/cli-storybook/src/automigrate/multi-project.ts @@ -243,6 +243,7 @@ export async function promptForAutomigrations( message: 'Select automigrations to run', options: choices, initialValues: choices.filter((c) => c.defaultSelected).map((c) => c.value), + required: false, }); return automigrations.filter((am) => selectedIds.includes(am.fix.id)); diff --git a/code/lib/cli-storybook/src/codemod/csf-factories.ts b/code/lib/cli-storybook/src/codemod/csf-factories.ts index 3c97dc62c8bd..ab05ddc55b87 100644 --- a/code/lib/cli-storybook/src/codemod/csf-factories.ts +++ b/code/lib/cli-storybook/src/codemod/csf-factories.ts @@ -67,23 +67,21 @@ export const csfFactories: CommandFix = { if (!optionalEnvToBoolean(process.env.IN_STORYBOOK_SANDBOX)) { // prompt whether the user wants to use imports map logger.logBox(dedent` - The CSF factories format benefits from subpath imports (the imports property in your \`package.json\`), which is a node standard for module resolution (commonly known as alias imports). This makes it more convenient to import the preview config in your story files. - - However, please note that this might not work if you have an outdated tsconfig, use custom paths, or have type alias plugins configured in your project. You can always rerun this codemod and select another option to update your code later. - - More info: ${picocolors.yellow('https://storybook.js.org/docs/api/csf/csf-factories#subpath-imports?ref=upgrade')} - - As we modify your story files, we can create two types of imports: - - - ${picocolors.bold('Subpath imports (recommended):')} ${picocolors.cyan("`import preview from '#.storybook/preview'`")} - - ${picocolors.bold('Relative imports (suitable for mono repos):')} ${picocolors.cyan("`import preview from '../../.storybook/preview'`")} + 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. + + However, we cannot broadly recommend it for all projects, because it might not work in some monorepo setups or if you have an outdated tsconfig, use custom paths, or have type alias plugins configured in your project. You can always rerun this codemod and select another option to update your code later. + + More info: ${picocolors.yellow('https://storybook.js.org/docs/10/api/csf/csf-next#subpath-imports?ref=upgrade')} `); useSubPathImports = await prompt.select({ - message: 'Which would you like to use?', + message: 'Which import type would you like to use for your story files?', options: [ - { label: 'Subpath imports (alias)', value: true }, - { label: 'Relative imports', value: false }, + { + label: "Relative imports (import preview from '../../.storybook/preview')", + value: false, + }, + { label: "Subpath imports (import preview from '#.storybook/preview')", value: true }, ], }); } @@ -128,7 +126,7 @@ export const csfFactories: CommandFix = { You can now run Storybook with the new CSF factories format. For more info, check out the docs: - ${picocolors.yellow('https://storybook.js.org/docs/api/csf/csf-factories?ref=upgrade')} + ${picocolors.yellow('https://storybook.js.org/docs/10/api/csf/csf-next?ref=upgrade')} ` ); }, 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 1c93d3fe7e10..c6875ca8ad94 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 @@ -10,6 +10,7 @@ import { storyToCsfFactory } from './story-to-csf-factory'; vi.mock('storybook/internal/node-logger', () => ({ logger: { + log: vi.fn(), warn: vi.fn(), }, })); @@ -34,11 +35,13 @@ describe('stories codemod', () => { transform(dedent` const meta = { title: 'Component' }; export default meta; + export const A = {}; `) ).resolves.toMatchInlineSnapshot(` import preview from '#.storybook/preview'; const meta = preview.meta({ title: 'Component' }); + export const A = meta.story(); `); }); @@ -46,6 +49,7 @@ describe('stories codemod', () => { await expect( transform(dedent` export default { title: 'Component' }; + export const A = {}; `) ).resolves.toMatchInlineSnapshot(` import preview from '#.storybook/preview'; @@ -53,6 +57,8 @@ describe('stories codemod', () => { const meta = preview.meta({ title: 'Component', }); + + export const A = meta.story(); `); }); @@ -61,11 +67,13 @@ describe('stories codemod', () => { transform(dedent` const componentMeta = { title: 'Component' }; export default componentMeta; + export const A = {}; `) ).resolves.toMatchInlineSnapshot(` import preview from '#.storybook/preview'; const componentMeta = preview.meta({ title: 'Component' }); + export const A = componentMeta.story(); `); }); @@ -349,6 +357,7 @@ describe('stories codemod', () => { source: dedent` import preview, { extra } from '../../../.storybook/preview'; export default {}; + export const A = {}; `, path: 'Component.stories.tsx', }, @@ -359,6 +368,7 @@ describe('stories codemod', () => { import preview, { extra } from '#.storybook/preview'; const meta = preview.meta({}); + export const A = meta.story(); `); await expect( @@ -369,6 +379,7 @@ describe('stories codemod', () => { source: dedent` import preview, { extra } from '#.storybook/preview'; export default {}; + export const A = {}; `, path: 'Component.stories.tsx', }, @@ -379,6 +390,7 @@ describe('stories codemod', () => { import preview, { extra } from '../../preview'; const meta = preview.meta({}); + export const A = meta.story(); `); } finally { relativeMock.mockRestore(); @@ -666,6 +678,24 @@ describe('stories codemod', () => { // expect(transformed).toContain('C = meta.story'); }); + it('should bail transformation when no stories can be transformed', async () => { + const source = dedent` + export default { + title: 'Component', + }; + `; + const transformed = await transform(source); + const formattedSource = await formatFileContent('Component.stories.tsx', source); + expect(transformed).toEqual(formattedSource); + + expect(transformed).not.toContain('preview.meta'); + expect(transformed).not.toContain('meta.story'); + + expect(vi.mocked(logger.warn).mock.calls[0][0]).toMatchInlineSnapshot( + `Skipping codemod for Component.stories.tsx: no stories were transformed. Either there are no stories, file has been already transformed or some stories are written in an unsupported format.` + ); + }); + it('should bail transformation and warn if some stories are not transformed to avoid mixed CSF formats', async () => { const source = dedent` export default { @@ -700,7 +730,7 @@ describe('stories codemod', () => { const formattedSource = await formatFileContent('Component.stories.tsx', source); expect(transformed).toEqual(formattedSource); - expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.log).not.toHaveBeenCalled(); }); }); }); 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 93ce52e94c3b..c6e784abc1b4 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 @@ -239,12 +239,16 @@ export async function storyToCsfFactory( }, }); + // If no stories were transformed, bail early to avoid having a mixed CSF syntax and therefore a broken indexer. + if (transformedStoryExports.size === 0) { + logger.warn( + `Skipping codemod for ${info.path}: no stories were transformed. Either there are no stories, file has been already transformed or some stories are written in an unsupported format.` + ); + return info.source; + } + // If some stories were detected but not all could be transformed, we skip the codemod to avoid mixed csf syntax and therefore a broken indexer. - if ( - detectedStoryNames.length > 0 && - transformedStoryExports.size > 0 && - transformedStoryExports.size !== detectedStoryNames.length - ) { + if (detectedStoryNames.length > 0 && transformedStoryExports.size !== detectedStoryNames.length) { logger.warn( `Skipping codemod for ${info.path}:\nSome of the detected stories [${detectedStoryNames .map((name) => `"${name}"`) diff --git a/code/package.json b/code/package.json index a1c8606bb8a4..971cd09cadac 100644 --- a/code/package.json +++ b/code/package.json @@ -283,5 +283,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "10.0.0-beta.12" } diff --git a/docs/_snippets/vite-includeStorybookNextjsPlugin.md b/docs/_snippets/vite-includeStorybookNextjsPlugin.md new file mode 100644 index 000000000000..005f7bd93602 --- /dev/null +++ b/docs/_snippets/vite-includeStorybookNextjsPlugin.md @@ -0,0 +1,9 @@ +```ts filename="vitest.config.ts" renderer="react" language="ts" +import { defineConfig } from "vite"; +import { storybookNextJsPlugin } from '@storybook/nextjs-vite/vite-plugin' + +export default defineConfig({ + // only necessary when not using @storybook/addon-vitest, otherwise the plugin is loaded automatically + plugins: [storybookNextJsPlugin()], +}); +``` diff --git a/docs/versions/next.json b/docs/versions/next.json index 10daefa60c8a..787706ec28d8 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"10.0.0-beta.11","info":{"plain":"- Automigration: Improve the viewport/backgrounds automigration - [#32619](https://github.com/storybookjs/storybook/pull/32619), thanks @valentinpalkovic!\n- CSF: Enhance config-to-csf-factory to support type wrappers - [#32543](https://github.com/storybookjs/storybook/pull/32543), thanks @yannbf!\n- Core: Ensure valid QR code URL - [#32661](https://github.com/storybookjs/storybook/pull/32661), thanks @ghengeveld!\n- Mocking: Fix `sb.mock` usage in Storybook's deployed in subpaths - [#32678](https://github.com/storybookjs/storybook/pull/32678), thanks @valentinpalkovic!\n- NextJS-Vite: Automatically fix bad PostCSS configuration - [#32691](https://github.com/storybookjs/storybook/pull/32691), thanks @ndelangen!\n- Nextjs: Fix Nextjs version detection with prereleases - [#32724](https://github.com/storybookjs/storybook/pull/32724), thanks @yannbf!\n- Presets: Support extensionless imports in TS-based presets - [#32641](https://github.com/storybookjs/storybook/pull/32641), thanks @JReinhold!\n- React Native Web: Fix REACT_NATIVE_AND_RNW should detect vite builder - [#32718](https://github.com/storybookjs/storybook/pull/32718), thanks @dannyhw!"}} \ No newline at end of file +{"version":"10.0.0-beta.12","info":{"plain":"- CLI: Avoid mixed CSF in files with unconventional stories - [#32716](https://github.com/storybookjs/storybook/pull/32716), thanks @yannbf!\n- CLI: Fix CSF factories addon syncing in storybook add command - [#32728](https://github.com/storybookjs/storybook/pull/32728), thanks @yannbf!\n- CLI: Make relative imports default in csf-factories codemod - [#32610](https://github.com/storybookjs/storybook/pull/32610), thanks @copilot-swe-agent!\n- Core: Prevent navigating to hidden (filtered) item - [#32715](https://github.com/storybookjs/storybook/pull/32715), thanks @ghengeveld!\n- Fix: Allow proceeding without selecting automigrations in upgrade command - [#32597](https://github.com/storybookjs/storybook/pull/32597), thanks @copilot-swe-agent!"}} \ No newline at end of file diff --git a/docs/writing-tests/integrations/stories-in-unit-tests.mdx b/docs/writing-tests/integrations/stories-in-unit-tests.mdx index d6ff7de0ce0f..88312033f8f7 100644 --- a/docs/writing-tests/integrations/stories-in-unit-tests.mdx +++ b/docs/writing-tests/integrations/stories-in-unit-tests.mdx @@ -79,6 +79,16 @@ Storybook provides community-led addons for other frameworks like [Vue 2](https: {/* prettier-ignore-end */} + + ### Next.js Vite cannot find the module + + If you are seeing error messages like `Cannot find module 'sb-original/image-context'` ensure you have included `storybookNextJsPlugin`. + + {/* prettier-ignore-start */} + + + + {/* prettier-ignore-end */}