diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index dc1270061fa3..bade8a334b3a 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,11 @@ +## 8.3.0-alpha.9 + +- Addon Viewport: Add default options via parameters - [#28944](https://github.com/storybookjs/storybook/pull/28944), thanks @ndelangen! +- CLI: Make PackageJson optional for starting a dev server - [#28594](https://github.com/storybookjs/storybook/pull/28594), thanks @tobiasdiez! +- Svelte: Fix events not being logged in Actions when a story has decorators - [#28247](https://github.com/storybookjs/storybook/pull/28247), thanks @JReinhold! +- Vitest: Fix default viewport - [#28943](https://github.com/storybookjs/storybook/pull/28943), thanks @kasperpeulen! +- Vitest: Implement add command for vitest addon - [#28920](https://github.com/storybookjs/storybook/pull/28920), thanks @kasperpeulen! + ## 8.3.0-alpha.8 - Addon Vitest: Improve transformation logic to avoid duplicate tests - [#28929](https://github.com/storybookjs/storybook/pull/28929), thanks @yannbf! diff --git a/code/.storybook/preview.tsx b/code/.storybook/preview.tsx index 638217cad31d..1f366074846f 100644 --- a/code/.storybook/preview.tsx +++ b/code/.storybook/preview.tsx @@ -342,9 +342,6 @@ export const parameters = { 'slategray', ], }, - viewport: { - options: MINIMAL_VIEWPORTS, - }, themes: { disable: true, }, diff --git a/code/addons/viewport/preview.js b/code/addons/viewport/preview.js index 6a4da7cfa953..49ad602f79f4 100644 --- a/code/addons/viewport/preview.js +++ b/code/addons/viewport/preview.js @@ -1 +1 @@ -export { globals } from './dist/preview'; +export * from './dist/preview'; diff --git a/code/addons/viewport/src/preview.ts b/code/addons/viewport/src/preview.ts index a59790415d2f..21f45e310635 100644 --- a/code/addons/viewport/src/preview.ts +++ b/code/addons/viewport/src/preview.ts @@ -1,4 +1,5 @@ import { PARAM_KEY as KEY } from './constants'; +import { MINIMAL_VIEWPORTS } from './defaults'; import type { GlobalState } from './types'; const modern: Record = { @@ -9,3 +10,11 @@ const modern: Record = { const legacy = { viewport: 'reset', viewportRotated: false }; export const initialGlobals = FEATURES?.viewportStoryGlobals ? modern : legacy; + +export const parameters = FEATURES?.viewportStoryGlobals + ? { + [KEY]: { + options: MINIMAL_VIEWPORTS, + }, + } + : {}; diff --git a/code/addons/vitest/package.json b/code/addons/vitest/package.json index 07272c276664..7b701bbfe643 100644 --- a/code/addons/vitest/package.json +++ b/code/addons/vitest/package.json @@ -75,6 +75,9 @@ }, "devDependencies": { "@vitest/browser": "^2.0.0", + "find-up": "^7.0.0", + "tinyrainbow": "^1.2.0", + "ts-dedent": "^2.2.0", "vitest": "^2.0.0" }, "peerDependencies": { diff --git a/code/addons/vitest/src/plugin/viewports.test.ts b/code/addons/vitest/src/plugin/viewports.test.ts index cd83f5edc518..a175077cf649 100644 --- a/code/addons/vitest/src/plugin/viewports.test.ts +++ b/code/addons/vitest/src/plugin/viewports.test.ts @@ -1,6 +1,8 @@ /* eslint-disable no-underscore-dangle */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport'; + import { page } from '@vitest/browser/context'; import { DEFAULT_VIEWPORT_DIMENSIONS, type ViewportsParam, setViewport } from './viewports'; @@ -42,6 +44,7 @@ describe('setViewport', () => { it('should set the dimensions of viewport from INITIAL_VIEWPORTS', async () => { const viewportsParam: any = { + viewports: INITIAL_VIEWPORTS, // supported by default in addon viewports defaultViewport: 'ipad', }; diff --git a/code/addons/vitest/src/plugin/viewports.ts b/code/addons/vitest/src/plugin/viewports.ts index c68047877006..c779eb3a6633 100644 --- a/code/addons/vitest/src/plugin/viewports.ts +++ b/code/addons/vitest/src/plugin/viewports.ts @@ -3,7 +3,7 @@ import { UnsupportedViewportDimensionError } from 'storybook/internal/preview-er import { page } from '@vitest/browser/context'; -import { INITIAL_VIEWPORTS } from '../../../viewport/src/defaults'; +import { MINIMAL_VIEWPORTS } from '../../../viewport/src/defaults'; import type { ViewportMap, ViewportStyles } from '../../../viewport/src/types'; declare global { @@ -50,12 +50,12 @@ const parseDimension = (value: string, dimension: 'width' | 'height') => { export const setViewport = async (viewportsParam: ViewportsParam = {} as ViewportsParam) => { const defaultViewport = viewportsParam.defaultViewport; - if (!page || !globalThis.__vitest_browser__ || !defaultViewport) { + if (!page || !globalThis.__vitest_browser__) { return; } const viewports = { - ...INITIAL_VIEWPORTS, + ...MINIMAL_VIEWPORTS, ...viewportsParam.viewports, }; diff --git a/code/addons/vitest/src/postinstall.ts b/code/addons/vitest/src/postinstall.ts index 47bcad1f9349..53361975704b 100644 --- a/code/addons/vitest/src/postinstall.ts +++ b/code/addons/vitest/src/postinstall.ts @@ -1,3 +1,254 @@ -export default async function postinstall(context: any) { - console.log('[addon-vitest] postinstall with', context); +import { existsSync } from 'node:fs'; +import * as fs from 'node:fs/promises'; +import { writeFile } from 'node:fs/promises'; +import { dirname, join, relative, resolve } from 'node:path'; +import * as path from 'node:path'; + +import { + JsPackageManagerFactory, + extractProperFrameworkName, + loadAllPresets, + loadMainConfig, + validateFrameworkName, +} from 'storybook/internal/common'; +import { logger } from 'storybook/internal/node-logger'; + +import { findUp } from 'find-up'; +import c from 'tinyrainbow'; +import dedent from 'ts-dedent'; + +import { type PostinstallOptions } from '../../../lib/cli-storybook/src/add'; + +const extensions = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs']; + +export default async function postInstall(options: PostinstallOptions) { + const packageManager = JsPackageManagerFactory.getPackageManager({ + force: options.packageManager, + }); + + const info = await getFrameworkInfo(options); + + if ( + info.frameworkPackageName !== '@storybook/nextjs' && + info.builderPackageName !== '@storybook/builder-vite' + ) { + logger.info( + 'The Vitest addon can only be used with a Vite-based Storybook framework or Next.js.' + ); + return; + } + + const annotationsImport = [ + '@storybook/nextjs', + '@storybook/experimental-nextjs-vite', + '@storybook/sveltekit', + ].includes(info.frameworkPackageName) + ? info.frameworkPackageName + : info.rendererPackageName && + ['@storybook/react', '@storybook/svelte', '@storybook/vue3'].includes( + info.rendererPackageName + ) + ? info.rendererPackageName + : null; + + if (!annotationsImport) { + logger.info('The Vitest addon cannot yet be used with: ' + info.frameworkPackageName); + return; + } + + const vitestInfo = getVitestPluginInfo(info.frameworkPackageName); + + const packages = ['vitest@latest', '@vitest/browser@latest', 'playwright@latest']; + + logger.info( + dedent` + We detected that you're using Next.js. + We will configure the vite-plugin-storybook-nextjs plugin to allow you to run tests in Vitest. + ` + ); + + if (info.frameworkPackageName === '@storybook/nextjs') { + packages.push('vite-plugin-storybook-nextjs@latest'); + } + logger.info(c.bold('Installing packages...')); + logger.info(packages.join(', ')); + await packageManager.addDependencies({ installAsDevDependencies: true }, packages); + + logger.info(c.bold('Executing npx playwright install chromium --with-deps ...')); + await packageManager.executeCommand({ + command: 'npx', + args: ['playwright', 'install', 'chromium', '--with-deps'], + }); + + logger.info(c.bold('Writing .storybook/vitest.setup.ts file...')); + + const previewExists = extensions + .map((ext) => path.resolve(path.join(options.configDir, `preview${ext}`))) + .some((config) => existsSync(config)); + + await writeFile( + resolve(options.configDir, 'vitest.setup.ts'), + dedent` + import { beforeAll } from 'vitest' + import { setProjectAnnotations } from '${annotationsImport}' + ${previewExists ? `import * as projectAnnotations from './preview'` : ''} + + const project = setProjectAnnotations(${previewExists ? 'projectAnnotations' : '[]'}) + + beforeAll(project.beforeAll) + ` + ); + + const configFiles = extensions.map((ext) => 'vitest.config' + ext); + + const rootConfig = await findUp(configFiles, { + cwd: process.cwd(), + }); + + if (rootConfig) { + const extname = rootConfig ? path.extname(rootConfig) : '.ts'; + const browserWorkspaceFile = resolve(dirname(rootConfig), `vitest.workspace${extname}`); + if (existsSync(browserWorkspaceFile)) { + logger.info( + dedent` + We can not automatically setup the plugin when you use Vitest with workspaces. + Please refer to the documentation to complete the setup manually: + https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual + ` + ); + } else { + logger.info(c.bold('Writing vitest.workspace.ts file...')); + await writeFile( + browserWorkspaceFile, + dedent` + import { defineWorkspace } from 'vitest/config'; + import { storybookTest } from '@storybook/experimental-addon-vitest/plugin'; + ${vitestInfo.frameworkPluginImport ? vitestInfo.frameworkPluginImport + '\n' : ''} + export default defineWorkspace([ + '${relative(dirname(browserWorkspaceFile), rootConfig)}', + { + plugins: [ + storybookTest(),${vitestInfo.frameworkPluginCall ? '\n' + vitestInfo.frameworkPluginCall : ''} + ], + test: { + include: ['**/*.stories.?(m)[jt]s?(x)'], + browser: { + enabled: true, + name: 'chromium', + provider: 'playwright', + headless: true, + }, + // Disabling isolation is faster and is similar to how tests are isolated in storybook itself. + // Consider removing this if you are seeing problems with your tests. + isolate: false, + setupFiles: ['./.storybook/vitest.setup.ts'], + }, + }, + ]); + ` + ); + } + } else { + logger.info(c.bold('Writing vitest.config.ts file...')); + await writeFile( + resolve('vitest.config.ts'), + dedent` + import { defineConfig } from "vitest/config"; + import { storybookTest } from "@storybook/experimental-addon-vitest/plugin"; + ${vitestInfo.frameworkPluginImport ? vitestInfo.frameworkPluginImport + '\n' : ''} + export default defineConfig({ + plugins: [ + storybookTest(),${vitestInfo.frameworkPluginCall ? '\n' + vitestInfo.frameworkPluginCall : ''} + ], + test: { + include: ['**/*.stories.?(m)[jt]s?(x)'], + browser: { + enabled: true, + name: 'chromium', + provider: 'playwright', + headless: true, + }, + // Disabling isolation is faster and is similar to how tests are isolated in storybook itself. + // Consider removing this, if you have flaky tests. + isolate: false, + setupFiles: ['./.storybook/vitest.setup.ts'], + }, + }); + ` + ); + } + + logger.info( + dedent` + The Vitest addon is now configured and you're ready to run your tests! + Check the documentation for more information about its features and options at: + https://storybook.js.org/docs/writing-tests/test-runner-with-vitest + ` + ); +} + +const getVitestPluginInfo = (framework: string) => { + let frameworkPluginImport = ''; + let frameworkPluginCall = ''; + + if (framework === '@storybook/nextjs') { + frameworkPluginImport = "import vitePluginNext from 'vite-plugin-storybook-nextjs'"; + frameworkPluginCall = 'vitePluginNext()'; + } + + if (framework === '@storybook/sveltekit') { + frameworkPluginImport = "import { storybookSveltekitPlugin } from '@storybook/sveltekit/vite'"; + frameworkPluginCall = 'storybookSveltekitPlugin()'; + } + + return { frameworkPluginImport, frameworkPluginCall }; +}; + +async function getFrameworkInfo({ configDir, packageManager: pkgMgr }: PostinstallOptions) { + const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); + const packageJson = await packageManager.retrievePackageJson(); + + const config = await loadMainConfig({ configDir, noCache: true }); + const { framework } = config; + + const frameworkName = typeof framework === 'string' ? framework : framework?.name; + validateFrameworkName(frameworkName); + const frameworkPackageName = extractProperFrameworkName(frameworkName); + + console.log(configDir); + const presets = await loadAllPresets({ + corePresets: [join(frameworkName, 'preset')], + overridePresets: [ + require.resolve('@storybook/core/core-server/presets/common-override-preset'), + ], + configDir, + packageJson, + isCritical: true, + }); + + const core = await presets.apply('core', {}); + + const { builder, renderer } = core; + + if (!builder) { + throw new Error('Could not detect your Storybook builder.'); + } + + const builderPackageJson = await fs.readFile( + `${typeof builder === 'string' ? builder : builder.name}/package.json`, + 'utf8' + ); + const builderPackageName = JSON.parse(builderPackageJson).name; + + let rendererPackageName: string | undefined; + if (renderer) { + const rendererPackageJson = await fs.readFile(`${renderer}/package.json`, 'utf8'); + rendererPackageName = JSON.parse(rendererPackageJson).name; + } + + return { + frameworkPackageName, + builderPackageName, + rendererPackageName, + }; } diff --git a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts index 461a36aec648..ea8b55d4973c 100644 --- a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts +++ b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts @@ -165,7 +165,7 @@ export default async ( inject: false, template, templateParameters: { - version: packageJson.version, + version: packageJson?.version, globals: { CONFIG_TYPE: configType, LOGLEVEL: logLevel, diff --git a/code/core/src/core-server/build-dev.ts b/code/core/src/core-server/build-dev.ts index fc07aa11ab95..5c87a7d9ccd8 100644 --- a/code/core/src/core-server/build-dev.ts +++ b/code/core/src/core-server/build-dev.ts @@ -1,6 +1,7 @@ import { join, relative, resolve } from 'node:path'; import { + getConfigInfo, getProjectRoot, loadAllPresets, loadMainConfig, @@ -8,6 +9,7 @@ import { resolvePathInStorybookCache, serverResolve, validateFrameworkName, + versions, } from '@storybook/core/common'; import { oneWayHash, telemetry } from '@storybook/core/telemetry'; import type { BuilderOptions, CLIOptions, LoadOptions, Options } from '@storybook/core/types'; @@ -32,18 +34,33 @@ import { warnOnIncompatibleAddons } from './utils/warnOnIncompatibleAddons'; import { warnWhenUsingArgTypesRegex } from './utils/warnWhenUsingArgTypesRegex'; export async function buildDevStandalone( - options: CLIOptions & LoadOptions & BuilderOptions + options: CLIOptions & + LoadOptions & + BuilderOptions & { + storybookVersion?: string; + previewConfigPath?: string; + } ): Promise<{ port: number; address: string; networkAddress: string }> { const { packageJson, versionUpdates } = options; - invariant( - packageJson.version !== undefined, - `Expected package.json#version to be defined in the "${packageJson.name}" package}` - ); + let { storybookVersion, previewConfigPath } = options; + const configDir = resolve(options.configDir); + if (packageJson) { + invariant( + packageJson.version !== undefined, + `Expected package.json#version to be defined in the "${packageJson.name}" package}` + ); + storybookVersion = packageJson.version; + previewConfigPath = getConfigInfo(packageJson, configDir).previewConfig ?? undefined; + } else { + if (!storybookVersion) { + storybookVersion = versions.storybook; + } + } // updateInfo are cached, so this is typically pretty fast const [port, versionCheck] = await Promise.all([ getServerPort(options.port, { exactPort: options.exactPort }), versionUpdates - ? updateCheck(packageJson.version) + ? updateCheck(storybookVersion) : Promise.resolve({ success: false, cached: false, data: {}, time: Date.now() }), ]); @@ -60,7 +77,6 @@ export async function buildDevStandalone( } const rootDir = getProjectRoot(); - const configDir = resolve(options.configDir); const cacheKey = oneWayHash(relative(rootDir, configDir)); const cacheOutputDir = resolvePathInStorybookCache('public', cacheKey); @@ -92,13 +108,13 @@ export async function buildDevStandalone( frameworkName = frameworkName || 'custom'; try { - await warnOnIncompatibleAddons(packageJson.version); + await warnOnIncompatibleAddons(storybookVersion); } catch (e) { console.warn('Storybook failed to check addon compatibility', e); } try { - await warnWhenUsingArgTypesRegex(packageJson, configDir, config); + await warnWhenUsingArgTypesRegex(previewConfigPath, config); } catch (e) {} // Load first pass: We need to determine the builder @@ -224,7 +240,7 @@ export async function buildDevStandalone( if (!options.quiet) { outputStartupInformation({ updateInfo: versionCheck, - version: packageJson.version, + version: storybookVersion, name, address, networkAddress, diff --git a/code/core/src/core-server/presets/common-preset.ts b/code/core/src/core-server/presets/common-preset.ts index 506ceb55ea2a..bca9a1c9cfe6 100644 --- a/code/core/src/core-server/presets/common-preset.ts +++ b/code/core/src/core-server/presets/common-preset.ts @@ -141,7 +141,7 @@ export const babel = async (_: unknown, options: Options) => { }; export const title = (previous: string, options: Options) => - previous || options.packageJson.name || false; + previous || options.packageJson?.name || false; export const logLevel = (previous: any, options: Options) => previous || options.loglevel || 'info'; diff --git a/code/core/src/core-server/utils/warnWhenUsingArgTypesRegex.ts b/code/core/src/core-server/utils/warnWhenUsingArgTypesRegex.ts index b36755fcd6ab..207e762ede60 100644 --- a/code/core/src/core-server/utils/warnWhenUsingArgTypesRegex.ts +++ b/code/core/src/core-server/utils/warnWhenUsingArgTypesRegex.ts @@ -1,5 +1,4 @@ -import { getConfigInfo } from '@storybook/core/common'; -import type { PackageJson, StorybookConfig } from '@storybook/core/types'; +import type { StorybookConfig } from '@storybook/core/types'; import { babelParse } from '@storybook/core/csf-tools'; @@ -10,12 +9,10 @@ import { readFile } from 'fs-extra'; import { dedent } from 'ts-dedent'; export async function warnWhenUsingArgTypesRegex( - packageJson: PackageJson, - configDir: string, + previewConfigPath: string | undefined, config: StorybookConfig ) { - const { previewConfig } = getConfigInfo(packageJson, configDir); - const previewContent = previewConfig ? await readFile(previewConfig, 'utf8') : ''; + const previewContent = previewConfigPath ? await readFile(previewConfigPath, 'utf8') : ''; const hasVisualTestAddon = config?.addons?.some((it) => @@ -24,10 +21,10 @@ export async function warnWhenUsingArgTypesRegex( : it.name === '@chromatic-com/storybook' ) ?? false; - if (hasVisualTestAddon && previewConfig && previewContent.includes('argTypesRegex')) { + if (hasVisualTestAddon && previewConfigPath && previewContent.includes('argTypesRegex')) { // @ts-expect-error File is not yet exposed, see https://github.com/babel/babel/issues/11350#issuecomment-644118606 const file: BabelFile = new babel.File( - { filename: previewConfig }, + { filename: previewConfigPath }, { code: previewContent, ast: babelParse(previewContent) } ); @@ -39,7 +36,7 @@ export async function warnWhenUsingArgTypesRegex( 'actions.argTypesRegex' )} together with the visual test addon: - ${path.buildCodeFrameError(previewConfig).message} + ${path.buildCodeFrameError(previewConfigPath).message} We recommend removing the ${chalk.cyan( 'argTypesRegex' diff --git a/code/core/src/preview-api/modules/store/csf/portable-stories.ts b/code/core/src/preview-api/modules/store/csf/portable-stories.ts index f81ec95eca77..1525b6e3e6d8 100644 --- a/code/core/src/preview-api/modules/store/csf/portable-stories.ts +++ b/code/core/src/preview-api/modules/store/csf/portable-stories.ts @@ -165,8 +165,12 @@ export function composeStory {}, - showError: (error) => {}, - showException: (error) => {}, + showError: (error): void => { + throw new Error(`${error.title}\n${error.description}`); + }, + showException: (error): void => { + throw error; + }, forceRemount: true, storyContext: context, storyFn: () => story.unboundStoryFn(context), diff --git a/code/core/src/types/modules/core-common.ts b/code/core/src/types/modules/core-common.ts index fb2f8542d514..8e71a4cadb2a 100644 --- a/code/core/src/types/modules/core-common.ts +++ b/code/core/src/types/modules/core-common.ts @@ -151,7 +151,7 @@ export type PackageJson = PackageJsonFromTypeFest & Record; // TODO: This could be exported to the outside world and used in `options.ts` file of each `@storybook/APP` // like it's described in docs/api/new-frameworks.md export interface LoadOptions { - packageJson: PackageJson; + packageJson?: PackageJson; outputDir?: string; configDir?: string; cacheKey?: string; diff --git a/code/lib/cli-storybook/src/add.test.ts b/code/lib/cli-storybook/src/add.test.ts index 10e7b90b23ac..137e60a72008 100644 --- a/code/lib/cli-storybook/src/add.test.ts +++ b/code/lib/cli-storybook/src/add.test.ts @@ -144,6 +144,7 @@ describe('add (extra)', () => { expect(MockedPostInstall.postinstallAddon).toHaveBeenCalledWith('@storybook/addon-docs', { packageManager: 'npm', + configDir: '.storybook', }); }); }); diff --git a/code/lib/cli-storybook/src/add.ts b/code/lib/cli-storybook/src/add.ts index 409c91c41fb6..df3ac84f140f 100644 --- a/code/lib/cli-storybook/src/add.ts +++ b/code/lib/cli-storybook/src/add.ts @@ -17,6 +17,7 @@ import { postinstallAddon } from './postinstallAddon'; export interface PostinstallOptions { packageManager: PackageManagerName; + configDir: string; } /** @@ -139,7 +140,7 @@ export async function add( await writeConfig(main); if (!skipPostinstall && isCoreAddon(addonName)) { - await postinstallAddon(addonName, { packageManager: packageManager.type }); + await postinstallAddon(addonName, { packageManager: packageManager.type, configDir }); } } function isValidVersion(version: string) { diff --git a/code/package.json b/code/package.json index 264fb23412bb..d6adab474ae7 100644 --- a/code/package.json +++ b/code/package.json @@ -292,5 +292,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.3.0-alpha.9" } diff --git a/code/presets/create-react-app/src/helpers/processCraConfig.ts b/code/presets/create-react-app/src/helpers/processCraConfig.ts index befe78f217b7..eeb28fe91b2e 100644 --- a/code/presets/create-react-app/src/helpers/processCraConfig.ts +++ b/code/presets/create-react-app/src/helpers/processCraConfig.ts @@ -39,7 +39,7 @@ export const processCraConfig = async ( * * See: https://github.com/storybookjs/storybook/pull/9157 */ - const storybookVersion = semver.coerce(options.packageJson.version) || ''; + const storybookVersion = semver.coerce(options.packageJson?.version) || ''; const isStorybook530 = semver.gte(storybookVersion, '5.3.0'); const babelOptions = await options.presets.apply('babel'); diff --git a/code/renderers/svelte/src/components/PreviewRender.svelte b/code/renderers/svelte/src/components/PreviewRender.svelte index b2900a0ddb24..3a3ef4ad5801 100644 --- a/code/renderers/svelte/src/components/PreviewRender.svelte +++ b/code/renderers/svelte/src/components/PreviewRender.svelte @@ -15,6 +15,8 @@ props = {}, /** @type {{[string]: () => {}}} Attach svelte event handlers */ on, + /** @type {any} whether this level of the decorator chain is the last, ie. the actual story */ + argTypes, } = storyFn(); let firstTime = true; @@ -29,20 +31,16 @@ Component, props, on, + argTypes, }; } return storyFn(); } // reactive, re-render on storyFn change - $: ({ Component, props = {}, on } = getStoryFnValue(storyFn)); - - const eventsFromArgTypes = Object.fromEntries( - Object.entries(storyContext.argTypes) - .filter(([k, v]) => v.action && props[k] != null) - .map(([k, v]) => [v.action, props[k]]) - ); + $: ({ Component, props = {}, on, argTypes } = getStoryFnValue(storyFn)); + // set the argTypes context, read by the last SlotDecorator that renders the original story if (!Component) { showError({ title: `Expecting a Svelte component from the story: "${name}" of "${title}".`, @@ -55,4 +53,4 @@ } - + diff --git a/code/renderers/svelte/src/components/SlotDecorator.svelte b/code/renderers/svelte/src/components/SlotDecorator.svelte index 8391eb0af6e3..27b80aae3412 100644 --- a/code/renderers/svelte/src/components/SlotDecorator.svelte +++ b/code/renderers/svelte/src/components/SlotDecorator.svelte @@ -6,20 +6,36 @@ export let Component; export let props = {}; export let on = undefined; + export let argTypes = undefined; let instance; let decoratorInstance; const svelteVersion = VERSION[0]; + + /* + Svelte Docgen will create argTypes for events with the name 'event_eventName' + The Actions addon will convert these to args because they are type: 'action' + We need to filter these args out so they are not passed to the component + */ + let propsWithoutDocgenEvents; + $: propsWithoutDocgenEvents = Object.fromEntries( + Object.entries(props).filter(([key]) => !key.startsWith('event_')) + ); - if (on && svelteVersion < 5) { + if (argTypes && svelteVersion < 5) { + const eventsFromArgTypes = Object.fromEntries( + Object.entries(argTypes) + .filter(([key, value]) => value.action && props[key] != null) + .map(([key, value]) => [value.action, props[key]]) + ); // Attach Svelte event listeners in Svelte v4 // In Svelte v5 this is not possible anymore as instances are no longer classes with $on() properties, so it will be a no-op onMount(() => { - Object.entries(on).forEach(([eventName, eventCallback]) => { - // instance can be undefined if a decorator doesn't have + Object.entries({ ...eventsFromArgTypes, ...on }).forEach(([eventName, eventCallback]) => { + // instance can be undefined if a decorator doesn't have a const inst = instance ?? decoratorInstance; - inst?.$on?.(eventName, eventCallback) + inst?.$on?.(eventName, eventCallback); }); }); } @@ -27,8 +43,8 @@ {#if decorator} - + {:else} - + {/if} diff --git a/code/renderers/svelte/src/decorators.ts b/code/renderers/svelte/src/decorators.ts index 15ba2acf4831..200c6b41f006 100644 --- a/code/renderers/svelte/src/decorators.ts +++ b/code/renderers/svelte/src/decorators.ts @@ -74,7 +74,8 @@ function prepareStory( }; } - return preparedStory; + // no innerStory means this is the last story in the decorator chain, so it should create events from argTypes + return { ...preparedStory, argTypes: context.argTypes }; } export function decorateStory(storyFn: any, decorators: any[]) { diff --git a/code/yarn.lock b/code/yarn.lock index dea4e0ea405f..9a26af3780c1 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6132,6 +6132,9 @@ __metadata: dependencies: "@storybook/csf": "npm:^0.1.11" "@vitest/browser": "npm:^2.0.0" + find-up: "npm:^7.0.0" + tinyrainbow: "npm:^1.2.0" + ts-dedent: "npm:^2.2.0" vitest: "npm:^2.0.0" peerDependencies: "@vitest/browser": ^2.0.0 diff --git a/docs/versions/next.json b/docs/versions/next.json index 713ecd1b6677..551af3e7b3d7 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"8.3.0-alpha.8","info":{"plain":"- Addon Vitest: Improve transformation logic to avoid duplicate tests - [#28929](https://github.com/storybookjs/storybook/pull/28929), thanks @yannbf!\n- Addon Vitest: Set default viewport if applicable - [#28905](https://github.com/storybookjs/storybook/pull/28905), thanks @yannbf!\n- Addon-docs: Remove babel dependency - [#28915](https://github.com/storybookjs/storybook/pull/28915), thanks @shilman!\n- Blocks: Fix scroll to non-ascii anchors - [#28826](https://github.com/storybookjs/storybook/pull/28826), thanks @SkReD!\n- Core: Introduce setProjectAnnotations API to more renderers and frameworks - [#28907](https://github.com/storybookjs/storybook/pull/28907), thanks @yannbf!\n- Dependencies: Upgrade `commander` - [#28857](https://github.com/storybookjs/storybook/pull/28857), thanks @43081j!\n- SvelteKit: Introduce portable stories support - [#28918](https://github.com/storybookjs/storybook/pull/28918), thanks @yannbf!"}} +{"version":"8.3.0-alpha.9","info":{"plain":"- Addon Viewport: Add default options via parameters - [#28944](https://github.com/storybookjs/storybook/pull/28944), thanks @ndelangen!\n- CLI: Make PackageJson optional for starting a dev server - [#28594](https://github.com/storybookjs/storybook/pull/28594), thanks @tobiasdiez!\n- Svelte: Fix events not being logged in Actions when a story has decorators - [#28247](https://github.com/storybookjs/storybook/pull/28247), thanks @JReinhold!\n- Vitest: Fix default viewport - [#28943](https://github.com/storybookjs/storybook/pull/28943), thanks @kasperpeulen!\n- Vitest: Implement add command for vitest addon - [#28920](https://github.com/storybookjs/storybook/pull/28920), thanks @kasperpeulen!"}} diff --git a/scripts/tasks/sandbox-parts.ts b/scripts/tasks/sandbox-parts.ts index 1efffdb68593..976154350b7c 100644 --- a/scripts/tasks/sandbox-parts.ts +++ b/scripts/tasks/sandbox-parts.ts @@ -545,10 +545,11 @@ export async function addExtraDependencies({ await packageManager.addDependencies({ installAsDevDependencies: true }, extraDevDeps); if (extraDeps) { + const versionedExtraDeps = await packageManager.getVersionedPackages(extraDeps); if (debug) { - logger.log('\uD83C\uDF81 Adding extra deps', extraDeps); + logger.log('\uD83C\uDF81 Adding extra deps', versionedExtraDeps); } - await packageManager.addDependencies({ installAsDevDependencies: true }, extraDeps); + await packageManager.addDependencies({ installAsDevDependencies: true }, versionedExtraDeps); } await packageManager.installDependencies(); } diff --git a/scripts/tasks/sandbox.ts b/scripts/tasks/sandbox.ts index c7848a748c57..b650c80df276 100644 --- a/scripts/tasks/sandbox.ts +++ b/scripts/tasks/sandbox.ts @@ -81,7 +81,12 @@ export const sandbox: Task = { await addStories(details, options); } - const extraDeps = details.template.modifications?.extraDependencies ?? []; + const extraDeps = [ + ...(details.template.modifications?.extraDependencies ?? []), + // The storybook package forwards some CLI commands to @storybook/cli with npx. + // Adding the dep makes sure that even npx will use the linked workspace version. + '@storybook/cli', + ]; if (!details.template.skipTasks?.includes('vitest-integration')) { extraDeps.push( 'happy-dom',