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