diff --git a/.circleci/config.yml b/.circleci/config.yml index cd966b042091..3e5c0b499cc6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -403,7 +403,7 @@ jobs: parallelism: type: integer executor: - class: large + class: xlarge name: sb_playwright parallelism: << parameters.parallelism >> steps: @@ -723,7 +723,7 @@ workflows: requires: - build-sandboxes - vitest-integration: - parallelism: 4 + parallelism: 5 requires: - create-sandboxes - bench: @@ -789,7 +789,7 @@ workflows: requires: - build-sandboxes - vitest-integration: - parallelism: 4 + parallelism: 5 requires: - create-sandboxes - test-portable-stories: @@ -856,7 +856,7 @@ workflows: requires: - build-sandboxes - vitest-integration: - parallelism: 8 + parallelism: 11 requires: - create-sandboxes - test-portable-stories: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 61566b49d5bb..45c580300e1c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -159,8 +159,8 @@ jobs: run: | git checkout next git pull - git push --force origin latest-release - git push --force origin main + git push origin --force next:latest-release + git push origin --force next:main - name: Sync CHANGELOG.md from `main` to `next` if: steps.target.outputs.target == 'main' @@ -174,6 +174,7 @@ jobs: git commit -m "Update CHANGELOG.md for v${{ steps.version.outputs.current-version }} [skip ci]" || true git push origin next + # TODO: remove this step - @JReinhold - name: Sync version JSONs from `next-release` to `main` if: github.ref_name == 'next-release' working-directory: . diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 212933922023..dc1270061fa3 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,13 @@ +## 8.3.0-alpha.8 + +- Addon Vitest: Improve transformation logic to avoid duplicate tests - [#28929](https://github.com/storybookjs/storybook/pull/28929), thanks @yannbf! +- Addon Vitest: Set default viewport if applicable - [#28905](https://github.com/storybookjs/storybook/pull/28905), thanks @yannbf! +- Addon-docs: Remove babel dependency - [#28915](https://github.com/storybookjs/storybook/pull/28915), thanks @shilman! +- Blocks: Fix scroll to non-ascii anchors - [#28826](https://github.com/storybookjs/storybook/pull/28826), thanks @SkReD! +- Core: Introduce setProjectAnnotations API to more renderers and frameworks - [#28907](https://github.com/storybookjs/storybook/pull/28907), thanks @yannbf! +- Dependencies: Upgrade `commander` - [#28857](https://github.com/storybookjs/storybook/pull/28857), thanks @43081j! +- SvelteKit: Introduce portable stories support - [#28918](https://github.com/storybookjs/storybook/pull/28918), thanks @yannbf! + ## 8.3.0-alpha.7 - Addon Vitest: Set screenshotFailures to false by default - [#28908](https://github.com/storybookjs/storybook/pull/28908), thanks @yannbf! diff --git a/code/addons/docs/package.json b/code/addons/docs/package.json index 28933b229b1a..23e63535db89 100644 --- a/code/addons/docs/package.json +++ b/code/addons/docs/package.json @@ -98,7 +98,6 @@ "prep": "jiti ../../../scripts/prepare/bundle.ts" }, "dependencies": { - "@babel/core": "^7.24.4", "@mdx-js/react": "^3.0.0", "@storybook/blocks": "workspace:*", "@storybook/csf-plugin": "workspace:*", diff --git a/code/addons/docs/template/stories/docs2/UtfSymbolScroll.mdx b/code/addons/docs/template/stories/docs2/UtfSymbolScroll.mdx new file mode 100644 index 000000000000..11c902ce3baa --- /dev/null +++ b/code/addons/docs/template/stories/docs2/UtfSymbolScroll.mdx @@ -0,0 +1,14 @@ +import { Meta } from '@storybook/addon-docs'; + + + +## Instruction + +> Instruction below works only in iframe.html. Unknown code in normal mode (with manager) removes hash from url. + +Click on [link](#anchor-with-utf-symbols-абвг). That will jump scroll to anchor after green block below. Then reload page and +it should smooth-scroll to that anchor. + +
Space for scroll test
+ +## Anchor with utf symbols (абвг) \ No newline at end of file diff --git a/code/addons/vitest/src/plugin/index.ts b/code/addons/vitest/src/plugin/index.ts index c1fbabb8b626..322e1d5bb74b 100644 --- a/code/addons/vitest/src/plugin/index.ts +++ b/code/addons/vitest/src/plugin/index.ts @@ -3,8 +3,12 @@ import { join, resolve } from 'node:path'; import type { Plugin } from 'vitest/config'; -import { loadAllPresets, validateConfigurationFiles } from 'storybook/internal/common'; -import { vitestTransform } from 'storybook/internal/csf-tools'; +import { + getInterpretedFile, + loadAllPresets, + validateConfigurationFiles, +} from 'storybook/internal/common'; +import { readConfig, vitestTransform } from 'storybook/internal/csf-tools'; import { MainFileMissingError } from 'storybook/internal/server-errors'; import type { StoriesEntry } from 'storybook/internal/types'; @@ -16,6 +20,16 @@ const defaultOptions: UserOptions = { storybookUrl: 'http://localhost:6006', }; +const extractTagsFromPreview = async (configDir: string) => { + const previewConfigPath = getInterpretedFile(join(resolve(configDir), 'preview')); + + if (!previewConfigPath) { + return []; + } + const previewConfig = await readConfig(previewConfigPath); + return previewConfig.getFieldValue(['tags']) ?? []; +}; + export const storybookTest = (options?: UserOptions): Plugin => { const finalOptions = { ...defaultOptions, @@ -45,27 +59,33 @@ export const storybookTest = (options?: UserOptions): Plugin => { finalOptions.configDir = resolve(process.cwd(), finalOptions.configDir); } + let previewLevelTags: string[]; + return { name: 'vite-plugin-storybook-test', enforce: 'pre', async buildStart() { + // evaluate main.js and preview.js so we can extract + // stories for autotitle support and tags for tags filtering support + const configDir = finalOptions.configDir; try { - await validateConfigurationFiles(finalOptions.configDir); + await validateConfigurationFiles(configDir); } catch (err) { throw new MainFileMissingError({ - location: finalOptions.configDir, + location: configDir, source: 'vitest', }); } const presets = await loadAllPresets({ - configDir: finalOptions.configDir, + configDir, corePresets: [], overridePresets: [], packageJson: {}, }); stories = await presets.apply('stories', []); + previewLevelTags = await extractTagsFromPreview(configDir); }, async config(config) { // If we end up needing to know if we are running in browser mode later @@ -123,6 +143,7 @@ export const storybookTest = (options?: UserOptions): Plugin => { configDir: finalOptions.configDir, tagsFilter: finalOptions.tags, stories, + previewLevelTags, }); } }, diff --git a/code/addons/vitest/src/plugin/test-utils.ts b/code/addons/vitest/src/plugin/test-utils.ts index a86a178cc429..632ace561619 100644 --- a/code/addons/vitest/src/plugin/test-utils.ts +++ b/code/addons/vitest/src/plugin/test-utils.ts @@ -3,33 +3,29 @@ /* eslint-disable no-underscore-dangle */ import { type RunnerTask, type TaskContext, type TaskMeta, type TestContext } from 'vitest'; -import type { ComposedStoryFn } from 'storybook/internal/types'; +import { composeStory } from 'storybook/internal/preview-api'; +import type { ComponentAnnotations, ComposedStoryFn } from 'storybook/internal/types'; -import type { UserOptions } from './types'; import { setViewport } from './viewports'; -type TagsFilter = Required; - -export const isValidTest = (storyTags: string[], tagsFilter: TagsFilter) => { - const isIncluded = - tagsFilter?.include.length === 0 || tagsFilter?.include.some((tag) => storyTags.includes(tag)); - const isNotExcluded = tagsFilter?.exclude.every((tag) => !storyTags.includes(tag)); - - return isIncluded && isNotExcluded; -}; - -export const testStory = (Story: ComposedStoryFn, tagsFilter: TagsFilter) => { +export const testStory = ( + exportName: string, + story: ComposedStoryFn, + meta: ComponentAnnotations, + skipTags: string[] +) => { + const composedStory = composeStory(story, meta, undefined, undefined, exportName); return async (context: TestContext & TaskContext & { story: ComposedStoryFn }) => { - if (Story === undefined || tagsFilter?.skip.some((tag) => Story.tags.includes(tag))) { + if (composedStory === undefined || skipTags?.some((tag) => composedStory.tags.includes(tag))) { context.skip(); } - context.story = Story; + context.story = composedStory; const _task = context.task as RunnerTask & { meta: TaskMeta & { storyId: string } }; - _task.meta.storyId = Story.id; + _task.meta.storyId = composedStory.id; - await setViewport(Story.parameters.viewport); - await Story.run(); + await setViewport(composedStory.parameters.viewport); + await composedStory.run(); }; }; diff --git a/code/addons/vitest/src/plugin/viewports.test.ts b/code/addons/vitest/src/plugin/viewports.test.ts new file mode 100644 index 000000000000..cd83f5edc518 --- /dev/null +++ b/code/addons/vitest/src/plugin/viewports.test.ts @@ -0,0 +1,154 @@ +/* eslint-disable no-underscore-dangle */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { page } from '@vitest/browser/context'; + +import { DEFAULT_VIEWPORT_DIMENSIONS, type ViewportsParam, setViewport } from './viewports'; + +vi.mock('@vitest/browser/context', () => ({ + page: { + viewport: vi.fn(), + }, +})); + +describe('setViewport', () => { + beforeEach(() => { + vi.clearAllMocks(); + globalThis.__vitest_browser__ = true; + }); + + afterEach(() => { + globalThis.__vitest_browser__ = false; + }); + + it('should no op outside when not in Vitest browser mode', async () => { + globalThis.__vitest_browser__ = false; + + await setViewport(); + expect(page.viewport).not.toHaveBeenCalled(); + }); + + it('should fall back to DEFAULT_VIEWPORT_DIMENSIONS if defaultViewport does not exist', async () => { + const viewportsParam: any = { + defaultViewport: 'nonExistentViewport', + }; + + await setViewport(viewportsParam); + expect(page.viewport).toHaveBeenCalledWith( + DEFAULT_VIEWPORT_DIMENSIONS.width, + DEFAULT_VIEWPORT_DIMENSIONS.height + ); + }); + + it('should set the dimensions of viewport from INITIAL_VIEWPORTS', async () => { + const viewportsParam: any = { + // supported by default in addon viewports + defaultViewport: 'ipad', + }; + + await setViewport(viewportsParam); + expect(page.viewport).toHaveBeenCalledWith(768, 1024); + }); + + it('should set custom defined viewport dimensions', async () => { + const viewportsParam: ViewportsParam = { + defaultViewport: 'customViewport', + viewports: { + customViewport: { + name: 'Custom Viewport', + type: 'mobile', + styles: { + width: '800px', + height: '600px', + }, + }, + }, + }; + + await setViewport(viewportsParam); + expect(page.viewport).toHaveBeenCalledWith(800, 600); + }); + + it('should correctly handle percentage-based dimensions', async () => { + const viewportsParam: ViewportsParam = { + defaultViewport: 'percentageViewport', + viewports: { + percentageViewport: { + name: 'Percentage Viewport', + type: 'desktop', + styles: { + width: '50%', + height: '50%', + }, + }, + }, + }; + + await setViewport(viewportsParam); + expect(page.viewport).toHaveBeenCalledWith(600, 450); // 50% of 1920 and 1080 + }); + + it('should correctly handle vw and vh based dimensions', async () => { + const viewportsParam: ViewportsParam = { + defaultViewport: 'viewportUnits', + viewports: { + viewportUnits: { + name: 'VW/VH Viewport', + type: 'desktop', + styles: { + width: '50vw', + height: '50vh', + }, + }, + }, + }; + + await setViewport(viewportsParam); + expect(page.viewport).toHaveBeenCalledWith(600, 450); // 50% of 1920 and 1080 + }); + + it('should correctly handle em based dimensions', async () => { + const viewportsParam: ViewportsParam = { + defaultViewport: 'viewportUnits', + viewports: { + viewportUnits: { + name: 'em/rem Viewport', + type: 'mobile', + styles: { + width: '20em', + height: '40rem', + }, + }, + }, + }; + + await setViewport(viewportsParam); + expect(page.viewport).toHaveBeenCalledWith(320, 640); // dimensions * 16 + }); + + it('should throw an error for unsupported dimension values', async () => { + const viewportsParam: ViewportsParam = { + defaultViewport: 'invalidViewport', + viewports: { + invalidViewport: { + name: 'Invalid Viewport', + type: 'desktop', + styles: { + width: 'calc(100vw - 20px)', + height: '10pc', + }, + }, + }, + }; + + await expect(setViewport(viewportsParam)).rejects.toThrowErrorMatchingInlineSnapshot(` + [SB_ADDON_VITEST_0001 (UnsupportedViewportDimensionError): Encountered an unsupported value "calc(100vw - 20px)" when setting the viewport width dimension. + + The Storybook plugin only supports values in the following units: + - px, vh, vw, em, rem and %. + + You can either change the viewport for this story to use one of the supported units or skip the test by adding '!test' to the story's tags per https://storybook.js.org/docs/writing-stories/tags] + `); + expect(page.viewport).not.toHaveBeenCalled(); + }); +}); diff --git a/code/addons/vitest/src/plugin/viewports.ts b/code/addons/vitest/src/plugin/viewports.ts index ec9fa8706f5f..c68047877006 100644 --- a/code/addons/vitest/src/plugin/viewports.ts +++ b/code/addons/vitest/src/plugin/viewports.ts @@ -1,4 +1,6 @@ /* eslint-disable no-underscore-dangle */ +import { UnsupportedViewportDimensionError } from 'storybook/internal/preview-errors'; + import { page } from '@vitest/browser/context'; import { INITIAL_VIEWPORTS } from '../../../viewport/src/defaults'; @@ -9,16 +11,47 @@ declare global { var __vitest_browser__: boolean; } -interface ViewportsParam { +export interface ViewportsParam { defaultViewport: string; viewports: ViewportMap; } +export const DEFAULT_VIEWPORT_DIMENSIONS = { + width: 1200, + height: 900, +}; + +const validPixelOrNumber = /^\d+(px)?$/; +const percentagePattern = /^(\d+(\.\d+)?%)$/; +const vwPattern = /^(\d+(\.\d+)?vw)$/; +const vhPattern = /^(\d+(\.\d+)?vh)$/; +const emRemPattern = /^(\d+)(em|rem)$/; + +const parseDimension = (value: string, dimension: 'width' | 'height') => { + if (validPixelOrNumber.test(value)) { + return Number.parseInt(value, 10); + } else if (percentagePattern.test(value)) { + const percentageValue = parseFloat(value) / 100; + return Math.round(DEFAULT_VIEWPORT_DIMENSIONS[dimension] * percentageValue); + } else if (vwPattern.test(value)) { + const vwValue = parseFloat(value) / 100; + return Math.round(DEFAULT_VIEWPORT_DIMENSIONS.width * vwValue); + } else if (vhPattern.test(value)) { + const vhValue = parseFloat(value) / 100; + return Math.round(DEFAULT_VIEWPORT_DIMENSIONS.height * vhValue); + } else if (emRemPattern.test(value)) { + const emRemValue = Number.parseInt(value, 10); + return emRemValue * 16; + } else { + throw new UnsupportedViewportDimensionError({ dimension, value }); + } +}; + export const setViewport = async (viewportsParam: ViewportsParam = {} as ViewportsParam) => { const defaultViewport = viewportsParam.defaultViewport; if (!page || !globalThis.__vitest_browser__ || !defaultViewport) { - return null; + return; } const viewports = { @@ -26,16 +59,17 @@ export const setViewport = async (viewportsParam: ViewportsParam = {} as Viewpor ...viewportsParam.viewports, }; + let viewportWidth = DEFAULT_VIEWPORT_DIMENSIONS.width; + let viewportHeight = DEFAULT_VIEWPORT_DIMENSIONS.height; + if (defaultViewport in viewports) { const styles = viewports[defaultViewport].styles as ViewportStyles; if (styles?.width && styles?.height) { - const { width, height } = { - width: Number.parseInt(styles.width, 10), - height: Number.parseInt(styles.height, 10), - }; - await page.viewport(width, height); + const { width, height } = styles; + viewportWidth = parseDimension(width, 'width'); + viewportHeight = parseDimension(height, 'height'); } } - return null; + await page.viewport(viewportWidth, viewportHeight); }; diff --git a/code/core/package.json b/code/core/package.json index 271766123982..e32184d7e822 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -338,7 +338,7 @@ "chai": "^4.4.1", "chalk": "^5.3.0", "cli-table3": "^0.6.1", - "commander": "^6.2.1", + "commander": "^12.1.0", "comment-parser": "^1.4.1", "compression": "^1.7.4", "copy-to-clipboard": "^3.3.1", diff --git a/code/core/src/cli/bin/index.ts b/code/core/src/cli/bin/index.ts index 38cbce23efb1..20cc55c1d809 100644 --- a/code/core/src/cli/bin/index.ts +++ b/code/core/src/cli/bin/index.ts @@ -4,7 +4,7 @@ import { addToGlobalContext } from '@storybook/core/telemetry'; import { logger } from '@storybook/core/node-logger'; import chalk from 'chalk'; -import program from 'commander'; +import { program } from 'commander'; import { findPackageSync } from 'fd-package-json'; import leven from 'leven'; import invariant from 'tiny-invariant'; @@ -69,7 +69,7 @@ command('dev') 'URL path to be appended when visiting Storybook for the first time' ) .action(async (options) => { - logger.setLevel(program.loglevel); + logger.setLevel(options.loglevel); consoleLogger.log(chalk.bold(`${pkg.name} v${pkg.version}`) + chalk.reset('\n')); // The key is the field created in `options` variable for @@ -109,7 +109,7 @@ command('build') .option('--test', 'Build stories optimized for testing purposes.') .action(async (options) => { process.env.NODE_ENV = process.env.NODE_ENV || 'production'; - logger.setLevel(program.loglevel); + logger.setLevel(options.loglevel); consoleLogger.log(chalk.bold(`${pkg.name} v${pkg.version}\n`)); // The key is the field created in `options` variable for @@ -132,8 +132,7 @@ program.on('command:*', ([invalidCmd]) => { ' Invalid command: %s.\n See --help for a list of available commands.', invalidCmd ); - // eslint-disable-next-line no-underscore-dangle - const availableCommands = program.commands.map((cmd) => cmd._name); + const availableCommands = program.commands.map((cmd) => cmd.name()); const suggestion = availableCommands.find((cmd) => leven(cmd, invalidCmd) < 3); if (suggestion) { consoleLogger.info(`\n Did you mean ${suggestion}?`); diff --git a/code/core/src/csf-tools/vitest-plugin/transformer.test.ts b/code/core/src/csf-tools/vitest-plugin/transformer.test.ts index 76ec0458ab9a..a92af8c4cc2f 100644 --- a/code/core/src/csf-tools/vitest-plugin/transformer.test.ts +++ b/code/core/src/csf-tools/vitest-plugin/transformer.test.ts @@ -24,13 +24,21 @@ const transform = async ({ fileName = 'src/components/Button.stories.js', tagsFilter = { include: ['test'], - exclude: [], - skip: [], + exclude: [] as string[], + skip: [] as string[], }, configDir = '.storybook', stories = [], + previewLevelTags = [], }) => { - const transformed = await originalTransform({ code, fileName, configDir, stories, tagsFilter }); + const transformed = await originalTransform({ + code, + fileName, + configDir, + stories, + tagsFilter, + previewLevelTags, + }); if (typeof transformed === 'string') { return { code: transformed, map: null }; } @@ -53,10 +61,10 @@ describe('transformer', () => { describe('default exports (meta)', () => { it('should add title to inline default export if not present', async () => { const code = ` - import { _test } from 'bla'; export default { component: Button, }; + export const Story = {}; `; const result = await transform({ code }); @@ -64,15 +72,18 @@ describe('transformer', () => { expect(getStoryTitle).toHaveBeenCalled(); expect(result.code).toMatchInlineSnapshot(` - import { test as _test2 } from "vitest"; - import { composeStory as _composeStory } from "storybook/internal/preview-api"; - import { testStory as _testStory, isValidTest as _isValidTest } from "@storybook/experimental-addon-vitest/internal/test-utils"; - import { _test } from 'bla'; + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-vitest/internal/test-utils"; const _meta = { component: Button, title: "automatic/calculated/title" }; export default _meta; + export const Story = {}; + const _isRunningFromThisFile = import.meta.url.includes(_expect.getState().testPath ?? globalThis.__vitest_worker__.filepath); + if (_isRunningFromThisFile) { + _test("Story", _testStory("Story", Story, _meta, [])); + } `); }); @@ -82,6 +93,7 @@ describe('transformer', () => { title: 'Button', component: Button, }; + export const Story = {}; `; const result = await transform({ code }); @@ -89,14 +101,18 @@ describe('transformer', () => { expect(getStoryTitle).toHaveBeenCalled(); expect(result.code).toMatchInlineSnapshot(` - import { test as _test } from "vitest"; - import { composeStory as _composeStory } from "storybook/internal/preview-api"; - import { testStory as _testStory, isValidTest as _isValidTest } from "@storybook/experimental-addon-vitest/internal/test-utils"; + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-vitest/internal/test-utils"; const _meta = { title: "automatic/calculated/title", component: Button }; export default _meta; + export const Story = {}; + const _isRunningFromThisFile = import.meta.url.includes(_expect.getState().testPath ?? globalThis.__vitest_worker__.filepath); + if (_isRunningFromThisFile) { + _test("Story", _testStory("Story", Story, _meta, [])); + } `); }); @@ -105,8 +121,9 @@ describe('transformer', () => { const meta = { component: Button, }; - export default meta; + + export const Story = {}; `; const result = await transform({ code }); @@ -114,14 +131,18 @@ describe('transformer', () => { expect(getStoryTitle).toHaveBeenCalled(); expect(result.code).toMatchInlineSnapshot(` - import { test as _test } from "vitest"; - import { composeStory as _composeStory } from "storybook/internal/preview-api"; - import { testStory as _testStory, isValidTest as _isValidTest } from "@storybook/experimental-addon-vitest/internal/test-utils"; + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-vitest/internal/test-utils"; const meta = { component: Button, title: "automatic/calculated/title" }; export default meta; + export const Story = {}; + const _isRunningFromThisFile = import.meta.url.includes(_expect.getState().testPath ?? globalThis.__vitest_worker__.filepath); + if (_isRunningFromThisFile) { + _test("Story", _testStory("Story", Story, meta, [])); + } `); }); @@ -130,9 +151,10 @@ describe('transformer', () => { const meta = { title: 'Button', component: Button, - }; - + }; export default meta; + + export const Story = {}; `; const result = await transform({ code }); @@ -140,14 +162,18 @@ describe('transformer', () => { expect(getStoryTitle).toHaveBeenCalled(); expect(result.code).toMatchInlineSnapshot(` - import { test as _test } from "vitest"; - import { composeStory as _composeStory } from "storybook/internal/preview-api"; - import { testStory as _testStory, isValidTest as _isValidTest } from "@storybook/experimental-addon-vitest/internal/test-utils"; + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-vitest/internal/test-utils"; const meta = { title: "automatic/calculated/title", component: Button }; export default meta; + export const Story = {}; + const _isRunningFromThisFile = import.meta.url.includes(_expect.getState().testPath ?? globalThis.__vitest_worker__.filepath); + if (_isRunningFromThisFile) { + _test("Story", _testStory("Story", Story, meta, [])); + } `); }); }); @@ -155,22 +181,21 @@ describe('transformer', () => { describe('named exports (stories)', () => { it('should add test statement to inline exported stories', async () => { const code = ` - export default { - component: Button, - } - export const Primary = { - args: { - label: 'Primary Button', - }, - }; - `; + export default { + component: Button, + } + export const Primary = { + args: { + label: 'Primary Button', + }, + }; + `; const result = await transform({ code }); expect(result.code).toMatchInlineSnapshot(` - import { test as _test } from "vitest"; - import { composeStory as _composeStory } from "storybook/internal/preview-api"; - import { testStory as _testStory, isValidTest as _isValidTest } from "@storybook/experimental-addon-vitest/internal/test-utils"; + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-vitest/internal/test-utils"; const _meta = { component: Button, title: "automatic/calculated/title" @@ -181,31 +206,65 @@ describe('transformer', () => { label: 'Primary Button' } }; - const _composedPrimary = _composeStory(Primary, _meta, undefined, undefined, "Primary"); - if (_isValidTest(_composedPrimary.tags, {"include":["test"],"exclude":[],"skip":[]})) { - _test("Primary", _testStory(_composedPrimary, {"include":["test"],"exclude":[],"skip":[]})); + const _isRunningFromThisFile = import.meta.url.includes(_expect.getState().testPath ?? globalThis.__vitest_worker__.filepath); + if (_isRunningFromThisFile) { + _test("Primary", _testStory("Primary", Primary, _meta, [])); } `); }); it('should add test statement to const declared exported stories', async () => { const code = ` - export default {}; - const Primary = { - args: { - label: 'Primary Button', - }, - }; + export default {}; + const Primary = { + args: { + label: 'Primary Button', + }, + }; - export { Primary }; - `; + export { Primary }; + `; const result = await transform({ code }); expect(result.code).toMatchInlineSnapshot(` - import { test as _test } from "vitest"; - import { composeStory as _composeStory } from "storybook/internal/preview-api"; - import { testStory as _testStory, isValidTest as _isValidTest } from "@storybook/experimental-addon-vitest/internal/test-utils"; + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-vitest/internal/test-utils"; + const _meta = { + title: "automatic/calculated/title" + }; + export default _meta; + const Primary = { + args: { + label: 'Primary Button' + } + }; + export { Primary }; + const _isRunningFromThisFile = import.meta.url.includes(_expect.getState().testPath ?? globalThis.__vitest_worker__.filepath); + if (_isRunningFromThisFile) { + _test("Primary", _testStory("Primary", Primary, _meta, [])); + } + `); + }); + + it('should add tests for multiple stories', async () => { + const code = ` + export default {}; + const Primary = { + args: { + label: 'Primary Button', + }, + }; + + export const Secondary = {} + + export { Primary }; + `; + + const result = await transform({ code }); + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-vitest/internal/test-utils"; const _meta = { title: "automatic/calculated/title" }; @@ -215,37 +274,160 @@ describe('transformer', () => { label: 'Primary Button' } }; + export const Secondary = {}; export { Primary }; - const _composedPrimary = _composeStory(Primary, _meta, undefined, undefined, "Primary"); - if (_isValidTest(_composedPrimary.tags, {"include":["test"],"exclude":[],"skip":[]})) { - _test("Primary", _testStory(_composedPrimary, {"include":["test"],"exclude":[],"skip":[]})); + const _isRunningFromThisFile = import.meta.url.includes(_expect.getState().testPath ?? globalThis.__vitest_worker__.filepath); + if (_isRunningFromThisFile) { + _test("Secondary", _testStory("Secondary", Secondary, _meta, [])); + _test("Primary", _testStory("Primary", Primary, _meta, [])); } `); }); it('should exclude exports via excludeStories', async () => { const code = ` - export default { - title: 'Button', - component: Button, - excludeStories: ['nonStory'], - } - export const nonStory = 123 - `; + export default { + title: 'Button', + component: Button, + excludeStories: ['nonStory'], + } + export const Story = {}; + export const nonStory = 123 + `; const result = await transform({ code }); expect(result.code).toMatchInlineSnapshot(` - import { test as _test } from "vitest"; - import { composeStory as _composeStory } from "storybook/internal/preview-api"; - import { testStory as _testStory, isValidTest as _isValidTest } from "@storybook/experimental-addon-vitest/internal/test-utils"; + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-vitest/internal/test-utils"; const _meta = { title: "automatic/calculated/title", component: Button, excludeStories: ['nonStory'] }; export default _meta; + export const Story = {}; export const nonStory = 123; + const _isRunningFromThisFile = import.meta.url.includes(_expect.getState().testPath ?? globalThis.__vitest_worker__.filepath); + if (_isRunningFromThisFile) { + _test("Story", _testStory("Story", Story, _meta, [])); + } + `); + }); + + it('should return a describe with skip if there are no valid stories', async () => { + const code = ` + export default { + title: 'Button', + component: Button, + tags: ['!test'] + } + export const Story = {} + `; + const result = await transform({ code }); + + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, describe as _describe } from "vitest"; + const _meta = { + title: "automatic/calculated/title", + component: Button, + tags: ['!test'] + }; + export default _meta; + export const Story = {}; + _describe.skip("No valid tests found"); + `); + }); + }); + + describe('tags filtering mechanism', () => { + it('should only include stories from tags.include', async () => { + const code = ` + export default {}; + export const Included = { tags: ['include-me'] }; + + export const NotIncluded = {} + `; + + const result = await transform({ + code, + tagsFilter: { include: ['include-me'], exclude: [], skip: [] }, + }); + + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-vitest/internal/test-utils"; + const _meta = { + title: "automatic/calculated/title" + }; + export default _meta; + export const Included = { + tags: ['include-me'] + }; + export const NotIncluded = {}; + const _isRunningFromThisFile = import.meta.url.includes(_expect.getState().testPath ?? globalThis.__vitest_worker__.filepath); + if (_isRunningFromThisFile) { + _test("Included", _testStory("Included", Included, _meta, [])); + } + `); + }); + + it('should exclude stories from tags.exclude', async () => { + const code = ` + export default {}; + export const Included = {}; + + export const NotIncluded = { tags: ['exclude-me'] } + `; + + const result = await transform({ + code, + tagsFilter: { include: ['test'], exclude: ['exclude-me'], skip: [] }, + }); + + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-vitest/internal/test-utils"; + const _meta = { + title: "automatic/calculated/title" + }; + export default _meta; + export const Included = {}; + export const NotIncluded = { + tags: ['exclude-me'] + }; + const _isRunningFromThisFile = import.meta.url.includes(_expect.getState().testPath ?? globalThis.__vitest_worker__.filepath); + if (_isRunningFromThisFile) { + _test("Included", _testStory("Included", Included, _meta, [])); + } + `); + }); + + it('should pass skip tags to testStory call using tags.skip', async () => { + const code = ` + export default {}; + export const Skipped = { tags: ['skip-me'] }; + `; + + const result = await transform({ + code, + tagsFilter: { include: ['test'], exclude: [], skip: ['skip-me'] }, + }); + + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-vitest/internal/test-utils"; + const _meta = { + title: "automatic/calculated/title" + }; + export default _meta; + export const Skipped = { + tags: ['skip-me'] + }; + const _isRunningFromThisFile = import.meta.url.includes(_expect.getState().testPath ?? globalThis.__vitest_worker__.filepath); + if (_isRunningFromThisFile) { + _test("Skipped", _testStory("Skipped", Skipped, _meta, ["skip-me"])); + } `); }); }); @@ -266,18 +448,17 @@ describe('transformer', () => { }); expect(transformedCode).toMatchInlineSnapshot(` - import { test as _test } from "vitest"; - import { composeStory as _composeStory } from "storybook/internal/preview-api"; - import { testStory as _testStory, isValidTest as _isValidTest } from "@storybook/experimental-addon-vitest/internal/test-utils"; + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-vitest/internal/test-utils"; const meta = { title: "automatic/calculated/title", component: Button }; export default meta; export const Primary = {}; - const _composedPrimary = _composeStory(Primary, meta, undefined, undefined, "Primary"); - if (_isValidTest(_composedPrimary.tags, {"include":["test"],"exclude":[],"skip":[]})) { - _test("Primary", _testStory(_composedPrimary, {"include":["test"],"exclude":[],"skip":[]})); + const _isRunningFromThisFile = import.meta.url.includes(_expect.getState().testPath ?? globalThis.__vitest_worker__.filepath); + if (_isRunningFromThisFile) { + _test("Primary", _testStory("Primary", Primary, meta, [])); } `); diff --git a/code/core/src/csf-tools/vitest-plugin/transformer.ts b/code/core/src/csf-tools/vitest-plugin/transformer.ts index aafe709a2399..98ce9635f4eb 100644 --- a/code/core/src/csf-tools/vitest-plugin/transformer.ts +++ b/code/core/src/csf-tools/vitest-plugin/transformer.ts @@ -2,7 +2,8 @@ /* eslint-disable no-underscore-dangle */ import { getStoryTitle } from '@storybook/core/common'; -import type { StoriesEntry } from '@storybook/core/types'; +import type { StoriesEntry, Tag } from '@storybook/core/types'; +import { combineTags } from '@storybook/csf'; import * as t from '@babel/types'; import { dedent } from 'ts-dedent'; @@ -11,22 +12,34 @@ import { formatCsf, loadCsf } from '../CsfFile'; const logger = console; +type TagsFilter = { + include: string[]; + exclude: string[]; + skip: string[]; +}; + +const isValidTest = (storyTags: string[], tagsFilter: TagsFilter) => { + const isIncluded = + tagsFilter?.include.length === 0 || tagsFilter?.include.some((tag) => storyTags.includes(tag)); + const isNotExcluded = tagsFilter?.exclude.every((tag) => !storyTags.includes(tag)); + + return isIncluded && isNotExcluded; +}; + export async function vitestTransform({ code, fileName, configDir, stories, tagsFilter, + previewLevelTags = [], }: { code: string; fileName: string; configDir: string; - tagsFilter: { - include: string[]; - exclude: string[]; - skip: string[]; - }; + tagsFilter: TagsFilter; stories: StoriesEntry[]; + previewLevelTags: Tag[]; }) { const isStoryFile = /\.stor(y|ies)\./.test(fileName); if (!isStoryFile) { @@ -81,90 +94,171 @@ export async function vitestTransform({ ); } + // Filter out stories based on the passed tags filter + const validStories: (typeof parsed)['_storyStatements'] = {}; + Object.keys(parsed._stories).map((key) => { + const finalTags = combineTags( + 'test', + 'dev', + ...previewLevelTags, + ...(parsed.meta?.tags || []), + ...(parsed._stories[key].tags || []) + ); + + if (isValidTest(finalTags, tagsFilter)) { + validStories[key] = parsed._storyStatements[key]; + } + }); + const vitestTestId = parsed._file.path.scope.generateUidIdentifier('test'); - const composeStoryId = parsed._file.path.scope.generateUidIdentifier('composeStory'); - const testStoryId = parsed._file.path.scope.generateUidIdentifier('testStory'); - const isValidTestId = parsed._file.path.scope.generateUidIdentifier('isValidTest'); - - const tagsFilterId = t.identifier(JSON.stringify(tagsFilter)); - - const getTestStatementForStory = ({ exportName, node }: { exportName: string; node: t.Node }) => { - const composedStoryId = parsed._file.path.scope.generateUidIdentifier(`composed${exportName}`); - - const composeStoryCall = t.variableDeclaration('const', [ - t.variableDeclarator( - composedStoryId, - t.callExpression(composeStoryId, [ - t.identifier(exportName), - t.identifier(metaExportName), - t.identifier('undefined'), - t.identifier('undefined'), - t.stringLiteral(exportName), - ]) - ), - ]); - - // Preserve sourcemaps location - composeStoryCall.loc = node.loc; - - const isValidTestCall = t.ifStatement( - t.callExpression(isValidTestId, [ - t.memberExpression(composedStoryId, t.identifier('tags')), - tagsFilterId, - ]), - t.blockStatement([ - t.expressionStatement( - t.callExpression(vitestTestId, [ - t.stringLiteral(exportName), - t.callExpression(testStoryId, [composedStoryId, tagsFilterId]), - ]) - ), + const vitestDescribeId = parsed._file.path.scope.generateUidIdentifier('describe'); + + // if no valid stories are found, we just add describe.skip() to the file to avoid empty test files + if (Object.keys(validStories).length === 0) { + const describeSkipBlock = t.expressionStatement( + t.callExpression(t.memberExpression(vitestDescribeId, t.identifier('skip')), [ + t.stringLiteral('No valid tests found'), ]) ); - // Preserve sourcemaps location - isValidTestCall.loc = node.loc; - - return [composeStoryCall, isValidTestCall]; - }; - - Object.entries(parsed._storyStatements).forEach(([exportName, node]) => { - if (node === null) { - logger.warn( - dedent` - [Storybook]: Could not transform "${exportName}" story into test at "${fileName}". - Please make sure to define stories in the same file and not re-export stories coming from other files". - ` + + ast.program.body.push(describeSkipBlock); + const imports = [ + t.importDeclaration( + [ + t.importSpecifier(vitestTestId, t.identifier('test')), + t.importSpecifier(vitestDescribeId, t.identifier('describe')), + ], + t.stringLiteral('vitest') + ), + ]; + + ast.program.body.unshift(...imports); + } else { + const vitestExpectId = parsed._file.path.scope.generateUidIdentifier('expect'); + const testStoryId = parsed._file.path.scope.generateUidIdentifier('testStory'); + const skipTagsId = t.identifier(JSON.stringify(tagsFilter.skip)); + + /** + * In Storybook users might be importing stories from other story files. As a side effect, tests + * can get re-triggered. To avoid this, we add a guard to only run tests if the current file is + * the one running the test. + * + * Const isRunningFromThisFile = import.meta.url.includes(expect.getState().testPath ?? + * globalThis.**vitest_worker**.filepath) if(isRunningFromThisFile) { ... } + */ + function getTestGuardDeclaration() { + const isRunningFromThisFileId = + parsed._file.path.scope.generateUidIdentifier('isRunningFromThisFile'); + + // expect.getState().testPath + const testPathProperty = t.memberExpression( + t.callExpression(t.memberExpression(vitestExpectId, t.identifier('getState')), []), + t.identifier('testPath') + ); + + // There is a bug in Vitest where expect.getState().testPath is undefined when called outside of a test function so we add this fallback in the meantime + // https://github.com/vitest-dev/vitest/issues/6367 + // globalThis.__vitest_worker__.filepath + const filePathProperty = t.memberExpression( + t.memberExpression(t.identifier('globalThis'), t.identifier('__vitest_worker__')), + t.identifier('filepath') + ); + + // Combine testPath and filepath using the ?? operator + const nullishCoalescingExpression = t.logicalExpression( + '??', + testPathProperty, + filePathProperty ); - return; + + // Create the final expression: import.meta.url.includes(...) + const includesCall = t.callExpression( + t.memberExpression( + t.memberExpression( + t.memberExpression(t.identifier('import'), t.identifier('meta')), + t.identifier('url') + ), + t.identifier('includes') + ), + [nullishCoalescingExpression] + ); + + const isRunningFromThisFileDeclaration = t.variableDeclaration('const', [ + t.variableDeclarator(isRunningFromThisFileId, includesCall), + ]); + return { isRunningFromThisFileDeclaration, isRunningFromThisFileId }; } - ast.program.body.push( - ...getTestStatementForStory({ - exportName, - node, + const { isRunningFromThisFileDeclaration, isRunningFromThisFileId } = getTestGuardDeclaration(); + + ast.program.body.push(isRunningFromThisFileDeclaration); + + const getTestStatementForStory = ({ + exportName, + node, + }: { + exportName: string; + node: t.Node; + }) => { + // Create the _test expression directly using the exportName identifier + const testStoryCall = t.expressionStatement( + t.callExpression(vitestTestId, [ + t.stringLiteral(exportName), + t.callExpression(testStoryId, [ + t.stringLiteral(exportName), + t.identifier(exportName), + t.identifier(metaExportName), + skipTagsId, + ]), + ]) + ); + + // Preserve sourcemaps location + testStoryCall.loc = node.loc; + + // Return just the testStoryCall as composeStoryCall is not needed + return testStoryCall; + }; + + const storyTestStatements = Object.entries(validStories) + .map(([exportName, node]) => { + if (node === null) { + logger.warn( + dedent` + [Storybook]: Could not transform "${exportName}" story into test at "${fileName}". + Please make sure to define stories in the same file and not re-export stories coming from other files". + ` + ); + return; + } + + return getTestStatementForStory({ + exportName, + node, + }); }) - ); - }); + .filter((st) => !!st) as t.ExpressionStatement[]; + + const testBlock = t.ifStatement(isRunningFromThisFileId, t.blockStatement(storyTestStatements)); - const imports = [ - t.importDeclaration( - [t.importSpecifier(vitestTestId, t.identifier('test'))], - t.stringLiteral('vitest') - ), - t.importDeclaration( - [t.importSpecifier(composeStoryId, t.identifier('composeStory'))], - t.stringLiteral('storybook/internal/preview-api') - ), - t.importDeclaration( - [ - t.importSpecifier(testStoryId, t.identifier('testStory')), - t.importSpecifier(isValidTestId, t.identifier('isValidTest')), - ], - t.stringLiteral('@storybook/experimental-addon-vitest/internal/test-utils') - ), - ]; - - ast.program.body.unshift(...imports); + ast.program.body.push(testBlock); + + const imports = [ + t.importDeclaration( + [ + t.importSpecifier(vitestTestId, t.identifier('test')), + t.importSpecifier(vitestExpectId, t.identifier('expect')), + ], + t.stringLiteral('vitest') + ), + t.importDeclaration( + [t.importSpecifier(testStoryId, t.identifier('testStory'))], + t.stringLiteral('@storybook/experimental-addon-vitest/internal/test-utils') + ), + ]; + + ast.program.body.unshift(...imports); + } return formatCsf(parsed, { sourceMaps: true, sourceFileName: fileName }, code); } diff --git a/code/core/src/preview-errors.ts b/code/core/src/preview-errors.ts index 79df5e45d8dd..4bdf0fb3aefb 100644 --- a/code/core/src/preview-errors.ts +++ b/code/core/src/preview-errors.ts @@ -30,6 +30,7 @@ export enum Category { RENDERER_VUE3 = 'RENDERER_VUE3', RENDERER_WEB_COMPONENTS = 'RENDERER_WEB-COMPONENTS', FRAMEWORK_NEXTJS = 'FRAMEWORK_NEXTJS', + ADDON_VITEST = 'ADDON_VITEST', } export class MissingStoryAfterHmrError extends StorybookError { @@ -317,3 +318,22 @@ export class UnknownArgTypesError extends StorybookError { }); } } + +export class UnsupportedViewportDimensionError extends StorybookError { + constructor(public data: { dimension: string; value: string }) { + super({ + category: Category.ADDON_VITEST, + code: 1, + // TODO: Add documentation about viewports support + // documentation: '', + message: dedent` + Encountered an unsupported value "${data.value}" when setting the viewport ${data.dimension} dimension. + + The Storybook plugin only supports values in the following units: + - px, vh, vw, em, rem and %. + + You can either change the viewport for this story to use one of the supported units or skip the test by adding '!test' to the story's tags per https://storybook.js.org/docs/writing-stories/tags + `, + }); + } +} diff --git a/code/frameworks/angular/src/client/index.ts b/code/frameworks/angular/src/client/index.ts index 45388bcad324..221d365718d5 100644 --- a/code/frameworks/angular/src/client/index.ts +++ b/code/frameworks/angular/src/client/index.ts @@ -2,6 +2,7 @@ import './globals'; export * from './public-types'; +export * from './portable-stories'; export type { StoryFnAngularReturnType as IStory } from './types'; diff --git a/code/frameworks/angular/src/client/portable-stories.ts b/code/frameworks/angular/src/client/portable-stories.ts new file mode 100644 index 000000000000..4f4246ddf3e4 --- /dev/null +++ b/code/frameworks/angular/src/client/portable-stories.ts @@ -0,0 +1,42 @@ +import { + setProjectAnnotations as originalSetProjectAnnotations, + setDefaultProjectAnnotations, +} from 'storybook/internal/preview-api'; +import { + NamedOrDefaultProjectAnnotations, + NormalizedProjectAnnotations, +} from 'storybook/internal/types'; + +import * as INTERNAL_DEFAULT_PROJECT_ANNOTATIONS from './render'; +import { AngularRenderer } from './types'; + +/** + * Function that sets the globalConfig of your storybook. The global config is the preview module of + * your .storybook folder. + * + * It should be run a single time, so that your global config (e.g. decorators) is applied to your + * stories when using `composeStories` or `composeStory`. + * + * Example: + * + * ```jsx + * // setup-file.js + * import { setProjectAnnotations } from '@storybook/angular'; + * + * import projectAnnotations from './.storybook/preview'; + * + * setProjectAnnotations(projectAnnotations); + * ``` + * + * @param projectAnnotations - E.g. (import projectAnnotations from '../.storybook/preview') + */ +export function setProjectAnnotations( + projectAnnotations: + | NamedOrDefaultProjectAnnotations + | NamedOrDefaultProjectAnnotations[] +): NormalizedProjectAnnotations { + setDefaultProjectAnnotations(INTERNAL_DEFAULT_PROJECT_ANNOTATIONS); + return originalSetProjectAnnotations( + projectAnnotations + ) as NormalizedProjectAnnotations; +} diff --git a/code/frameworks/experimental-nextjs-vite/src/portable-stories.ts b/code/frameworks/experimental-nextjs-vite/src/portable-stories.ts index f03e89bce5b5..1fbbcc24dd8c 100644 --- a/code/frameworks/experimental-nextjs-vite/src/portable-stories.ts +++ b/code/frameworks/experimental-nextjs-vite/src/portable-stories.ts @@ -32,7 +32,7 @@ import * as nextJsAnnotations from './preview'; * Example: * * ```jsx - * // setup.js (for jest) + * // setup-file.js * import { setProjectAnnotations } from '@storybook/experimental-nextjs-vite'; * import projectAnnotations from './.storybook/preview'; * diff --git a/code/frameworks/nextjs/src/portable-stories.ts b/code/frameworks/nextjs/src/portable-stories.ts index 28295342b4c8..a49d6bdf8162 100644 --- a/code/frameworks/nextjs/src/portable-stories.ts +++ b/code/frameworks/nextjs/src/portable-stories.ts @@ -32,7 +32,7 @@ import * as nextJsAnnotations from './preview'; * Example: * * ```jsx - * // setup.js (for jest) + * // setup-file.js * import { setProjectAnnotations } from '@storybook/nextjs'; * import projectAnnotations from './.storybook/preview'; * diff --git a/code/frameworks/sveltekit/package.json b/code/frameworks/sveltekit/package.json index 6cfad9cf82a8..15962e6efa5a 100644 --- a/code/frameworks/sveltekit/package.json +++ b/code/frameworks/sveltekit/package.json @@ -25,8 +25,8 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "node": "./dist/index.js", "import": "./dist/index.mjs", + "node": "./dist/index.js", "require": "./dist/index.js" }, "./dist/preview.mjs": { @@ -36,6 +36,11 @@ "types": "./dist/preset.d.ts", "require": "./dist/preset.js" }, + "./vite": { + "types": "./dist/vite.d.ts", + "require": "./dist/vite.js", + "import": "./dist/vite.mjs" + }, "./package.json": "./package.json" }, "main": "dist/index.js", @@ -78,7 +83,8 @@ "entries": [ "./src/index.ts", "./src/preview.ts", - "./src/preset.ts" + "./src/preset.ts", + "./src/vite.ts" ], "platform": "node" }, diff --git a/code/frameworks/sveltekit/src/index.ts b/code/frameworks/sveltekit/src/index.ts index fcb073fefcd6..a904f93ec89d 100644 --- a/code/frameworks/sveltekit/src/index.ts +++ b/code/frameworks/sveltekit/src/index.ts @@ -1 +1,2 @@ export * from './types'; +export * from './portable-stories'; diff --git a/code/frameworks/sveltekit/src/portable-stories.ts b/code/frameworks/sveltekit/src/portable-stories.ts new file mode 100644 index 000000000000..9edaaf8ac55b --- /dev/null +++ b/code/frameworks/sveltekit/src/portable-stories.ts @@ -0,0 +1,51 @@ +import { + composeConfigs, + setProjectAnnotations as originalSetProjectAnnotations, + setDefaultProjectAnnotations, +} from 'storybook/internal/preview-api'; +import type { + NamedOrDefaultProjectAnnotations, + NormalizedProjectAnnotations, + ProjectAnnotations, +} from 'storybook/internal/types'; + +import type { SvelteRenderer } from '@storybook/svelte'; +import { INTERNAL_DEFAULT_PROJECT_ANNOTATIONS as svelteAnnotations } from '@storybook/svelte'; + +import * as svelteKitAnnotations from './preview'; + +/** + * Function that sets the globalConfig of your storybook. The global config is the preview module of + * your .storybook folder. + * + * It should be run a single time, so that your global config (e.g. decorators) is applied to your + * stories when using `composeStories` or `composeStory`. + * + * Example: + * + * ```jsx + * // setup-file.js + * import { setProjectAnnotations } from '@storybook/sveltekit'; + * import projectAnnotations from './.storybook/preview'; + * + * setProjectAnnotations(projectAnnotations); + * ``` + * + * @param projectAnnotations - E.g. (import projectAnnotations from '../.storybook/preview') + */ +export function setProjectAnnotations( + projectAnnotations: + | NamedOrDefaultProjectAnnotations + | NamedOrDefaultProjectAnnotations[] +): NormalizedProjectAnnotations { + setDefaultProjectAnnotations(INTERNAL_DEFAULT_PROJECT_ANNOTATIONS); + return originalSetProjectAnnotations( + projectAnnotations + ) as NormalizedProjectAnnotations; +} + +// This will not be necessary once we have auto preset loading +const INTERNAL_DEFAULT_PROJECT_ANNOTATIONS: ProjectAnnotations = composeConfigs([ + svelteAnnotations, + svelteKitAnnotations, +]); diff --git a/code/frameworks/sveltekit/src/vite.ts b/code/frameworks/sveltekit/src/vite.ts new file mode 100644 index 000000000000..b3fd4f3e6dbe --- /dev/null +++ b/code/frameworks/sveltekit/src/vite.ts @@ -0,0 +1,5 @@ +import { mockSveltekitStores } from './plugins/mock-sveltekit-stores'; + +export const storybookSveltekitPlugin = () => { + return [mockSveltekitStores()]; +}; diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/Forms.svelte b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/Forms.svelte index 371a17656bea..8513ae2a7064 100644 --- a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/Forms.svelte +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/Forms.svelte @@ -2,6 +2,6 @@ import { enhance } from '$app/forms'; -
+
\ No newline at end of file diff --git a/code/lib/blocks/src/blocks/DocsContainer.tsx b/code/lib/blocks/src/blocks/DocsContainer.tsx index 01df02857251..3fb52a32ca53 100644 --- a/code/lib/blocks/src/blocks/DocsContainer.tsx +++ b/code/lib/blocks/src/blocks/DocsContainer.tsx @@ -41,7 +41,7 @@ export const DocsContainer: FC> = ({ try { url = new URL(globalWindow.parent.location.toString()); if (url.hash) { - const element = document.getElementById(url.hash.substring(1)); + const element = document.getElementById(decodeURIComponent(url.hash.substring(1))); if (element) { // Introducing a delay to ensure scrolling works when it's a full refresh. setTimeout(() => { diff --git a/code/lib/cli-storybook/package.json b/code/lib/cli-storybook/package.json index 6dc6c3ddd058..40328f3d1c49 100644 --- a/code/lib/cli-storybook/package.json +++ b/code/lib/cli-storybook/package.json @@ -45,7 +45,7 @@ "@storybook/codemod": "workspace:*", "@types/semver": "^7.3.4", "chalk": "^4.1.0", - "commander": "^6.2.1", + "commander": "^12.1.0", "create-storybook": "workspace:*", "cross-spawn": "^7.0.3", "envinfo": "^7.7.3", diff --git a/code/lib/cli-storybook/src/bin/index.ts b/code/lib/cli-storybook/src/bin/index.ts index de33b71dbc62..34f70b540a0b 100644 --- a/code/lib/cli-storybook/src/bin/index.ts +++ b/code/lib/cli-storybook/src/bin/index.ts @@ -8,7 +8,7 @@ import { logger } from 'storybook/internal/node-logger'; import { addToGlobalContext, telemetry } from 'storybook/internal/telemetry'; import chalk from 'chalk'; -import program from 'commander'; +import { program } from 'commander'; import envinfo from 'envinfo'; import { findPackageSync } from 'fd-package-json'; import leven from 'leven'; @@ -186,8 +186,7 @@ program.on('command:*', ([invalidCmd]) => { ' Invalid command: %s.\n See --help for a list of available commands.', invalidCmd ); - // eslint-disable-next-line no-underscore-dangle - const availableCommands = program.commands.map((cmd) => cmd._name); + const availableCommands = program.commands.map((cmd) => cmd.name()); const suggestion = availableCommands.find((cmd) => leven(cmd, invalidCmd) < 3); if (suggestion) { consoleLogger.info(`\n Did you mean ${suggestion}?`); diff --git a/code/lib/cli-storybook/src/sandbox-templates.ts b/code/lib/cli-storybook/src/sandbox-templates.ts index bad1d34ba51b..ffdbccdd5e4d 100644 --- a/code/lib/cli-storybook/src/sandbox-templates.ts +++ b/code/lib/cli-storybook/src/sandbox-templates.ts @@ -449,8 +449,7 @@ const baseTemplates = { renderer: '@storybook/svelte', builder: '@storybook/builder-vite', }, - // TODO: remove vitest-integration filter once project annotations exist for sveltekit - skipTasks: ['e2e-tests-dev', 'bench', 'vitest-integration'], + skipTasks: ['e2e-tests-dev', 'bench'], }, 'svelte-kit/skeleton-ts': { name: 'SvelteKit Latest (Vite | TypeScript)', @@ -461,8 +460,7 @@ const baseTemplates = { renderer: '@storybook/svelte', builder: '@storybook/builder-vite', }, - // TODO: remove vitest-integration filter once project annotations exist for sveltekit - skipTasks: ['e2e-tests-dev', 'bench', 'vitest-integration'], + skipTasks: ['e2e-tests-dev', 'bench'], }, 'svelte-kit/prerelease-ts': { name: 'SvelteKit Prerelease (Vite | TypeScript)', @@ -473,7 +471,7 @@ const baseTemplates = { renderer: '@storybook/svelte', builder: '@storybook/builder-vite', }, - skipTasks: ['e2e-tests-dev', 'bench', 'vitest-integration'], + skipTasks: ['e2e-tests-dev', 'bench'], }, 'lit-vite/default-js': { name: 'Lit Latest (Vite | JavaScript)', diff --git a/code/lib/create-storybook/package.json b/code/lib/create-storybook/package.json index 0da0a1f76f3e..86c3f757490a 100644 --- a/code/lib/create-storybook/package.json +++ b/code/lib/create-storybook/package.json @@ -57,7 +57,7 @@ "dependencies": { "@types/semver": "^7.3.4", "chalk": "^4.1.0", - "commander": "^6.2.1", + "commander": "^12.1.0", "execa": "^5.0.0", "fd-package-json": "^1.2.0", "find-up": "^5.0.0", diff --git a/code/lib/create-storybook/src/bin/index.ts b/code/lib/create-storybook/src/bin/index.ts index 758365d3b1ca..187e2811c38f 100644 --- a/code/lib/create-storybook/src/bin/index.ts +++ b/code/lib/create-storybook/src/bin/index.ts @@ -1,7 +1,7 @@ import { versions } from 'storybook/internal/common'; import { addToGlobalContext } from 'storybook/internal/telemetry'; -import program from 'commander'; +import { program } from 'commander'; import { findPackageSync } from 'fd-package-json'; import invariant from 'tiny-invariant'; diff --git a/code/package.json b/code/package.json index 6ffc9cdc3ea7..d17d76eb0998 100644 --- a/code/package.json +++ b/code/package.json @@ -292,5 +292,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.3.0-alpha.8" } diff --git a/code/renderers/html/src/index.ts b/code/renderers/html/src/index.ts index 13fea98639ff..80c844382e83 100644 --- a/code/renderers/html/src/index.ts +++ b/code/renderers/html/src/index.ts @@ -2,6 +2,7 @@ import './globals'; export * from './public-types'; +export * from './portable-stories'; // optimization: stop HMR propagation in webpack diff --git a/code/renderers/html/src/portable-stories.ts b/code/renderers/html/src/portable-stories.ts new file mode 100644 index 000000000000..f19a5e0d6561 --- /dev/null +++ b/code/renderers/html/src/portable-stories.ts @@ -0,0 +1,41 @@ +import { + setProjectAnnotations as originalSetProjectAnnotations, + setDefaultProjectAnnotations, +} from 'storybook/internal/preview-api'; +import type { + NamedOrDefaultProjectAnnotations, + NormalizedProjectAnnotations, +} from 'storybook/internal/types'; + +import * as INTERNAL_DEFAULT_PROJECT_ANNOTATIONS from './entry-preview'; +import type { HtmlRenderer } from './types'; + +/** + * Function that sets the globalConfig of your storybook. The global config is the preview module of + * your .storybook folder. + * + * It should be run a single time, so that your global config (e.g. decorators) is applied to your + * stories when using `composeStories` or `composeStory`. + * + * Example: + * + * ```jsx + * // setup-file.js + * import { setProjectAnnotations } from '@storybook/preact'; + * import projectAnnotations from './.storybook/preview'; + * + * setProjectAnnotations(projectAnnotations); + * ``` + * + * @param projectAnnotations - E.g. (import projectAnnotations from '../.storybook/preview') + */ +export function setProjectAnnotations( + projectAnnotations: + | NamedOrDefaultProjectAnnotations + | NamedOrDefaultProjectAnnotations[] +): NormalizedProjectAnnotations { + setDefaultProjectAnnotations(INTERNAL_DEFAULT_PROJECT_ANNOTATIONS); + return originalSetProjectAnnotations( + projectAnnotations + ) as NormalizedProjectAnnotations; +} diff --git a/code/renderers/preact/src/index.ts b/code/renderers/preact/src/index.ts index 13fea98639ff..80c844382e83 100644 --- a/code/renderers/preact/src/index.ts +++ b/code/renderers/preact/src/index.ts @@ -2,6 +2,7 @@ import './globals'; export * from './public-types'; +export * from './portable-stories'; // optimization: stop HMR propagation in webpack diff --git a/code/renderers/preact/src/portable-stories.ts b/code/renderers/preact/src/portable-stories.ts new file mode 100644 index 000000000000..cc9ca05a2627 --- /dev/null +++ b/code/renderers/preact/src/portable-stories.ts @@ -0,0 +1,41 @@ +import { + setProjectAnnotations as originalSetProjectAnnotations, + setDefaultProjectAnnotations, +} from 'storybook/internal/preview-api'; +import type { + NamedOrDefaultProjectAnnotations, + NormalizedProjectAnnotations, +} from 'storybook/internal/types'; + +import * as INTERNAL_DEFAULT_PROJECT_ANNOTATIONS from './entry-preview'; +import type { PreactRenderer } from './types'; + +/** + * Function that sets the globalConfig of your storybook. The global config is the preview module of + * your .storybook folder. + * + * It should be run a single time, so that your global config (e.g. decorators) is applied to your + * stories when using `composeStories` or `composeStory`. + * + * Example: + * + * ```jsx + * // setup-file.js + * import { setProjectAnnotations } from '@storybook/preact'; + * import projectAnnotations from './.storybook/preview'; + * + * setProjectAnnotations(projectAnnotations); + * ``` + * + * @param projectAnnotations - E.g. (import projectAnnotations from '../.storybook/preview') + */ +export function setProjectAnnotations( + projectAnnotations: + | NamedOrDefaultProjectAnnotations + | NamedOrDefaultProjectAnnotations[] +): NormalizedProjectAnnotations { + setDefaultProjectAnnotations(INTERNAL_DEFAULT_PROJECT_ANNOTATIONS); + return originalSetProjectAnnotations( + projectAnnotations + ) as NormalizedProjectAnnotations; +} diff --git a/code/renderers/react/src/portable-stories.tsx b/code/renderers/react/src/portable-stories.tsx index 6969be0d65e8..2ea196e85b4b 100644 --- a/code/renderers/react/src/portable-stories.tsx +++ b/code/renderers/react/src/portable-stories.tsx @@ -31,7 +31,7 @@ import type { ReactRenderer } from './types'; * Example: * * ```jsx - * // setup.js (for jest) + * // setup-file.js * import { setProjectAnnotations } from '@storybook/react'; * import projectAnnotations from './.storybook/preview'; * diff --git a/code/renderers/svelte/src/portable-stories.ts b/code/renderers/svelte/src/portable-stories.ts index 416025c0346e..309b6c4071d4 100644 --- a/code/renderers/svelte/src/portable-stories.ts +++ b/code/renderers/svelte/src/portable-stories.ts @@ -49,7 +49,7 @@ type MapToComposed = { * Example: * * ```jsx - * // setup.js (for jest) + * // setup-file.js * import { setProjectAnnotations } from '@storybook/svelte'; * import projectAnnotations from './.storybook/preview'; * diff --git a/code/renderers/vue3/src/portable-stories.ts b/code/renderers/vue3/src/portable-stories.ts index eeba697c0023..e02b36b53628 100644 --- a/code/renderers/vue3/src/portable-stories.ts +++ b/code/renderers/vue3/src/portable-stories.ts @@ -39,7 +39,7 @@ type MapToJSXAble = { * Example: * * ```jsx - * // setup.js (for jest) + * // setup-file.js * import { setProjectAnnotations } from '@storybook/vue3'; * import projectAnnotations from './.storybook/preview'; * diff --git a/code/renderers/web-components/src/index.ts b/code/renderers/web-components/src/index.ts index 58076b752f2b..0399ac731032 100644 --- a/code/renderers/web-components/src/index.ts +++ b/code/renderers/web-components/src/index.ts @@ -7,6 +7,7 @@ const { window, EventSource } = global; export * from './public-types'; export * from './framework-api'; +export * from './portable-stories'; // TODO: disable HMR and do full page loads because of customElements.define if (typeof module !== 'undefined' && module?.hot?.decline) { diff --git a/code/renderers/web-components/src/portable-stories.ts b/code/renderers/web-components/src/portable-stories.ts new file mode 100644 index 000000000000..c5eb2c9883cd --- /dev/null +++ b/code/renderers/web-components/src/portable-stories.ts @@ -0,0 +1,41 @@ +import { + setProjectAnnotations as originalSetProjectAnnotations, + setDefaultProjectAnnotations, +} from 'storybook/internal/preview-api'; +import type { + NamedOrDefaultProjectAnnotations, + NormalizedProjectAnnotations, +} from 'storybook/internal/types'; + +import * as webComponentsAnnotations from './entry-preview'; +import type { WebComponentsRenderer } from './types'; + +/** + * Function that sets the globalConfig of your storybook. The global config is the preview module of + * your .storybook folder. + * + * It should be run a single time, so that your global config (e.g. decorators) is applied to your + * stories when using `composeStories` or `composeStory`. + * + * Example: + * + * ```jsx + * // setup-file.js + * import { setProjectAnnotations } from '@storybook/web-components'; + * import projectAnnotations from './.storybook/preview'; + * + * setProjectAnnotations(projectAnnotations); + * ``` + * + * @param projectAnnotations - E.g. (import projectAnnotations from '../.storybook/preview') + */ +export function setProjectAnnotations( + projectAnnotations: + | NamedOrDefaultProjectAnnotations + | NamedOrDefaultProjectAnnotations[] +): NormalizedProjectAnnotations { + setDefaultProjectAnnotations(webComponentsAnnotations); + return originalSetProjectAnnotations( + projectAnnotations + ) as NormalizedProjectAnnotations; +} diff --git a/code/yarn.lock b/code/yarn.lock index 34cf0e23881e..dea4e0ea405f 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -5321,7 +5321,6 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/addon-docs@workspace:addons/docs" dependencies: - "@babel/core": "npm:^7.24.4" "@mdx-js/mdx": "npm:^3.0.0" "@mdx-js/react": "npm:^3.0.0" "@rollup/pluginutils": "npm:^5.0.2" @@ -5783,7 +5782,7 @@ __metadata: "@types/semver": "npm:^7.3.4" boxen: "npm:^7.1.1" chalk: "npm:^4.1.0" - commander: "npm:^6.2.1" + commander: "npm:^12.1.0" create-storybook: "workspace:*" cross-spawn: "npm:^7.0.3" envinfo: "npm:^7.7.3" @@ -5959,7 +5958,7 @@ __metadata: chai: "npm:^4.4.1" chalk: "npm:^5.3.0" cli-table3: "npm:^0.6.1" - commander: "npm:^6.2.1" + commander: "npm:^12.1.0" comment-parser: "npm:^1.4.1" compression: "npm:^1.7.4" copy-to-clipboard: "npm:^3.3.1" @@ -11941,6 +11940,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^12.1.0": + version: 12.1.0 + resolution: "commander@npm:12.1.0" + checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 + languageName: node + linkType: hard + "commander@npm:^2.18.0, commander@npm:^2.19.0, commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -11955,13 +11961,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^6.2.1": - version: 6.2.1 - resolution: "commander@npm:6.2.1" - checksum: 10c0/85748abd9d18c8bc88febed58b98f66b7c591d9b5017cad459565761d7b29ca13b7783ea2ee5ce84bf235897333706c4ce29adf1ce15c8252780e7000e2ce9ea - languageName: node - linkType: hard - "commander@npm:^8.3.0": version: 8.3.0 resolution: "commander@npm:8.3.0" @@ -12357,7 +12356,7 @@ __metadata: "@types/util-deprecate": "npm:^1.0.0" boxen: "npm:^7.1.1" chalk: "npm:^4.1.0" - commander: "npm:^6.2.1" + commander: "npm:^12.1.0" execa: "npm:^5.0.0" fd-package-json: "npm:^1.2.0" find-up: "npm:^5.0.0" diff --git a/docs/versions/next.json b/docs/versions/next.json index a47c722e043a..713ecd1b6677 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"8.3.0-alpha.7","info":{"plain":"- Addon Vitest: Set screenshotFailures to false by default - [#28908](https://github.com/storybookjs/storybook/pull/28908), thanks @yannbf!\n- Core: Add Rsbuild frameworks to known frameworks - [#28694](https://github.com/storybookjs/storybook/pull/28694), thanks @fi3ework!\n- Test: Fix support for TS < 4.7 - [#28887](https://github.com/storybookjs/storybook/pull/28887), thanks @ndelangen!\n- Vitest Addon: Fix error message logic in set up file - [#28906](https://github.com/storybookjs/storybook/pull/28906), thanks @yannbf!"}} +{"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!"}} diff --git a/package.json b/package.json index b5ea17ab9180..d0eb7795735e 100644 --- a/package.json +++ b/package.json @@ -15,5 +15,6 @@ "vite-ecosystem-ci:before-test": "node ./scripts/vite-ecosystem-ci/before-test.js && cd ./sandbox/react-vite-default-ts && yarn install", "vite-ecosystem-ci:build": "yarn task --task sandbox --template react-vite/default-ts --start-from=install", "vite-ecosystem-ci:test": "yarn task --task test-runner-dev --template react-vite/default-ts --start-from=dev" - } + }, + "packageManager": "yarn@4.4.0+sha512.91d93b445d9284e7ed52931369bc89a663414e5582d00eea45c67ddc459a2582919eece27c412d6ffd1bd0793ff35399381cb229326b961798ce4f4cc60ddfdb" } diff --git a/scripts/build-package.ts b/scripts/build-package.ts index dda542237300..60ac5b012e13 100644 --- a/scripts/build-package.ts +++ b/scripts/build-package.ts @@ -1,5 +1,5 @@ import chalk from 'chalk'; -import program from 'commander'; +import { program } from 'commander'; import { execaCommand } from 'execa'; import { readJSON } from 'fs-extra'; import { posix, resolve, sep } from 'path'; @@ -70,9 +70,10 @@ async function run() { .parse(process.argv); Object.keys(tasks).forEach((key) => { + const opts = program.opts(); // checks if a flag is passed e.g. yarn build --@storybook/addon-docs --watch - const containsFlag = program.rawArgs.includes(tasks[key].suffix); - tasks[key].value = containsFlag || program.all; + const containsFlag = program.args.includes(tasks[key].suffix); + tasks[key].value = containsFlag || opts.all; }); let selection; diff --git a/scripts/check-package.ts b/scripts/check-package.ts index 4a54ef43a897..c8873e6cf846 100644 --- a/scripts/check-package.ts +++ b/scripts/check-package.ts @@ -2,7 +2,7 @@ // without having to build dts files for all packages in the monorepo. // It is not implemented yet for angular, svelte and vue. import chalk from 'chalk'; -import program from 'commander'; +import { program } from 'commander'; import { execaCommand } from 'execa'; import { readJSON } from 'fs-extra'; import { resolve } from 'path'; @@ -60,9 +60,10 @@ async function run() { .parse(process.argv); Object.keys(tasks).forEach((key) => { + const opts = program.opts(); // checks if a flag is passed e.g. yarn check --@storybook/addon-docs --watch - const containsFlag = program.rawArgs.includes(tasks[key].suffix); - tasks[key].value = containsFlag || program.all; + const containsFlag = program.args.includes(tasks[key].suffix); + tasks[key].value = containsFlag || opts.all; }); let selection; diff --git a/scripts/package.json b/scripts/package.json index a85ca1935fd8..2ba48e28dac5 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -100,7 +100,7 @@ "chalk": "^4.1.0", "chromatic": "^11.5.5", "codecov": "^3.8.1", - "commander": "^6.2.1", + "commander": "^12.1.0", "cross-env": "^7.0.3", "cross-spawn": "^7.0.3", "danger": "^12.3.3", @@ -193,6 +193,7 @@ "verdaccio": "^5.31.1", "verdaccio-auth-memory": "^10.2.2" }, + "packageManager": "yarn@4.4.0+sha512.91d93b445d9284e7ed52931369bc89a663414e5582d00eea45c67ddc459a2582919eece27c412d6ffd1bd0793ff35399381cb229326b961798ce4f4cc60ddfdb", "engines": { "node": ">=22.0.0" } diff --git a/scripts/release/cancel-preparation-runs.ts b/scripts/release/cancel-preparation-runs.ts index 82fdcf85e878..aa5916de8208 100644 --- a/scripts/release/cancel-preparation-runs.ts +++ b/scripts/release/cancel-preparation-runs.ts @@ -3,7 +3,7 @@ * for the preparation workflows, and cancel them. */ import chalk from 'chalk'; -import program from 'commander'; +import { program } from 'commander'; import { dedent } from 'ts-dedent'; import { esMain } from '../utils/esmain'; diff --git a/scripts/release/generate-pr-description.ts b/scripts/release/generate-pr-description.ts index 38f6a9639d01..f1a79da2d6db 100644 --- a/scripts/release/generate-pr-description.ts +++ b/scripts/release/generate-pr-description.ts @@ -1,6 +1,6 @@ import { setOutput } from '@actions/core'; import chalk from 'chalk'; -import program from 'commander'; +import { program } from 'commander'; import semver from 'semver'; import { dedent } from 'ts-dedent'; import { z } from 'zod'; diff --git a/scripts/release/is-pr-frozen.ts b/scripts/release/is-pr-frozen.ts index d1fdec3a9de8..af5a957af8ae 100644 --- a/scripts/release/is-pr-frozen.ts +++ b/scripts/release/is-pr-frozen.ts @@ -2,7 +2,7 @@ import { join } from 'node:path'; import { setOutput } from '@actions/core'; import chalk from 'chalk'; -import program from 'commander'; +import { program } from 'commander'; import { readJson } from 'fs-extra'; import { esMain } from '../utils/esmain'; diff --git a/scripts/release/is-prerelease.ts b/scripts/release/is-prerelease.ts index d154991b27cc..d92f17279b82 100644 --- a/scripts/release/is-prerelease.ts +++ b/scripts/release/is-prerelease.ts @@ -1,6 +1,6 @@ import { setOutput } from '@actions/core'; import chalk from 'chalk'; -import program from 'commander'; +import { program } from 'commander'; import semver from 'semver'; import { esMain } from '../utils/esmain'; diff --git a/scripts/release/is-version-published.ts b/scripts/release/is-version-published.ts index 90d46399ab90..6af757eb654c 100644 --- a/scripts/release/is-version-published.ts +++ b/scripts/release/is-version-published.ts @@ -1,6 +1,6 @@ import { setOutput } from '@actions/core'; import chalk from 'chalk'; -import program from 'commander'; +import { program } from 'commander'; import { esMain } from '../utils/esmain'; import { getCurrentVersion } from './get-current-version'; diff --git a/scripts/release/label-patches.ts b/scripts/release/label-patches.ts index a0d82f44fe60..c1d8fa945e11 100644 --- a/scripts/release/label-patches.ts +++ b/scripts/release/label-patches.ts @@ -1,4 +1,4 @@ -import program from 'commander'; +import { program } from 'commander'; import ora from 'ora'; import { v4 as uuidv4 } from 'uuid'; diff --git a/scripts/release/pick-patches.ts b/scripts/release/pick-patches.ts index 49bb55317529..824f8c6d0cd9 100644 --- a/scripts/release/pick-patches.ts +++ b/scripts/release/pick-patches.ts @@ -1,6 +1,6 @@ import { setOutput } from '@actions/core'; import chalk from 'chalk'; -import program from 'commander'; +import { program } from 'commander'; import ora from 'ora'; import invariant from 'tiny-invariant'; diff --git a/scripts/release/publish.ts b/scripts/release/publish.ts index aa58c5fb8e46..65347010181d 100644 --- a/scripts/release/publish.ts +++ b/scripts/release/publish.ts @@ -1,7 +1,7 @@ import { join } from 'node:path'; import chalk from 'chalk'; -import program from 'commander'; +import { program } from 'commander'; import { execaCommand } from 'execa'; import { readJson } from 'fs-extra'; import pRetry from 'p-retry'; diff --git a/scripts/release/unreleased-changes-exists.ts b/scripts/release/unreleased-changes-exists.ts index e52dd198b05c..49c50ccd7d42 100644 --- a/scripts/release/unreleased-changes-exists.ts +++ b/scripts/release/unreleased-changes-exists.ts @@ -1,6 +1,6 @@ import { setOutput } from '@actions/core'; import chalk from 'chalk'; -import program from 'commander'; +import { program } from 'commander'; import { intersection } from 'lodash'; import { z } from 'zod'; diff --git a/scripts/release/version.ts b/scripts/release/version.ts index b5e775792209..422de3501bc0 100644 --- a/scripts/release/version.ts +++ b/scripts/release/version.ts @@ -2,7 +2,7 @@ import { join } from 'node:path'; import { setOutput } from '@actions/core'; import chalk from 'chalk'; -import program from 'commander'; +import { program } from 'commander'; import { execaCommand } from 'execa'; import { readFile, readJson, writeFile, writeJson } from 'fs-extra'; import semver from 'semver'; diff --git a/scripts/release/write-changelog.ts b/scripts/release/write-changelog.ts index 630db01f0441..8f927b3c6434 100644 --- a/scripts/release/write-changelog.ts +++ b/scripts/release/write-changelog.ts @@ -1,7 +1,7 @@ import { join } from 'node:path'; import chalk from 'chalk'; -import program from 'commander'; +import { program } from 'commander'; import { readFile, writeFile, writeJson } from 'fs-extra'; import semver from 'semver'; import { z } from 'zod'; diff --git a/scripts/run-registry.ts b/scripts/run-registry.ts index df8004d64f9f..4f837b952794 100755 --- a/scripts/run-registry.ts +++ b/scripts/run-registry.ts @@ -5,7 +5,7 @@ import type { Server } from 'node:http'; import { join, resolve as resolvePath } from 'node:path'; import chalk from 'chalk'; -import program from 'commander'; +import { program } from 'commander'; import { execa, execaSync } from 'execa'; import { pathExists, readJSON, remove } from 'fs-extra'; import pLimit from 'p-limit'; @@ -25,6 +25,8 @@ const logger = console; const root = resolvePath(__dirname, '..'); +const opts = program.opts(); + const startVerdaccio = async () => { const ready = { proxy: false, @@ -197,13 +199,13 @@ const run = async () => { logger.log(`📦 found ${packages.length} storybook packages at version ${chalk.blue(version)}`); - if (program.publish) { + if (opts.publish) { await publish(packages, 'http://localhost:6002'); } await execa('npx', ['rimraf', '.npmrc'], { cwd: root }); - if (!program.open) { + if (!opts.open) { verdaccioServer.close(); process.exit(0); } diff --git a/scripts/sandbox/publish.ts b/scripts/sandbox/publish.ts index ad8f5f11fcd8..7ddb549c3900 100755 --- a/scripts/sandbox/publish.ts +++ b/scripts/sandbox/publish.ts @@ -1,4 +1,4 @@ -import program from 'commander'; +import { program } from 'commander'; import { execaCommand } from 'execa'; import { existsSync } from 'fs'; import { copy, emptyDir, remove, writeFile } from 'fs-extra'; diff --git a/scripts/tasks/sandbox-parts.ts b/scripts/tasks/sandbox-parts.ts index ed157aad2075..1efffdb68593 100644 --- a/scripts/tasks/sandbox-parts.ts +++ b/scripts/tasks/sandbox-parts.ts @@ -360,13 +360,44 @@ async function linkPackageStories( ); } +const getVitestPluginInfo = (details: TemplateDetails) => { + let frameworkPluginImport = ''; + let frameworkPluginCall = ''; + + const framework = details.template.expected.framework; + const isNextjs = framework.includes('nextjs'); + const isSveltekit = framework.includes('sveltekit'); + + if (isNextjs) { + frameworkPluginImport = "import vitePluginNext from 'vite-plugin-storybook-nextjs'"; + frameworkPluginCall = 'vitePluginNext()'; + } + + if (isSveltekit) { + frameworkPluginImport = "import { storybookSveltekitPlugin } from '@storybook/sveltekit/vite'"; + frameworkPluginCall = 'storybookSveltekitPlugin()'; + } + + return { frameworkPluginImport, frameworkPluginCall }; +}; + export async function setupVitest(details: TemplateDetails, options: PassedOptionValues) { const { sandboxDir, template } = details; const isVue = template.expected.renderer === '@storybook/vue3'; const isNextjs = template.expected.framework.includes('nextjs'); + const { frameworkPluginCall, frameworkPluginImport } = getVitestPluginInfo(details); // const isAngular = template.expected.framework === '@storybook/angular'; - const storybookPackage = isNextjs ? template.expected.framework : template.expected.renderer; + + const portableStoriesFrameworks = [ + '@storybook/nextjs', + '@storybook/experimental-nextjs-vite', + '@storybook/sveltekit', + // TODO: add angular once we enable their sandboxes + ]; + const storybookPackage = portableStoriesFrameworks.includes(template.expected.framework) + ? template.expected.framework + : template.expected.renderer; const viteConfigPath = template.name.includes('JavaScript') ? 'vite.config.js' : 'vite.config.ts'; await writeFile( @@ -401,7 +432,7 @@ export async function setupVitest(details: TemplateDetails, options: PassedOptio dedent` import { defineWorkspace, defaultExclude } from "vitest/config"; import { storybookTest } from "@storybook/experimental-addon-vitest/plugin"; - ${isNextjs ? "import vitePluginNext from 'vite-plugin-storybook-nextjs'" : ''} + ${frameworkPluginImport} export default defineWorkspace([ { @@ -413,7 +444,7 @@ export async function setupVitest(details: TemplateDetails, options: PassedOptio include: ["vitest"], }, }), - ${isNextjs ? 'vitePluginNext(),' : ''} + ${frameworkPluginCall} ], ${ isNextjs @@ -444,14 +475,6 @@ export async function setupVitest(details: TemplateDetails, options: PassedOptio ...defaultExclude, // TODO: investigate TypeError: Cannot read properties of null (reading 'useContext') "**/*argtypes*", - // TODO (SVELTEKIT): Failures related to missing framework annotations - "**/frameworks/sveltekit_svelte-kit-skeleton-ts/navigation.stories*", - "**/frameworks/sveltekit_svelte-kit-skeleton-ts/hrefs.stories*", - // TODO (SVELTEKIT): Investigate Error: use:enhance can only be used on
fields with method="POST" - "**/frameworks/sveltekit_svelte-kit-skeleton-ts/forms.stories*", - // TODO (SVELTE|SVELTEKIT): Typescript preprocessor issue - "**/frameworks/svelte-vite_svelte-vite-default-ts/ts-docs.stories.*", - "**/frameworks/sveltekit_svelte-kit-skeleton-ts/ts-docs.stories.*", ], /** * TODO: Either fix or acknowledge limitation of: @@ -480,7 +503,8 @@ export async function setupVitest(details: TemplateDetails, options: PassedOptio packageJson.scripts = { ...packageJson.scripts, - vitest: 'vitest --pass-with-no-tests --reporter=default --reporter=hanging-process', + vitest: + 'vitest --pass-with-no-tests --reporter=default --reporter=hanging-process --test-timeout=5000', }; // This workaround is needed because Vitest seems to have issues in link mode diff --git a/scripts/utils/options.ts b/scripts/utils/options.ts index af83b282d3aa..5add8c25dd29 100644 --- a/scripts/utils/options.ts +++ b/scripts/utils/options.ts @@ -1,6 +1,6 @@ /** Use commander and prompts to gather a list of options for a script */ import chalk from 'chalk'; -import program from 'commander'; +import { type Command, type Option as CommanderOption, program } from 'commander'; // eslint-disable-next-line import/extensions import kebabCase from 'lodash/kebabCase.js'; import prompts from 'prompts'; @@ -102,8 +102,14 @@ function longFlag(key: OptionId, option: Option) { return inverse ? `no-${kebabCase(key)}` : kebabCase(key); } -function optionFlags(key: OptionId, option: Option) { - const base = `-${shortFlag(key, option)}, --${longFlag(key, option)}`; +function optionFlags(key: OptionId, option: Option, existingOptions: CommanderOption[]) { + const optionShortFlag = `-${shortFlag(key, option)}`; + let base; + if (existingOptions.some((opt) => opt.short === optionShortFlag)) { + base = `--${longFlag(key, option)}`; + } else { + base = `${optionShortFlag}, --${longFlag(key, option)}`; + } if (option.type === 'string' || option.type === 'string[]') { return `${base} <${key}>`; } @@ -111,13 +117,13 @@ function optionFlags(key: OptionId, option: Option) { } export function getOptions( - command: program.Command, + command: Command, options: TOptions, argv: string[] ): MaybeOptionValues { Object.entries(options) .reduce((acc, [key, option]) => { - const flags = optionFlags(key, option); + const flags = optionFlags(key, option, acc.options as any); if (option.type === 'boolean') { return acc.option(flags, option.description, !!option.inverse); diff --git a/scripts/yarn.lock b/scripts/yarn.lock index ad7f7dc69e67..2021d0f8f28d 100644 --- a/scripts/yarn.lock +++ b/scripts/yarn.lock @@ -1551,7 +1551,7 @@ __metadata: chalk: "npm:^4.1.0" chromatic: "npm:^11.5.5" codecov: "npm:^3.8.1" - commander: "npm:^6.2.1" + commander: "npm:^12.1.0" cross-env: "npm:^7.0.3" cross-spawn: "npm:^7.0.3" danger: "npm:^12.3.3" @@ -4151,6 +4151,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^12.1.0, commander@npm:~12.1.0": + version: 12.1.0 + resolution: "commander@npm:12.1.0" + checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 + languageName: node + linkType: hard + "commander@npm:^2.18.0, commander@npm:^2.8.1": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -4165,20 +4172,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^6.2.1": - version: 6.2.1 - resolution: "commander@npm:6.2.1" - checksum: 10c0/85748abd9d18c8bc88febed58b98f66b7c591d9b5017cad459565761d7b29ca13b7783ea2ee5ce84bf235897333706c4ce29adf1ce15c8252780e7000e2ce9ea - languageName: node - linkType: hard - -"commander@npm:~12.1.0": - version: 12.1.0 - resolution: "commander@npm:12.1.0" - checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 - languageName: node - linkType: hard - "comment-parser@npm:^1.4.0": version: 1.4.1 resolution: "comment-parser@npm:1.4.1"