diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index abe84ed2b356..4381e3ec634f 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,8 @@ +## 8.4.0-alpha.7 + +- CLI: Install Svelte CSF v5 in Svelte5 projects - [#29323](https://github.com/storybookjs/storybook/pull/29323), thanks @shilman! +- Manager: Add tags property to ComponentEntry objects - [#29343](https://github.com/storybookjs/storybook/pull/29343), thanks @Sidnioulz! + ## 8.4.0-alpha.6 - Addon-docs, blocks: Prebundle dependencies - [#29301](https://github.com/storybookjs/storybook/pull/29301), thanks @JReinhold! diff --git a/code/core/src/manager-api/lib/intersect.ts b/code/core/src/manager-api/lib/intersect.ts new file mode 100644 index 000000000000..84eae122322d --- /dev/null +++ b/code/core/src/manager-api/lib/intersect.ts @@ -0,0 +1,14 @@ +export default (a: T[], b: T[]): T[] => { + // no point in intersecting if one of the input is ill-defined + if (!a || !b) { + return []; + } + + return a.reduce((acc: T[], aValue) => { + if (b.includes(aValue)) { + acc.push(aValue); + } + + return acc; + }, []); +}; diff --git a/code/core/src/manager-api/lib/stories.ts b/code/core/src/manager-api/lib/stories.ts index 556acba0076d..eed677371ef0 100644 --- a/code/core/src/manager-api/lib/stories.ts +++ b/code/core/src/manager-api/lib/stories.ts @@ -24,6 +24,7 @@ import memoize from 'memoizerific'; import { dedent } from 'ts-dedent'; import { type API, type State, combineParameters } from '../root'; +import intersect from './intersect'; import merge from './merge'; const TITLE_PATH_SEPARATOR = /\s*\/\s*/; @@ -273,6 +274,9 @@ export const transformStoryIndexToStoriesHash = ( children: [childId], }), }); + // merge computes a union of arrays but we want an intersection on this + // specific array property, so it's easier to add it after the merge. + acc[id].tags = intersect(acc[id]?.tags ?? item.tags, item.tags); } else { acc[id] = merge((acc[id] || {}) as API_GroupEntry, { type: 'group', diff --git a/code/core/src/manager-api/tests/intersect.test.ts b/code/core/src/manager-api/tests/intersect.test.ts new file mode 100644 index 000000000000..e1e02362bc79 --- /dev/null +++ b/code/core/src/manager-api/tests/intersect.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; + +import intersect from '../lib/intersect'; + +describe('Manager API utilities - intersect', () => { + it('returns identity when intersecting identity', () => { + const a = ['foo', 'bar']; + expect(intersect(a, a)).toEqual(a); + }); + + it('returns a when b is a superset of a', () => { + const a = ['foo', 'bar']; + const b = ['a', 'foo', 'b', 'bar', 'c', 'ter']; + expect(intersect(a, b)).toEqual(a); + }); + + it('returns b when a is a superset of b', () => { + const a = ['a', 'foo', 'b', 'bar', 'c', 'ter']; + const b = ['foo', 'bar']; + expect(intersect(a, b)).toEqual(b); + }); + + it('returns an intersection', () => { + const a = ['a', 'bar', 'b', 'c']; + const b = ['foo', 'bar', 'ter']; + expect(intersect(a, b)).toEqual(['bar']); + }); + + it('returns an empty set when there is no overlap', () => { + const a = ['a', 'b', 'c']; + const b = ['foo', 'bar', 'ter']; + expect(intersect(a, b)).toEqual([]); + }); + + it('returns an empty set if a is undefined', () => { + const b = ['foo', 'bar', 'ter']; + expect(intersect(undefined as unknown as [], b)).toEqual([]); + }); + + it('returns an empty set if b is undefined', () => { + const a = ['foo', 'bar', 'ter']; + expect(intersect(a, undefined as unknown as [])).toEqual([]); + }); +}); diff --git a/code/core/src/manager-api/tests/stories.test.ts b/code/core/src/manager-api/tests/stories.test.ts index 16b5c926c9c2..9d3c4433fdec 100644 --- a/code/core/src/manager-api/tests/stories.test.ts +++ b/code/core/src/manager-api/tests/stories.test.ts @@ -281,6 +281,57 @@ describe('stories API', () => { name: '1', }); }); + it('intersects story/docs tags to compute tags for component entries', () => { + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; + api.setIndex({ + v: 5, + entries: { + 'a--1': { + type: 'story', + id: 'a--1', + title: 'a', + name: '1', + tags: ['shared', 'one-specific'], + importPath: './a.ts', + }, + 'a--2': { + type: 'story', + id: 'a--2', + title: 'a', + name: '2', + tags: ['shared', 'two-specific'], + importPath: './a.ts', + }, + }, + }); + const { index } = store.getState(); + // We need exact key ordering, even if in theory JS doesn't guarantee it + expect(Object.keys(index!)).toEqual(['a', 'a--1', 'a--2']); + expect(index!.a).toMatchObject({ + type: 'component', + id: 'a', + tags: ['shared'], + children: ['a--1', 'a--2'], + }); + expect(index!['a--1']).toMatchObject({ + type: 'story', + id: 'a--1', + parent: 'a', + title: 'a', + name: '1', + tags: ['shared', 'one-specific'], + }); + expect(index!['a--2']).toMatchObject({ + type: 'story', + id: 'a--2', + parent: 'a', + title: 'a', + name: '2', + tags: ['shared', 'two-specific'], + }); + }); // Stories can get out of order for a few reasons -- see reproductions on // https://github.com/storybookjs/storybook/issues/5518 it('does the right thing for out of order stories', async () => { @@ -1453,6 +1504,7 @@ describe('stories API', () => { "name": "a", "parent": undefined, "renderLabel": undefined, + "tags": [], "type": "component", }, "a--1": { @@ -1518,6 +1570,7 @@ describe('stories API', () => { "name": "a", "parent": undefined, "renderLabel": undefined, + "tags": [], "type": "component", }, "a--1": { @@ -1559,6 +1612,7 @@ describe('stories API', () => { "name": "a", "parent": undefined, "renderLabel": undefined, + "tags": [], "type": "component", }, "a--1": { diff --git a/code/core/src/manager/components/sidebar/__tests__/Sidebar.test.tsx b/code/core/src/manager/components/sidebar/__tests__/Sidebar.test.tsx index adc05cb07a55..2e1583fdcf87 100644 --- a/code/core/src/manager/components/sidebar/__tests__/Sidebar.test.tsx +++ b/code/core/src/manager/components/sidebar/__tests__/Sidebar.test.tsx @@ -59,6 +59,7 @@ const generateStories = ({ title, refId }: { title: string; refId?: string }): A name: componentName, children: [docsId], parent: rootId, + tags: [], }, // @ts-expect-error the missing fields are deprecated and replaced by the type prop { diff --git a/code/core/src/types/modules/api-stories.ts b/code/core/src/types/modules/api-stories.ts index 29e82c40e74a..c53d379b7192 100644 --- a/code/core/src/types/modules/api-stories.ts +++ b/code/core/src/types/modules/api-stories.ts @@ -27,6 +27,7 @@ export interface API_ComponentEntry extends API_BaseEntry { type: 'component'; parent?: StoryId; children: StoryId[]; + tags: Tag[]; } export interface API_DocsEntry extends API_BaseEntry { diff --git a/code/lib/create-storybook/src/generators/SVELTE/index.test.ts b/code/lib/create-storybook/src/generators/SVELTE/index.test.ts new file mode 100644 index 000000000000..0fb124025879 --- /dev/null +++ b/code/lib/create-storybook/src/generators/SVELTE/index.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; + +import type { JsPackageManager } from '@storybook/core/common'; + +import { getAddonSvelteCsfVersion } from './index'; + +describe('installed', () => { + it.each([ + ['3.0.0', ''], + ['4.0.0', '4'], + ['5.0.0', '^5.0.0-next.0'], + ['6.0.0', ''], + ['3.0.0-next.0', ''], + ['4.0.0-next.0', '4'], + ['4.2.19::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2Fsvelte%2F-%2Fsvelte-4.2.19.tgz', '4'], + ['5.0.0-next.0', '^5.0.0-next.0'], + ['6.0.0-next.0', ''], + ])('svelte %s => %s', async (svelteVersion, expectedAddonSpecifier) => { + const packageManager = { + getInstalledVersion: async (pkg: string) => (pkg === 'svelte' ? svelteVersion : undefined), + getAllDependencies: async () => ({ svelte: `^${svelteVersion}` }), + } as any as JsPackageManager; + await expect(getAddonSvelteCsfVersion(packageManager)).resolves.toBe(expectedAddonSpecifier); + }); +}); + +describe('uninstalled', () => { + it.each([ + ['^3', ''], + ['^4', '4'], + ['^5', '^5.0.0-next.0'], + ['^6', ''], + ['^3.0.0-next.0', ''], + ['^4.0.0-next.0', '4'], + ['^5.0.0-next.0', '^5.0.0-next.0'], + ['^6.0.0-next.0', ''], + ])('svelte %s => %s', async (svelteSpecifier, expectedAddonSpecifier) => { + const packageManager = { + getInstalledVersion: async (pkg: string) => undefined, + getAllDependencies: async () => ({ svelte: svelteSpecifier }), + } as any as JsPackageManager; + await expect(getAddonSvelteCsfVersion(packageManager)).resolves.toBe(expectedAddonSpecifier); + }); +}); diff --git a/code/lib/create-storybook/src/generators/SVELTE/index.ts b/code/lib/create-storybook/src/generators/SVELTE/index.ts index d3b4a89a7351..21661f5b02b9 100644 --- a/code/lib/create-storybook/src/generators/SVELTE/index.ts +++ b/code/lib/create-storybook/src/generators/SVELTE/index.ts @@ -1,10 +1,48 @@ +import type { JsPackageManager } from 'storybook/internal/common'; + +import { coerce, major } from 'semver'; + import { baseGenerator } from '../baseGenerator'; import type { Generator } from '../types'; +const versionHelper = (svelteMajor?: number) => { + if (svelteMajor === 4) { + return '4'; + } + // TODO: update when addon-svelte-csf v5 is released + if (svelteMajor === 5) { + return '^5.0.0-next.0'; + } + return ''; +}; + +export const getAddonSvelteCsfVersion = async (packageManager: JsPackageManager) => { + const svelteVersion = await packageManager.getInstalledVersion('svelte'); + try { + if (svelteVersion) { + return versionHelper(major(coerce(svelteVersion) || '')); + } else { + const deps = await packageManager.getAllDependencies(); + const svelteSpecifier = deps['svelte']; + const coerced = coerce(svelteSpecifier); + if (coerced?.version) { + return versionHelper(major(coerced.version)); + } + } + } catch { + // fallback to latest version + } + return ''; +}; + const generator: Generator = async (packageManager, npmOptions, options) => { + const addonSvelteCsfVersion = await getAddonSvelteCsfVersion(packageManager); + await baseGenerator(packageManager, npmOptions, options, 'svelte', { extensions: ['js', 'ts', 'svelte'], - extraAddons: ['@storybook/addon-svelte-csf'], + extraAddons: [ + `@storybook/addon-svelte-csf${addonSvelteCsfVersion && `@${addonSvelteCsfVersion}`}`, + ], }); }; diff --git a/code/lib/create-storybook/src/generators/SVELTEKIT/index.ts b/code/lib/create-storybook/src/generators/SVELTEKIT/index.ts index 4a891b9a68bf..1a17c0ac5b67 100644 --- a/code/lib/create-storybook/src/generators/SVELTEKIT/index.ts +++ b/code/lib/create-storybook/src/generators/SVELTEKIT/index.ts @@ -1,9 +1,12 @@ import { CoreBuilder } from 'storybook/internal/cli'; +import { getAddonSvelteCsfVersion } from '../SVELTE'; import { baseGenerator } from '../baseGenerator'; import type { Generator } from '../types'; const generator: Generator = async (packageManager, npmOptions, options) => { + const addonSvelteCsfVersion = await getAddonSvelteCsfVersion(packageManager); + await baseGenerator( packageManager, npmOptions, @@ -11,7 +14,9 @@ const generator: Generator = async (packageManager, npmOptions, options) => { 'svelte', { extensions: ['js', 'ts', 'svelte'], - extraAddons: ['@storybook/addon-svelte-csf'], + extraAddons: [ + `@storybook/addon-svelte-csf${addonSvelteCsfVersion && `@${addonSvelteCsfVersion}`}`, + ], }, 'sveltekit' ); diff --git a/code/package.json b/code/package.json index 5dab4f6f2052..fd61251fa13f 100644 --- a/code/package.json +++ b/code/package.json @@ -293,5 +293,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.4.0-alpha.7" } diff --git a/docs/_snippets/storybook-a11y-add.md b/docs/_snippets/storybook-a11y-add.md new file mode 100644 index 000000000000..f95b3f3b5b66 --- /dev/null +++ b/docs/_snippets/storybook-a11y-add.md @@ -0,0 +1,11 @@ +```shell renderer="common" language="js" packageManager="npm" +npx storybook add @storybook/addon-a11y +``` + +```shell renderer="common" language="js" packageManager="pnpm" +pnpm exec storybook add @storybook/addon-a11y +``` + +```shell renderer="common" language="js" packageManager="yarn" +yarn exec storybook add @storybook/addon-a11y +``` diff --git a/docs/_snippets/storybook-a11y-install.md b/docs/_snippets/storybook-a11y-install.md index 066faccd6c8f..5837429854f8 100644 --- a/docs/_snippets/storybook-a11y-install.md +++ b/docs/_snippets/storybook-a11y-install.md @@ -9,4 +9,3 @@ pnpm add --save-dev @storybook/addon-a11y ```shell renderer="common" language="js" packageManager="yarn" yarn add --dev @storybook/addon-a11y ``` - diff --git a/docs/_snippets/storybook-a11y-register.md b/docs/_snippets/storybook-a11y-register.md index e29f5c9bde9e..774d14622063 100644 --- a/docs/_snippets/storybook-a11y-register.md +++ b/docs/_snippets/storybook-a11y-register.md @@ -25,4 +25,3 @@ const config: StorybookConfig = { export default config; ``` - diff --git a/docs/versions/next.json b/docs/versions/next.json index ef66fd6e9fc3..b7cf99de3ecf 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"8.4.0-alpha.6","info":{"plain":"- Addon-docs, blocks: Prebundle dependencies - [#29301](https://github.com/storybookjs/storybook/pull/29301), thanks @JReinhold!\n- React: Prebundle all of `renderers/react`'s dependencies - [#29298](https://github.com/storybookjs/storybook/pull/29298), thanks @ndelangen!\n- Vite: Cleanup and prebundle dependencies - [#29302](https://github.com/storybookjs/storybook/pull/29302), thanks @JReinhold!"}} +{"version":"8.4.0-alpha.7","info":{"plain":"- CLI: Install Svelte CSF v5 in Svelte5 projects - [#29323](https://github.com/storybookjs/storybook/pull/29323), thanks @shilman!\n- Manager: Add tags property to ComponentEntry objects - [#29343](https://github.com/storybookjs/storybook/pull/29343), thanks @Sidnioulz!"}} diff --git a/docs/writing-tests/accessibility-testing.mdx b/docs/writing-tests/accessibility-testing.mdx index faa147e958c6..c5560e85daf4 100644 --- a/docs/writing-tests/accessibility-testing.mdx +++ b/docs/writing-tests/accessibility-testing.mdx @@ -19,23 +19,19 @@ Storybook provides an official [a11y addon](https://storybook.js.org/addons/@sto ### Set up the a11y addon -If you want to check accessibility for your stories using the [addon](https://storybook.js.org/addons/@storybook/addon-a11y/), you'll need to install it and add it to your Storybook. - -Run the following command to install the addon. +If you want to check accessibility for your stories using the [addon](https://storybook.js.org/addons/@storybook/addon-a11y/), you'll need to add it to your Storybook. You can do this by running the following command: {/* prettier-ignore-start */} - + {/* prettier-ignore-end */} -Update your Storybook configuration (in `.storybook/main.js|ts`) to include the accessibility addon. - -{/* prettier-ignore-start */} + - +The CLI's [`add`](../api/cli-options.mdx#add) command automates the addon's installation and setup. To install it manually, see our [documentation](../addons/install-addons.mdx#manual-installation) on how to install addons. -{/* prettier-ignore-end */} + Start your Storybook, and you will see some noticeable differences in the UI. A new toolbar icon and the accessibility panel where you can inspect the results of the tests.