diff --git a/.circleci/config.yml b/.circleci/config.yml index 70b387f58cb5..bd2d13b2af33 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -390,7 +390,7 @@ jobs: parallelism: type: integer executor: - class: medium + class: large name: sb_playwright parallelism: << parameters.parallelism >> steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b3dd726dbd7..444fe7ae0bea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## 8.2.6 + +- CPC: Fix missing exports for addon-kit - [#28691](https://github.com/storybookjs/storybook/pull/28691), thanks @ndelangen! + +## 8.2.5 + +- CPC: Add the globals export for manager - [#28650](https://github.com/storybookjs/storybook/pull/28650), thanks @ndelangen! +- CPC: Correct path to the `@storybook/theming/create` alias - [#28643](https://github.com/storybookjs/storybook/pull/28643), thanks @Averethel! +- Components: Remove external overrides - [#28632](https://github.com/storybookjs/storybook/pull/28632), thanks @kasperpeulen! +- Core: Fix header for MountMustBeDestructuredError message - [#28590](https://github.com/storybookjs/storybook/pull/28590), thanks @0916dhkim! +- Onboarding: Fix code snippet when story name differs from export name - [#28649](https://github.com/storybookjs/storybook/pull/28649), thanks @ghengeveld! +- Telemetry: Add mount, beforeEach, moduleMock stats - [#28624](https://github.com/storybookjs/storybook/pull/28624), thanks @shilman! +- Telemetry: CSF feature usage - [#28622](https://github.com/storybookjs/storybook/pull/28622), thanks @shilman! + ## 8.2.4 - CLI: Add diagnostic when the `storybook` package is missing - [#28604](https://github.com/storybookjs/storybook/pull/28604), thanks @kasperpeulen! diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index c77cec1c61b7..a5ab53b9e199 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,18 @@ +## 8.3.0-alpha.3 + +- Angular: Fix Angular template error for props with a circular reference - [#28498](https://github.com/storybookjs/storybook/pull/28498), thanks @Marklb! +- Angular: Fix template props not able to use dot notation - [#28588](https://github.com/storybookjs/storybook/pull/28588), thanks @Marklb! +- CLI: Fix the initialization of Storybook in workspaces - [#28699](https://github.com/storybookjs/storybook/pull/28699), thanks @valentinpalkovic! +- CPC: Fix missing exports for addon-kit - [#28691](https://github.com/storybookjs/storybook/pull/28691), thanks @ndelangen! +- CPC: Fix type usage in renderers - [#28745](https://github.com/storybookjs/storybook/pull/28745), thanks @ndelangen! +- Controls: Add disableSave parameter - [#28734](https://github.com/storybookjs/storybook/pull/28734), thanks @valentinpalkovic! +- React: Avoid 'Dynamic require of react is not possible' issue - [#28730](https://github.com/storybookjs/storybook/pull/28730), thanks @valentinpalkovic! +- Telemetry: Add mount, beforeEach, moduleMock stats - [#28624](https://github.com/storybookjs/storybook/pull/28624), thanks @shilman! +- Telemetry: CSF feature usage - [#28622](https://github.com/storybookjs/storybook/pull/28622), thanks @shilman! +- Types: Adjust beforeAll to be non-nullable in NormalizedProjectAnnotations - [#28671](https://github.com/storybookjs/storybook/pull/28671), thanks @kasperpeulen! +- Vue: Fix out of memory error when using vue-component-meta for events and slots - [#28674](https://github.com/storybookjs/storybook/pull/28674), thanks @larsrickert! +- Vue: Improve generated code snippets - [#27194](https://github.com/storybookjs/storybook/pull/27194), thanks @larsrickert! + ## 8.3.0-alpha.2 - Addon-Interactions: Fix status in panel tab - [#28580](https://github.com/storybookjs/storybook/pull/28580), thanks @yannbf! diff --git a/code/.storybook/main.ts b/code/.storybook/main.ts index 962bef2124da..7c4f74c8c2ef 100644 --- a/code/.storybook/main.ts +++ b/code/.storybook/main.ts @@ -8,34 +8,78 @@ const managerApiPath = path.join(__dirname, '../core/src/manager-api'); const config: StorybookConfig = { stories: [ + { + directory: '../core/template/stories', + titlePrefix: 'core', + }, { directory: '../core/src/manager', - titlePrefix: '@manager', + titlePrefix: 'manager', }, { directory: '../core/src/preview-api', - titlePrefix: '@preview', + titlePrefix: 'preview', + }, + { + directory: '../core/src/components/brand', + titlePrefix: 'brand', }, { - directory: '../core/src/components', - titlePrefix: '@components', + directory: '../core/src/components/components', + titlePrefix: 'components', }, { directory: '../lib/blocks/src', - titlePrefix: '@blocks', + titlePrefix: 'blocks', + }, + { + directory: '../addons/a11y/template/stories', + titlePrefix: 'addons/a11y', + }, + { + directory: '../addons/actions/template/stories', + titlePrefix: 'addons/actions', + }, + { + directory: '../addons/backgrounds/template/stories', + titlePrefix: 'addons/backgrounds', }, { directory: '../addons/controls/src', - titlePrefix: '@addons/controls', + titlePrefix: 'addons/controls', + }, + { + directory: '../addons/controls/template/stories', + titlePrefix: 'addons/controls', + }, + { + directory: '../addons/docs/template/stories', + titlePrefix: 'addons/docs', + }, + { + directory: '../addons/links/template/stories', + titlePrefix: 'addons/links', + }, + { + directory: '../addons/viewport/template/stories', + titlePrefix: 'addons/viewport', + }, + { + directory: '../addons/toolbars/template/stories', + titlePrefix: 'addons/toolbars', }, { directory: '../addons/onboarding/src', - titlePrefix: '@addons/onboarding', + titlePrefix: 'addons/onboarding', }, { directory: '../addons/interactions/src', - titlePrefix: '@addons/interactions', + titlePrefix: 'addons/interactions', }, + // { + // directory: '../addons/interactions/template/stories', + // titlePrefix: 'addons/interactions', + // }, ], addons: [ '@storybook/addon-links', @@ -46,6 +90,11 @@ const config: StorybookConfig = { '@storybook/addon-a11y', '@chromatic-com/storybook', ], + previewAnnotations: [ + './core/template/stories/preview.ts', + './addons/toolbars/template/stories/preview.ts', + './renderers/react/template/components/index.js', + ], build: { test: { // we have stories for the blocks here, we can't exclude them @@ -58,9 +107,21 @@ const config: StorybookConfig = { name: '@storybook/react-vite', options: {}, }, + refs: { + icons: { + title: 'Icons', + url: 'https://main--64b56e737c0aeefed9d5e675.chromatic.com', + expanded: false, + }, + }, core: { disableTelemetry: true, }, + features: { + viewportStoryGlobals: true, + themesStoryGlobals: true, + backgroundsStoryGlobals: true, + }, viteFinal: (viteConfig, { configType }) => mergeConfig(viteConfig, { resolve: { @@ -81,7 +142,7 @@ const config: StorybookConfig = { sourcemap: process.env.CI !== 'true', }, }), - logLevel: 'debug', + // logLevel: 'debug', }; export default config; diff --git a/code/.storybook/preview.tsx b/code/.storybook/preview.tsx index e1c81fe960e7..c80bdcea2937 100644 --- a/code/.storybook/preview.tsx +++ b/code/.storybook/preview.tsx @@ -296,20 +296,3 @@ export const parameters = { ], }, }; - -export const globalTypes = { - theme: { - name: 'Theme', - description: 'Global theme for components', - toolbar: { - icon: 'circlehollow', - title: 'Theme', - items: [ - { value: 'light', icon: 'circlehollow', title: 'light' }, - { value: 'dark', icon: 'circle', title: 'dark' }, - { value: 'side-by-side', icon: 'sidebar', title: 'side by side' }, - { value: 'stacked', icon: 'bottombar', title: 'stacked' }, - ], - }, - }, -}; diff --git a/code/addons/controls/src/ControlsPanel.tsx b/code/addons/controls/src/ControlsPanel.tsx index 80feaa374b51..20d3e695c2bd 100644 --- a/code/addons/controls/src/ControlsPanel.tsx +++ b/code/addons/controls/src/ControlsPanel.tsx @@ -34,6 +34,7 @@ interface ControlsParameters { sort?: SortType; expanded?: boolean; presetColors?: PresetColor[]; + disableSaveFromUI?: boolean; } interface ControlsPanelProps { @@ -46,7 +47,12 @@ export const ControlsPanel = ({ saveStory, createStory }: ControlsPanelProps) => const [args, updateArgs, resetArgs, initialArgs] = useArgs(); const [globals] = useGlobals(); const rows = useArgTypes(); - const { expanded, sort, presetColors } = useParameter(PARAM_KEY, {}); + const { + expanded, + sort, + presetColors, + disableSaveFromUI = false, + } = useParameter(PARAM_KEY, {}); const { path, previewInitialized } = useStorybookState(); // If the story is prepared, then show the args table @@ -84,9 +90,10 @@ export const ControlsPanel = ({ saveStory, createStory }: ControlsPanelProps) => sort={sort} isLoading={isLoading} /> - {hasControls && hasUpdatedArgs && global.CONFIG_TYPE === 'DEVELOPMENT' && ( - - )} + {hasControls && + hasUpdatedArgs && + global.CONFIG_TYPE === 'DEVELOPMENT' && + disableSaveFromUI !== true && } ); }; diff --git a/code/addons/docs/docs/recipes.md b/code/addons/docs/docs/recipes.md index e98c89145b78..0a982974f390 100644 --- a/code/addons/docs/docs/recipes.md +++ b/code/addons/docs/docs/recipes.md @@ -184,7 +184,7 @@ The Storybook UI is a workshop for developing components in isolation. Storybook To address this, we’ve added a CLI flag to only export the docs. This flag is also available in dev mode: ```sh -yarn build-storybook --docs +yarn storybook build --docs ``` ## Disabling docs stories diff --git a/code/addons/docs/template/stories/docspage/error.stories.ts b/code/addons/docs/template/stories/docspage/error.stories.ts index c0194422a0df..545448902b6c 100644 --- a/code/addons/docs/template/stories/docspage/error.stories.ts +++ b/code/addons/docs/template/stories/docspage/error.stories.ts @@ -2,7 +2,7 @@ import { global as globalThis } from '@storybook/global'; export default { component: globalThis.Components.Button, - tags: ['autodocs'], + tags: ['autodocs', '!test'], args: { label: 'Click Me!' }, parameters: { chromatic: { disable: true } }, }; @@ -12,10 +12,7 @@ export default { */ export const ErrorStory = { decorators: [ - (storyFn) => { - // Don't throw in the test runner; there's no easy way to skip (yet) - if (window?.navigator?.userAgent?.match(/StorybookTestRunner/)) return storyFn(); - + () => { throw new Error('Story did something wrong'); }, ], diff --git a/code/addons/interactions/template/stories/unhandled-errors.stories.ts b/code/addons/interactions/template/stories/unhandled-errors.stories.ts index 336c44b72f3a..0daa0850584b 100644 --- a/code/addons/interactions/template/stories/unhandled-errors.stories.ts +++ b/code/addons/interactions/template/stories/unhandled-errors.stories.ts @@ -13,6 +13,7 @@ export default { actions: { argTypesRegex: '^on[A-Z].*' }, chromatic: { disable: true }, }, + tags: ['!test'], }; export const Default = { diff --git a/code/addons/links/package.json b/code/addons/links/package.json index d766b0899819..d38a54820858 100644 --- a/code/addons/links/package.json +++ b/code/addons/links/package.json @@ -67,7 +67,7 @@ "prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/addon-bundle.ts" }, "dependencies": { - "@storybook/csf": "0.1.11", + "@storybook/csf": "^0.1.11", "@storybook/global": "^5.0.0", "ts-dedent": "^2.0.0" }, diff --git a/code/core/package.json b/code/core/package.json index 45a211d3b689..e1a53367825d 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -253,7 +253,7 @@ "prep": "bun ./scripts/prep.ts" }, "dependencies": { - "@storybook/csf": "0.1.11", + "@storybook/csf": "^0.1.11", "@types/express": "^4.17.21", "@types/node": "^18.0.0", "browser-assert": "^1.2.1", @@ -285,7 +285,7 @@ "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-slot": "^1.0.2", - "@storybook/docs-mdx": "4.0.0-next.0", + "@storybook/docs-mdx": "4.0.0-next.1", "@storybook/global": "^5.0.0", "@storybook/icons": "^1.2.5", "@tanstack/react-virtual": "^3.3.0", diff --git a/code/core/src/common/js-package-manager/JsPackageManager.ts b/code/core/src/common/js-package-manager/JsPackageManager.ts index 1000cc7bf174..5a18b572f248 100644 --- a/code/core/src/common/js-package-manager/JsPackageManager.ts +++ b/code/core/src/common/js-package-manager/JsPackageManager.ts @@ -64,22 +64,6 @@ export abstract class JsPackageManager { return packageJSON ? packageJSON.version ?? null : null; } - // NOTE: for some reason yarn prefers the npm registry in - // local development, so always use npm - async setRegistryURL(url: string) { - if (url) { - await this.executeCommand({ command: 'npm', args: ['config', 'set', 'registry', url] }); - } else { - await this.executeCommand({ command: 'npm', args: ['config', 'delete', 'registry'] }); - } - } - - async getRegistryURL() { - const res = await this.executeCommand({ command: 'npm', args: ['config', 'get', 'registry'] }); - const url = res.trim(); - return url === 'undefined' ? undefined : url; - } - constructor(options?: JsPackageManagerOptions) { this.cwd = options?.cwd || process.cwd(); } @@ -487,6 +471,8 @@ export abstract class JsPackageManager { ): // Use generic and conditional type to force `string[]` if fetchAllVersions is true and `string` if false Promise; + public abstract getRegistryURL(): Promise; + public abstract runPackageCommand( command: string, args: string[], diff --git a/code/core/src/common/js-package-manager/NPMProxy.test.ts b/code/core/src/common/js-package-manager/NPMProxy.test.ts index d1bb289112f1..9c88ea60af45 100644 --- a/code/core/src/common/js-package-manager/NPMProxy.test.ts +++ b/code/core/src/common/js-package-manager/NPMProxy.test.ts @@ -34,21 +34,6 @@ describe('NPM Proxy', () => { }); }); - describe('setRegistryUrl', () => { - it('should run `npm config set registry https://foo.bar`', async () => { - const executeCommandSpy = vi.spyOn(npmProxy, 'executeCommand').mockResolvedValueOnce(''); - - await npmProxy.setRegistryURL('https://foo.bar'); - - expect(executeCommandSpy).toHaveBeenCalledWith( - expect.objectContaining({ - command: 'npm', - args: ['config', 'set', 'registry', 'https://foo.bar'], - }) - ); - }); - }); - describe('installDependencies', () => { describe('npm6', () => { it('should run `npm install`', async () => { diff --git a/code/core/src/common/js-package-manager/NPMProxy.ts b/code/core/src/common/js-package-manager/NPMProxy.ts index ff77aedfa95a..d4d711915925 100644 --- a/code/core/src/common/js-package-manager/NPMProxy.ts +++ b/code/core/src/common/js-package-manager/NPMProxy.ts @@ -181,6 +181,17 @@ export class NPMProxy extends JsPackageManager { }); } + public async getRegistryURL() { + const res = await this.executeCommand({ + command: 'npm', + // "npm config" commands are not allowed in workspaces per default + // https://github.com/npm/cli/issues/6099#issuecomment-1847584792 + args: ['config', 'get', 'registry', '-ws=false', '-iwr'], + }); + const url = res.trim(); + return url === 'undefined' ? undefined : url; + } + protected async runAddDeps(dependencies: string[], installAsDevDependencies: boolean) { const { logStream, readLogFile, moveLogFile, removeLogFile } = await createLogStream(); let args = [...dependencies]; diff --git a/code/core/src/common/js-package-manager/PNPMProxy.test.ts b/code/core/src/common/js-package-manager/PNPMProxy.test.ts index 2430221cc2e8..cca2ca4ea364 100644 --- a/code/core/src/common/js-package-manager/PNPMProxy.test.ts +++ b/code/core/src/common/js-package-manager/PNPMProxy.test.ts @@ -24,21 +24,6 @@ describe('PNPM Proxy', () => { }); }); - describe('setRegistryUrl', () => { - it('should run `npm config set registry https://foo.bar`', async () => { - const executeCommandSpy = vi.spyOn(pnpmProxy, 'executeCommand').mockResolvedValueOnce(''); - - await pnpmProxy.setRegistryURL('https://foo.bar'); - - expect(executeCommandSpy).toHaveBeenCalledWith( - expect.objectContaining({ - command: 'npm', - args: ['config', 'set', 'registry', 'https://foo.bar'], - }) - ); - }); - }); - describe('installDependencies', () => { it('should run `pnpm install`', async () => { const executeCommandSpy = vi diff --git a/code/core/src/common/js-package-manager/PNPMProxy.ts b/code/core/src/common/js-package-manager/PNPMProxy.ts index 41c2858763c8..6d3f434c87c9 100644 --- a/code/core/src/common/js-package-manager/PNPMProxy.ts +++ b/code/core/src/common/js-package-manager/PNPMProxy.ts @@ -90,6 +90,15 @@ export class PNPMProxy extends JsPackageManager { }); } + public async getRegistryURL() { + const res = await this.executeCommand({ + command: 'pnpm', + args: ['config', 'get', 'registry'], + }); + const url = res.trim(); + return url === 'undefined' ? undefined : url; + } + async runPackageCommand(command: string, args: string[], cwd?: string): Promise { return this.executeCommand({ command: 'pnpm', diff --git a/code/core/src/common/js-package-manager/Yarn1Proxy.test.ts b/code/core/src/common/js-package-manager/Yarn1Proxy.test.ts index a26ce81efeb9..c20f496fed80 100644 --- a/code/core/src/common/js-package-manager/Yarn1Proxy.test.ts +++ b/code/core/src/common/js-package-manager/Yarn1Proxy.test.ts @@ -25,21 +25,6 @@ describe('Yarn 1 Proxy', () => { }); }); - describe('setRegistryUrl', () => { - it('should run `yarn config set npmRegistryServer https://foo.bar`', async () => { - const executeCommandSpy = vi.spyOn(yarn1Proxy, 'executeCommand').mockResolvedValueOnce(''); - - await yarn1Proxy.setRegistryURL('https://foo.bar'); - - expect(executeCommandSpy).toHaveBeenCalledWith( - expect.objectContaining({ - command: 'npm', - args: ['config', 'set', 'registry', 'https://foo.bar'], - }) - ); - }); - }); - describe('installDependencies', () => { it('should run `yarn`', async () => { const executeCommandSpy = vi.spyOn(yarn1Proxy, 'executeCommand').mockResolvedValueOnce(''); diff --git a/code/core/src/common/js-package-manager/Yarn1Proxy.ts b/code/core/src/common/js-package-manager/Yarn1Proxy.ts index b193d4db4f15..44a8f0ca3d45 100644 --- a/code/core/src/common/js-package-manager/Yarn1Proxy.ts +++ b/code/core/src/common/js-package-manager/Yarn1Proxy.ts @@ -83,6 +83,15 @@ export class Yarn1Proxy extends JsPackageManager { return JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as Record; } + public async getRegistryURL() { + const res = await this.executeCommand({ + command: 'yarn', + args: ['config', 'get', 'registry'], + }); + const url = res.trim(); + return url === 'undefined' ? undefined : url; + } + public async findInstallations(pattern: string[], { depth = 99 }: { depth?: number } = {}) { const yarnArgs = ['list', '--pattern', pattern.map((p) => `"${p}"`).join(' '), '--json']; diff --git a/code/core/src/common/js-package-manager/Yarn2Proxy.test.ts b/code/core/src/common/js-package-manager/Yarn2Proxy.test.ts index 21656ff1ccd2..4e4441aedb4d 100644 --- a/code/core/src/common/js-package-manager/Yarn2Proxy.test.ts +++ b/code/core/src/common/js-package-manager/Yarn2Proxy.test.ts @@ -53,21 +53,6 @@ describe('Yarn 2 Proxy', () => { }); }); - describe('setRegistryUrl', () => { - it('should run `yarn config set npmRegistryServer https://foo.bar`', async () => { - const executeCommandSpy = vi.spyOn(yarn2Proxy, 'executeCommand').mockResolvedValueOnce(''); - - await yarn2Proxy.setRegistryURL('https://foo.bar'); - - expect(executeCommandSpy).toHaveBeenCalledWith( - expect.objectContaining({ - command: 'npm', - args: ['config', 'set', 'registry', 'https://foo.bar'], - }) - ); - }); - }); - describe('addDependencies', () => { it('with devDep it should run `yarn install -D @storybook/core`', async () => { const executeCommandSpy = vi.spyOn(yarn2Proxy, 'executeCommand').mockResolvedValueOnce(''); diff --git a/code/core/src/common/js-package-manager/Yarn2Proxy.ts b/code/core/src/common/js-package-manager/Yarn2Proxy.ts index 7917bc7e1ebd..317a56b40090 100644 --- a/code/core/src/common/js-package-manager/Yarn2Proxy.ts +++ b/code/core/src/common/js-package-manager/Yarn2Proxy.ts @@ -239,6 +239,15 @@ export class Yarn2Proxy extends JsPackageManager { await removeLogFile(); } + public async getRegistryURL() { + const res = await this.executeCommand({ + command: 'yarn', + args: ['config', 'get', 'npmRegistryServer'], + }); + const url = res.trim(); + return url === 'undefined' ? undefined : url; + } + protected async runRemoveDeps(dependencies: string[]) { const args = [...dependencies]; diff --git a/code/core/src/core-server/README.md b/code/core/src/core-server/README.md index 4af52cb89409..3dcc1dfcbac2 100644 --- a/code/core/src/core-server/README.md +++ b/code/core/src/core-server/README.md @@ -6,8 +6,8 @@ It contains: - CLI arg parsing - Storybook UI "manager" webpack configuration -- `start-storybook` dev server -- `build-storybook` static builder +- `storybook dev` dev server +- `storybook build` static builder - presets handling The "preview" (aka iframe) side is implemented in pluggable builders: diff --git a/code/core/src/core-server/utils/StoryIndexGenerator.test.ts b/code/core/src/core-server/utils/StoryIndexGenerator.test.ts index 2d2b8d2b19c4..e07b46ba83af 100644 --- a/code/core/src/core-server/utils/StoryIndexGenerator.test.ts +++ b/code/core/src/core-server/utils/StoryIndexGenerator.test.ts @@ -68,7 +68,8 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); - expect(await generator.getIndex()).toMatchInlineSnapshot(` + const { storyIndex, stats } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` { "entries": { "a--story-one": { @@ -89,6 +90,18 @@ describe('StoryIndexGenerator', () => { "v": 5, } `); + + expect(stats).toMatchInlineSnapshot(` + { + "beforeEach": 0, + "loaders": 0, + "moduleMock": 0, + "mount": 0, + "play": 0, + "render": 0, + "storyFn": 0, + } + `); }); }); describe('single file .story specifier', () => { @@ -101,7 +114,8 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); - expect(await generator.getIndex()).toMatchInlineSnapshot(` + const { storyIndex } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` { "entries": { "f--story-one": { @@ -133,7 +147,8 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); - expect(await generator.getIndex()).toMatchInlineSnapshot(` + const { storyIndex } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` { "entries": { "stories--story-one": { @@ -165,7 +180,8 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); - expect(await generator.getIndex()).toMatchInlineSnapshot(` + const { storyIndex } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` { "entries": { "componentpath-extension--story-one": { @@ -245,7 +261,8 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); - expect(await generator.getIndex()).toMatchInlineSnapshot(` + const { storyIndex, stats } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` { "entries": { "a--story-one": { @@ -336,6 +353,68 @@ describe('StoryIndexGenerator', () => { "title": "first-nested/deeply/F", "type": "story", }, + "first-nested-deeply-features--with-csf-1": { + "componentPath": undefined, + "id": "first-nested-deeply-features--with-csf-1", + "importPath": "./src/first-nested/deeply/Features.stories.jsx", + "name": "With CSF 1", + "tags": [ + "dev", + "test", + ], + "title": "first-nested/deeply/Features", + "type": "story", + }, + "first-nested-deeply-features--with-play": { + "componentPath": undefined, + "id": "first-nested-deeply-features--with-play", + "importPath": "./src/first-nested/deeply/Features.stories.jsx", + "name": "With Play", + "tags": [ + "dev", + "test", + "play-fn", + ], + "title": "first-nested/deeply/Features", + "type": "story", + }, + "first-nested-deeply-features--with-render": { + "componentPath": undefined, + "id": "first-nested-deeply-features--with-render", + "importPath": "./src/first-nested/deeply/Features.stories.jsx", + "name": "With Render", + "tags": [ + "dev", + "test", + ], + "title": "first-nested/deeply/Features", + "type": "story", + }, + "first-nested-deeply-features--with-story-fn": { + "componentPath": undefined, + "id": "first-nested-deeply-features--with-story-fn", + "importPath": "./src/first-nested/deeply/Features.stories.jsx", + "name": "With Story Fn", + "tags": [ + "dev", + "test", + ], + "title": "first-nested/deeply/Features", + "type": "story", + }, + "first-nested-deeply-features--with-test": { + "componentPath": undefined, + "id": "first-nested-deeply-features--with-test", + "importPath": "./src/first-nested/deeply/Features.stories.jsx", + "name": "With Test", + "tags": [ + "dev", + "test", + "play-fn", + ], + "title": "first-nested/deeply/Features", + "type": "story", + }, "h--story-one": { "componentPath": undefined, "id": "h--story-one", @@ -378,6 +457,18 @@ describe('StoryIndexGenerator', () => { "v": 5, } `); + + expect(stats).toMatchInlineSnapshot(` + { + "beforeEach": 1, + "loaders": 1, + "moduleMock": 0, + "mount": 1, + "play": 2, + "render": 1, + "storyFn": 1, + } + `); }); }); @@ -395,7 +486,8 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], autodocsOptions); await generator.initialize(); - expect(await generator.getIndex()).toMatchInlineSnapshot(` + const { storyIndex, stats } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` { "entries": { "a--story-one": { @@ -512,6 +604,68 @@ describe('StoryIndexGenerator', () => { "title": "first-nested/deeply/F", "type": "story", }, + "first-nested-deeply-features--with-csf-1": { + "componentPath": undefined, + "id": "first-nested-deeply-features--with-csf-1", + "importPath": "./src/first-nested/deeply/Features.stories.jsx", + "name": "With CSF 1", + "tags": [ + "dev", + "test", + ], + "title": "first-nested/deeply/Features", + "type": "story", + }, + "first-nested-deeply-features--with-play": { + "componentPath": undefined, + "id": "first-nested-deeply-features--with-play", + "importPath": "./src/first-nested/deeply/Features.stories.jsx", + "name": "With Play", + "tags": [ + "dev", + "test", + "play-fn", + ], + "title": "first-nested/deeply/Features", + "type": "story", + }, + "first-nested-deeply-features--with-render": { + "componentPath": undefined, + "id": "first-nested-deeply-features--with-render", + "importPath": "./src/first-nested/deeply/Features.stories.jsx", + "name": "With Render", + "tags": [ + "dev", + "test", + ], + "title": "first-nested/deeply/Features", + "type": "story", + }, + "first-nested-deeply-features--with-story-fn": { + "componentPath": undefined, + "id": "first-nested-deeply-features--with-story-fn", + "importPath": "./src/first-nested/deeply/Features.stories.jsx", + "name": "With Story Fn", + "tags": [ + "dev", + "test", + ], + "title": "first-nested/deeply/Features", + "type": "story", + }, + "first-nested-deeply-features--with-test": { + "componentPath": undefined, + "id": "first-nested-deeply-features--with-test", + "importPath": "./src/first-nested/deeply/Features.stories.jsx", + "name": "With Test", + "tags": [ + "dev", + "test", + "play-fn", + ], + "title": "first-nested/deeply/Features", + "type": "story", + }, "h--docs": { "id": "h--docs", "importPath": "./src/H.stories.mjs", @@ -567,6 +721,18 @@ describe('StoryIndexGenerator', () => { "v": 5, } `); + + expect(stats).toMatchInlineSnapshot(` + { + "beforeEach": 1, + "loaders": 1, + "moduleMock": 0, + "mount": 1, + "play": 2, + "render": 1, + "storyFn": 1, + } + `); }); const autodocsTrueOptions = { @@ -603,6 +769,12 @@ describe('StoryIndexGenerator', () => { "componentpath-package--story-one", "first-nested-deeply-f--docs", "first-nested-deeply-f--story-one", + "first-nested-deeply-features--docs", + "first-nested-deeply-features--with-play", + "first-nested-deeply-features--with-story-fn", + "first-nested-deeply-features--with-render", + "first-nested-deeply-features--with-test", + "first-nested-deeply-features--with-csf-1", "nested-button--docs", "nested-button--story-one", "second-nested-g--docs", @@ -639,6 +811,12 @@ describe('StoryIndexGenerator', () => { "componentpath-package--story-one", "first-nested-deeply-f--docs", "first-nested-deeply-f--story-one", + "first-nested-deeply-features--docs", + "first-nested-deeply-features--with-play", + "first-nested-deeply-features--with-story-fn", + "first-nested-deeply-features--with-render", + "first-nested-deeply-features--with-test", + "first-nested-deeply-features--with-csf-1", "nested-button--docs", "nested-button--story-one", "second-nested-g--docs", @@ -730,7 +908,8 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([csfSpecifier, docsSpecifier], autodocsOptions); await generator.initialize(); - expect(await generator.getIndex()).toMatchInlineSnapshot(` + const { storyIndex } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` { "entries": { "b--docs": { @@ -794,7 +973,8 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([csfSpecifier, docsSpecifier], autodocsOptions); await generator.initialize(); - expect(await generator.getIndex()).toMatchInlineSnapshot(` + const { storyIndex } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` { "entries": { "b--docs": { @@ -862,7 +1042,8 @@ describe('StoryIndexGenerator', () => { ); await generator.initialize(); - expect(await generator.getIndex()).toMatchInlineSnapshot(` + const { storyIndex } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` { "entries": { "a--docs": { @@ -919,7 +1100,8 @@ describe('StoryIndexGenerator', () => { generator.getProjectTags = () => ['dev', 'test', 'autodocs']; await generator.initialize(); - expect(await generator.getIndex()).toMatchInlineSnapshot(` + const { storyIndex } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` { "entries": { "a--docs": { @@ -970,7 +1152,8 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], autodocsOptions); await generator.initialize(); - expect(await generator.getIndex()).toMatchInlineSnapshot(` + const { storyIndex } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` { "entries": { "duplicate-a--docs": { @@ -1030,7 +1213,8 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([csfSpecifier], autodocsOptions); await generator.initialize(); - expect(await generator.getIndex()).toMatchInlineSnapshot(` + const { storyIndex } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` { "entries": {}, "v": 5, @@ -1047,7 +1231,8 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([csfSpecifier], autodocsOptions); await generator.initialize(); - expect(await generator.getIndex()).toMatchInlineSnapshot(` + const { storyIndex } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` { "entries": { "my-component-a--docs": { @@ -1088,7 +1273,8 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([storiesSpecifier, docsSpecifier], options); await generator.initialize(); - expect(await generator.getIndex()).toMatchInlineSnapshot(` + const { storyIndex } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` { "entries": { "a--metaof": { @@ -1222,7 +1408,8 @@ describe('StoryIndexGenerator', () => { }); await generator.initialize(); - expect(await generator.getIndex()).toMatchInlineSnapshot(` + const { storyIndex } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` { "entries": { "a--metaof": { @@ -1328,7 +1515,8 @@ describe('StoryIndexGenerator', () => { options ); await generator.initialize(); - expect(await generator.getIndex()).toMatchInlineSnapshot(` + const { storyIndex } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` { "entries": { "a--story-one": { @@ -1395,7 +1583,8 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([csfSpecifier, docsSpecifier], options); await generator.initialize(); - expect(await generator.getIndex()).toMatchInlineSnapshot(` + const { storyIndex } = await generator.getIndexAndStats(); + expect(storyIndex).toMatchInlineSnapshot(` { "entries": { "my-component-b--docs": { @@ -1603,6 +1792,11 @@ describe('StoryIndexGenerator', () => { "componentpath-noextension--story-one", "componentpath-package--story-one", "first-nested-deeply-f--story-one", + "first-nested-deeply-features--with-play", + "first-nested-deeply-features--with-story-fn", + "first-nested-deeply-features--with-render", + "first-nested-deeply-features--with-test", + "first-nested-deeply-features--with-csf-1", ] `); }); @@ -1621,7 +1815,7 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); await generator.getIndex(); - expect(readCsfMock).toHaveBeenCalledTimes(10); + expect(readCsfMock).toHaveBeenCalledTimes(11); readCsfMock.mockClear(); await generator.getIndex(); @@ -1679,7 +1873,7 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); await generator.getIndex(); - expect(readCsfMock).toHaveBeenCalledTimes(10); + expect(readCsfMock).toHaveBeenCalledTimes(11); generator.invalidate(specifier, './src/B.stories.ts', false); @@ -1764,7 +1958,7 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); await generator.getIndex(); - expect(readCsfMock).toHaveBeenCalledTimes(10); + expect(readCsfMock).toHaveBeenCalledTimes(11); generator.invalidate(specifier, './src/B.stories.ts', true); @@ -1803,7 +1997,7 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); await generator.getIndex(); - expect(readCsfMock).toHaveBeenCalledTimes(10); + expect(readCsfMock).toHaveBeenCalledTimes(11); generator.invalidate(specifier, './src/B.stories.ts', true); diff --git a/code/core/src/core-server/utils/StoryIndexGenerator.ts b/code/core/src/core-server/utils/StoryIndexGenerator.ts index dfcf2d4a5f51..64aa9744e00e 100644 --- a/code/core/src/core-server/utils/StoryIndexGenerator.ts +++ b/code/core/src/core-server/utils/StoryIndexGenerator.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-underscore-dangle */ import path from 'node:path'; import chalk from 'chalk'; import fs from 'fs-extra'; @@ -17,6 +18,7 @@ import type { StoryIndex, Indexer, StorybookConfigRaw, + IndexInputStats, } from '@storybook/core/types'; import { userOrAutoTitleFromSpecifier, sortStoriesV7 } from '@storybook/core/preview-api'; import { commonGlobOptions, normalizeStoryPath } from '@storybook/core/common'; @@ -26,14 +28,17 @@ import { storyNameFromExport, toId, combineTags } from '@storybook/csf'; import { dedent } from 'ts-dedent'; import { autoName } from './autoName'; import { IndexingError, MultipleIndexingError } from './IndexingError'; +import { addStats, type IndexStatsSummary } from './summarizeStats'; // Extended type to keep track of the csf meta id so we know the component id when referencing docs in `extractDocs` -type StoryIndexEntryWithMetaId = StoryIndexEntry & { metaId?: string }; +type StoryIndexEntryWithExtra = StoryIndexEntry & { + extra: { metaId?: string; stats: IndexInputStats }; +}; /** A .mdx file will produce a docs entry */ type DocsCacheEntry = DocsIndexEntry; /** A *.stories.* file will produce a list of stories and possibly a docs entry */ type StoriesCacheEntry = { - entries: (StoryIndexEntryWithMetaId | DocsIndexEntry)[]; + entries: (StoryIndexEntryWithExtra | DocsIndexEntry)[]; dependents: Path[]; type: 'stories'; }; @@ -103,6 +108,9 @@ export class StoryIndexGenerator { // - the preview changes [not yet implemented] private lastIndex?: StoryIndex | null; + // Cache the last value stats calculation, mirroring lastIndex + private lastStats?: IndexStatsSummary; + // Same as the above but for the error case private lastError?: Error | null; @@ -221,7 +229,7 @@ export class StoryIndexGenerator { projectTags, }: { projectTags?: Tag[]; - }): Promise<(IndexEntry | ErrorEntry)[]> { + }): Promise<{ entries: (IndexEntry | ErrorEntry)[]; stats: IndexStatsSummary }> { // First process all the story files. Then, in a second pass, // process the docs files. The reason for this is that the docs // files may use the `` syntax, which requires @@ -236,7 +244,8 @@ export class StoryIndexGenerator { this.extractDocs(specifier, absolutePath, projectTags) ); - return this.specifiers.flatMap((specifier) => { + const statsSummary = {} as IndexStatsSummary; + const entries = this.specifiers.flatMap((specifier) => { const cache = this.specifierToCache.get(specifier); invariant( cache, @@ -251,12 +260,17 @@ export class StoryIndexGenerator { return entry.entries.map((item) => { if (item.type === 'docs') return item; - // Drop the meta id as it isn't part of the index, we just used it for record keeping in `extractDocs` - const { metaId, ...existing } = item; + + addStats(item.extra.stats, statsSummary); + + // Drop extra data used for internal bookkeeping + const { extra, ...existing } = item; return existing; }); }); }); + + return { entries, stats: statsSummary }; } findDependencies(absoluteImports: Path[]) { @@ -340,7 +354,7 @@ export class StoryIndexGenerator { ]); } - const entries: ((StoryIndexEntryWithMetaId | DocsCacheEntry) & { tags: Tag[] })[] = + const entries: ((StoryIndexEntryWithExtra | DocsCacheEntry) & { tags: Tag[] })[] = indexInputs.map((input) => { const name = input.name ?? storyNameFromExport(input.exportName); const componentPath = @@ -348,14 +362,16 @@ export class StoryIndexGenerator { this.resolveComponentPath(input.rawComponentPath, absolutePath, matchPath); const title = input.title ?? defaultMakeTitle(); - // eslint-disable-next-line no-underscore-dangle const id = input.__id ?? toId(input.metaId ?? title, storyNameFromExport(input.exportName)); const tags = combineTags(...projectTags, ...(input.tags ?? [])); return { type: 'story', id, - metaId: input.metaId, + extra: { + metaId: input.metaId, + stats: input.__stats ?? {}, + }, name, title, importPath, @@ -428,12 +444,12 @@ export class StoryIndexGenerator { // Also, if `result.of` is set, it means that we're using the `` syntax, // so find the `title` defined the file that `meta` points to. - let csfEntry: StoryIndexEntryWithMetaId | undefined; + let csfEntry: StoryIndexEntryWithExtra | undefined; if (result.of) { const absoluteOf = makeAbsolute(result.of, normalizedPath, this.options.workingDir); dependencies.forEach((dep) => { if (dep.entries.length > 0) { - const first = dep.entries.find((e) => e.type !== 'docs') as StoryIndexEntryWithMetaId; + const first = dep.entries.find((e) => e.type !== 'docs') as StoryIndexEntryWithExtra; if ( path @@ -475,7 +491,7 @@ export class StoryIndexGenerator { result.name || (csfEntry ? autoName(importPath, csfEntry.importPath, defaultName) : defaultName); - const id = toId(csfEntry?.metaId || title, name); + const id = toId(csfEntry?.extra.metaId || title, name); const tags = combineTags( ...projectTags, @@ -598,7 +614,12 @@ export class StoryIndexGenerator { } async getIndex() { - if (this.lastIndex) return this.lastIndex; + return (await this.getIndexAndStats()).storyIndex; + } + + async getIndexAndStats(): Promise<{ storyIndex: StoryIndex; stats: IndexStatsSummary }> { + if (this.lastIndex && this.lastStats) + return { storyIndex: this.lastIndex, stats: this.lastStats }; if (this.lastError) throw this.lastError; const previewCode = await this.getPreviewCode(); @@ -606,7 +627,7 @@ export class StoryIndexGenerator { // Extract any entries that are currently missing // Pull out each file's stories into a list of stories, to be composed and sorted - const storiesList = await this.ensureExtracted({ projectTags }); + const { entries: storiesList, stats } = await this.ensureExtracted({ projectTags }); try { const errorEntries = storiesList.filter((entry) => entry.type === 'error'); @@ -635,12 +656,13 @@ export class StoryIndexGenerator { previewCode && getStorySortParameter(previewCode) ); + this.lastStats = stats; this.lastIndex = { v: 5, entries: sorted, }; - return this.lastIndex; + return { storyIndex: this.lastIndex, stats: this.lastStats }; } catch (err) { this.lastError = err == null || err instanceof Error ? err : undefined; invariant(this.lastError); diff --git a/code/core/src/core-server/utils/__mockdata__/src/first-nested/deeply/Features.stories.jsx b/code/core/src/core-server/utils/__mockdata__/src/first-nested/deeply/Features.stories.jsx new file mode 100644 index 000000000000..90d96b590b6a --- /dev/null +++ b/code/core/src/core-server/utils/__mockdata__/src/first-nested/deeply/Features.stories.jsx @@ -0,0 +1,27 @@ +const component = {}; +export default { + component, +}; + +export const WithPlay = { + play: async () => {}, +}; + +export const WithStoryFn = () => {}; + +export const WithRender = { + render: () => {}, +}; + +export const WithTest = { + beforeEach: async () => {}, + play: async ({ mount }) => { + await mount(); + }, +}; + +export const WithCSF1 = { + parameters: {}, + decorators: [], + loaders: [], +}; diff --git a/code/core/src/core-server/utils/__tests__/index-extraction.test.ts b/code/core/src/core-server/utils/__tests__/index-extraction.test.ts index 01a880479b99..6fbbdeb41c6c 100644 --- a/code/core/src/core-server/utils/__tests__/index-extraction.test.ts +++ b/code/core/src/core-server/utils/__tests__/index-extraction.test.ts @@ -62,9 +62,12 @@ describe('story extraction', () => { "entries": [ { "componentPath": undefined, + "extra": { + "metaId": "a", + "stats": {}, + }, "id": "a--story-one", "importPath": "./src/A.stories.js", - "metaId": "a", "name": "Story One", "tags": [ "story-tag-from-indexer", @@ -74,9 +77,12 @@ describe('story extraction', () => { }, { "componentPath": undefined, + "extra": { + "metaId": "custom-id", + "stats": {}, + }, "id": "some-fully-custom-id", "importPath": "./src/A.stories.js", - "metaId": "custom-id", "name": "Another Story Name", "tags": [ "story-tag-from-indexer", @@ -118,9 +124,12 @@ describe('story extraction', () => { "entries": [ { "componentPath": undefined, + "extra": { + "metaId": undefined, + "stats": {}, + }, "id": "f--story-one", "importPath": "./src/first-nested/deeply/F.stories.js", - "metaId": undefined, "name": "Story One", "tags": [], "title": "F", @@ -164,9 +173,12 @@ describe('story extraction', () => { "entries": [ { "componentPath": undefined, + "extra": { + "metaId": "a", + "stats": {}, + }, "id": "a--story-one", "importPath": "./src/first-nested/deeply/F.stories.js", - "metaId": "a", "name": "Story One", "tags": [ "story-tag-from-indexer", @@ -212,9 +224,12 @@ describe('story extraction', () => { "entries": [ { "componentPath": undefined, + "extra": { + "metaId": "a", + "stats": {}, + }, "id": "a--story-one", "importPath": "./src/A.stories.js", - "metaId": "a", "name": "Story One", "tags": [ "story-tag-from-indexer", @@ -278,9 +293,12 @@ describe('story extraction', () => { "entries": [ { "componentPath": undefined, + "extra": { + "metaId": undefined, + "stats": {}, + }, "id": "a--story-one", "importPath": "./src/A.stories.js", - "metaId": undefined, "name": "Story One", "tags": [ "story-tag-from-indexer", @@ -290,9 +308,12 @@ describe('story extraction', () => { }, { "componentPath": undefined, + "extra": { + "metaId": undefined, + "stats": {}, + }, "id": "custom-title--story-two", "importPath": "./src/A.stories.js", - "metaId": undefined, "name": "Custom Name For Second Story", "tags": [ "story-tag-from-indexer", @@ -302,9 +323,12 @@ describe('story extraction', () => { }, { "componentPath": undefined, + "extra": { + "metaId": "custom-meta-id", + "stats": {}, + }, "id": "custom-meta-id--story-three", "importPath": "./src/A.stories.js", - "metaId": "custom-meta-id", "name": "Story Three", "tags": [ "story-tag-from-indexer", @@ -347,9 +371,12 @@ describe('story extraction', () => { "entries": [ { "componentPath": undefined, + "extra": { + "metaId": undefined, + "stats": {}, + }, "id": "a--story-one", "importPath": "./src/A.stories.js", - "metaId": undefined, "name": "Story One", "tags": [ "story-tag-from-indexer", @@ -397,9 +424,12 @@ describe('docs entries from story extraction', () => { "entries": [ { "componentPath": undefined, + "extra": { + "metaId": undefined, + "stats": {}, + }, "id": "a--story-one", "importPath": "./src/A.stories.js", - "metaId": undefined, "name": "Story One", "tags": [ "story-tag-from-indexer", @@ -457,9 +487,12 @@ describe('docs entries from story extraction', () => { }, { "componentPath": undefined, + "extra": { + "metaId": undefined, + "stats": {}, + }, "id": "a--story-one", "importPath": "./src/A.stories.js", - "metaId": undefined, "name": "Story One", "tags": [ "autodocs", @@ -506,9 +539,12 @@ describe('docs entries from story extraction', () => { "entries": [ { "componentPath": undefined, + "extra": { + "metaId": undefined, + "stats": {}, + }, "id": "a--story-one", "importPath": "./src/A.stories.js", - "metaId": undefined, "name": "Story One", "tags": [ "autodocs", diff --git a/code/core/src/core-server/utils/doTelemetry.ts b/code/core/src/core-server/utils/doTelemetry.ts index b7d039119c3b..be9479cf3348 100644 --- a/code/core/src/core-server/utils/doTelemetry.ts +++ b/code/core/src/core-server/utils/doTelemetry.ts @@ -15,9 +15,9 @@ export async function doTelemetry( ) { if (!core?.disableTelemetry) { initializedStoryIndexGenerator.then(async (generator) => { - let storyIndex: StoryIndex | undefined; + let indexAndStats; try { - storyIndex = await generator?.getIndex(); + indexAndStats = await generator?.getIndexAndStats(); } catch (err) { // If we fail to get the index, treat it as a recoverable error, but send it up to telemetry // as if we crashed. In the future we will revisit this to send a distinct error @@ -36,10 +36,11 @@ export async function doTelemetry( const payload = { precedingUpgrade: await getPrecedingUpgrade(), }; - if (storyIndex) { + if (indexAndStats) { Object.assign(payload, { versionStatus: versionUpdates && versionCheck ? versionStatus(versionCheck) : 'disabled', - storyIndex: summarizeIndex(storyIndex), + storyIndex: summarizeIndex(indexAndStats.storyIndex), + storyStats: indexAndStats.stats, }); } telemetry('dev', payload, { configDir: options.configDir }); diff --git a/code/core/src/core-server/utils/stories-json.test.ts b/code/core/src/core-server/utils/stories-json.test.ts index 84b201ab62f1..a4ad0fa780b7 100644 --- a/code/core/src/core-server/utils/stories-json.test.ts +++ b/code/core/src/core-server/utils/stories-json.test.ts @@ -263,6 +263,63 @@ describe('useStoriesJson', () => { "title": "first-nested/deeply/F", "type": "story", }, + "first-nested-deeply-features--with-csf-1": { + "id": "first-nested-deeply-features--with-csf-1", + "importPath": "./src/first-nested/deeply/Features.stories.jsx", + "name": "With CSF 1", + "tags": [ + "dev", + "test", + ], + "title": "first-nested/deeply/Features", + "type": "story", + }, + "first-nested-deeply-features--with-play": { + "id": "first-nested-deeply-features--with-play", + "importPath": "./src/first-nested/deeply/Features.stories.jsx", + "name": "With Play", + "tags": [ + "dev", + "test", + "play-fn", + ], + "title": "first-nested/deeply/Features", + "type": "story", + }, + "first-nested-deeply-features--with-render": { + "id": "first-nested-deeply-features--with-render", + "importPath": "./src/first-nested/deeply/Features.stories.jsx", + "name": "With Render", + "tags": [ + "dev", + "test", + ], + "title": "first-nested/deeply/Features", + "type": "story", + }, + "first-nested-deeply-features--with-story-fn": { + "id": "first-nested-deeply-features--with-story-fn", + "importPath": "./src/first-nested/deeply/Features.stories.jsx", + "name": "With Story Fn", + "tags": [ + "dev", + "test", + ], + "title": "first-nested/deeply/Features", + "type": "story", + }, + "first-nested-deeply-features--with-test": { + "id": "first-nested-deeply-features--with-test", + "importPath": "./src/first-nested/deeply/Features.stories.jsx", + "name": "With Test", + "tags": [ + "dev", + "test", + "play-fn", + ], + "title": "first-nested/deeply/Features", + "type": "story", + }, "h--story-one": { "id": "h--story-one", "importPath": "./src/H.stories.mjs", diff --git a/code/core/src/core-server/utils/summarizeStats.test.ts b/code/core/src/core-server/utils/summarizeStats.test.ts new file mode 100644 index 000000000000..e1e342e94260 --- /dev/null +++ b/code/core/src/core-server/utils/summarizeStats.test.ts @@ -0,0 +1,18 @@ +import { it, expect } from 'vitest'; +import { summarizeStats } from './summarizeStats'; + +it('should summarize stats', () => { + const stats = [ + { play: true, render: true, storyFn: false }, + { play: true, render: false, storyFn: false }, + { play: false, render: false, storyFn: false }, + ]; + const result = summarizeStats(stats); + expect(result).toMatchInlineSnapshot(` + { + "play": 2, + "render": 1, + "storyFn": 0, + } + `); +}); diff --git a/code/core/src/core-server/utils/summarizeStats.ts b/code/core/src/core-server/utils/summarizeStats.ts new file mode 100644 index 000000000000..bdd0ef026ce5 --- /dev/null +++ b/code/core/src/core-server/utils/summarizeStats.ts @@ -0,0 +1,18 @@ +import type { IndexInputStats } from '@storybook/core/types'; + +export type IndexStatsSummary = Record; + +export const addStats = (stat: IndexInputStats, acc: IndexStatsSummary) => { + Object.entries(stat).forEach(([key, value]) => { + const statsKey = key as keyof IndexInputStats; + if (!acc[statsKey]) acc[statsKey] = 0; + acc[statsKey] += value ? 1 : 0; + }); +}; + +export const summarizeStats = (stats: IndexInputStats[]): IndexStatsSummary => { + return stats.reduce((acc, stat) => { + addStats(stat, acc); + return acc; + }, {} as IndexStatsSummary); +}; diff --git a/code/core/src/csf-tools/CsfFile.test.ts b/code/core/src/csf-tools/CsfFile.test.ts index 4c87576d2474..0988e938a51b 100644 --- a/code/core/src/csf-tools/CsfFile.test.ts +++ b/code/core/src/csf-tools/CsfFile.test.ts @@ -2,7 +2,7 @@ import { dedent } from 'ts-dedent'; import { describe, it, expect, vi } from 'vitest'; import yaml from 'js-yaml'; -import { loadCsf } from './CsfFile'; +import { loadCsf, isModuleMock } from './CsfFile'; expect.addSnapshotSerializer({ print: (val: any) => yaml.dump(val).trimEnd(), @@ -42,11 +42,27 @@ describe('CsfFile', () => { parameters: __isArgsStory: false __id: foo-bar--a + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false - id: foo-bar--b name: B parameters: __isArgsStory: true __id: foo-bar--b + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -69,10 +85,26 @@ describe('CsfFile', () => { name: A parameters: __id: foo-bar--a + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false - id: foo-bar--b name: B parameters: __id: foo-bar--b + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false `); }); @@ -94,6 +126,14 @@ describe('CsfFile', () => { parameters: __isArgsStory: false __id: foo-bar--basic + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -116,6 +156,14 @@ describe('CsfFile', () => { stories: - id: foo-bar--a name: A + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -136,6 +184,14 @@ describe('CsfFile', () => { stories: - id: foo-bar--include-a name: Include A + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -154,6 +210,14 @@ describe('CsfFile', () => { stories: - id: foo-bar--a name: Some story + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -173,8 +237,24 @@ describe('CsfFile', () => { stories: - id: default-title--a name: A + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false - id: default-title--b name: B + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -194,8 +274,24 @@ describe('CsfFile', () => { stories: - id: custom-id--a name: A + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false - id: custom-id--b name: B + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -215,8 +311,24 @@ describe('CsfFile', () => { stories: - id: custom-meta-id--just-custom-meta-id name: Just Custom Meta Id + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false - id: custom-id name: Custom Paremeters Id + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false `); }); @@ -237,8 +349,24 @@ describe('CsfFile', () => { stories: - id: foo-bar-baz--a name: A + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false - id: foo-bar-baz--b name: B + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -263,11 +391,27 @@ describe('CsfFile', () => { parameters: __isArgsStory: true __id: foo-bar--a + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false - id: foo-bar--b name: B parameters: __isArgsStory: true __id: foo-bar--b + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false `); }); @@ -292,11 +436,27 @@ describe('CsfFile', () => { parameters: __isArgsStory: true __id: foo-bar--a + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false - id: foo-bar--b name: B parameters: __isArgsStory: true __id: foo-bar--b + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false `); }); @@ -318,8 +478,24 @@ describe('CsfFile', () => { stories: - id: foo-bar-baz--a name: A + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false - id: foo-bar-baz--b name: B + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -341,8 +517,24 @@ describe('CsfFile', () => { stories: - id: foo-bar-baz--a name: A + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false - id: foo-bar-baz--b name: B + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -362,8 +554,24 @@ describe('CsfFile', () => { stories: - id: default-title--a name: A + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false - id: default-title--b name: B + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -387,6 +595,14 @@ describe('CsfFile', () => { parameters: __isArgsStory: true __id: foo-bar--a + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -409,6 +625,14 @@ describe('CsfFile', () => { parameters: __isArgsStory: false __id: foo-bar--a + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -432,6 +656,14 @@ describe('CsfFile', () => { __isArgsStory: false __id: foo-bar--page docsOnly: true + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -460,6 +692,14 @@ describe('CsfFile', () => { __isArgsStory: false __id: foo-bar--page docsOnly: true + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -483,11 +723,27 @@ describe('CsfFile', () => { parameters: __isArgsStory: false __id: foo-bar--a + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false - id: foo-bar--b name: B parameters: __isArgsStory: true __id: foo-bar--b + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -506,8 +762,24 @@ describe('CsfFile', () => { stories: - id: foo-bar--a name: A + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false - id: foo-bar--b name: B + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false `); }); @@ -531,11 +803,27 @@ describe('CsfFile', () => { parameters: __isArgsStory: true __id: foo-bar--b + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false - id: foo-bar--a name: A parameters: __isArgsStory: false __id: foo-bar--a + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -560,6 +848,14 @@ describe('CsfFile', () => { name: A parameters: __id: foo-bar--a + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -631,8 +927,24 @@ describe('CsfFile', () => { stories: - id: default-title--a name: A + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false - id: default-title--b name: B + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); @@ -677,8 +989,24 @@ describe('CsfFile', () => { stories: - id: foo-bar--a name: A + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false - id: foo-bar--b name: B + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false `); }); }); @@ -747,6 +1075,14 @@ describe('CsfFile', () => { parameters: __isArgsStory: false __id: foo-bar--a + __stats: + play: false + render: true + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false `); }); @@ -770,6 +1106,14 @@ describe('CsfFile', () => { parameters: __isArgsStory: true __id: foo-bar--a + __stats: + play: false + render: true + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false `); }); @@ -791,6 +1135,14 @@ describe('CsfFile', () => { parameters: __isArgsStory: true __id: foo-bar--a + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false `); }); @@ -814,6 +1166,14 @@ describe('CsfFile', () => { parameters: __isArgsStory: true __id: foo-bar--a + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false `); }); @@ -886,6 +1246,14 @@ describe('CsfFile', () => { stories: - id: foo-bar--a name: A + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false tags: - 'Y' `); @@ -910,6 +1278,14 @@ describe('CsfFile', () => { stories: - id: foo-bar--a name: A + __stats: + play: false + render: true + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false tags: - 'Y' `); @@ -936,6 +1312,14 @@ describe('CsfFile', () => { stories: - id: foo-bar--a name: A + __stats: + play: false + render: true + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false tags: - 'Y' `); @@ -987,6 +1371,14 @@ describe('CsfFile', () => { stories: - id: foo-bar--a name: A + __stats: + play: true + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false tags: - 'Y' - play-fn @@ -1013,12 +1405,108 @@ describe('CsfFile', () => { stories: - id: foo-bar--a name: A + __stats: + play: true + render: true + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false tags: - 'Y' - play-fn `); }); + it('mount', () => { + expect( + parse( + dedent` + export default { title: 'foo/bar' }; + export const A = { + play: ({ mount }) => {}, + }; + ` + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + stories: + - id: foo-bar--a + name: A + __stats: + play: true + render: false + loaders: false + beforeEach: false + storyFn: false + mount: true + moduleMock: false + tags: + - play-fn + `); + }); + + it('mount renamed', () => { + expect( + parse( + dedent` + export default { title: 'foo/bar' }; + export const A = { + play: ({ mount: mountRenamed, context }) => {}, + }; + ` + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + stories: + - id: foo-bar--a + name: A + __stats: + play: true + render: false + loaders: false + beforeEach: false + storyFn: false + mount: true + moduleMock: false + tags: + - play-fn + `); + }); + + it('mount meta', () => { + expect( + parse( + dedent` + export default { + title: 'foo/bar', + play: ({ context, mount: mountRenamed }) => {}, + }; + export const A = {}; + ` + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + tags: + - play-fn + stories: + - id: foo-bar--a + name: A + __stats: + play: true + render: false + loaders: false + beforeEach: false + storyFn: false + mount: true + moduleMock: false + `); + }); + it('meta csf2', () => { expect( parse( @@ -1026,6 +1514,7 @@ describe('CsfFile', () => { export default { title: 'foo/bar', play: () => {}, tags: ['X'] }; export const A = { render: () => {}, + loaders: [], tags: ['Y'], }; ` @@ -1039,6 +1528,14 @@ describe('CsfFile', () => { stories: - id: foo-bar--a name: A + __stats: + play: true + render: true + loaders: true + beforeEach: false + storyFn: false + mount: false + moduleMock: false tags: - 'Y' `); @@ -1062,6 +1559,14 @@ describe('CsfFile', () => { stories: - id: foo-bar--a name: A + __stats: + play: true + render: false + loaders: false + beforeEach: false + storyFn: true + mount: false + moduleMock: false tags: - 'Y' `); @@ -1103,6 +1608,14 @@ describe('CsfFile', () => { - story-tag - play-fn __id: component-id--a + __stats: + play: true + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false - type: story importPath: foo/bar.stories.js exportName: B @@ -1114,6 +1627,14 @@ describe('CsfFile', () => { - story-tag - play-fn __id: component-id--b + __stats: + play: true + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false `); }); @@ -1143,6 +1664,14 @@ describe('CsfFile', () => { tags: - component-tag __id: custom-story-id + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false `); }); @@ -1177,6 +1706,14 @@ describe('CsfFile', () => { - story-tag-dup - inherit-tag-dup __id: custom-foo-title--a + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false `); }); @@ -1226,6 +1763,14 @@ describe('CsfFile', () => { title: custom foo title tags: [] __id: custom-foo-title--a + __stats: + play: false + render: true + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false `); }); @@ -1254,6 +1799,14 @@ describe('CsfFile', () => { title: custom foo title tags: [] __id: custom-foo-title--a + __stats: + play: false + render: true + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false `); }); @@ -1282,6 +1835,14 @@ describe('CsfFile', () => { title: custom foo title tags: [] __id: custom-foo-title--a + __stats: + play: false + render: true + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false `); }); @@ -1310,7 +1871,117 @@ describe('CsfFile', () => { title: custom foo title tags: [] __id: custom-foo-title--a + __stats: + play: false + render: true + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: false + `); + }); + }); + + describe('beforeEach', () => { + it('basic', () => { + expect( + parse( + dedent` + export default { title: 'foo/bar' }; + export const A = { + beforeEach: async () => {}, + }; + ` + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + stories: + - id: foo-bar--a + name: A + __stats: + play: false + render: false + loaders: false + beforeEach: true + storyFn: false + mount: false + moduleMock: false `); }); }); + + describe('module mocks', () => { + it('alias', () => { + expect( + parse( + dedent` + import foo from '#bar.mock'; + export default { title: 'foo/bar' }; + export const A = {}; + ` + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + stories: + - id: foo-bar--a + name: A + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: true + `); + }); + it('relative', () => { + expect( + parse( + dedent` + import foo from './bar.mock'; + export default { title: 'foo/bar' }; + export const A = {}; + ` + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + stories: + - id: foo-bar--a + name: A + __stats: + play: false + render: false + loaders: false + beforeEach: false + storyFn: false + mount: false + moduleMock: true + `); + }); + }); +}); + +describe('isModuleMock', () => { + it('prefix', () => { + expect(isModuleMock('#foo.mock')).toBe(true); + expect(isModuleMock('./foo.mock')).toBe(true); + expect(isModuleMock('../foo.mock')).toBe(true); + expect(isModuleMock('/foo.mock')).toBe(true); + + expect(isModuleMock('foo.mock')).toBe(false); + expect(isModuleMock('@/foo.mock')).toBe(false); + }); + it('sufixes', () => { + expect(isModuleMock('#foo.mock.js')).toBe(true); + expect(isModuleMock('#foo.mock.mjs')).toBe(true); + expect(isModuleMock('#foo.mock.vue')).toBe(true); + + expect(isModuleMock('#foo.mocktail')).toBe(false); + expect(isModuleMock('#foo.mock.test.ts')).toBe(false); + }); }); diff --git a/code/core/src/csf-tools/CsfFile.ts b/code/core/src/csf-tools/CsfFile.ts index 2ab3bfa27408..149a182bcf7d 100644 --- a/code/core/src/csf-tools/CsfFile.ts +++ b/code/core/src/csf-tools/CsfFile.ts @@ -9,12 +9,18 @@ import bt from '@babel/traverse'; import * as recast from 'recast'; import { toId, isExportStory, storyNameFromExport } from '@storybook/csf'; -import type { ComponentAnnotations, StoryAnnotations, Tag } from '@storybook/core/types'; +import type { + Tag, + StoryAnnotations, + ComponentAnnotations, + IndexedCSFFile, + IndexInput, + IndexInputStats, +} from '@storybook/core/types'; import type { Options } from 'recast'; import { babelParse } from './babelParse'; import { findVarInitialization } from './findVarInitialization'; import type { PrintResultType } from './PrintResultType'; -import type { IndexInput, IndexedCSFFile } from '@storybook/core/types'; // @ts-expect-error (needed due to it's use of `exports.default`) const traverse = (bt.default || bt) as typeof bt; @@ -54,6 +60,8 @@ const formatLocation = (node: t.Node, fileName?: string) => { return `${fileName || ''} (line ${line}, col ${column})`.trim(); }; +export const isModuleMock = (importPath: string) => MODULE_MOCK_REGEX.test(importPath); + const isArgsStory = (init: t.Node, parent: t.Node, csf: CsfFile) => { let storyFn: t.Node = init; // export const Foo = Bar.bind({}) @@ -110,6 +118,25 @@ const sortExports = (exportByName: Record, order: string[]) => { ); }; +const hasMount = (play: t.Node | undefined) => { + if (t.isArrowFunctionExpression(play) || t.isFunctionDeclaration(play)) { + const params = play.params; + if (params.length >= 1) { + const [arg] = params; + if (t.isObjectPattern(arg)) { + return !!arg.properties.find((prop) => { + if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) { + return prop.key.name === 'mount'; + } + }); + } + } + } + return false; +}; + +const MODULE_MOCK_REGEX = /^[.\/#].*\.mock($|\.[^.]*$)/i; + export interface CsfOptions { fileName?: string; makeTitle: (userTitle: string) => string; @@ -136,6 +163,7 @@ export interface StaticMeta export interface StaticStory extends Pick { id: string; + __stats: IndexInputStats; } export class CsfFile { @@ -392,6 +420,7 @@ export class CsfFile { id: 'FIXME', name, parameters, + __stats: {}, }; } }); @@ -422,7 +451,12 @@ export class CsfFile { } } else { self._storyAnnotations[exportName] = {}; - self._stories[exportName] = { id: 'FIXME', name: exportName, parameters: {} }; + self._stories[exportName] = { + id: 'FIXME', + name: exportName, + parameters: {}, + __stats: {}, + }; } } }); @@ -520,7 +554,8 @@ export class CsfFile { parameters.docsOnly = true; } acc[key] = { ...story, id, parameters }; - const { tags, play } = self._storyAnnotations[key]; + const storyAnnotations = self._storyAnnotations[key]; + const { tags, play } = storyAnnotations; if (tags) { const node = t.isIdentifier(tags) ? findVarInitialization(tags.name, this._ast.program) @@ -530,6 +565,18 @@ export class CsfFile { if (play) { acc[key].tags = [...(acc[key].tags || []), 'play-fn']; } + const stats = acc[key].__stats; + ['play', 'render', 'loaders', 'beforeEach'].forEach((annotation) => { + stats[annotation as keyof IndexInputStats] = + !!storyAnnotations[annotation] || !!self._metaAnnotations[annotation]; + }); + const storyExport = self.getStoryExport(key); + stats.storyFn = !!( + t.isArrowFunctionExpression(storyExport) || t.isFunctionDeclaration(storyExport) + ); + stats.mount = hasMount(storyAnnotations.play ?? self._metaAnnotations.play); + stats.moduleMock = !!self.imports.find((fname) => isModuleMock(fname)); + return acc; }, {} as Record @@ -589,6 +636,7 @@ export class CsfFile { metaId: this.meta?.id, tags, __id: story.id, + __stats: story.__stats, }; }); } diff --git a/code/core/src/types/modules/indexer.ts b/code/core/src/types/modules/indexer.ts index ceb3bf915e51..4e92b647a489 100644 --- a/code/core/src/types/modules/indexer.ts +++ b/code/core/src/types/modules/indexer.ts @@ -87,6 +87,16 @@ export type DocsIndexEntry = BaseIndexEntry & { export type IndexEntry = StoryIndexEntry | DocsIndexEntry; +export interface IndexInputStats { + loaders?: boolean; + play?: boolean; + render?: boolean; + storyFn?: boolean; + mount?: boolean; + beforeEach?: boolean; + moduleMock?: boolean; +} + /** * The base input for indexing a story or docs entry. */ @@ -115,6 +125,10 @@ export type BaseIndexInput = { * Only use this if you need to override the auto-generated id. */ __id?: StoryId; + /** + * Stats about language feature usage that the indexer can optionally report + */ + __stats?: IndexInputStats; }; /** diff --git a/code/core/src/types/modules/story.ts b/code/core/src/types/modules/story.ts index 69d10027bbe9..afe3edf6c6b0 100644 --- a/code/core/src/types/modules/story.ts +++ b/code/core/src/types/modules/story.ts @@ -6,6 +6,7 @@ import type { CleanupCallback, StepRunner, Canvas, + BeforeAll, } from '@storybook/csf'; import type { @@ -57,13 +58,14 @@ export type NamedOrDefaultProjectAnnotations = Omit< ProjectAnnotations, - 'decorators' | 'loaders' | 'runStep' + 'decorators' | 'loaders' | 'runStep' | 'beforeAll' > & { argTypes?: StrictArgTypes; globalTypes?: StrictGlobalTypes; decorators?: DecoratorFunction[]; loaders?: LoaderFunction[]; runStep: StepRunner; + beforeAll: BeforeAll; }; export type NormalizedComponentAnnotations = Omit< diff --git a/code/core/template/stories/rendering.stories.ts b/code/core/template/stories/rendering.stories.ts index f197d4078a77..0ca4976ec2c2 100644 --- a/code/core/template/stories/rendering.stories.ts +++ b/code/core/template/stories/rendering.stories.ts @@ -24,9 +24,6 @@ export const ForceRemount = { */ parameters: { chromatic: { disableSnapshot: true } }, play: async ({ canvasElement, id }: PlayFunctionContext) => { - if (window?.navigator.userAgent.match(/StorybookTestRunner/)) { - return; - } const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; const button = await within(canvasElement).findByRole('button'); @@ -39,6 +36,7 @@ export const ForceRemount = { // By forcing the component to remount, we reset the focus state await channel.emit(FORCE_REMOUNT, { storyId: id }); }, + tags: ['!test'], }; export const ChangeArgs = { diff --git a/code/deprecated/manager/globals.cjs b/code/deprecated/manager/globals.cjs new file mode 100644 index 000000000000..d6ee8ec2d144 --- /dev/null +++ b/code/deprecated/manager/globals.cjs @@ -0,0 +1 @@ +module.exports = require('storybook/internal/manager/globals'); diff --git a/code/deprecated/manager/globals.d.ts b/code/deprecated/manager/globals.d.ts new file mode 100644 index 000000000000..a2b581c8ca04 --- /dev/null +++ b/code/deprecated/manager/globals.d.ts @@ -0,0 +1,2 @@ +export * from 'storybook/internal/manager/globals'; +export type * from 'storybook/internal/manager/globals'; diff --git a/code/deprecated/manager/globals.js b/code/deprecated/manager/globals.js index d6ee8ec2d144..0dab1b6906f8 100644 --- a/code/deprecated/manager/globals.js +++ b/code/deprecated/manager/globals.js @@ -1 +1 @@ -module.exports = require('storybook/internal/manager/globals'); +export * from 'storybook/internal/manager/globals'; diff --git a/code/deprecated/manager/package.json b/code/deprecated/manager/package.json index de415ee731cf..779cd2f09ba3 100644 --- a/code/deprecated/manager/package.json +++ b/code/deprecated/manager/package.json @@ -20,6 +20,14 @@ }, "license": "MIT", "sideEffects": false, + "type": "module", + "exports": { + "./globals": { + "type": "./globals.d.ts", + "import": "./globals.js", + "require": "./globals.cjs" + } + }, "files": [ "README.md", "*.js", diff --git a/code/deprecated/preview/globals.cjs b/code/deprecated/preview/globals.cjs new file mode 100644 index 000000000000..91340ce6505b --- /dev/null +++ b/code/deprecated/preview/globals.cjs @@ -0,0 +1 @@ +module.exports = require('storybook/internal/preview/globals'); diff --git a/code/deprecated/preview/globals.d.ts b/code/deprecated/preview/globals.d.ts new file mode 100644 index 000000000000..165a3a44a8fb --- /dev/null +++ b/code/deprecated/preview/globals.d.ts @@ -0,0 +1,2 @@ +export * from 'storybook/internal/preview/globals'; +export type * from 'storybook/internal/preview/globals'; diff --git a/code/deprecated/preview/globals.js b/code/deprecated/preview/globals.js index 91340ce6505b..c04bc1063044 100644 --- a/code/deprecated/preview/globals.js +++ b/code/deprecated/preview/globals.js @@ -1 +1 @@ -module.exports = require('storybook/internal/preview/globals'); +export * from 'storybook/internal/preview/globals'; diff --git a/code/deprecated/preview/package.json b/code/deprecated/preview/package.json index 6f21598fc62b..c634a85c29b6 100644 --- a/code/deprecated/preview/package.json +++ b/code/deprecated/preview/package.json @@ -20,6 +20,14 @@ }, "license": "MIT", "sideEffects": false, + "type": "module", + "exports": { + "./globals": { + "type": "./globals.d.ts", + "import": "./globals.js", + "require": "./globals.cjs" + } + }, "files": [ "README.md", "*.js", diff --git a/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.test.ts b/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.test.ts index fca3ee9e37b4..cc12902b5c05 100644 --- a/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.test.ts +++ b/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.test.ts @@ -1,11 +1,314 @@ import { Component } from '@angular/core'; import { ArgTypes } from 'storybook/internal/types'; import { describe, it, expect } from 'vitest'; -import { computesTemplateSourceFromComponent } from './ComputesTemplateFromComponent'; +import { + computesTemplateFromComponent, + computesTemplateSourceFromComponent, +} from './ComputesTemplateFromComponent'; import { ISomeInterface, ButtonAccent, InputComponent } from './__testfixtures__/input.component'; +describe('angular template decorator', () => { + it('with props should generate tag with properties', () => { + const component = InputComponent; + const props = { + isDisabled: true, + label: 'Hello world', + accent: ButtonAccent.High, + counter: 4, + 'aria-label': 'Hello world', + }; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual( + `` + ); + }); + + it('with props should generate tag with outputs', () => { + const component = InputComponent; + const props = { + isDisabled: true, + label: 'Hello world', + onClick: ($event: any) => {}, + 'dash-out': ($event: any) => {}, + }; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual( + `` + ); + }); + + it('with no props should generate simple tag', () => { + const component = InputComponent; + const props = {}; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual(''); + }); + + describe('with component without selector', () => { + @Component({ + template: `The content`, + }) + class WithoutSelectorComponent {} + + it('should add component ng-container', async () => { + const component = WithoutSelectorComponent; + const props = {}; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual(``); + }); + }); + + describe('with component with attribute selector', () => { + @Component({ + selector: 'doc-button[foo]', + template: '', + }) + class WithAttributeComponent {} + + it('should add attribute to template', async () => { + const component = WithAttributeComponent; + const props = {}; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual(``); + }); + }); + + describe('with component with attribute and value selector', () => { + @Component({ + selector: 'doc-button[foo="bar"]', + template: '', + }) + class WithAttributeValueComponent {} + + it('should add attribute to template', async () => { + const component = WithAttributeValueComponent; + const props = {}; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual(``); + }); + }); + + describe('with component with attribute only selector', () => { + @Component({ + selector: '[foo]', + template: '', + }) + class WithAttributeOnlyComponent {} + + it('should create a div and add attribute to template', async () => { + const component = WithAttributeOnlyComponent; + const props = {}; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual(`
`); + }); + }); + + describe('with component with void element and attribute selector', () => { + @Component({ + selector: 'input[foo]', + template: '', + }) + class VoidElementWithAttributeComponent {} + + it('should create without separate closing tag', async () => { + const component = VoidElementWithAttributeComponent; + const props = {}; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual(``); + }); + }); + + describe('with component with attribute and value only selector', () => { + @Component({ + selector: '[foo="bar"]', + template: '', + }) + class WithAttributeOnlyComponent {} + + it('should create a div and add attribute to template', async () => { + const component = WithAttributeOnlyComponent; + const props = {}; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual(`
`); + }); + }); + + describe('with component with void element, attribute and value only selector', () => { + @Component({ + selector: 'input[foo="bar"]', + template: '', + }) + class VoidElementWithAttributeComponent {} + + it('should create and add attribute to template without separate closing tag', async () => { + const component = VoidElementWithAttributeComponent; + const props = {}; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual(``); + }); + }); + + describe('with component with class selector', () => { + @Component({ + selector: 'doc-button.foo', + template: '', + }) + class WithClassComponent {} + + it('should add class to template', async () => { + const component = WithClassComponent; + const props = {}; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual(``); + }); + }); + + describe('with component with class only selector', () => { + @Component({ + selector: '.foo', + template: '', + }) + class WithClassComponent {} + + it('should create a div and add attribute to template', async () => { + const component = WithClassComponent; + const props = {}; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual(`
`); + }); + }); + + describe('with component with multiple selectors', () => { + @Component({ + selector: 'doc-button, doc-button2', + template: '', + }) + class WithMultipleSelectorsComponent {} + + it('should use the first selector', async () => { + const component = WithMultipleSelectorsComponent; + const props = {}; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual(``); + }); + }); + + describe('with component with multiple selectors starting with attribute', () => { + @Component({ + selector: 'doc-button[foo], doc-button2', + template: '', + }) + class WithMultipleSelectorsComponent {} + + it('should use the first selector', async () => { + const component = WithMultipleSelectorsComponent; + const props = {}; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual(``); + }); + }); + + describe('with component with multiple selectors starting with attribute and value', () => { + @Component({ + selector: 'doc-button[foo="bar"], doc-button2', + template: '', + }) + class WithMultipleSelectorsComponent {} + + it('should use the first selector', async () => { + const component = WithMultipleSelectorsComponent; + const props = {}; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual(``); + }); + }); + + describe('with component with multiple selectors including 2 attributes and a class', () => { + @Component({ + selector: 'doc-button, button[foo], .button[foo], button[baz]', + template: '', + }) + class WithMultipleSelectorsComponent {} + + it('should use the first selector', async () => { + const component = WithMultipleSelectorsComponent; + const props = {}; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual(``); + }); + }); + + describe('with component with multiple selectors with line breaks', () => { + @Component({ + selector: `doc-button, + doc-button2`, + template: '', + }) + class WithMultipleSelectorsComponent {} + + it('should use the first selector', async () => { + const component = WithMultipleSelectorsComponent; + const props = {}; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual(``); + }); + }); + + describe('with component with multiple selectors starting with attribute only with line breaks', () => { + @Component({ + selector: `[foo], + doc-button2`, + template: '', + }) + class WithMultipleSelectorsComponent {} + + it('should use the first selector', async () => { + const component = WithMultipleSelectorsComponent; + const props = {}; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual(`
`); + }); + }); + + it('with props should generate tag with properties', () => { + const component = InputComponent; + const props = { + isDisabled: true, + label: 'Hello world', + accent: ButtonAccent.High, + counter: 4, + }; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual( + `` + ); + }); + + it('with props should generate tag with outputs', () => { + const component = InputComponent; + const props = { + isDisabled: true, + label: 'Hello world', + onClick: ($event: any) => {}, + }; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual( + `` + ); + }); + + it('should generate correct property for overridden name for Input', () => { + const component = InputComponent; + const props = { + color: '#ffffff', + }; + const source = computesTemplateFromComponent(component, props); + expect(source).toEqual(``); + }); +}); + describe('angular source decorator', () => { - it('With no props should generate simple tag', () => { + it('with no props should generate simple tag', () => { const component = InputComponent; const props = {}; const argTypes: ArgTypes = {}; @@ -264,32 +567,34 @@ describe('angular source decorator', () => { const source = computesTemplateSourceFromComponent(component, props, argTypes); expect(source).toEqual(``); }); - it('With props should generate tag with properties', () => { + it('with props should generate tag with properties', () => { const component = InputComponent; const props = { isDisabled: true, label: 'Hello world', accent: ButtonAccent.High, counter: 4, + 'aria-label': 'Hello world', }; const argTypes: ArgTypes = {}; const source = computesTemplateSourceFromComponent(component, props, argTypes); expect(source).toEqual( - `` + `` ); }); - it('With props should generate tag with outputs', () => { + it('with props should generate tag with outputs', () => { const component = InputComponent; const props = { isDisabled: true, label: 'Hello world', onClick: ($event: any) => {}, + 'dash-out': ($event: any) => {}, }; const argTypes: ArgTypes = {}; const source = computesTemplateSourceFromComponent(component, props, argTypes); expect(source).toEqual( - `` + `` ); }); @@ -305,7 +610,7 @@ describe('angular source decorator', () => { }); describe('with argTypes (from compodoc)', () => { - it('Should handle enum as strongly typed enum', () => { + it('should handle enum as strongly typed enum', () => { const component = InputComponent; const props = { isDisabled: false, @@ -335,7 +640,7 @@ describe('angular source decorator', () => { ); }); - it('Should handle enum without values as string', () => { + it('should handle enum without values as string', () => { const component = InputComponent; const props = { isDisabled: false, @@ -365,7 +670,7 @@ describe('angular source decorator', () => { ); }); - it('Should handle objects correctly', () => { + it('should handle simple object as stringified', () => { const component = InputComponent; const someDataObject: ISomeInterface = { @@ -400,5 +705,42 @@ describe('angular source decorator', () => { `` ); }); + + it('should handle circular object as stringified', () => { + const component = InputComponent; + + const someDataObject: ISomeInterface = { + one: 'Hello world', + two: true, + three: [ + `a string literal with "double quotes"`, + `a string literal with 'single quotes'`, + 'a single quoted string with "double quotes"', + "a double quoted string with 'single quotes'", + // eslint-disable-next-line prettier/prettier + 'a single quoted string with escaped \'single quotes\'', + // eslint-disable-next-line prettier/prettier + "a double quoted string with escaped \"double quotes\"", + + `a string literal with \'escaped single quotes\'`, + + `a string literal with \"escaped double quotes\"`, + ], + }; + someDataObject.ref = someDataObject; + + const props = { + isDisabled: false, + label: 'Hello world', + someDataObject, + }; + + const source = computesTemplateSourceFromComponent(component, props, null); + // Ideally we should stringify the object, but that could cause the story to break because of unescaped values in the JSON object. + // This will have to do for now + expect(source).toEqual( + `` + ); + }); }); }); diff --git a/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts b/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts index 7c585b8ff489..9e0d38f17986 100644 --- a/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts +++ b/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts @@ -7,6 +7,20 @@ import { getComponentInputsOutputs, } from './utils/NgComponentAnalyzer'; +/** + * Check if the name matches the criteria for a valid identifier. + * A valid identifier can only contain letters, digits, underscores, or dollar signs. + * It cannot start with a digit. + */ +const isValidIdentifier = (name: string): boolean => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name); + +/** + * Returns the property name, if it can be accessed with dot notation. If not, + * it returns `this['propertyName']`. + */ +export const formatPropInTemplate = (propertyName: string) => + isValidIdentifier(propertyName) ? propertyName : `this['${propertyName}']`; + const separateInputsOutputsAttributes = ( ngComponentInputsOutputs: ComponentInputsOutputs, props: ICollection = {} @@ -50,10 +64,12 @@ export const computesTemplateFromComponent = ( ); const templateInputs = - initialInputs.length > 0 ? ` ${initialInputs.map((i) => `[${i}]="${i}"`).join(' ')}` : ''; + initialInputs.length > 0 + ? ` ${initialInputs.map((i) => `[${i}]="${formatPropInTemplate(i)}"`).join(' ')}` + : ''; const templateOutputs = initialOutputs.length > 0 - ? ` ${initialOutputs.map((i) => `(${i})="${i}($event)"`).join(' ')}` + ? ` ${initialOutputs.map((i) => `(${i})="${formatPropInTemplate(i)}($event)"`).join(' ')}` : ''; return buildTemplate( @@ -64,6 +80,22 @@ export const computesTemplateFromComponent = ( ); }; +/** + * Stringify an object with a placholder in the circular references. + */ +function stringifyCircular(obj: any) { + const seen = new Set(); + return JSON.stringify(obj, (key, value) => { + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return '[Circular]'; + } + seen.add(value); + } + return value; + }); +} + const createAngularInputProperty = ({ propertyName, value, @@ -79,7 +111,7 @@ const createAngularInputProperty = ({ templateValue = `'${value}'`; break; case 'object': - templateValue = JSON.stringify(value) + templateValue = stringifyCircular(value) .replace(/'/g, '\u2019') .replace(/\\"/g, '\u201D') .replace(/"([^-"]+)":/g, '$1: ') @@ -137,7 +169,7 @@ export const computesTemplateSourceFromComponent = ( : ''; const templateOutputs = initialOutputs.length > 0 - ? ` ${initialOutputs.map((i) => `(${i})="${i}($event)"`).join(' ')}` + ? ` ${initialOutputs.map((i) => `(${i})="${formatPropInTemplate(i)}($event)"`).join(' ')}` : ''; return buildTemplate(ngComponentMetadata.selector, '', templateInputs, templateOutputs); diff --git a/code/frameworks/angular/src/client/angular-beta/__testfixtures__/input.component.ts b/code/frameworks/angular/src/client/angular-beta/__testfixtures__/input.component.ts index abf4205eeaf5..b0ae93c94b99 100644 --- a/code/frameworks/angular/src/client/angular-beta/__testfixtures__/input.component.ts +++ b/code/frameworks/angular/src/client/angular-beta/__testfixtures__/input.component.ts @@ -11,6 +11,7 @@ export interface ISomeInterface { one: string; two: boolean; three: any[]; + ref?: ISomeInterface; } @Component({ @@ -39,9 +40,13 @@ export class InputComponent { @Input() public label: string; + @Input('aria-label') public ariaLabel: string; + /** Specifies some arbitrary object */ @Input() public someDataObject: ISomeInterface; @Output() public onClick = new EventEmitter(); + + @Output('dash-out') public dashOut = new EventEmitter(); } diff --git a/code/frameworks/angular/src/client/argsToTemplate.test.ts b/code/frameworks/angular/src/client/argsToTemplate.test.ts index b7405aba645a..29a51acb1b9d 100644 --- a/code/frameworks/angular/src/client/argsToTemplate.test.ts +++ b/code/frameworks/angular/src/client/argsToTemplate.test.ts @@ -100,4 +100,10 @@ describe('argsToTemplate', () => { const result = argsToTemplate(args, {}); expect(result).toEqual('[input]="input" (event1)="event1($event)"'); }); + + it('should format for non dot notation', () => { + const args = { 'non-dot': 'Value1', 'dash-out': () => {} }; + const result = argsToTemplate(args, {}); + expect(result).toEqual('[non-dot]="this[\'non-dot\']" (dash-out)="this[\'dash-out\']($event)"'); + }); }); diff --git a/code/frameworks/angular/src/client/argsToTemplate.ts b/code/frameworks/angular/src/client/argsToTemplate.ts index 0072aa84743d..5b29b627029f 100644 --- a/code/frameworks/angular/src/client/argsToTemplate.ts +++ b/code/frameworks/angular/src/client/argsToTemplate.ts @@ -1,3 +1,5 @@ +import { formatPropInTemplate } from './angular-beta/ComputesTemplateFromComponent'; + /** * Options for controlling the behavior of the argsToTemplate function. * @@ -68,7 +70,9 @@ export function argsToTemplate>( return true; }) .map(([key, value]) => - typeof value === 'function' ? `(${key})="${key}($event)"` : `[${key}]="${key}"` + typeof value === 'function' + ? `(${key})="${formatPropInTemplate(key)}($event)"` + : `[${key}]="${formatPropInTemplate(key)}"` ) .join(' '); } diff --git a/code/frameworks/nextjs/template/stories/RSC.stories.jsx b/code/frameworks/nextjs/template/stories/RSC.stories.jsx index e14456b50e58..505c6a3acab4 100644 --- a/code/frameworks/nextjs/template/stories/RSC.stories.jsx +++ b/code/frameworks/nextjs/template/stories/RSC.stories.jsx @@ -9,7 +9,7 @@ export default { export const Default = {}; export const DisableRSC = { - tags: ['test-skip'], + tags: ['!test'], parameters: { chromatic: { disable: true }, nextjs: { rsc: false }, @@ -17,7 +17,7 @@ export const DisableRSC = { }; export const Error = { - tags: ['test-skip'], + tags: ['!test'], parameters: { chromatic: { disable: true }, }, diff --git a/code/frameworks/nextjs/template/stories_nextjs-default-ts/Redirect.stories.tsx b/code/frameworks/nextjs/template/stories_nextjs-default-ts/Redirect.stories.tsx index f76d8a7f5a90..05f4d5a68eab 100644 --- a/code/frameworks/nextjs/template/stories_nextjs-default-ts/Redirect.stories.tsx +++ b/code/frameworks/nextjs/template/stories_nextjs-default-ts/Redirect.stories.tsx @@ -44,6 +44,7 @@ export default { }, }, }, + tags: ['!test'], } as Meta; export const SingletonStateGetsInvalidatedAfterRedirecting: StoryObj = { diff --git a/code/frameworks/nextjs/template/stories_nextjs-default-ts/ServerActions.stories.tsx b/code/frameworks/nextjs/template/stories_nextjs-default-ts/ServerActions.stories.tsx index cb3f5bbdb2dd..24f199f89ce2 100644 --- a/code/frameworks/nextjs/template/stories_nextjs-default-ts/ServerActions.stories.tsx +++ b/code/frameworks/nextjs/template/stories_nextjs-default-ts/ServerActions.stories.tsx @@ -31,6 +31,7 @@ function Component() { export default { component: Component, + tags: ['!test'], parameters: { nextjs: { appDirectory: true, diff --git a/code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts b/code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts index a263635a44f8..c7218e328fc6 100644 --- a/code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts +++ b/code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts @@ -9,6 +9,7 @@ import { createCheckerByJson, type ComponentMeta, type MetaCheckerOptions, + type PropertyMetaSchema, } from 'vue-component-meta'; import { parseMulti } from 'vue-docgen-api'; @@ -52,21 +53,14 @@ export async function vueComponentMeta(tsconfigPath = 'tsconfig.json'): Promise< // we remove nested object schemas here since they are not used inside Storybook (we don't generate controls for object properties) // and they can cause "out of memory" issues for large/complex schemas (e.g. HTMLElement) - // it also reduced the bundle size when running "Storybook build" when such schemas are used - (['props', 'exposed'] as const).forEach((key) => { + // it also reduced the bundle size when running "storybook build" when such schemas are used + (['props', 'events', 'slots', 'exposed'] as const).forEach((key) => { meta[key].forEach((value) => { - if (typeof value.schema !== 'object') return; - - // we need to use Object.defineProperty here since schema is a getter so we can not set it directly - Object.defineProperty(value, 'schema', { - configurable: true, - enumerable: true, - value: { - kind: value.schema.kind, - type: value.schema.type, - // note that value.schema.schema is not included here (see comment above) - }, - }); + if (Array.isArray(value.schema)) { + value.schema.forEach((eventSchema) => removeNestedSchemas(eventSchema)); + } else { + removeNestedSchemas(value.schema); + } }); }); @@ -270,3 +264,12 @@ async function getTsConfigReferences(tsConfigPath: string) { return []; } } + +/** + * Removes any nested schemas from the given main schema (e.g. from a prop, event, slot or exposed). + * Useful to drastically reduce build size and prevent out of memory issues when large schemas (e.g. HTMLElement, MouseEvent) are used. + */ +function removeNestedSchemas(schema: PropertyMetaSchema) { + if (typeof schema !== 'object') return; + delete schema.schema; +} diff --git a/code/lib/blocks/package.json b/code/lib/blocks/package.json index c02019bc00a2..0acb8450e13b 100644 --- a/code/lib/blocks/package.json +++ b/code/lib/blocks/package.json @@ -44,7 +44,7 @@ "prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/bundle.ts" }, "dependencies": { - "@storybook/csf": "0.1.11", + "@storybook/csf": "^0.1.11", "@storybook/global": "^5.0.0", "@storybook/icons": "^1.2.5", "@types/lodash": "^4.14.167", diff --git a/code/lib/blocks/src/blocks/ArgTypes.stories.tsx b/code/lib/blocks/src/blocks/ArgTypes.stories.tsx index 509497af2be1..39d4a1c66b9c 100644 --- a/code/lib/blocks/src/blocks/ArgTypes.stories.tsx +++ b/code/lib/blocks/src/blocks/ArgTypes.stories.tsx @@ -49,7 +49,7 @@ export const OfUndefined: Story = { of: ExampleStories.NotDefined, }, parameters: { chromatic: { disableSnapshot: true } }, - decorators: [(s) => (window?.navigator.userAgent.match(/StorybookTestRunner/) ?
: s())], + tags: ['!test'], }; export const OfStoryUnattached: Story = { diff --git a/code/lib/blocks/src/blocks/Canvas.stories.tsx b/code/lib/blocks/src/blocks/Canvas.stories.tsx index bc5f889b995a..ac32440ba608 100644 --- a/code/lib/blocks/src/blocks/Canvas.stories.tsx +++ b/code/lib/blocks/src/blocks/Canvas.stories.tsx @@ -61,7 +61,7 @@ export const OfUndefined: Story = { of: ButtonStories.NotDefined, }, parameters: { chromatic: { disableSnapshot: true } }, - decorators: [(s) => (window?.navigator.userAgent.match(/StorybookTestRunner/) ?
: s())], + tags: ['!test'], }; export const PropWithToolbar: Story = { diff --git a/code/lib/blocks/src/blocks/Controls.stories.tsx b/code/lib/blocks/src/blocks/Controls.stories.tsx index 598485dd93a6..b004b08bd614 100644 --- a/code/lib/blocks/src/blocks/Controls.stories.tsx +++ b/code/lib/blocks/src/blocks/Controls.stories.tsx @@ -46,7 +46,7 @@ export const OfUndefined: Story = { of: ExampleStories.NotDefined, }, parameters: { chromatic: { disableSnapshot: true } }, - decorators: [(s) => (window?.navigator.userAgent.match(/StorybookTestRunner/) ?
: s())], + tags: ['!test'], }; export const IncludeProp: Story = { diff --git a/code/lib/blocks/src/blocks/Description.stories.tsx b/code/lib/blocks/src/blocks/Description.stories.tsx index a00a22f7a0b5..d57d808ff554 100644 --- a/code/lib/blocks/src/blocks/Description.stories.tsx +++ b/code/lib/blocks/src/blocks/Description.stories.tsx @@ -121,7 +121,7 @@ export const OfUndefinedAttached: Story = { relativeCsfPaths: ['../examples/Button.stories'], attached: true, }, - decorators: [(s) => (window?.navigator.userAgent.match(/StorybookTestRunner/) ?
: s())], + tags: ['!test'], }; export const OfStringComponentAttached: Story = { name: 'Of "component" Attached', diff --git a/code/lib/blocks/src/blocks/Source.stories.tsx b/code/lib/blocks/src/blocks/Source.stories.tsx index 4fffbbd0a9e0..70d85b9d40bd 100644 --- a/code/lib/blocks/src/blocks/Source.stories.tsx +++ b/code/lib/blocks/src/blocks/Source.stories.tsx @@ -66,7 +66,7 @@ export const OfUndefined: Story = { of: ParametersStories.NotDefined, }, parameters: { chromatic: { disableSnapshot: true } }, - decorators: [(s) => (window?.navigator.userAgent.match(/StorybookTestRunner/) ?
: s())], + tags: ['!test'], }; export const OfTypeProp: Story = { diff --git a/code/lib/blocks/src/blocks/Story.stories.tsx b/code/lib/blocks/src/blocks/Story.stories.tsx index 6dcd2bfa9fb7..27d0770395a2 100644 --- a/code/lib/blocks/src/blocks/Story.stories.tsx +++ b/code/lib/blocks/src/blocks/Story.stories.tsx @@ -54,7 +54,7 @@ export const OfUndefined: Story = { of: ButtonStories.NotDefined, }, parameters: { chromatic: { disableSnapshot: true } }, - decorators: [(s) => (window?.navigator.userAgent.match(/StorybookTestRunner/) ?
: s())], + tags: ['!test'], }; export const Inline: Story = { diff --git a/code/lib/blocks/src/blocks/Subtitle.stories.tsx b/code/lib/blocks/src/blocks/Subtitle.stories.tsx index 6ee1e865baba..77accb6d447a 100644 --- a/code/lib/blocks/src/blocks/Subtitle.stories.tsx +++ b/code/lib/blocks/src/blocks/Subtitle.stories.tsx @@ -89,7 +89,7 @@ export const OfUndefinedAttached: Story = { relativeCsfPaths: ['../examples/Button.stories'], attached: true, }, - decorators: [(s) => (window?.navigator.userAgent.match(/StorybookTestRunner/) ?
: s())], + tags: ['!test'], }; export const OfStringMetaAttached: Story = { name: 'Of "meta" Attached', diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json index fc3a27cb61de..5c14d632cd36 100644 --- a/code/lib/codemod/package.json +++ b/code/lib/codemod/package.json @@ -58,7 +58,7 @@ "@babel/preset-env": "^7.24.4", "@babel/types": "^7.24.0", "@storybook/core": "workspace:*", - "@storybook/csf": "0.1.11", + "@storybook/csf": "^0.1.11", "@types/cross-spawn": "^6.0.2", "cross-spawn": "^7.0.3", "globby": "^14.0.1", diff --git a/code/lib/react-dom-shim/src/react-16.tsx b/code/lib/react-dom-shim/src/react-16.tsx index a519dbd51bb6..8c7b2c8f5a67 100644 --- a/code/lib/react-dom-shim/src/react-16.tsx +++ b/code/lib/react-dom-shim/src/react-16.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/no-deprecated */ import type { ReactElement } from 'react'; -import ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom'; export const renderElement = async (node: ReactElement, el: Element) => { return new Promise((resolve) => { diff --git a/code/lib/react-dom-shim/src/react-18.tsx b/code/lib/react-dom-shim/src/react-18.tsx index 5b9e88f98a84..254fdfbdab7e 100644 --- a/code/lib/react-dom-shim/src/react-18.tsx +++ b/code/lib/react-dom-shim/src/react-18.tsx @@ -1,7 +1,7 @@ import type { FC, ReactElement } from 'react'; -import React, { useLayoutEffect, useRef } from 'react'; +import * as React from 'react'; import type { Root as ReactRoot, RootOptions } from 'react-dom/client'; -import ReactDOM from 'react-dom/client'; +import * as ReactDOM from 'react-dom/client'; // A map of all rendered React 18 nodes const nodes = new Map(); @@ -11,8 +11,8 @@ const WithCallback: FC<{ callback: () => void; children: ReactElement }> = ({ children, }) => { // See https://github.com/reactwg/react-18/discussions/5#discussioncomment-2276079 - const once = useRef<() => void>(); - useLayoutEffect(() => { + const once = React.useRef<() => void>(); + React.useLayoutEffect(() => { if (once.current === callback) return; once.current = callback; callback(); diff --git a/code/lib/source-loader/package.json b/code/lib/source-loader/package.json index b4c0a48bef18..5c2963555975 100644 --- a/code/lib/source-loader/package.json +++ b/code/lib/source-loader/package.json @@ -45,7 +45,7 @@ "prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/bundle.ts" }, "dependencies": { - "@storybook/csf": "0.1.11", + "@storybook/csf": "^0.1.11", "estraverse": "^5.2.0", "lodash": "^4.17.21", "prettier": "^3.1.1" diff --git a/code/lib/test/package.json b/code/lib/test/package.json index dee867c3a356..8fbcf1ba574f 100644 --- a/code/lib/test/package.json +++ b/code/lib/test/package.json @@ -44,7 +44,7 @@ "prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/bundle.ts" }, "dependencies": { - "@storybook/csf": "0.1.11", + "@storybook/csf": "^0.1.11", "@storybook/instrumenter": "workspace:*", "@testing-library/dom": "10.1.0", "@testing-library/jest-dom": "6.4.5", diff --git a/code/lib/test/template/stories/loader-enhancements.stories.ts b/code/lib/test/template/stories/loader-enhancements.stories.ts index 997b40e12644..b759a73b56c1 100644 --- a/code/lib/test/template/stories/loader-enhancements.stories.ts +++ b/code/lib/test/template/stories/loader-enhancements.stories.ts @@ -12,7 +12,7 @@ export default meta; export const canvas_is_equal_to_within_canvas_element = { async play({ canvas, canvasElement }) { const oldCanvas = within(canvasElement); - await expect(canvas satisfies typeof oldCanvas).toEqual(oldCanvas); + await expect(Object.keys(canvas)).toEqual(Object.keys(oldCanvas)); }, }; diff --git a/code/package.json b/code/package.json index 526bfacf36ac..4b49218f50f1 100644 --- a/code/package.json +++ b/code/package.json @@ -118,7 +118,7 @@ "@storybook/codemod": "workspace:*", "@storybook/core": "workspace:*", "@storybook/core-webpack": "workspace:*", - "@storybook/csf": "0.1.11", + "@storybook/csf": "^0.1.11", "@storybook/csf-plugin": "workspace:*", "@storybook/ember": "workspace:*", "@storybook/eslint-config-storybook": "^4.0.0", @@ -206,7 +206,7 @@ "typescript": "^5.4.3", "util": "^0.12.4", "vite": "^4.0.0", - "vitest": "^1.2.2", + "vitest": "^1.6.0", "wait-on": "^7.0.1" }, "dependenciesMeta": { @@ -278,5 +278,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.3.0-alpha.3" } diff --git a/code/renderers/react/src/mount.ts b/code/renderers/react/src/mount.ts index 13db1f03a0d8..d27e66809a5e 100644 --- a/code/renderers/react/src/mount.ts +++ b/code/renderers/react/src/mount.ts @@ -1,5 +1,5 @@ import { type StoryContext, type ReactRenderer } from './public-types'; -import type { BaseAnnotations } from '@storybook/types'; +import type { BaseAnnotations } from 'storybook/internal/types'; export const mount: BaseAnnotations['mount'] = (context: StoryContext) => async (ui) => { diff --git a/code/renderers/react/src/renderToCanvas.tsx b/code/renderers/react/src/renderToCanvas.tsx index 909f1613d598..e33d61fd984e 100644 --- a/code/renderers/react/src/renderToCanvas.tsx +++ b/code/renderers/react/src/renderToCanvas.tsx @@ -1,7 +1,6 @@ import { global } from '@storybook/global'; import type { FC } from 'react'; import React, { Component as ReactComponent, StrictMode, Fragment } from 'react'; -import { renderElement, unmountElement } from '@storybook/react-dom-shim'; import type { RenderContext } from 'storybook/internal/types'; @@ -54,6 +53,7 @@ export async function renderToCanvas( }: RenderContext, canvasElement: ReactRenderer['canvasElement'] ) { + const { renderElement, unmountElement } = await import('@storybook/react-dom-shim'); const Story = unboundStoryFn as FC>; const content = ( diff --git a/code/renderers/react/template/stories/csf1.stories.tsx b/code/renderers/react/template/stories/csf1.stories.tsx new file mode 100644 index 000000000000..63a53672b396 --- /dev/null +++ b/code/renderers/react/template/stories/csf1.stories.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export default { + component: {}, + parameters: { + chromatic: { disable: true }, + }, +}; + +export const Hello1 = () =>
Hello1
; +export const Hello2 = () =>
Hello2
; diff --git a/code/renderers/react/template/stories/csf2.stories.tsx b/code/renderers/react/template/stories/csf2.stories.tsx new file mode 100644 index 000000000000..6b78e54e36e2 --- /dev/null +++ b/code/renderers/react/template/stories/csf2.stories.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +export default { + component: {}, + parameters: { + chromatic: { disable: true }, + }, +}; + +const Template = ({ label }: { label: string }) =>
{label}
; +Template.args = { label: 'Hello' }; + +export const Hello1 = Template.bind({}); + +export const Hello2 = Template.bind({}); + +export const Hello3 = Template.bind({}); diff --git a/code/renderers/react/template/stories/errors.stories.tsx b/code/renderers/react/template/stories/errors.stories.tsx index cdbe1f5df662..8db00f803d68 100644 --- a/code/renderers/react/template/stories/errors.stories.tsx +++ b/code/renderers/react/template/stories/errors.stories.tsx @@ -8,10 +8,7 @@ export default { parameters: { chromatic: { disable: true }, }, - decorators: [ - // Skip errors if we are running in the test runner - (storyFn: any) => window?.navigator?.userAgent?.match(/StorybookTestRunner/) || storyFn(), - ], + tags: ['!test'], }; export const RenderThrows = { diff --git a/code/renderers/server/package.json b/code/renderers/server/package.json index 1039c32b46af..3646540c0d07 100644 --- a/code/renderers/server/package.json +++ b/code/renderers/server/package.json @@ -47,7 +47,7 @@ }, "dependencies": { "@storybook/components": "workspace:^", - "@storybook/csf": "0.1.11", + "@storybook/csf": "^0.1.11", "@storybook/global": "^5.0.0", "@storybook/manager-api": "workspace:^", "@storybook/preview-api": "workspace:^", diff --git a/code/renderers/svelte/src/mount.ts b/code/renderers/svelte/src/mount.ts index 5730f4cce403..50d17f811812 100644 --- a/code/renderers/svelte/src/mount.ts +++ b/code/renderers/svelte/src/mount.ts @@ -1,5 +1,5 @@ import { type StoryContext, type SvelteRenderer } from './public-types'; -import { type BaseAnnotations } from '@storybook/types'; +import { type BaseAnnotations } from 'storybook/internal/types'; export const mount: BaseAnnotations['mount'] = (context: StoryContext) => { return async (Component, options) => { diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index d695d7979c37..4f92bcd9cb27 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -1,304 +1,250 @@ -import { describe, expect, it } from 'vitest'; - +import { expect, test } from 'vitest'; +import { h } from 'vue'; +import type { SourceCodeGeneratorContext } from './sourceDecorator'; import { - mapAttributesAndDirectives, - generateAttributesSource, - attributeSource, - htmlEventAttributeToVueEventAttribute as htmlEventToVueEvent, + generatePropsSourceCode, + generateSlotSourceCode, + generateSourceCode, + getFunctionParamNames, + parseDocgenInfo, } from './sourceDecorator'; -expect.addSnapshotSerializer({ - print: (val: any) => val, - test: (val: unknown) => typeof val === 'string', -}); +test('should generate source code for props', () => { + const ctx: SourceCodeGeneratorContext = { + scriptVariables: {}, + imports: {}, + }; -describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { - it('camelCase boolean Arg', () => { - expect(mapAttributesAndDirectives({ camelCaseBooleanArg: true })).toMatchInlineSnapshot(` - [ - { - arg: { - content: camel-case-boolean-arg, - loc: { - source: camel-case-boolean-arg, - }, - }, - exp: { - isStatic: false, - loc: { - source: true, - }, - }, - loc: { - source: :camel-case-boolean-arg="true", - }, - modifiers: [ - , - ], - name: bind, - type: 6, - }, - ] - `); - }); - it('camelCase string Arg', () => { - expect(mapAttributesAndDirectives({ camelCaseStringArg: 'foo' })).toMatchInlineSnapshot(` - [ - { - arg: { - content: camel-case-string-arg, - loc: { - source: camel-case-string-arg, - }, - }, - exp: { - isStatic: false, - loc: { - source: foo, - }, - }, - loc: { - source: camel-case-string-arg="foo", - }, - modifiers: [ - , - ], - name: bind, - type: 6, - }, - ] - `); - }); - it('boolean arg', () => { - expect(mapAttributesAndDirectives({ booleanarg: true })).toMatchInlineSnapshot(` - [ - { - arg: { - content: booleanarg, - loc: { - source: booleanarg, - }, - }, - exp: { - isStatic: false, - loc: { - source: true, - }, - }, - loc: { - source: :booleanarg="true", - }, - modifiers: [ - , - ], - name: bind, - type: 6, - }, - ] - `); - }); - it('string arg', () => { - expect(mapAttributesAndDirectives({ stringarg: 'bar' })).toMatchInlineSnapshot(` - [ - { - arg: { - content: stringarg, - loc: { - source: stringarg, - }, - }, - exp: { - isStatic: false, - loc: { - source: bar, - }, - }, - loc: { - source: stringarg="bar", - }, - modifiers: [ - , - ], - name: bind, - type: 6, - }, - ] - `); - }); - it('number arg', () => { - expect(mapAttributesAndDirectives({ numberarg: 2023 })).toMatchInlineSnapshot(` - [ - { - arg: { - content: numberarg, - loc: { - source: numberarg, - }, - }, - exp: { - isStatic: false, - loc: { - source: 2023, - }, - }, - loc: { - source: :numberarg="2023", - }, - modifiers: [ - , - ], - name: bind, - type: 6, - }, - ] - `); - }); - it('camelCase boolean, string, and number Args', () => { - expect( - mapAttributesAndDirectives({ - camelCaseBooleanArg: true, - camelCaseStringArg: 'foo', - cameCaseNumberArg: 2023, - }) - ).toMatchInlineSnapshot(` - [ - { - arg: { - content: camel-case-boolean-arg, - loc: { - source: camel-case-boolean-arg, - }, - }, - exp: { - isStatic: false, - loc: { - source: true, - }, - }, - loc: { - source: :camel-case-boolean-arg="true", - }, - modifiers: [ - , - ], - name: bind, - type: 6, - }, - { - arg: { - content: camel-case-string-arg, - loc: { - source: camel-case-string-arg, - }, - }, - exp: { - isStatic: false, - loc: { - source: foo, - }, - }, - loc: { - source: camel-case-string-arg="foo", - }, - modifiers: [ - , - ], - name: bind, - type: 6, - }, - { - arg: { - content: came-case-number-arg, - loc: { - source: came-case-number-arg, - }, - }, - exp: { - isStatic: false, - loc: { - source: 2023, - }, - }, - loc: { - source: :came-case-number-arg="2023", - }, - modifiers: [ - , - ], - name: bind, - type: 6, - }, - ] - `); + const code = generatePropsSourceCode( + { + a: 'foo', + b: '"I am double quoted"', + c: 42, + d: true, + e: false, + f: [1, 2, 3], + g: { + g1: 'foo', + g2: 42, + }, + h: undefined, + i: null, + j: '', + k: BigInt(9007199254740991), + l: Symbol(), + m: Symbol('foo'), + modelValue: 'test-v-model', + otherModelValue: 42, + default: 'default slot', + testSlot: 'test slot', + }, + ['default', 'testSlot'], + ['update:modelValue', 'update:otherModelValue'], + ctx + ); + + expect(code).toBe( + `a="foo" b='"I am double quoted"' :c="42" d :e="false" :f="f" :g="g" :k="BigInt(9007199254740991)" :l="Symbol()" :m="Symbol('foo')" v-model="modelValue" v-model:otherModelValue="otherModelValue"` + ); + + expect(ctx.scriptVariables).toStrictEqual({ + f: `[1,2,3]`, + g: `{"g1":"foo","g2":42}`, + modelValue: 'ref("test-v-model")', + otherModelValue: 'ref(42)', }); + + expect(Array.from(ctx.imports.vue.values())).toStrictEqual(['ref']); }); -describe('Vue3: sourceDecorator->generateAttributesSource()', () => { - it('camelCase boolean Arg', () => { - expect( - generateAttributesSource( - mapAttributesAndDirectives({ camelCaseBooleanArg: true }), - { camelCaseBooleanArg: true }, - [{ camelCaseBooleanArg: { type: 'boolean' } }] as any - ) - ).toMatchInlineSnapshot(`:camel-case-boolean-arg="true"`); - }); - it('camelCase string Arg', () => { - expect( - generateAttributesSource( - mapAttributesAndDirectives({ camelCaseStringArg: 'foo' }), - { camelCaseStringArg: 'foo' }, - [{ camelCaseStringArg: { type: 'string' } }] as any - ) - ).toMatchInlineSnapshot(`camel-case-string-arg="foo"`); +test('should generate source code for slots', () => { + // slot code generator should support primitive values (string, number etc.) + // but also VNodes (e.g. created using h()) so custom Vue components can also be used + // inside slots with proper generated code + + const slots = { + default: 'default content', + a: 'a content', + b: 42, + c: true, + // single VNode without props + d: h('div', 'd content'), + // VNode with props and single child + e: h('div', { style: 'color:red' }, 'e content'), + // VNode with props and single child returned as getter + f: h('div', { style: 'color:red' }, () => 'f content'), + // VNode with multiple children + g: h('div', { style: 'color:red' }, [ + 'child 1', + h('span', { style: 'color:green' }, 'child 2'), + ]), + // VNode multiple children but returned as getter + h: h('div', { style: 'color:red' }, () => [ + 'child 1', + h('span', { style: 'color:green' }, 'child 2'), + ]), + // VNode with multiple and nested children + i: h('div', { style: 'color:red' }, [ + 'child 1', + h('span', { style: 'color:green' }, ['nested child 1', h('p', 'nested child 2')]), + ]), + j: ['child 1', 'child 2'], + k: null, + l: { foo: 'bar' }, + m: BigInt(9007199254740991), + }; + + const expectedCode = `default content + + + + + + + + + + + + + + + + + + + + + + + +`; + + let actualCode = generateSlotSourceCode(slots, Object.keys(slots), { + scriptVariables: {}, + imports: {}, }); + expect(actualCode).toBe(expectedCode); + + // should generate the same code if getters/functions are used to return the slot content + const slotsWithGetters = Object.entries(slots).reduce< + Record (typeof slots)[keyof typeof slots]> + >((obj, [slotName, value]) => { + obj[slotName] = () => value; + return obj; + }, {}); - it('camelCase boolean, string, and number Args', () => { - expect( - generateAttributesSource( - mapAttributesAndDirectives({ - camelCaseBooleanArg: true, - camelCaseStringArg: 'foo', - cameCaseNumberArg: 2023, - }), - { - camelCaseBooleanArg: true, - camelCaseStringArg: 'foo', - cameCaseNumberArg: 2023, - }, - [] as any - ) - ).toMatchInlineSnapshot( - `:camel-case-boolean-arg="true" camel-case-string-arg="foo" :came-case-number-arg="2023"` - ); + actualCode = generateSlotSourceCode(slotsWithGetters, Object.keys(slotsWithGetters), { + scriptVariables: {}, + imports: {}, }); + expect(actualCode).toBe(expectedCode); }); -describe('Vue3: sourceDecorator->attributeSoure()', () => { - it('camelCase boolean Arg', () => { - expect(attributeSource('stringArg', 'foo')).toMatchInlineSnapshot(`stringArg="foo"`); - }); +test('should generate source code for slots with bindings', () => { + type TestBindings = { + foo: string; + bar?: number; + }; - it('html event attribute should convert to vue event directive', () => { - expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); - expect(attributeSource('onclick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); - }); - it('normal html attribute should not convert to vue event directive', () => { - expect(attributeSource('on-click', () => {})).toMatchInlineSnapshot(`on-click='()=>({})'`); - }); - it('The value undefined or empty string must not be returned.', () => { - expect(attributeSource('icon', undefined)).toMatchInlineSnapshot(`icon=""`); - expect(attributeSource('icon', '')).toMatchInlineSnapshot(`icon=""`); + const slots = { + a: ({ foo, bar }: TestBindings) => `Slot with bindings ${foo} and ${bar}`, + b: ({ foo }: TestBindings) => h('a', { href: foo, target: foo }, `Test link: ${foo}`), + }; + + const expectedCode = ` + +`; + + const actualCode = generateSlotSourceCode(slots, Object.keys(slots), { + imports: {}, + scriptVariables: {}, }); - it('htmlEventAttributeToVueEventAttribute onEv => v-on:', () => { - const htmlEventAttributeToVueEventAttribute = (attribute: string) => { - return htmlEventToVueEvent(attribute); - }; - expect(/^on[A-Za-z]/.test('onClick')).toBeTruthy(); - expect(htmlEventAttributeToVueEventAttribute('onclick')).toMatchInlineSnapshot(`v-on:click`); - expect(htmlEventAttributeToVueEventAttribute('onClick')).toMatchInlineSnapshot(`v-on:click`); - expect(htmlEventAttributeToVueEventAttribute('onChange')).toMatchInlineSnapshot(`v-on:change`); - expect(htmlEventAttributeToVueEventAttribute('onFocus')).toMatchInlineSnapshot(`v-on:focus`); - expect(htmlEventAttributeToVueEventAttribute('on-focus')).toMatchInlineSnapshot(`on-focus`); + expect(actualCode).toBe(expectedCode); +}); + +test('should generate source code with + +`); +}); + +test.each([ + { __docgenInfo: 'invalid-value', slotNames: [] }, + { __docgenInfo: {}, slotNames: [] }, + { __docgenInfo: { slots: 'invalid-value' }, slotNames: [] }, + { __docgenInfo: { slots: ['invalid-value'] }, slotNames: [] }, + { + __docgenInfo: { slots: [{ name: 'slot-1' }, { name: 'slot-2' }, { notName: 'slot-3' }] }, + slotNames: ['slot-1', 'slot-2'], + }, +])('should parse slots names from __docgenInfo', ({ __docgenInfo, slotNames }) => { + const docgenInfo = parseDocgenInfo({ __docgenInfo }); + expect(docgenInfo.slotNames).toStrictEqual(slotNames); +}); + +test.each([ + { __docgenInfo: 'invalid-value', eventNames: [] }, + { __docgenInfo: {}, eventNames: [] }, + { __docgenInfo: { events: 'invalid-value' }, eventNames: [] }, + { __docgenInfo: { events: ['invalid-value'] }, eventNames: [] }, + { + __docgenInfo: { events: [{ name: 'event-1' }, { name: 'event-2' }, { notName: 'event-3' }] }, + eventNames: ['event-1', 'event-2'], + }, +])('should parse event names from __docgenInfo', ({ __docgenInfo, eventNames }) => { + const docgenInfo = parseDocgenInfo({ __docgenInfo }); + expect(docgenInfo.eventNames).toStrictEqual(eventNames); +}); + +test.each<{ fn: (...args: any[]) => unknown; expectedNames: string[] }>([ + { fn: () => ({}), expectedNames: [] }, + { fn: (a) => ({}), expectedNames: ['a'] }, + { fn: (a, b) => ({}), expectedNames: ['a', 'b'] }, + { fn: (a, b, { c }) => ({}), expectedNames: ['a', 'b', '{', 'c', '}'] }, + { fn: ({ a, b }) => ({}), expectedNames: ['{', 'a', 'b', '}'] }, + { + fn: { + // simulate minified function after running "storybook build" + toString: () => '({a:foo,b:bar})=>({})', + } as (...args: any[]) => unknown, + expectedNames: ['{', 'a', 'b', '}'], + }, +])('should extract function parameter names', ({ fn, expectedNames }) => { + const paramNames = getFunctionParamNames(fn); + expect(paramNames).toStrictEqual(expectedNames); }); diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 28277a23b95b..764aacf91fe7 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -1,46 +1,119 @@ /* eslint-disable no-underscore-dangle */ +import { SNIPPET_RENDERED, SourceType } from 'storybook/internal/docs-tools'; import { addons } from 'storybook/internal/preview-api'; -import type { ArgTypes, Args, StoryContext } from 'storybook/internal/types'; - -import { SourceType, SNIPPET_RENDERED } from 'storybook/internal/docs-tools'; - -import type { - ElementNode, - AttributeNode, - DirectiveNode, - TextNode, - InterpolationNode, - TemplateChildNode, -} from '@vue/compiler-core'; -import { baseParse } from '@vue/compiler-core'; -import type { ConcreteComponent, FunctionalComponent, VNode } from 'vue'; -import { h, isVNode, watch } from 'vue'; -import kebabCase from 'lodash/kebabCase'; -import { - attributeSource, - htmlEventAttributeToVueEventAttribute, - omitEvent, - evalExp, - replaceValueWithRef, - generateExpression, -} from './utils'; -import type { VueRenderer } from '../types'; +import type { VNode } from 'vue'; +import { isVNode, watch } from 'vue'; +import type { Args, Decorator, StoryContext } from '../public-types'; /** - * Check if the sourcecode should be generated. - * - * @param context StoryContext + * Context that is passed down to nested components/slots when generating the source code for a single story. */ -const skipSourceRender = (context: StoryContext) => { - const sourceParams = context?.parameters.docs?.source; - const isArgsStory = context?.parameters.__isArgsStory; - const isDocsViewMode = context?.viewMode === 'docs'; +export type SourceCodeGeneratorContext = { + /** + * Properties/variables that should be placed inside a ` + +${template}`; +}; + +/** + * Checks if the source code generation should be skipped for the given Story context. + * Will be true if one of the following is true: + * - view mode is not "docs" + * - story is no arg story + * - story has set custom source code via parameters.docs.source.code + * - story has set source type to "code" via parameters.docs.source.type + */ +export const shouldSkipSourceCodeGeneration = (context: StoryContext): boolean => { + const sourceParams = context?.parameters.docs?.source; if (sourceParams?.type === SourceType.DYNAMIC) { + // always render if the user forces it return false; } + const isArgsStory = context?.parameters.__isArgsStory; + const isDocsViewMode = context?.viewMode === 'docs'; + // never render if the user is forcing the block to render code, or // if the user provides code, or if it's not an args story. return ( @@ -49,271 +122,441 @@ const skipSourceRender = (context: StoryContext) => { }; /** - * - * @param _args - * @param argTypes - * @param byRef - */ -export function generateAttributesSource( - tempArgs: (AttributeNode | DirectiveNode)[], - args: Args, - argTypes: ArgTypes, - byRef?: boolean -): string { - return Object.keys(tempArgs) - .map((key: any) => { - const source = tempArgs[key].loc.source.replace(/\$props/g, 'args'); - const argKey = (tempArgs[key] as DirectiveNode).arg?.loc.source; - return byRef && argKey - ? replaceValueWithRef(source, args, argKey) - : evalExp(source, omitEvent(args)); - }) - .join(' '); -} -/** - * map attributes and directives - * @param props + * Parses the __docgenInfo of the given component. + * Requires Storybook docs addon to be enabled. + * Default slot will always be sorted first, remaining slots are sorted alphabetically. */ -function mapAttributesAndDirectives(props: Args) { - const tranformKey = (key: string) => (key.startsWith('on') ? key : kebabCase(key)); - return Object.keys(props).map( - (key) => - ({ - name: 'bind', - type: ['v-', '@', 'v-on'].includes(key) ? 7 : 6, // 6 is attribute, 7 is directive - arg: { content: tranformKey(key), loc: { source: tranformKey(key) } }, // attribute name or directive name (v-bind, v-on, v-model) - loc: { source: attributeSource(tranformKey(key), props[key]) }, // attribute value or directive value - exp: { isStatic: false, loc: { source: props[key] } }, // directive expression - modifiers: [''], - }) as unknown as AttributeNode - ); -} +export const parseDocgenInfo = ( + component?: StoryContext['component'] & { __docgenInfo?: unknown } +) => { + // type check __docgenInfo to prevent errors + if ( + !component || + !('__docgenInfo' in component) || + !component.__docgenInfo || + typeof component.__docgenInfo !== 'object' + ) { + return { + displayName: component?.__name, + eventNames: [], + slotNames: [], + }; + } + + const docgenInfo = component.__docgenInfo as Record; + + const displayName = + 'displayName' in docgenInfo && typeof docgenInfo.displayName === 'string' + ? docgenInfo.displayName + : undefined; + + const parseNames = (key: 'slots' | 'events') => { + if (!(key in docgenInfo) || !Array.isArray(docgenInfo[key])) return []; + + const values = docgenInfo[key] as unknown[]; + + return values + .map((i) => (i && typeof i === 'object' && 'name' in i ? i.name : undefined)) + .filter((i): i is string => typeof i === 'string'); + }; + + return { + displayName: displayName || component.__name, + slotNames: parseNames('slots').sort((a, b) => { + if (a === 'default') return -1; + if (b === 'default') return 1; + return a.localeCompare(b); + }), + eventNames: parseNames('events'), + }; +}; + /** - * map slots - * @param slotsArgs + * Generates the source code for the given Vue component properties. + * Props with complex values (objects and arrays) and v-models will be added to the ctx.scriptVariables because they should be + * generated in a ``; -} /** - * get template components one or more - * @param renderFn + * Generates the source code for the given slot children (the code inside ). */ -function getTemplateComponents( - renderFn: any, - context?: StoryContext -): (TemplateChildNode | VNode)[] { - try { - const originalStoryFn = renderFn; - - const storyFn = originalStoryFn ? originalStoryFn(context?.args, context) : context?.component; - const story = typeof storyFn === 'function' ? storyFn() : storyFn; - - const { template } = story; - - if (!template) return [h(story, context?.args)]; - return getComponents(template); - } catch (e) { - return []; - } -} +const generateSlotChildrenSourceCode = ( + children: unknown[], + ctx: SourceCodeGeneratorContext +): string => { + const slotChildrenSourceCodes: string[] = []; + + /** + * Recursively generates the source code for a single slot child and all its children. + * @returns Source code for child and all nested children or empty string if child is of a non-supported type. + */ + const generateSingleChildSourceCode = (child: unknown): string => { + if (isVNode(child)) { + return generateVNodeSourceCode(child, ctx); + } + + switch (typeof child) { + case 'string': + case 'number': + case 'boolean': + return child.toString(); + + case 'object': + if (child === null) return ''; + if (Array.isArray(child)) { + // if child also has children, we generate them recursively + return child + .map(generateSingleChildSourceCode) + .filter((code) => code !== '') + .join('\n'); + } + return JSON.stringify(child); + + case 'function': { + const paramNames = getFunctionParamNames(child).filter( + (param) => !['{', '}'].includes(param) + ); + + const parameters = paramNames.reduce>((obj, param) => { + obj[param] = `{{ ${param} }}`; + return obj; + }, {}); + + const returnValue = child(parameters); + let slotSourceCode = generateSlotChildrenSourceCode([returnValue], ctx); -function getComponents(template: string): (TemplateChildNode | VNode)[] { - const ast = baseParse(template, { - isNativeTag: () => true, - decodeEntities: (rawtext, asAttr) => rawtext, + // if slot bindings are used for properties of other components, our {{ paramName }} is incorrect because + // it would generate e.g. my-prop="{{ paramName }}", therefore, we replace it here to e.g. :my-prop="paramName" + paramNames.forEach((param) => { + slotSourceCode = slotSourceCode.replaceAll( + new RegExp(` (\\S+)="{{ ${param} }}"`, 'g'), + ` :$1="${param}"` + ); + }); + + return slotSourceCode; + } + + case 'bigint': + return `{{ BigInt(${child.toString()}) }}`; + + // the only missing case here is "symbol" + // because rendering a symbol as slot / HTML does not make sense and is not supported by Vue + default: + return ''; + } + }; + + children.forEach((child) => { + const sourceCode = generateSingleChildSourceCode(child); + if (sourceCode !== '') slotChildrenSourceCodes.push(sourceCode); }); - const components = ast?.children; - if (!components) return []; - return components; -} + + return slotChildrenSourceCodes.join('\n'); +}; /** - * Generate a vue3 template. + * Generates source code for the given VNode and all its children (e.g. created using `h(MyComponent)` or `h("div")`). + */ +const generateVNodeSourceCode = (vnode: VNode, ctx: SourceCodeGeneratorContext): string => { + const componentName = getVNodeName(vnode); + let childrenCode = ''; + + if (typeof vnode.children === 'string') { + childrenCode = vnode.children; + } else if (Array.isArray(vnode.children)) { + childrenCode = generateSlotChildrenSourceCode(vnode.children, ctx); + } else if (vnode.children) { + // children are an object, just like if regular Story args where used + // so we can generate the source code with the regular "generateSlotSourceCode()". + childrenCode = generateSlotSourceCode( + vnode.children, + // $stable is a default property in vnode.children so we need to filter it out + // to not generate source code for it + Object.keys(vnode.children).filter((i) => i !== '$stable'), + ctx + ); + } + + const props = vnode.props ? generatePropsSourceCode(vnode.props, [], [], ctx) : ''; + + // prefer self closing tag if no children exist + if (childrenCode) { + return `<${componentName}${props ? ` ${props}` : ''}>${childrenCode}`; + } + return `<${componentName}${props ? ` ${props}` : ''} />`; +}; + +/** + * Gets the name for the given VNode. + * Will return "component" if name could not be extracted. * - * @param component Component - * @param args Args - * @param argTypes ArgTypes - * @param slotProp Prop used to simulate a slot + * @example "div" for `h("div")` or "MyComponent" for `h(MyComponent)` */ +const getVNodeName = (vnode: VNode) => { + // this is e.g. the case when rendering native HTML elements like, h("div") + if (typeof vnode.type === 'string') return vnode.type; -export function generateTemplateSource( - componentOrNodes: (ConcreteComponent | TemplateChildNode)[] | TemplateChildNode | VNode, - { args, argTypes }: { args: Args; argTypes: ArgTypes }, - byRef = false -) { - const isElementNode = (node: any) => node && node.type === 1; - const isInterpolationNode = (node: any) => node && node.type === 5; - const isTextNode = (node: any) => node && node.type === 2; - - const generateComponentSource = ( - componentOrNode: ConcreteComponent | TemplateChildNode | VNode - ) => { - if (isElementNode(componentOrNode)) { - const { tag: name, props: attributes, children } = componentOrNode as ElementNode; - const childSources: string = - typeof children === 'string' - ? children - : children.map((child: TemplateChildNode) => generateComponentSource(child)).join(''); - const props = generateAttributesSource(attributes, args, argTypes, byRef); - - return childSources === '' - ? `<${name} ${props} />` - : `<${name} ${props}>${childSources}`; + if (typeof vnode.type === 'object') { + // this is the case when using custom Vue components like h(MyComponent) + if ('name' in vnode.type && vnode.type.name) { + // prefer custom component name set by the developer + return vnode.type.name; + } else if ('__name' in vnode.type && vnode.type.__name) { + // otherwise use name inferred by Vue from the file name + return vnode.type.__name; } + } - if (isTextNode(componentOrNode)) { - const { content } = componentOrNode as TextNode; - return content; - } - if (isInterpolationNode(componentOrNode)) { - const { content } = componentOrNode as InterpolationNode; - const expValue = evalExp(content.loc.source, args); - if (expValue === content.loc.source) return `{{${expValue}}}`; - return eval(expValue); - } - if (isVNode(componentOrNode)) { - const vnode = componentOrNode as VNode; - const { props, type, children } = vnode; - const slotsProps = typeof children === 'string' ? undefined : (children as Args); - const componentSlots = (type as any)?.__docgenInfo?.slots; - - const attrsProps = slotsProps - ? Object.fromEntries( - Object.entries(props ?? {}) - .filter(([key, value]) => !slotsProps[key] && !['class', 'style'].includes(key)) - .map(([key, value]) => [key, value]) - ) - : props; - const attributes = mapAttributesAndDirectives(attrsProps ?? {}); - const slotArgs = Object.fromEntries( - Object.entries(props ?? {}).filter(([key, value]) => slotsProps?.[key]) - ); + return 'component'; +}; - const childSources: string = children - ? typeof children === 'string' - ? children - : mapSlots(slotArgs as Args, generateComponentSource, componentSlots ?? []) - .map((child) => child.content) - .join('') - : ''; - const name = - typeof type === 'string' - ? type - : (type as FunctionalComponent).name || - (type as ConcreteComponent).__name || - (type as any).__docgenInfo?.displayName; - const propsSource = generateAttributesSource(attributes, args, argTypes, byRef); - return childSources.trim() === '' - ? `<${name} ${propsSource}/>` - : `<${name} ${propsSource}>${childSources}`; - } +/** + * Gets a list of parameters for the given function since func.arguments can not be used since + * it throws a TypeError. + * + * If the arguments are destructured (e.g. "func({ foo, bar })"), the returned array will also + * include "{" and "}". + * + * @see Based on https://stackoverflow.com/a/9924463 + */ +// eslint-disable-next-line @typescript-eslint/ban-types +export const getFunctionParamNames = (func: Function): string[] => { + const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm; + const ARGUMENT_NAMES = /([^\s,]+)/g; - return null; - }; + const fnStr = func.toString().replace(STRIP_COMMENTS, ''); + const result = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')).match(ARGUMENT_NAMES); + if (!result) return []; - const componentsOrNodes = Array.isArray(componentOrNodes) ? componentOrNodes : [componentOrNodes]; - const source = componentsOrNodes - .map((componentOrNode) => generateComponentSource(componentOrNode)) - .join(' '); - return source || null; -} + // when running "storybook build", the function will be minified, so result for e.g. + // `({ foo, bar }) => { // function body }` will be `["{foo:e", "bar:a}"]` + // therefore we need to remove the :e and :a mappings and extract the "{" and "}"" from the destructured object + // so the final result becomes `["{", "foo", "bar", "}"]` + return result.flatMap((param) => { + if (['{', '}'].includes(param)) return param; + const nonMinifiedName = param.split(':')[0].trim(); + if (nonMinifiedName.startsWith('{')) { + return ['{', nonMinifiedName.substring(1)]; + } + if (param.endsWith('}') && !nonMinifiedName.endsWith('}')) { + return [nonMinifiedName, '}']; + } + return nonMinifiedName; + }); +}; /** - * source decorator. - * @param storyFn Fn - * @param context StoryContext + * Converts the given slot bindings/parameters to a string. + * + * @example + * If no params: '#slotName' + * If params: '#slotName="{ foo, bar }"' */ -export const sourceDecorator = (storyFn: any, context: StoryContext) => { - const skip = skipSourceRender(context); - const story = storyFn(); +const slotBindingsToString = ( + slotName: string, + params: string[] +): `#${string}` | `#${string}="${string}"` => { + if (!params.length) return `#${slotName}`; + if (params.length === 1) return `#${slotName}="${params[0]}"`; - watch( - () => context.args, - () => { - if (!skip) { - generateSource(context); - } - }, - { immediate: true, deep: true } - ); - return story; + // parameters might be destructured so remove duplicated brackets here + return `#${slotName}="{ ${params.filter((i) => !['{', '}'].includes(i)).join(', ')} }"`; }; -export function generateSource(context: StoryContext) { - const channel = addons.getChannel(); - const { args = {}, argTypes = {}, id } = context || {}; - const storyComponents = getTemplateComponents(context?.originalStoryFn, context); +/** + * Formats the given object as string. + * Will format in single line if it only contains non-object values. + * Otherwise will format multiline. + */ +export const formatObject = (obj: object): string => { + const isPrimitive = Object.values(obj).every( + (value) => value == null || typeof value !== 'object' + ); - const withScript = context?.parameters?.docs?.source?.withScriptSetup || false; - const generatedScript = withScript ? generateScriptSetup(args, argTypes, storyComponents) : ''; - const generatedTemplate = generateTemplateSource(storyComponents, context, withScript); + // if object/array only contains non-object values, we format all values in one line + if (isPrimitive) return JSON.stringify(obj); - if (generatedTemplate) { - const source = `${generatedScript}\n `; - channel.emit(SNIPPET_RENDERED, { id, args, source, format: 'vue' }); - return source; - } - return null; -} -// export local function for testing purpose -export { - generateScriptSetup, - getTemplateComponents as getComponentsFromRenderFn, - getComponents as getComponentsFromTemplate, - mapAttributesAndDirectives, - attributeSource, - htmlEventAttributeToVueEventAttribute, + // otherwise, we use a "pretty" formatting with newlines and spaces + return JSON.stringify(obj, null, 2); }; diff --git a/code/renderers/vue3/src/entry-preview-docs.ts b/code/renderers/vue3/src/entry-preview-docs.ts index 66e848f2bc12..16c9338a164b 100644 --- a/code/renderers/vue3/src/entry-preview-docs.ts +++ b/code/renderers/vue3/src/entry-preview-docs.ts @@ -1,6 +1,9 @@ -import type { ArgTypesEnhancer, DecoratorFunction } from 'storybook/internal/types'; -import type { ArgTypesExtractor } from 'storybook/internal/docs-tools'; -import { extractComponentDescription, enhanceArgTypes } from 'storybook/internal/docs-tools'; +import { + enhanceArgTypes, + extractComponentDescription, + type ArgTypesExtractor, +} from 'storybook/internal/docs-tools'; +import type { ArgTypesEnhancer } from 'storybook/internal/types'; import { extractArgTypes } from './docs/extractArgTypes'; import { sourceDecorator } from './docs/sourceDecorator'; import type { VueRenderer } from './types'; @@ -21,6 +24,6 @@ export const parameters: { }, }; -export const decorators: DecoratorFunction[] = [sourceDecorator]; +export const decorators = [sourceDecorator]; export const argTypesEnhancers: ArgTypesEnhancer[] = [enhanceArgTypes]; diff --git a/code/renderers/vue3/src/mount.ts b/code/renderers/vue3/src/mount.ts index 73bc7a3326ad..8cd6190dab8c 100644 --- a/code/renderers/vue3/src/mount.ts +++ b/code/renderers/vue3/src/mount.ts @@ -1,6 +1,6 @@ import { type StoryContext, type VueRenderer } from './public-types'; import { h } from 'vue'; -import { type BaseAnnotations } from '@storybook/types'; +import { type BaseAnnotations } from 'storybook/internal/types'; export const mount: BaseAnnotations['mount'] = (context: StoryContext) => { return async (Component, options) => { diff --git a/code/renderers/vue3/template/stories_vue3-vite-default-ts/SourceCode.stories.ts b/code/renderers/vue3/template/stories_vue3-vite-default-ts/SourceCode.stories.ts new file mode 100644 index 000000000000..c9500c509a48 --- /dev/null +++ b/code/renderers/vue3/template/stories_vue3-vite-default-ts/SourceCode.stories.ts @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/vue3'; +import { h } from 'vue'; +import SourceCode from './SourceCode.vue'; + +const meta: Meta = { + component: SourceCode, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default = { + args: { + foo: 'Example string', + bar: 42, + array: ['A', 'B', 'C'], + object: { + a: 'Test A', + b: 42, + }, + modelValue: 'Model value', + default: 'Default slot content', + namedSlot: ({ foo }) => [ + 'Plain text', + h('div', { style: 'color:red' }, ['Div child', h('span', foo)]), + ], + }, +} satisfies Story; diff --git a/code/renderers/vue3/template/stories_vue3-vite-default-ts/SourceCode.vue b/code/renderers/vue3/template/stories_vue3-vite-default-ts/SourceCode.vue new file mode 100644 index 000000000000..6fae0e0ec5d8 --- /dev/null +++ b/code/renderers/vue3/template/stories_vue3-vite-default-ts/SourceCode.vue @@ -0,0 +1,27 @@ + + + diff --git a/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/DefineSlots.stories.ts b/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/DefineSlots.stories.ts index 1a06ce6bb504..da11664027c9 100644 --- a/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/DefineSlots.stories.ts +++ b/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/DefineSlots.stories.ts @@ -11,8 +11,8 @@ export default meta; export const Default: Story = { args: { - default: ({ num }) => `Default slot { num=${num} }`, - named: ({ str }) => `Named slot { str=${str} }`, - vbind: ({ num, str }) => `Named v-bind slot { num=${num}, str=${str} }`, + default: ({ num }) => `Default slot: num=${num}`, + named: ({ str }) => `Named slot: str=${str}`, + vbind: ({ num, str }) => `Named v-bind slot: num=${num}, str=${str}`, }, }; diff --git a/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/TemplateSlots.stories.ts b/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/TemplateSlots.stories.ts index 817bdcba42df..7ae73f819caf 100644 --- a/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/TemplateSlots.stories.ts +++ b/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/TemplateSlots.stories.ts @@ -11,8 +11,8 @@ export default meta; export const Default: Story = { args: { - default: ({ num }) => `Default slot { num=${num} }`, - named: ({ str }) => `Named slot { str=${str} }`, - vbind: ({ num, str }) => `Named v-bind slot { num=${num}, str=${str} }`, + default: ({ num }) => `Default slot: num=${num}`, + named: ({ str }) => `Named slot: str=${str}`, + vbind: ({ num, str }) => `Named v-bind slot: num=${num}, str=${str}`, }, }; diff --git a/code/yarn.lock b/code/yarn.lock index 7c563aac8e27..1b831f85c140 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -5287,7 +5287,7 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/addon-links@workspace:addons/links" dependencies: - "@storybook/csf": "npm:0.1.11" + "@storybook/csf": "npm:^0.1.11" "@storybook/global": "npm:^5.0.0" fs-extra: "npm:^11.1.0" ts-dedent: "npm:^2.0.0" @@ -5514,7 +5514,7 @@ __metadata: resolution: "@storybook/blocks@workspace:lib/blocks" dependencies: "@storybook/addon-actions": "workspace:*" - "@storybook/csf": "npm:0.1.11" + "@storybook/csf": "npm:^0.1.11" "@storybook/global": "npm:^5.0.0" "@storybook/icons": "npm:^1.2.5" "@storybook/react": "workspace:*" @@ -5667,7 +5667,7 @@ __metadata: "@babel/preset-env": "npm:^7.24.4" "@babel/types": "npm:^7.24.0" "@storybook/core": "workspace:*" - "@storybook/csf": "npm:0.1.11" + "@storybook/csf": "npm:^0.1.11" "@types/cross-spawn": "npm:^6.0.2" "@types/jscodeshift": "npm:^0.11.10" ansi-regex: "npm:^6.0.1" @@ -5761,8 +5761,8 @@ __metadata: "@radix-ui/react-dialog": "npm:^1.0.5" "@radix-ui/react-scroll-area": "npm:^1.0.5" "@radix-ui/react-slot": "npm:^1.0.2" - "@storybook/csf": "npm:0.1.11" - "@storybook/docs-mdx": "npm:4.0.0-next.0" + "@storybook/csf": "npm:^0.1.11" + "@storybook/docs-mdx": "npm:4.0.0-next.1" "@storybook/global": "npm:^5.0.0" "@storybook/icons": "npm:^1.2.5" "@tanstack/react-virtual": "npm:^3.3.0" @@ -5897,15 +5897,6 @@ __metadata: languageName: unknown linkType: soft -"@storybook/csf@npm:0.1.11": - version: 0.1.11 - resolution: "@storybook/csf@npm:0.1.11" - dependencies: - type-fest: "npm:^2.19.0" - checksum: 10c0/c5329fc13e7d762049b5c91df1bc1c0e510a1a898c401b72b68f1ff64139a85ab64a92f8e681d2fcb226c0a4a55d0f23b569b2bdb517e0f067bd05ea46228356 - languageName: node - linkType: hard - "@storybook/csf@npm:^0.0.1": version: 0.0.1 resolution: "@storybook/csf@npm:0.0.1" @@ -5915,12 +5906,21 @@ __metadata: languageName: node linkType: hard -"@storybook/docs-mdx@npm:4.0.0-next.0": - version: 4.0.0-next.0 - resolution: "@storybook/docs-mdx@npm:4.0.0-next.0" +"@storybook/csf@npm:^0.1.11": + version: 0.1.11 + resolution: "@storybook/csf@npm:0.1.11" + dependencies: + type-fest: "npm:^2.19.0" + checksum: 10c0/c5329fc13e7d762049b5c91df1bc1c0e510a1a898c401b72b68f1ff64139a85ab64a92f8e681d2fcb226c0a4a55d0f23b569b2bdb517e0f067bd05ea46228356 + languageName: node + linkType: hard + +"@storybook/docs-mdx@npm:4.0.0-next.1": + version: 4.0.0-next.1 + resolution: "@storybook/docs-mdx@npm:4.0.0-next.1" dependencies: acorn: "npm:^8.12.1" - checksum: 10c0/6253361e4e3c6c716c4f4c8cc30c082bcdab66b35b30183f6574d94720d875e28927916be8bda0bff4987090c3e50d348ca898160b1812a90c5afa845400414e + checksum: 10c0/8779279014a0a48c00d5884d310b3ca7828a49057c7403371e4eaf0fd053d8c93a412084cbbd6e5ea65e509e27f96752e8de7dadacdfa89198158b8b10deabdc languageName: node linkType: hard @@ -6507,7 +6507,7 @@ __metadata: "@storybook/codemod": "workspace:*" "@storybook/core": "workspace:*" "@storybook/core-webpack": "workspace:*" - "@storybook/csf": "npm:0.1.11" + "@storybook/csf": "npm:^0.1.11" "@storybook/csf-plugin": "workspace:*" "@storybook/ember": "workspace:*" "@storybook/eslint-config-storybook": "npm:^4.0.0" @@ -6595,7 +6595,7 @@ __metadata: typescript: "npm:^5.4.3" util: "npm:^0.12.4" vite: "npm:^4.0.0" - vitest: "npm:^1.2.2" + vitest: "npm:^1.6.0" wait-on: "npm:^7.0.1" dependenciesMeta: ejs: @@ -6643,7 +6643,7 @@ __metadata: resolution: "@storybook/server@workspace:renderers/server" dependencies: "@storybook/components": "workspace:^" - "@storybook/csf": "npm:0.1.11" + "@storybook/csf": "npm:^0.1.11" "@storybook/global": "npm:^5.0.0" "@storybook/manager-api": "workspace:^" "@storybook/preview-api": "workspace:^" @@ -6662,7 +6662,7 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/source-loader@workspace:lib/source-loader" dependencies: - "@storybook/csf": "npm:0.1.11" + "@storybook/csf": "npm:^0.1.11" estraverse: "npm:^5.2.0" lodash: "npm:^4.17.21" prettier: "npm:^3.1.1" @@ -6768,7 +6768,7 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/test@workspace:lib/test" dependencies: - "@storybook/csf": "npm:0.1.11" + "@storybook/csf": "npm:^0.1.11" "@storybook/instrumenter": "workspace:*" "@testing-library/dom": "npm:10.1.0" "@testing-library/jest-dom": "npm:6.4.5" @@ -8487,34 +8487,25 @@ __metadata: languageName: node linkType: hard -"@vitest/runner@npm:1.2.2": - version: 1.2.2 - resolution: "@vitest/runner@npm:1.2.2" +"@vitest/runner@npm:1.6.0": + version: 1.6.0 + resolution: "@vitest/runner@npm:1.6.0" dependencies: - "@vitest/utils": "npm:1.2.2" + "@vitest/utils": "npm:1.6.0" p-limit: "npm:^5.0.0" pathe: "npm:^1.1.1" - checksum: 10c0/25a9c03cca5b40738fe606757b14ee9d60d25193115b4674e3cc402c2b2c3844d234902d48bfa7646cb205455ea27891fef96733e033a570b85fe74ed29ff81c + checksum: 10c0/27d67fa51f40effe0e41ee5f26563c12c0ef9a96161f806036f02ea5eb9980c5cdf305a70673942e7a1e3d472d4d7feb40093ae93024ef1ccc40637fc65b1d2f languageName: node linkType: hard -"@vitest/snapshot@npm:1.2.2": - version: 1.2.2 - resolution: "@vitest/snapshot@npm:1.2.2" +"@vitest/snapshot@npm:1.6.0": + version: 1.6.0 + resolution: "@vitest/snapshot@npm:1.6.0" dependencies: magic-string: "npm:^0.30.5" pathe: "npm:^1.1.1" pretty-format: "npm:^29.7.0" - checksum: 10c0/0f8a69a289aa6466c7dd56f8327190d56a0bc7ad10412127de001c94784f6dba5e5bccb757def21f565f4efa3e00c307b92e8b6c302f11fc57889b743ba18a95 - languageName: node - linkType: hard - -"@vitest/spy@npm:1.2.2": - version: 1.2.2 - resolution: "@vitest/spy@npm:1.2.2" - dependencies: - tinyspy: "npm:^2.2.0" - checksum: 10c0/5480048d26c0d82b524317552fbdcc05fed6ea626d887620647826453a344798a360f2a75af477512a1569b1b6c918eae62338e8b35575f875fc2d7ef51419f3 + checksum: 10c0/be027fd268d524589ff50c5fad7b4faa1ac5742b59ac6c1dc6f5a3930aad553560e6d8775e90ac4dfae4be746fc732a6f134ba95606a1519707ce70db3a772a5 languageName: node linkType: hard @@ -8527,18 +8518,6 @@ __metadata: languageName: node linkType: hard -"@vitest/utils@npm:1.2.2": - version: 1.2.2 - resolution: "@vitest/utils@npm:1.2.2" - dependencies: - diff-sequences: "npm:^29.6.3" - estree-walker: "npm:^3.0.3" - loupe: "npm:^2.3.7" - pretty-format: "npm:^29.7.0" - checksum: 10c0/32449cb7eca8ecea56e0fce280c9770f65fa6b60bbba73be06ca2891096818899b4b3220bd3c815df8beb4266034db394fcf235e4de8959cce686b8b360948d1 - languageName: node - linkType: hard - "@vitest/utils@npm:1.6.0, @vitest/utils@npm:^1.3.1": version: 1.6.0 resolution: "@vitest/utils@npm:1.6.0" @@ -17835,6 +17814,13 @@ __metadata: languageName: node linkType: hard +"js-tokens@npm:^9.0.0": + version: 9.0.0 + resolution: "js-tokens@npm:9.0.0" + checksum: 10c0/4ad1c12f47b8c8b2a3a99e29ef338c1385c7b7442198a425f3463f3537384dab6032012791bfc2f056ea5ecdb06b1ed4f70e11a3ab3f388d3dcebfe16a52b27d + languageName: node + linkType: hard + "js-yaml@npm:4.1.0, js-yaml@npm:^4.1.0": version: 4.1.0 resolution: "js-yaml@npm:4.1.0" @@ -25841,12 +25827,12 @@ __metadata: languageName: node linkType: hard -"strip-literal@npm:^1.3.0": - version: 1.3.0 - resolution: "strip-literal@npm:1.3.0" +"strip-literal@npm:^2.0.0": + version: 2.1.0 + resolution: "strip-literal@npm:2.1.0" dependencies: - acorn: "npm:^8.10.0" - checksum: 10c0/3c0c9ee41eb346e827eede61ef288457f53df30e3e6ff8b94fa81b636933b0c13ca4ea5c97d00a10d72d04be326da99ac819f8769f0c6407ba8177c98344a916 + js-tokens: "npm:^9.0.0" + checksum: 10c0/bc8b8c8346125ae3c20fcdaf12e10a498ff85baf6f69597b4ab2b5fbf2e58cfd2827f1a44f83606b852da99a5f6c8279770046ddea974c510c17c98934c9cc24 languageName: node linkType: hard @@ -26376,10 +26362,10 @@ __metadata: languageName: node linkType: hard -"tinypool@npm:^0.8.2": - version: 0.8.2 - resolution: "tinypool@npm:0.8.2" - checksum: 10c0/8998626614172fc37c394e9a14e701dc437727fc6525488a4d4fd42044a4b2b59d6f076d750cbf5c699f79c58dd4e40599ab09e2f1ae0df4b23516b98c9c3055 +"tinypool@npm:^0.8.3": + version: 0.8.4 + resolution: "tinypool@npm:0.8.4" + checksum: 10c0/779c790adcb0316a45359652f4b025958c1dff5a82460fe49f553c864309b12ad732c8288be52f852973bc76317f5e7b3598878aee0beb8a33322c0e72c4a66c languageName: node linkType: hard @@ -27756,9 +27742,9 @@ __metadata: languageName: node linkType: hard -"vite-node@npm:1.2.2": - version: 1.2.2 - resolution: "vite-node@npm:1.2.2" +"vite-node@npm:1.6.0": + version: 1.6.0 + resolution: "vite-node@npm:1.6.0" dependencies: cac: "npm:^6.7.14" debug: "npm:^4.3.4" @@ -27767,7 +27753,7 @@ __metadata: vite: "npm:^5.0.0" bin: vite-node: vite-node.mjs - checksum: 10c0/39a5b9d9c806a012aab208eee0f59e4e12446ec19a4cf149a6459e7ff86491c289e189fda4f55a63b7e37d713f5edbda0e9efed95af4f7ebefa6d39eee093c0b + checksum: 10c0/0807e6501ac7763e0efa2b4bd484ce99fb207e92c98624c9f8999d1f6727ac026e457994260fa7fdb7060d87546d197081e46a705d05b0136a38b6f03715cbc2 languageName: node linkType: hard @@ -27863,17 +27849,16 @@ __metadata: languageName: node linkType: hard -"vitest@npm:^1.2.2": - version: 1.2.2 - resolution: "vitest@npm:1.2.2" +"vitest@npm:^1.6.0": + version: 1.6.0 + resolution: "vitest@npm:1.6.0" dependencies: - "@vitest/expect": "npm:1.2.2" - "@vitest/runner": "npm:1.2.2" - "@vitest/snapshot": "npm:1.2.2" - "@vitest/spy": "npm:1.2.2" - "@vitest/utils": "npm:1.2.2" + "@vitest/expect": "npm:1.6.0" + "@vitest/runner": "npm:1.6.0" + "@vitest/snapshot": "npm:1.6.0" + "@vitest/spy": "npm:1.6.0" + "@vitest/utils": "npm:1.6.0" acorn-walk: "npm:^8.3.2" - cac: "npm:^6.7.14" chai: "npm:^4.3.10" debug: "npm:^4.3.4" execa: "npm:^8.0.1" @@ -27882,17 +27867,17 @@ __metadata: pathe: "npm:^1.1.1" picocolors: "npm:^1.0.0" std-env: "npm:^3.5.0" - strip-literal: "npm:^1.3.0" + strip-literal: "npm:^2.0.0" tinybench: "npm:^2.5.1" - tinypool: "npm:^0.8.2" + tinypool: "npm:^0.8.3" vite: "npm:^5.0.0" - vite-node: "npm:1.2.2" + vite-node: "npm:1.6.0" why-is-node-running: "npm:^2.2.2" peerDependencies: "@edge-runtime/vm": "*" "@types/node": ^18.0.0 || >=20.0.0 - "@vitest/browser": ^1.0.0 - "@vitest/ui": ^1.0.0 + "@vitest/browser": 1.6.0 + "@vitest/ui": 1.6.0 happy-dom: "*" jsdom: "*" peerDependenciesMeta: @@ -27910,7 +27895,7 @@ __metadata: optional: true bin: vitest: vitest.mjs - checksum: 10c0/085cb62146191b32dc98fac1a5b0de6d1c63c44cc1e7946a7d38309dd4135539432ec27b4bfad38ce79736688a0ce20d9b93f58de4ce4a41677cb3c5ca6ad980 + checksum: 10c0/065da5b8ead51eb174d93dac0cd50042ca9539856dc25e340ea905d668c41961f7e00df3e388e6c76125b2c22091db2e8465f993d0f6944daf9598d549e562e7 languageName: node linkType: hard diff --git a/docs/_snippets/storybook-theme-example-variables.md b/docs/_snippets/storybook-theme-example-variables.md index 8fb75f792aba..0f045dc6c52e 100644 --- a/docs/_snippets/storybook-theme-example-variables.md +++ b/docs/_snippets/storybook-theme-example-variables.md @@ -1,5 +1,5 @@ ```js filename=".storybook/YourTheme.js" renderer="common" language="js" -import { create } from '@storybook/theming/create'; +import { create } from '@storybook/theming'; export default create({ base: 'light', diff --git a/docs/addons/addon-knowledge-base.mdx b/docs/addons/addon-knowledge-base.mdx index 11ebc0eec556..c2dbdb497d30 100644 --- a/docs/addons/addon-knowledge-base.mdx +++ b/docs/addons/addon-knowledge-base.mdx @@ -44,33 +44,35 @@ Addon authors can develop their UIs using any React library. But we recommend us Use the components listed below with your next addon. -| Component | Source | Story | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| Action Bar | [See component implementation](https://github.com/storybookjs/storybook/blob/main/code/ui/components/src/ActionBar/ActionBar.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-actionbar--single-item) | -| Addon Panel | [See component implementation](https://github.com/storybookjs/storybook/blob/main/code/ui/components/src/addon-panel/addon-panel.tsx) | N/A | -| Badge | [See component implementation](https://github.com/storybookjs/storybook/blob/main/code/ui/components/src/Badge/Badge.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-badge--all-badges) | -| Button | [See component implementation](https://github.com/storybookjs/storybook/blob/main/code/ui/components/src/Button/Button.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-button--all-buttons) | -| Form | [See component implementation](https://github.com/storybookjs/storybook/blob/main/code/ui/components/src/form/index.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-form-button--sizes) | -| Loader | [See component implementation](https://github.com/storybookjs/storybook/blob/main/code/ui/components/src/Loader/Loader.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-loader--progress-bar) | -| PlaceHolder | [See component implementation](https://github.com/storybookjs/storybook/blob/main/code/ui/components/src/placeholder/placeholder.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-placeholder--single-child) | -| Scroll Area | [See component implementation](https://github.com/storybookjs/storybook/blob/main/code/ui/components/src/ScrollArea/ScrollArea.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-scrollarea--vertical) | -| Space | [See component implementation](https://github.com/storybookjs/storybook/blob/main/code/ui/components/src/spaced/Spaced.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-spaced--row) | -| Syntax Highlighter | [See component implementation](https://github.com/storybookjs/storybook/blob/main/code/ui/components/src/syntaxhighlighter/syntaxhighlighter.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-syntaxhighlighter--bash) | -| Tabs | [See component implementation](https://github.com/storybookjs/storybook/blob/main/code/ui/components/src/tabs/tabs.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-tabs--stateful-static) | -| ToolBar | [See component implementation](https://github.com/storybookjs/storybook/blob/main/code/ui/components/src/bar/bar.tsx) | N/A | -| ToolTip | [See component implementation](https://github.com/storybookjs/storybook/blob/main/code/ui/components/src/tooltip/Tooltip.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-tooltip-tooltip--basic-default) | -| Zoom | [See component implementation](https://github.com/storybookjs/storybook/blob/main/code/ui/components/src/Zoom/Zoom.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-zoom--element-actual-size) | +| Component | Source | Story | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| Action Bar | [See component implementation](https://github.com/storybookjs/storybook/blob/next/code/core/src/components/components/ActionBar/ActionBar.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-actionbar--single-item) | +| Addon Panel | [See component implementation](https://github.com/storybookjs/storybook/blob/next/code/core/src/components/components/addon-panel/addon-panel.tsx) | N/A | +| Badge | [See component implementation](https://github.com/storybookjs/storybook/blob/next/code/core/src/components/components/Badge/Badge.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-badge--all-badges) | +| Button | [See component implementation](https://github.com/storybookjs/storybook/blob/next/code/core/src/components/components/Button/Button.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-button--all-buttons) | +| Form | [See component implementation](https://github.com/storybookjs/storybook/blob/next/code/core/src/components/components/form/index.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-form-button--sizes) | +| Loader | [See component implementation](https://github.com/storybookjs/storybook/blob/next/code/core/src/components/components/Loader/Loader.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-loader--progress-bar) | +| PlaceHolder | [See component implementation](https://github.com/storybookjs/storybook/blob/next/code/core/src/components/components/placeholder/placeholder.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-placeholder--single-child) | +| Scroll Area | [See component implementation](https://github.com/storybookjs/storybook/blob/next/code/core/src/components/components/ScrollArea/ScrollArea.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-scrollarea--vertical) | +| Space | [See component implementation](https://github.com/storybookjs/storybook/blob/next/code/core/src/components/components/spaced/Spaced.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-spaced--row) | +| Syntax Highlighter | [See component implementation](https://github.com/storybookjs/storybook/blob/next/code/core/src/components/components/syntaxhighlighter/syntaxhighlighter.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-syntaxhighlighter--bash) | +| Tabs | [See component implementation](https://github.com/storybookjs/storybook/blob/next/code/core/src/components/components/tabs/tabs.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-tabs--stateful-static) | +| ToolBar | [See component implementation](https://github.com/storybookjs/storybook/blob/next/code/core/src/components/components/bar/bar.tsx) | N/A | +| ToolTip | [See component implementation](https://github.com/storybookjs/storybook/blob/next/code/core/src/components/components/tooltip/Tooltip.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-tooltip-tooltip--basic-default) | +| Zoom | [See component implementation](https://github.com/storybookjs/storybook/blob/next/code/core/src/components/components/Zoom/Zoom.tsx) | [See component story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-zoom--element-actual-size) | Complementing the components, also included is a set of UI primitives. Use the content listed below as a reference for styling your addon. -| Component | Source | Story | -| ------------------------------ | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -| Color Palette (see note below) | [See implementation](https://github.com/storybookjs/storybook/tree/master/code/ui/components/src/Colors) | [See story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-colorpalette--page) | -| Icon | [See implementation](https://github.com/storybookjs/storybook/blob/main/code/ui/components/src/icon/icons.tsx) | [See story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-icon--labels) | -| Typography | [See implementation](https://github.com/storybookjs/storybook/tree/master/code/ui/components/src/typography) | [See story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-typography--all) | +| Component | Source | Story | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| Color Palette (see note below) | [See implementation](https://github.com/storybookjs/storybook/tree/next/code/core/src/components/components/Colors) | [See story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-colorpalette--page) | +| Icon | [See implementation](https://github.com/storybookjs/storybook/blob/next/code/core/src/components/components/icon/icon.tsx)| [See story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-icon--labels) | +| Typography | [See implementation](https://github.com/storybookjs/storybook/tree/next/code/core/src/components/components/typography) | [See story](https://main--5a375b97f4b14f0020b0cda3.chromatic.com/?path=/story/basics-typography--all) | + The color palette implemented by `@storybook/components` is a high-level abstraction of the [`@storybook/theming`](https://github.com/storybookjs/storybook/tree/next/code/lib/theming/src) package. + ### Build system diff --git a/docs/api/main-config/main-config-build.mdx b/docs/api/main-config/main-config-build.mdx index 73d56d975273..d488fd06b0aa 100644 --- a/docs/api/main-config/main-config-build.mdx +++ b/docs/api/main-config/main-config-build.mdx @@ -31,7 +31,9 @@ Type: `TestBuildFlags` Configures Storybook's production builds for performance testing purposes by disabling certain features from the build. When running `build-storybook`, this feature is enabled by setting the `--test` [flag](../cli-options.mdx#build). - The options documented on this page are automatically enabled when the `--test` flag is provided to the `build-storybook` command. We encourage you to override these options only if you need to disable a specific feature for your project or if you are debugging a build issue. + + The options documented on this page are automatically enabled when the `--test` flag is provided to the [`storybook build`](../cli-options.mdx#build) command. We encourage you to override these options only if you need to disable a specific feature for your project or if you are debugging a build issue. + ### `test.disableBlocks` diff --git a/docs/essentials/controls.mdx b/docs/essentials/controls.mdx index 3c249c058e04..9066e7e4dc63 100644 --- a/docs/essentials/controls.mdx +++ b/docs/essentials/controls.mdx @@ -260,6 +260,10 @@ You can also update a control's value, then save the changes to the story. The s