diff --git a/.circleci/config.yml b/.circleci/config.yml index 7d187778889f..5e7cc4a22d24 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -195,6 +195,11 @@ jobs: command: | cd scripts yarn get-template --check + - run: + name: Type check + command: | + cd scripts + yarn check - run: name: Run tests command: | diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f24147199aeb..ba472744a8f1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,12 +1,33 @@ Closes # - + + + + ## What I did -## How to test +## Checklist for Contributors + +### Testing + + + +#### The changes in this PR are covered in the following automated tests: +- [ ] stories +- [ ] unit tests +- [ ] integration tests +- [ ] end-to-end tests + +#### Manual testing + +_This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!_ -## Checklist +### Documentation - + -- [ ] Make sure your changes are tested (stories and/or unit, integration, or end-to-end tests) -- [ ] Make sure to add/update documentation regarding your changes +- [ ] Add or update documentation reflecting your changes - [ ] If you are deprecating/removing a feature, make sure to update [MIGRATION.MD](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md) -#### Maintainers +## Checklist for Maintainers - [ ] When this PR is ready for testing, make sure to add `ci:normal`, `ci:merged` or `ci:daily` GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in `code/lib/cli/src/sandbox-templates.ts` -- [ ] Make sure this PR contains **one** of the labels below. - -`["cleanup", "BREAKING CHANGE", "feature request", "bug", "build", "documentation", "maintenance", "dependencies", "other"]` - - +- [ ] Make sure this PR contains **one** of the labels below: +
+ Available labels + + - `bug`: Internal changes that fixes incorrect behavior. + - `maintenance`: User-facing maintenance tasks. + - `dependencies`: Upgrading (sometimes downgrading) dependencies. + - `build`: Internal-facing build tooling & test updates. Will not show up in release changelog. + - `cleanup`: Minor cleanup style change. Will not show up in release changelog. + - `documentation`: Documentation **only** changes. Will not show up in release changelog. + - `feature request`: Introducing a new feature. + - `BREAKING CHANGE`: Changes that break compatibility in some way with current major version. + - `other`: Changes that don't fit in the above categories. + +
### πŸ¦‹ Canary release diff --git a/CHANGELOG.md b/CHANGELOG.md index 076226ae9e77..6f4d663ad990 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 7.3.2 + +- Maintenance: Revert "WebpackBuilder: Remove need for `react` as peerDependency" - [#23882](https://github.com/storybookjs/storybook/pull/23882), thanks [@vanessayuenn](https://github.com/vanessayuenn)! + ## 7.3.1 - Index: Fix `*.story.*` CSF indexing - [#23852](https://github.com/storybookjs/storybook/pull/23852), thanks [@shilman](https://github.com/shilman)! diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 071fd1a3d01a..039c76a34456 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,19 @@ +## 7.4.0-alpha.1 + +- Build: Migrate @storybook/scripts to strict-ts - [#23818](https://github.com/storybookjs/storybook/pull/23818), thanks [@stilt0n](https://github.com/stilt0n)! +- CLI: Exclude addon-styling from upgrade - [#23841](https://github.com/storybookjs/storybook/pull/23841), thanks [@Integrayshaun](https://github.com/Integrayshaun)! +- Core: Add error categorization framework - [#23653](https://github.com/storybookjs/storybook/pull/23653), thanks [@yannbf](https://github.com/yannbf)! +- Core: Fix error thrown if `docs.defaultName` is unset - [#23893](https://github.com/storybookjs/storybook/pull/23893), thanks [@stilt0n](https://github.com/stilt0n)! +- Core: Fix race-condition relating to `addons.setConfig` - [#23802](https://github.com/storybookjs/storybook/pull/23802), thanks [@ndelangen](https://github.com/ndelangen)! +- Maintenance: Move filtering of sidebar into the state - [#23911](https://github.com/storybookjs/storybook/pull/23911), thanks [@ndelangen](https://github.com/ndelangen)! +- Maintenance: Revert "WebpackBuilder: Remove need for `react` as peerDependency" - [#23882](https://github.com/storybookjs/storybook/pull/23882), thanks [@vanessayuenn](https://github.com/vanessayuenn)! +- Manager API: Fix `api.getAddonState`default value - [#23804](https://github.com/storybookjs/storybook/pull/23804), thanks [@sookmax](https://github.com/sookmax)! +- Publish: Don't distribute src files or unnecessary template files - [#23853](https://github.com/storybookjs/storybook/pull/23853), thanks [@shilman](https://github.com/shilman)! +- UI: Add an experimental API for adding sidebar filter functions at runtime - [#23722](https://github.com/storybookjs/storybook/pull/23722), thanks [@ndelangen](https://github.com/ndelangen)! +- UI: Removal of experimental components - [#23907](https://github.com/storybookjs/storybook/pull/23907), thanks [@ndelangen](https://github.com/ndelangen)! +- Vue3: Add support for Global Apps install - [#23772](https://github.com/storybookjs/storybook/pull/23772), thanks [@chakAs3](https://github.com/chakAs3)! +- Vue3: Use slot value directly if it's a string in source decorator - [#23784](https://github.com/storybookjs/storybook/pull/23784), thanks [@nasvillanueva](https://github.com/nasvillanueva)! + ## 7.4.0-alpha.0 - Index: Fix `*.story.*` CSF indexing - [#23852](https://github.com/storybookjs/storybook/pull/23852), thanks [@shilman](https://github.com/shilman)! diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 77e1f6fc0955..cc26a6bff211 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,18 @@ Storybook is developed against a specific node version. We recommend using [Volt The `yarn start` script will generate a React Vite TypeScript sandbox with a set of test stories inside it, as well as taking all steps required to get it running (building the various packages we need etc). There is no need to run `yarn` or `yarn install` as `yarn start` will do this for you. +## Issues + +If you run `yarn start` and encounter the following error, try rerunning `yarn start` a second time: + +```sh +> NX ENOENT: no such file or directory, open 'storybook/code/node_modules/nx/package.json' +``` + +## Forked repos + +If you have forked the repository, you should [disable Github Actions for your repo](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository) as many of them (e.g. pushing to sandbox) will fail without proper authorization. In your Github repo, go to Settings > Actions > General > set the Actions Permissions to **Disable actions**. + # Running against different sandbox templates You can also pick a specific template to use as your sandbox by running `yarn task`, which will prompt you to make further choices about which template you want and which task you want to run. diff --git a/code/.eslintrc.js b/code/.eslintrc.js index c764d4b28d83..e967113bb8eb 100644 --- a/code/.eslintrc.js +++ b/code/.eslintrc.js @@ -9,6 +9,7 @@ module.exports = { tsconfigRootDir: __dirname, project: ['./tsconfig.json'], }, + plugins: ['local-rules'], rules: { 'eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }], 'eslint-comments/no-unused-disable': 'error', @@ -166,5 +167,18 @@ module.exports = { 'import/no-unresolved': 'off', }, }, + { + files: ['**/*.ts', '!**/*.test.*', '!**/*.spec.*'], + rules: { + 'local-rules/no-uncategorized-errors': 'warn', + }, + }, + { + files: ['**/core-events/src/**/*'], + excludedFiles: ['**/*.test.*'], + rules: { + 'local-rules/no-duplicated-error-codes': 'error', + }, + }, ], }; diff --git a/code/addons/a11y/package.json b/code/addons/a11y/package.json index bd440e70d2de..36feadcbe854 100644 --- a/code/addons/a11y/package.json +++ b/code/addons/a11y/package.json @@ -56,7 +56,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/a11y/src/components/VisionSimulator.tsx b/code/addons/a11y/src/components/VisionSimulator.tsx index b43756897364..b66f8eef297d 100644 --- a/code/addons/a11y/src/components/VisionSimulator.tsx +++ b/code/addons/a11y/src/components/VisionSimulator.tsx @@ -1,8 +1,7 @@ import type { ReactNode } from 'react'; import React, { useState } from 'react'; import { Global, styled } from '@storybook/theming'; -import { IconButton, WithTooltip, TooltipLinkList } from '@storybook/components'; -import { Icon } from '@storybook/components/experimental'; +import { Icons, IconButton, WithTooltip, TooltipLinkList } from '@storybook/components'; import { Filters } from './ColorFilters'; @@ -145,7 +144,7 @@ export const VisionSimulator = () => { onDoubleClick={() => setFilter(null)} > - + diff --git a/code/addons/actions/package.json b/code/addons/actions/package.json index 4e3cbc75e602..bff07d8c6329 100644 --- a/code/addons/actions/package.json +++ b/code/addons/actions/package.json @@ -73,7 +73,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", @@ -93,7 +94,7 @@ "polished": "^4.2.2", "prop-types": "^15.7.2", "react-inspector": "^6.0.0", - "telejson": "^7.0.3", + "telejson": "^7.2.0", "ts-dedent": "^2.0.0", "uuid": "^9.0.0" }, diff --git a/code/addons/backgrounds/package.json b/code/addons/backgrounds/package.json index 10444bf1d704..01dfed29e3b0 100644 --- a/code/addons/backgrounds/package.json +++ b/code/addons/backgrounds/package.json @@ -69,7 +69,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/backgrounds/src/containers/BackgroundSelector.tsx b/code/addons/backgrounds/src/containers/BackgroundSelector.tsx index d44d7f58ab6b..078a2752a5e3 100644 --- a/code/addons/backgrounds/src/containers/BackgroundSelector.tsx +++ b/code/addons/backgrounds/src/containers/BackgroundSelector.tsx @@ -4,8 +4,7 @@ import memoize from 'memoizerific'; import { useParameter, useGlobals } from '@storybook/manager-api'; import { logger } from '@storybook/client-logger'; -import { IconButton, WithTooltip, TooltipLinkList } from '@storybook/components'; -import { Icon } from '@storybook/components/experimental'; +import { Icons, IconButton, WithTooltip, TooltipLinkList } from '@storybook/components'; import { PARAM_KEY as BACKGROUNDS_PARAM_KEY } from '../constants'; import { ColorIcon } from '../components/ColorIcon'; @@ -141,7 +140,7 @@ export const BackgroundSelector: FC = memo(function BackgroundSelector() { title="Change the background of the preview" active={selectedBackgroundColor !== 'transparent' || isTooltipVisible} > - + diff --git a/code/addons/backgrounds/src/containers/GridSelector.tsx b/code/addons/backgrounds/src/containers/GridSelector.tsx index b860209ae0ae..80d6477884b4 100644 --- a/code/addons/backgrounds/src/containers/GridSelector.tsx +++ b/code/addons/backgrounds/src/containers/GridSelector.tsx @@ -2,8 +2,7 @@ import type { FC } from 'react'; import React, { memo } from 'react'; import { useGlobals, useParameter } from '@storybook/manager-api'; -import { IconButton } from '@storybook/components'; -import { Icon } from '@storybook/components/experimental'; +import { Icons, IconButton } from '@storybook/components'; import { PARAM_KEY as BACKGROUNDS_PARAM_KEY } from '../constants'; @@ -31,7 +30,7 @@ export const GridSelector: FC = memo(function GridSelector() { }) } > - + ); }); diff --git a/code/addons/controls/package.json b/code/addons/controls/package.json index 1a4e105893f7..5a3369812b85 100644 --- a/code/addons/controls/package.json +++ b/code/addons/controls/package.json @@ -61,7 +61,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/docs/package.json b/code/addons/docs/package.json index 2538a9305e4b..52289b84f552 100644 --- a/code/addons/docs/package.json +++ b/code/addons/docs/package.json @@ -90,7 +90,8 @@ "lit/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/essentials/package.json b/code/addons/essentials/package.json index 9b9ae7be0b3a..e28df802153a 100644 --- a/code/addons/essentials/package.json +++ b/code/addons/essentials/package.json @@ -112,7 +112,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/gfm/package.json b/code/addons/gfm/package.json index 78cc94ef9fee..bf07ea3c8599 100644 --- a/code/addons/gfm/package.json +++ b/code/addons/gfm/package.json @@ -44,7 +44,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/highlight/README.md b/code/addons/highlight/README.md index 4713f3aae058..e4b245acc5fd 100644 --- a/code/addons/highlight/README.md +++ b/code/addons/highlight/README.md @@ -4,61 +4,143 @@ Storybook addon allows for highlighting specific DOM nodes within your story. Use it to call attention to particular parts of the story. Or use it to enhance other addons that you might be building. For example, [Accessibility](https://storybook.js.org/addons/@storybook/addon-a11y/) addon uses it to highlight DOM nodes that are failing accessibility checks. -![](https://user-images.githubusercontent.com/42671/160146801-179eaa79-fff8-4bff-b17c-9262fdc94918.png) +![Story with highlight](./docs/highlight.png) ## Usage -This addon requires Storybook 6.5 or later. Highlight is part of [essentials](https://storybook.js.org/docs/react/essentials/introduction) and so is installed in all new Storybooks by default. If you need to add it to your Storybook, you can run: +This addon requires Storybook 6.5 or later. Highlight is part of [essentials](https://storybook.js.org/docs/react/essentials/introduction) and so is installed in all new Storybooks by default. If you need to add it to your Storybook, you can run the following command: + +yarn: + +```sh +yarn add --dev @storybook/addon-highlight +``` + +npm: ```sh -npm i -D @storybook/addon-highlight +npm install @storybook/addon-highlight --save-dev ``` -Add `"@storybook/addon-highlight"` to the addons array in your `.storybook/main.js`: +pnpm: + +```sh +pnpm add --save-dev @storybook/addon-highlight +``` + +Add `"@storybook/addon-highlight"` to the addons array in your `.storybook/main.js|ts`: + +```ts +// .storybook/main.ts -```js -export default { +// Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite) +import type { StorybookConfig } from '@storybook/your-framework'; + +const config: StorybookConfig = { addons: ['@storybook/addon-highlight'], }; + +export default config; ``` -### Apply or clear highlights +### Highlighting DOM Elements -Highlight DOM nodes by emitting the `HIGHLIGHT` event from within a story or an addon. The event payload must contain a list of selectors you want to highlight. +Highlight DOM nodes by emitting the `HIGHLIGHT` event from within a story or an addon. The event payload must contain an `elements` property assigned to an array of selectors matching the elements you want to highlight. -```js -import React, { useEffect } from 'react'; -import { useChannel } from '@storybook/preview-api'; -import { HIGHLIGHT, RESET_HIGHLIGHT } from '@storybook/addon-highlight'; -import { MyComponent } from './MyComponent'; +```ts +// MyComponent.stories.ts -export default { component: MyComponent }; +import type { Meta, StoryObj } from '@storybook/react'; +import { useChannel } from '@storybook/preview-api'; +import { HIGHLIGHT } from '@storybook/addon-highlight'; -export const MyStory = () => { - const emit = useChannel({}); +import { MyComponent } from './MyComponent'; - useEffect(() => { - emit(HIGHLIGHT, { - elements: ['.title', '.subtitle'], - }); - }, []); +const meta: Meta = { + component: MyComponent, +}; - return ; +export default meta; +type Story = StoryObj; + +export const Highlighted: Story = { + decorators: [ + (storyFn) => { + const emit = useChannel({}); + emit(HIGHLIGHT, { + elements: ['.title', '.subtitle'], + }); + return storyFn(); + }, + ], }; ``` +### Reset highlighted elements + Highlights are automatically cleared when the story changes. You can also manually clear them by emitting the `RESET_HIGHLIGHT` event. -```js -emit(RESET_HIGHLIGHT); +```ts +// MyComponent.stories.ts|tsx + +import type { Meta, StoryObj } from '@storybook/react'; +import { useChannel } from '@storybook/preview-api'; +import { HIGHLIGHT, RESET_HIGHLIGHT } from '@storybook/addon-highlight'; + +import { MyComponent } from './MyComponent'; + +const meta: Meta = { + component: MyComponent, +}; + +export default meta; +type Story = StoryObj; + +export const ResetHighlight: Story = { + decorators: [ + (storyFn) => { + const emit = useChannel({}); + emit(RESET_HIGHLIGHT); //πŸ‘ˆ Remove previously highlighted elements + emit(HIGHLIGHT, { + elements: ['header', 'section', 'footer'], + }); + return storyFn(); + }, + ], +}; ``` ### Customize style -```js -emit(HIGHLIGHT, { - elements: ['.title', '.subtitle'], - color: 'red', - style: 'solid', // 'dotted' | 'dashed' | 'solid' | 'double' -}); +The addon applies a standard style to the highlighted elements you've enabled for the story. However, you can enable your custom style by extending the payload object and providing a `color` and/or `style` properties. For example: + +```ts +// MyComponent.stories.ts + +import type { Meta, StoryObj } from '@storybook/react'; +import { useChannel } from '@storybook/preview-api'; +import { HIGHLIGHT } from '@storybook/addon-highlight'; + +import { MyComponent } from './MyComponent'; + +const meta: Meta = { + component: MyComponent, +}; + +export default meta; +type Story = StoryObj; + +export const StyledHighlight: Story = { + decorators: [ + (storyFn) => { + const emit = useChannel({}); + emit(HIGHLIGHT, { + elements: ['.title', '.subtitle'], + color: 'red', + style: 'solid', // 'dotted' | 'dashed' | 'solid' | 'double' + }); + return storyFn(); + }, + ], +}; ``` diff --git a/code/addons/highlight/docs/highlight.png b/code/addons/highlight/docs/highlight.png new file mode 100644 index 000000000000..2d1aef2f56ea Binary files /dev/null and b/code/addons/highlight/docs/highlight.png differ diff --git a/code/addons/highlight/package.json b/code/addons/highlight/package.json index bb13a1c982b0..5653ea2600b5 100644 --- a/code/addons/highlight/package.json +++ b/code/addons/highlight/package.json @@ -54,7 +54,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/interactions/package.json b/code/addons/interactions/package.json index 204138c7be06..9e62cb8521f7 100644 --- a/code/addons/interactions/package.json +++ b/code/addons/interactions/package.json @@ -65,7 +65,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/interactions/src/components/Interaction.tsx b/code/addons/interactions/src/components/Interaction.tsx index c0bf532d9072..bacaea0fe358 100644 --- a/code/addons/interactions/src/components/Interaction.tsx +++ b/code/addons/interactions/src/components/Interaction.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; -import { IconButton, TooltipNote, WithTooltip } from '@storybook/components'; -import { Icon } from '@storybook/components/experimental'; +import { IconButton, Icons, TooltipNote, WithTooltip } from '@storybook/components'; import { type Call, CallStates, type ControlStates } from '@storybook/instrumenter'; import { styled, typography } from '@storybook/theming'; import { transparentize } from 'polished'; @@ -175,7 +174,7 @@ export const Interaction = ({ tooltip={} > - + )} diff --git a/code/addons/interactions/src/components/Subnav.tsx b/code/addons/interactions/src/components/Subnav.tsx index de78bf2c94b6..3d2e5c41d1a3 100644 --- a/code/addons/interactions/src/components/Subnav.tsx +++ b/code/addons/interactions/src/components/Subnav.tsx @@ -3,13 +3,13 @@ import React from 'react'; import { Button, IconButton, + Icons, Separator, P, TooltipNote, WithTooltip, Bar, } from '@storybook/components'; -import { Icon } from '@storybook/components/experimental'; import type { Call, ControlStates } from '@storybook/instrumenter'; import { CallStates } from '@storybook/instrumenter'; import { styled } from '@storybook/theming'; @@ -135,7 +135,7 @@ export const Subnav: React.FC = ({ onClick={controls.start} disabled={!controlStates.start} > - + @@ -146,7 +146,7 @@ export const Subnav: React.FC = ({ onClick={controls.back} disabled={!controlStates.back} > - + @@ -157,7 +157,7 @@ export const Subnav: React.FC = ({ onClick={controls.next} disabled={!controlStates.next} > - + @@ -168,13 +168,13 @@ export const Subnav: React.FC = ({ onClick={controls.end} disabled={!controlStates.end} > - + }> - + diff --git a/code/addons/jest/package.json b/code/addons/jest/package.json index 268bd58ea4fa..25718a3e86b9 100644 --- a/code/addons/jest/package.json +++ b/code/addons/jest/package.json @@ -63,7 +63,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/links/package.json b/code/addons/links/package.json index 197cb689d3ed..0e1d2d9d3ef8 100644 --- a/code/addons/links/package.json +++ b/code/addons/links/package.json @@ -73,7 +73,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/measure/package.json b/code/addons/measure/package.json index 7fdc6873ceaa..ad39e0801b02 100644 --- a/code/addons/measure/package.json +++ b/code/addons/measure/package.json @@ -68,7 +68,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/measure/src/Tool.tsx b/code/addons/measure/src/Tool.tsx index bb78447f8527..2e68078aa06b 100644 --- a/code/addons/measure/src/Tool.tsx +++ b/code/addons/measure/src/Tool.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect } from 'react'; import { useGlobals, useStorybookApi } from '@storybook/manager-api'; -import { IconButton } from '@storybook/components'; -import { Icon } from '@storybook/components/experimental'; +import { Icons, IconButton } from '@storybook/components'; import { TOOL_ID, ADDON_ID } from './constants'; export const Tool = () => { @@ -34,7 +33,7 @@ export const Tool = () => { title="Enable measure" onClick={toggleMeasure} > - + ); }; diff --git a/code/addons/outline/package.json b/code/addons/outline/package.json index d2771ebda9a4..ec168cdfdd17 100644 --- a/code/addons/outline/package.json +++ b/code/addons/outline/package.json @@ -71,7 +71,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/outline/src/OutlineSelector.tsx b/code/addons/outline/src/OutlineSelector.tsx index 63c826044ef5..93bdf321df07 100644 --- a/code/addons/outline/src/OutlineSelector.tsx +++ b/code/addons/outline/src/OutlineSelector.tsx @@ -1,7 +1,6 @@ import React, { memo, useCallback, useEffect } from 'react'; import { useGlobals, useStorybookApi } from '@storybook/manager-api'; -import { IconButton } from '@storybook/components'; -import { Icon } from '@storybook/components/experimental'; +import { Icons, IconButton } from '@storybook/components'; import { ADDON_ID, PARAM_KEY } from './constants'; export const OutlineSelector = memo(function OutlineSelector() { @@ -35,7 +34,7 @@ export const OutlineSelector = memo(function OutlineSelector() { title="Apply outlines to the preview" onClick={toggleOutline} > - + ); }); diff --git a/code/addons/storyshots-core/package.json b/code/addons/storyshots-core/package.json index 095dfb081256..f3d0a1862bcd 100644 --- a/code/addons/storyshots-core/package.json +++ b/code/addons/storyshots-core/package.json @@ -29,7 +29,8 @@ "README.md", "*.js", "*.mjs", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/storyshots-puppeteer/package.json b/code/addons/storyshots-puppeteer/package.json index 3a4f6f729505..a8239e7f3604 100644 --- a/code/addons/storyshots-puppeteer/package.json +++ b/code/addons/storyshots-puppeteer/package.json @@ -28,7 +28,8 @@ "README.md", "*.js", "*.mjs", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/storysource/package.json b/code/addons/storysource/package.json index 9630f77e0839..abc36836fb9e 100644 --- a/code/addons/storysource/package.json +++ b/code/addons/storysource/package.json @@ -46,7 +46,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/themes/package.json b/code/addons/themes/package.json index 81e7e322dc61..916b3a433bdd 100644 --- a/code/addons/themes/package.json +++ b/code/addons/themes/package.json @@ -65,7 +65,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/themes/src/theme-switcher.tsx b/code/addons/themes/src/theme-switcher.tsx index b950f780c1d5..3658a3ef78cc 100644 --- a/code/addons/themes/src/theme-switcher.tsx +++ b/code/addons/themes/src/theme-switcher.tsx @@ -1,8 +1,8 @@ import React, { Fragment, useMemo } from 'react'; import { useAddonState, useChannel, useGlobals, useParameter } from '@storybook/manager-api'; import { styled } from '@storybook/theming'; -import { IconButton, WithTooltip, TooltipLinkList } from '@storybook/components'; -import { Icon } from '@storybook/components/experimental'; +import { Icons, IconButton, WithTooltip, TooltipLinkList } from '@storybook/components'; + import type { ThemeAddonState, ThemeParameters } from './constants'; import { PARAM_KEY, @@ -74,7 +74,7 @@ export const ThemeSwitcher = () => { }} > - + {label && {label}} diff --git a/code/addons/toolbars/package.json b/code/addons/toolbars/package.json index a6ae4e537ea0..45fcc8b940de 100644 --- a/code/addons/toolbars/package.json +++ b/code/addons/toolbars/package.json @@ -61,7 +61,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/viewport/package.json b/code/addons/viewport/package.json index 063ec8d4c584..33cebf97e9f5 100644 --- a/code/addons/viewport/package.json +++ b/code/addons/viewport/package.json @@ -66,7 +66,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/viewport/src/Tool.tsx b/code/addons/viewport/src/Tool.tsx index e62ce3543eb3..df65988e5097 100644 --- a/code/addons/viewport/src/Tool.tsx +++ b/code/addons/viewport/src/Tool.tsx @@ -4,8 +4,8 @@ import memoize from 'memoizerific'; import { styled, Global, type Theme, withTheme } from '@storybook/theming'; -import { IconButton, WithTooltip, TooltipLinkList } from '@storybook/components'; -import { Icon } from '@storybook/components/experimental'; +import { Icons, IconButton, WithTooltip, TooltipLinkList } from '@storybook/components'; + import { useStorybookApi, useParameter, useAddonState } from '@storybook/manager-api'; import { registerShortcuts } from './shortcuts'; import { PARAM_KEY, ADDON_ID } from './constants'; @@ -188,7 +188,7 @@ export const ViewportTool: FC = memo( setState({ ...state, selected: responsiveViewport.id }); }} > - + {styles ? ( {isRotated ? `${item.title} (L)` : `${item.title} (P)`} @@ -234,7 +234,7 @@ export const ViewportTool: FC = memo( setState({ ...state, isRotated: !isRotated }); }} > - + {styles.height.replace('px', '')} diff --git a/code/builders/builder-manager/package.json b/code/builders/builder-manager/package.json index 2f3f95ceca0a..71991bc45cb1 100644 --- a/code/builders/builder-manager/package.json +++ b/code/builders/builder-manager/package.json @@ -36,7 +36,8 @@ "templates/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/builders/builder-vite/package.json b/code/builders/builder-vite/package.json index 3c79b3437d37..8c0e606d56bc 100644 --- a/code/builders/builder-vite/package.json +++ b/code/builders/builder-vite/package.json @@ -35,7 +35,8 @@ "input/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/builders/builder-webpack5/package.json b/code/builders/builder-webpack5/package.json index e557fa0c15fc..9d31c809a045 100644 --- a/code/builders/builder-webpack5/package.json +++ b/code/builders/builder-webpack5/package.json @@ -48,22 +48,31 @@ "templates/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", "prep": "../../../scripts/prepare/bundle.ts" }, "dependencies": { - "@babel/core": "^7.22.0", + "@babel/core": "^7.22.9", + "@storybook/addons": "workspace:*", "@storybook/channels": "workspace:*", + "@storybook/client-api": "workspace:*", "@storybook/client-logger": "workspace:*", + "@storybook/components": "workspace:*", "@storybook/core-common": "workspace:*", "@storybook/core-events": "workspace:*", "@storybook/core-webpack": "workspace:*", + "@storybook/global": "^5.0.0", + "@storybook/manager-api": "workspace:*", "@storybook/node-logger": "workspace:*", "@storybook/preview": "workspace:*", "@storybook/preview-api": "workspace:*", + "@storybook/router": "workspace:*", + "@storybook/store": "workspace:*", + "@storybook/theming": "workspace:*", "@swc/core": "^1.3.49", "@types/node": "^16.0.0", "@types/semver": "^7.3.4", @@ -101,6 +110,10 @@ "slash": "^5.0.0", "typescript": "~4.9.3" }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, "peerDependenciesMeta": { "typescript": { "optional": true diff --git a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts index 704d8fe2b98b..d2f6ec6affbc 100644 --- a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts +++ b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts @@ -31,34 +31,20 @@ const getAbsolutePath = (input: I): I => dirname(require.resolve(join(input, 'package.json'))) as any; const storybookPaths: Record = { - // this is a temporary hack to get webpack to alias this correctly - [`@storybook/components/experimental`]: `${getAbsolutePath( - `@storybook/components` - )}/dist/experimental`, ...[ - // these packages are not pre-bundled because of react dependencies. - // these are not dependencies of the builder anymore, thus resolving them can fail. - // we should remove the aliases in 8.0, I'm not sure why they are here in the first place. + // these packages are not pre-bundled because of react dependencies 'components', 'global', 'manager-api', 'router', 'theming', - ].reduce((acc, sbPackage) => { - let packagePath; - try { - packagePath = getAbsolutePath(`@storybook/${sbPackage}`); - } catch (e) { - // ignore - } - if (packagePath) { - return { - ...acc, - [`@storybook/${sbPackage}`]: getAbsolutePath(`@storybook/${sbPackage}`), - }; - } - return acc; - }, {}), + ].reduce( + (acc, sbPackage) => ({ + ...acc, + [`@storybook/${sbPackage}`]: getAbsolutePath(`@storybook/${sbPackage}`), + }), + {} + ), // deprecated, remove in 8.0 [`@storybook/api`]: getAbsolutePath(`@storybook/manager-api`), }; diff --git a/code/deprecated/addons/package.json b/code/deprecated/addons/package.json index 029f64e0f76e..628855e4405f 100644 --- a/code/deprecated/addons/package.json +++ b/code/deprecated/addons/package.json @@ -34,10 +34,10 @@ "types": "./dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/deprecated/channel-postmessage/package.json b/code/deprecated/channel-postmessage/package.json index b04f5a7dc3ce..a721b95682d2 100644 --- a/code/deprecated/channel-postmessage/package.json +++ b/code/deprecated/channel-postmessage/package.json @@ -36,7 +36,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/deprecated/channel-websocket/package.json b/code/deprecated/channel-websocket/package.json index 6efafe0664e8..8f5093e32b21 100644 --- a/code/deprecated/channel-websocket/package.json +++ b/code/deprecated/channel-websocket/package.json @@ -36,7 +36,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/deprecated/client-api/package.json b/code/deprecated/client-api/package.json index 0ff8ba72e988..a3f609e785d9 100644 --- a/code/deprecated/client-api/package.json +++ b/code/deprecated/client-api/package.json @@ -35,7 +35,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/deprecated/manager-api-shim/package.json b/code/deprecated/manager-api-shim/package.json index 69f8e71c402d..c2661e9789bc 100644 --- a/code/deprecated/manager-api-shim/package.json +++ b/code/deprecated/manager-api-shim/package.json @@ -35,7 +35,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/deprecated/preview-web/package.json b/code/deprecated/preview-web/package.json index 819c2d8a9c44..8278714652f6 100644 --- a/code/deprecated/preview-web/package.json +++ b/code/deprecated/preview-web/package.json @@ -35,7 +35,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/deprecated/store/package.json b/code/deprecated/store/package.json index 5c774caa8e5e..eb2e62685049 100644 --- a/code/deprecated/store/package.json +++ b/code/deprecated/store/package.json @@ -35,7 +35,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/angular/package.json b/code/frameworks/angular/package.json index 766b16a60549..575f53bdb903 100644 --- a/code/frameworks/angular/package.json +++ b/code/frameworks/angular/package.json @@ -25,11 +25,12 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", + "template/cli/**/*", "README.md", "*.js", "*.mjs", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/node_modules/.bin/tsc", @@ -58,7 +59,7 @@ "find-up": "^5.0.0", "read-pkg-up": "^7.0.1", "semver": "^7.3.7", - "telejson": "^7.0.3", + "telejson": "^7.2.0", "ts-dedent": "^2.0.0", "tsconfig-paths-webpack-plugin": "^4.0.1", "util-deprecate": "^1.0.2", diff --git a/code/frameworks/ember/package.json b/code/frameworks/ember/package.json index 6f59e3182835..21385a1979db 100644 --- a/code/frameworks/ember/package.json +++ b/code/frameworks/ember/package.json @@ -21,10 +21,11 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", + "template/cli/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/html-vite/package.json b/code/frameworks/html-vite/package.json index 7c1ad72005ea..52312c98fe89 100644 --- a/code/frameworks/html-vite/package.json +++ b/code/frameworks/html-vite/package.json @@ -37,10 +37,10 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/html-webpack5/package.json b/code/frameworks/html-webpack5/package.json index c7bac10b107e..d0a5bd15342f 100644 --- a/code/frameworks/html-webpack5/package.json +++ b/code/frameworks/html-webpack5/package.json @@ -37,10 +37,10 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json index 91a9d71cc053..14ec26a04924 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -48,10 +48,11 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", + "template/cli/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/preact-vite/package.json b/code/frameworks/preact-vite/package.json index e29f546064e6..a01c073a4995 100644 --- a/code/frameworks/preact-vite/package.json +++ b/code/frameworks/preact-vite/package.json @@ -39,7 +39,8 @@ "types/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/preact-webpack5/package.json b/code/frameworks/preact-webpack5/package.json index 7527750e5a2d..b79235490989 100644 --- a/code/frameworks/preact-webpack5/package.json +++ b/code/frameworks/preact-webpack5/package.json @@ -37,10 +37,10 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/react-vite/package.json b/code/frameworks/react-vite/package.json index 096f282a7e2f..771815accc91 100644 --- a/code/frameworks/react-vite/package.json +++ b/code/frameworks/react-vite/package.json @@ -37,10 +37,10 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/react-webpack5/package.json b/code/frameworks/react-webpack5/package.json index 35ad281680b7..4a7d7f299227 100644 --- a/code/frameworks/react-webpack5/package.json +++ b/code/frameworks/react-webpack5/package.json @@ -37,10 +37,10 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/server-webpack5/package.json b/code/frameworks/server-webpack5/package.json index 549653a022d1..00a2b306cd3b 100644 --- a/code/frameworks/server-webpack5/package.json +++ b/code/frameworks/server-webpack5/package.json @@ -37,10 +37,10 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/svelte-vite/package.json b/code/frameworks/svelte-vite/package.json index 8ff0407f6e4e..d409f5a5a24f 100644 --- a/code/frameworks/svelte-vite/package.json +++ b/code/frameworks/svelte-vite/package.json @@ -37,10 +37,10 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/svelte-webpack5/package.json b/code/frameworks/svelte-webpack5/package.json index 8b2bef6a0d5e..ec397d079f4e 100644 --- a/code/frameworks/svelte-webpack5/package.json +++ b/code/frameworks/svelte-webpack5/package.json @@ -37,10 +37,10 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/sveltekit/package.json b/code/frameworks/sveltekit/package.json index 60f338e47a41..340b6637b3ae 100644 --- a/code/frameworks/sveltekit/package.json +++ b/code/frameworks/sveltekit/package.json @@ -40,10 +40,10 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/vue-vite/package.json b/code/frameworks/vue-vite/package.json index 824b75a845d2..bfaababee6dc 100644 --- a/code/frameworks/vue-vite/package.json +++ b/code/frameworks/vue-vite/package.json @@ -37,10 +37,10 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/vue-webpack5/package.json b/code/frameworks/vue-webpack5/package.json index d8567278a331..ee6d015aae60 100644 --- a/code/frameworks/vue-webpack5/package.json +++ b/code/frameworks/vue-webpack5/package.json @@ -37,10 +37,10 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/vue3-vite/package.json b/code/frameworks/vue3-vite/package.json index daf22b53678c..950443767edd 100644 --- a/code/frameworks/vue3-vite/package.json +++ b/code/frameworks/vue3-vite/package.json @@ -37,10 +37,10 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/vue3-webpack5/package.json b/code/frameworks/vue3-webpack5/package.json index 0c37ea401c5f..5e7f95b842a6 100644 --- a/code/frameworks/vue3-webpack5/package.json +++ b/code/frameworks/vue3-webpack5/package.json @@ -37,10 +37,10 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/web-components-vite/package.json b/code/frameworks/web-components-vite/package.json index 5ed614ae340f..e9f8ad02fbf6 100644 --- a/code/frameworks/web-components-vite/package.json +++ b/code/frameworks/web-components-vite/package.json @@ -37,10 +37,10 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/frameworks/web-components-webpack5/package.json b/code/frameworks/web-components-webpack5/package.json index c3d48784d5d8..21a934770346 100644 --- a/code/frameworks/web-components-webpack5/package.json +++ b/code/frameworks/web-components-webpack5/package.json @@ -40,10 +40,10 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/channels/package.json b/code/lib/channels/package.json index 380837620aec..6b0ffae6aba5 100644 --- a/code/lib/channels/package.json +++ b/code/lib/channels/package.json @@ -61,7 +61,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", @@ -72,7 +73,7 @@ "@storybook/core-events": "workspace:*", "@storybook/global": "^5.0.0", "qs": "^6.10.0", - "telejson": "^7.0.3", + "telejson": "^7.2.0", "tiny-invariant": "^1.3.1" }, "devDependencies": { diff --git a/code/lib/channels/src/postmessage/index.ts b/code/lib/channels/src/postmessage/index.ts index 2de550dbce38..312a8d756295 100644 --- a/code/lib/channels/src/postmessage/index.ts +++ b/code/lib/channels/src/postmessage/index.ts @@ -72,6 +72,7 @@ export class PostMessageTransport implements ChannelTransport { allowFunction, allowSymbol, allowDate, + allowError, allowUndefined, allowClass, maxDepth, @@ -85,6 +86,7 @@ export class PostMessageTransport implements ChannelTransport { allowFunction, allowSymbol, allowDate, + allowError, allowUndefined, allowClass, maxDepth, diff --git a/code/lib/cli/package.json b/code/lib/cli/package.json index 83df1fd56a00..64c1f850a2d5 100644 --- a/code/lib/cli/package.json +++ b/code/lib/cli/package.json @@ -46,7 +46,8 @@ "templates/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/cli/src/initiate.ts b/code/lib/cli/src/initiate.ts index 0b2a75a4a488..5f84963bfc05 100644 --- a/code/lib/cli/src/initiate.ts +++ b/code/lib/cli/src/initiate.ts @@ -3,6 +3,7 @@ import chalk from 'chalk'; import prompts from 'prompts'; import { telemetry } from '@storybook/telemetry'; import { withTelemetry } from '@storybook/core-server'; +import { NxProjectDetectedError } from '@storybook/core-events/server-errors'; import dedent from 'ts-dedent'; import boxen from 'boxen'; @@ -156,11 +157,7 @@ const installStorybook = async ( ); case ProjectType.NX: - throw new Error(dedent` - We have detected Nx in your project. Please use "nx g @nrwl/storybook:configuration" to add Storybook to your project. - - For more information, please see https://nx.dev/packages/storybook - `); + throw new NxProjectDetectedError(); case ProjectType.SOLID: return solidGenerator(packageManager, npmOptions, generatorOptions).then( diff --git a/code/lib/cli/src/upgrade.test.ts b/code/lib/cli/src/upgrade.test.ts index 5e16a20ae938..0c70a5ad2e22 100644 --- a/code/lib/cli/src/upgrade.test.ts +++ b/code/lib/cli/src/upgrade.test.ts @@ -34,6 +34,8 @@ describe.each([ ['@storybook/preset-create-react-app', false], ['@storybook/linter-config', false], ['@storybook/design-system', false], + ['@storybook/addon-styling', false], + ['@storybook/addon-styling-webpack', false], ['@nx/storybook', false], ['@nrwl/storybook', false], ])('isCorePackage', (input, output) => { diff --git a/code/lib/cli/src/upgrade.ts b/code/lib/cli/src/upgrade.ts index af8e53ce6a95..a125e77a950f 100644 --- a/code/lib/cli/src/upgrade.ts +++ b/code/lib/cli/src/upgrade.ts @@ -29,6 +29,8 @@ const excludeList = [ '@storybook/addon-bench', '@storybook/addon-console', '@storybook/addon-postcss', + '@storybook/addon-styling', + '@storybook/addon-styling-webpack', '@storybook/babel-plugin-require-context-hook', '@storybook/bench', '@storybook/builder-vite', diff --git a/code/lib/client-logger/package.json b/code/lib/client-logger/package.json index 5639e61de441..4b2d17703c6f 100644 --- a/code/lib/client-logger/package.json +++ b/code/lib/client-logger/package.json @@ -36,7 +36,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json index 32b03cb05093..083d26b82457 100644 --- a/code/lib/codemod/package.json +++ b/code/lib/codemod/package.json @@ -44,6 +44,13 @@ "check": "../../../scripts/prepare/check.ts", "prep": "../../../scripts/prepare/bundle.ts" }, + "files": [ + "dist/**/*", + "README.md", + "*.js", + "*.d.ts", + "!src/**/*" + ], "dependencies": { "@babel/core": "^7.22.9", "@babel/preset-env": "^7.22.9", diff --git a/code/lib/core-common/package.json b/code/lib/core-common/package.json index 2adc5188412f..39d967f2bff8 100644 --- a/code/lib/core-common/package.json +++ b/code/lib/core-common/package.json @@ -36,7 +36,8 @@ "templates/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/core-events/package.json b/code/lib/core-events/package.json index 286439311dd2..b98966eda27a 100644 --- a/code/lib/core-events/package.json +++ b/code/lib/core-events/package.json @@ -27,21 +27,59 @@ "require": "./dist/index.js", "import": "./dist/index.mjs" }, + "./preview-errors": { + "types": "./dist/errors/preview-errors.d.ts", + "node": "./dist/errors/preview-errors.js", + "require": "./dist/errors/preview-errors.js", + "import": "./dist/errors/preview-errors.mjs" + }, + "./manager-errors": { + "types": "./dist/errors/manager-errors.d.ts", + "node": "./dist/errors/manager-errors.js", + "require": "./dist/errors/manager-errors.js", + "import": "./dist/errors/manager-errors.mjs" + }, + "./server-errors": { + "types": "./dist/errors/server-errors.d.ts", + "node": "./dist/errors/server-errors.js", + "require": "./dist/errors/server-errors.js", + "import": "./dist/errors/server-errors.mjs" + }, "./package.json": "./package.json" }, "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/index.d.ts" + ], + "preview-errors": [ + "dist/errors/preview-errors.d.ts" + ], + "manager-errors": [ + "dist/errors/manager-errors.d.ts" + ], + "server-errors": [ + "dist/errors/server-errors.d.ts" + ] + } + }, "files": [ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", "prep": "../../../scripts/prepare/bundle.ts" }, + "dependencies": { + "ts-dedent": "^2.0.0" + }, "devDependencies": { "typescript": "~4.9.3" }, @@ -50,7 +88,10 @@ }, "bundler": { "entries": [ - "./src/index.ts" + "./src/index.ts", + "./src/errors/preview-errors.ts", + "./src/errors/manager-errors.ts", + "./src/errors/server-errors.ts" ] }, "gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae17" diff --git a/code/lib/core-events/src/errors/README.md b/code/lib/core-events/src/errors/README.md new file mode 100644 index 000000000000..291fb5abe779 --- /dev/null +++ b/code/lib/core-events/src/errors/README.md @@ -0,0 +1,156 @@ +# Storybook Errors + +Storybook provides a utility to manage errors thrown from it. Each error is categorized and coded, and there is an ESLint plugin which enforces their usage, instead of throwing generic errors like `throw new Error()`. + +Storybook errors reside in this package and are categorized into: + +1. **[Preview errors](./preview-errors.ts)** + - Errors which occur in the preview part of Storybook (where user code executes) + - e.g. Rendering issues, etc. + - available in `@storybook/core-events/preview-errors` +2. **[Manager errors](./manager-errors.ts)** + - Errors which occur in the manager part of Storybook (manager UI) + - e.g. Sidebar, addons, Storybook UI, Storybook router, etc. + - available in `@storybook/core-events/server-errors` +3. **[Server errors](./server-errors.ts)** + - Errors which occur in node + - e.g. Storybook init command, dev command, builder errors (Webpack, Vite), etc. + - available in `@storybook/core-events/server-errors` + +## How to create errors + +First, **find which file your error should be part of**, based on the criteria above. +Second use the `StorybookError` class to define custom errors with specific codes and categories for use within the Storybook codebase. Below is a detailed documentation for the error properties: + +### Class Structure + +```typescript +import { StorybookError } from './storybook-error'; +export class YourCustomError extends StorybookError { + readonly category: Category; // The category to which the error belongs. Check the source in client-errors.ts or server-errors.ts for reference. + readonly code: number; // The numeric code for the error. + + template(): string { + // A function that returns the error message. + } +} +``` + +### Properties + +| Name | Type | Description | +| ------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| category | `Category` | The category to which the error belongs. | +| code | `number` | The numeric code for the error. | +| template | `() => string` | Function that returns a properly written error message. | +| data | `Object` | Optional. Data associated with the error. Used to provide additional information in the error message or to be passed to telemetry. | +| documentation | `boolean` or `string` | Optional. Should be set to `true` **if the error is documented on the Storybook website**. If defined as string, it should be a custom documentation link. | + +## Usage Example + +```typescript +// Define a custom error with a numeric code and a static error message template. +export class StorybookIndexGenerationError extends StorybookError { + category = Category.Generic; + code = 1; + + template(): string { + return `Storybook failed when generating an index for your stories. Check the stories field in your main.js`; + } +} + +// Define a custom error with a numeric code and a dynamic error message template based on properties from the constructor. +export class InvalidFileExtensionError extends StorybookError { + category = Category.Validation; + code = 2; + documentation = 'https://some-custom-documentation.com/validation-errors'; + + // extra properties are defined in the constructor via a data property, which is available in any class method + // always use this data Object notation! + constructor(public data: { extension: string }) { + super(); + } + + template(): string { + return `Invalid file extension found: ${this.data.extension}.`; + } +} + +// import the errors where you need them, i.e. +import { + StorybookIndexGenerationError, + InvalidFileExtensionError, +} from '@storybook/core-events/server-errors'; + +throw StorybookIndexGenerationError(); +// "SB_Generic_0001: Storybook failed when generating an index for your stories. Check the stories field in your main.js. + +throw InvalidFileExtensionError({ extension: 'mtsx' }); +// "SB_Validation_0002: Invalid file extension found: mtsx. More info: https://some-custom-documentation.com/validation-errors" +``` + +## How to write a proper error message + +Writing clear and informative error messages is crucial for effective debugging and troubleshooting. A well-crafted error message can save developers and users valuable time. Consider the following guidelines: + +- **Be clear and specific:** Provide straightforward error messages that precisely describe the issue. +- **Include relevant context:** Add details about the error's origin and relevant context to aid troubleshooting. +- **Provide guidance for resolution:** Offer actionable steps to resolve the error or suggest potential fixes. +- **Provide documentation links:** Whenever applicable, provide links for users to get guidance or more context to fix their issues. + + + +βœ… Here are a few recommended examples: + +Long: + +``` +Couldn't find story matching id 'component--button-primary' after HMR. + - Did you just rename a story? + - Did you remove it from your CSF file? + - Are you sure a story with the id 'component--button-primary' exists? + - Please check the values in the stories field of your main.js config and see if they would match your CSF File. + - Also check the browser console and terminal for potential error messages. +``` + +Medium: + +``` +Addon-docs no longer uses configureJsx or mdxBabelOptions in 7.0. + +To update your configuration, please see migration instructions here: + +https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#dropped-addon-docs-manual-babel-configuration +``` + +Short: + +``` +Failed to start Storybook. + +Do you have an error in your \`preview.js\`? Check your Storybook's browser console for errors. +``` + +❌ Here are a few unrecommended examples: + +``` +outputDir is required +``` + +``` +Cannot render story +``` + +``` +no builder configured! +``` + +## What's the motivation for this errors framework? + +Centralizing and categorizing errors offers several advantages: + +Better understanding of what is actually failing: By defining categories, error origins become more evident, easing the debugging process for developers and providing users with actionable insights. + +Improved Telemetry: Aggregating and filtering errors allows better assessment of their impact, which helps in prioritization and tackling the issues. + +Improved Documentation: Categorized errors lead to the creation of a helpful errors page on the Storybook website, benefiting users with better guidance and improving overall accessibility and user experience. diff --git a/code/lib/core-events/src/errors/manager-errors.ts b/code/lib/core-events/src/errors/manager-errors.ts new file mode 100644 index 000000000000..a97c5f8d7035 --- /dev/null +++ b/code/lib/core-events/src/errors/manager-errors.ts @@ -0,0 +1,46 @@ +import { StorybookError } from './storybook-error'; + +/** + * If you can't find a suitable category for your error, create one + * based on the package name/file path of which the error is thrown. + * For instance: + * If it's from @storybook/client-logger, then MANAGER_CLIENT-LOGGER + * + * Categories are prefixed by a logical grouping, e.g. MANAGER_ + * to prevent manager and preview errors from having the same category and error code. + */ +export enum Category { + MANAGER_UNCAUGHT = 'MANAGER_UNCAUGHT', + MANAGER_UI = 'MANAGER_UI', + MANAGER_API = 'MANAGER_API', + MANAGER_CLIENT_LOGGER = 'MANAGER_CLIENT-LOGGER', + MANAGER_CHANNELS = 'MANAGER_CHANNELS', + MANAGER_CORE_EVENTS = 'MANAGER_CORE-EVENTS', + MANAGER_ROUTER = 'MANAGER_ROUTER', + MANAGER_THEMING = 'MANAGER_THEMING', +} + +export class ProviderDoesNotExtendBaseProviderError extends StorybookError { + readonly category = Category.MANAGER_UI; + + readonly code = 1; + + template() { + return `The Provider passed into Storybook's UI is not extended from the base Provider. Please check your Provider implementation.`; + } +} + +export class UncaughtManagerError extends StorybookError { + readonly category = Category.MANAGER_UNCAUGHT; + + readonly code = 1; + + constructor(public error: Error) { + super(error.message); + this.stack = error.stack; + } + + template() { + return this.message; + } +} diff --git a/code/lib/core-events/src/errors/message-reference.png b/code/lib/core-events/src/errors/message-reference.png new file mode 100644 index 000000000000..b829f93689ea Binary files /dev/null and b/code/lib/core-events/src/errors/message-reference.png differ diff --git a/code/lib/core-events/src/errors/preview-errors.ts b/code/lib/core-events/src/errors/preview-errors.ts new file mode 100644 index 000000000000..3a2fe93aecf5 --- /dev/null +++ b/code/lib/core-events/src/errors/preview-errors.ts @@ -0,0 +1,69 @@ +import dedent from 'ts-dedent'; +import { StorybookError } from './storybook-error'; + +/** + * If you can't find a suitable category for your error, create one + * based on the package name/file path of which the error is thrown. + * For instance: + * If it's from @storybook/client-logger, then CLIENT-LOGGER + * + * Categories are prefixed by a logical grouping, e.g. PREVIEW_ or FRAMEWORK_ + * to prevent manager and preview errors from having the same category and error code. + */ +export enum Category { + PREVIEW_CLIENT_LOGGER = 'PREVIEW_CLIENT-LOGGER', + PREVIEW_CHANNELS = 'PREVIEW_CHANNELS', + PREVIEW_CORE_EVENTS = 'PREVIEW_CORE-EVENTS', + PREVIEW_INSTRUMENTER = 'PREVIEW_INSTRUMENTER', + PREVIEW_API = 'PREVIEW_API', + PREVIEW_REACT_DOM_SHIM = 'PREVIEW_REACT-DOM-SHIM', + PREVIEW_ROUTER = 'PREVIEW_ROUTER', + PREVIEW_THEMING = 'PREVIEW_THEMING', + FRAMEWORK_ANGULAR = 'FRAMEWORK_ANGULAR', + FRAMEWORK_EMBER = 'FRAMEWORK_EMBER', + FRAMEWORK_HTML_VITE = 'FRAMEWORK_HTML-VITE', + FRAMEWORK_HTML_WEBPACK5 = 'FRAMEWORK_HTML-WEBPACK5', + FRAMEWORK_NEXTJS = 'FRAMEWORK_NEXTJS', + FRAMEWORK_PREACT_VITE = 'FRAMEWORK_PREACT-VITE', + FRAMEWORK_PREACT_WEBPACK5 = 'FRAMEWORK_PREACT-WEBPACK5', + FRAMEWORK_REACT_VITE = 'FRAMEWORK_REACT-VITE', + FRAMEWORK_REACT_WEBPACK5 = 'FRAMEWORK_REACT-WEBPACK5', + FRAMEWORK_SERVER_WEBPACK5 = 'FRAMEWORK_SERVER-WEBPACK5', + FRAMEWORK_SVELTE_VITE = 'FRAMEWORK_SVELTE-VITE', + FRAMEWORK_SVELTE_WEBPACK5 = 'FRAMEWORK_SVELTE-WEBPACK5', + FRAMEWORK_SVELTEKIT = 'FRAMEWORK_SVELTEKIT', + FRAMEWORK_VUE_VITE = 'FRAMEWORK_VUE-VITE', + FRAMEWORK_VUE_WEBPACK5 = 'FRAMEWORK_VUE-WEBPACK5', + FRAMEWORK_VUE3_VITE = 'FRAMEWORK_VUE3-VITE', + FRAMEWORK_VUE3_WEBPACK5 = 'FRAMEWORK_VUE3-WEBPACK5', + FRAMEWORK_WEB_COMPONENTS_VITE = 'FRAMEWORK_WEB-COMPONENTS-VITE', + FRAMEWORK_WEB_COMPONENTS_WEBPACK5 = 'FRAMEWORK_WEB-COMPONENTS-WEBPACK5', + RENDERER_HTML = 'RENDERER_HTML', + RENDERER_PREACT = 'RENDERER_PREACT', + RENDERER_REACT = 'RENDERER_REACT', + RENDERER_SERVER = 'RENDERER_SERVER', + RENDERER_SVELTE = 'RENDERER_SVELTE', + RENDERER_VUE = 'RENDERER_VUE', + RENDERER_VUE3 = 'RENDERER_VUE3', + RENDERER_WEB_COMPONENTS = 'RENDERER_WEB-COMPONENTS', +} + +export class MissingStoryAfterHmrError extends StorybookError { + readonly category = Category.PREVIEW_API; + + readonly code = 1; + + constructor(public data: { storyId: string }) { + super(); + } + + template() { + return dedent` + Couldn't find story matching id '${this.data.storyId}' after HMR. + - Did you just rename a story? + - Did you remove it from your CSF file? + - Are you sure a story with the id '${this.data.storyId}' exists? + - Please check the values in the stories field of your main.js config and see if they would match your CSF File. + - Also check the browser console and terminal for potential error messages.`; + } +} diff --git a/code/lib/core-events/src/errors/server-errors.ts b/code/lib/core-events/src/errors/server-errors.ts new file mode 100644 index 000000000000..695b4cb09306 --- /dev/null +++ b/code/lib/core-events/src/errors/server-errors.ts @@ -0,0 +1,46 @@ +import dedent from 'ts-dedent'; +import { StorybookError } from './storybook-error'; + +/** + * If you can't find a suitable category for your error, create one + * based on the package name/file path of which the error is thrown. + * For instance: + * If it's from @storybook/node-logger, then NODE-LOGGER + * If it's from a package that is too broad, e.g. @storybook/cli in the init command, then use a combination like CLI_INIT + */ +export enum Category { + CLI = 'CLI', + CLI_INIT = 'CLI_INIT', + CLI_AUTOMIGRATE = 'CLI_AUTOMIGRATE', + CLI_UPGRADE = 'CLI_UPGRADE', + CLI_ADD = 'CLI_ADD', + CODEMOD = 'CODEMOD', + CORE_SERVER = 'CORE-SERVER', + CSF_PLUGIN = 'CSF-PLUGIN', + CSF_TOOLS = 'CSF-TOOLS', + CORE_COMMON = 'CORE-COMMON', + NODE_LOGGER = 'NODE-LOGGER', + TELEMETRY = 'TELEMETRY', + BUILDER_MANAGER = 'BUILDER-MANAGER', + BUILDER_VITE = 'BUILDER-VITE', + BUILDER_WEBPACK5 = 'BUILDER-WEBPACK5', + SOURCE_LOADER = 'SOURCE-LOADER', + POSTINSTALL = 'POSTINSTALL', + DOCS_TOOLS = 'DOCS-TOOLS', + CORE_WEBPACK = 'CORE-WEBPACK', +} + +export class NxProjectDetectedError extends StorybookError { + readonly category = Category.CLI_INIT; + + readonly code = 1; + + public readonly documentation = 'https://nx.dev/packages/storybook'; + + template() { + return dedent` + We have detected Nx in your project. Nx has its own Storybook initializer, so please use it instead. + Run "nx g @nx/storybook:configuration" to add Storybook to your project. + `; + } +} diff --git a/code/lib/core-events/src/errors/storybook-error.test.ts b/code/lib/core-events/src/errors/storybook-error.test.ts new file mode 100644 index 000000000000..328c27a827e4 --- /dev/null +++ b/code/lib/core-events/src/errors/storybook-error.test.ts @@ -0,0 +1,45 @@ +import { StorybookError } from './storybook-error'; + +describe('StorybookError', () => { + class TestError extends StorybookError { + category = 'TEST_CATEGORY'; + + code = 123; + + template() { + return 'This is a test error.'; + } + } + + it('should generate the correct error name', () => { + const error = new TestError(); + expect(error.name).toBe('SB_TEST_CATEGORY_0123'); + }); + + it('should generate the correct message without documentation link', () => { + const error = new TestError(); + const expectedMessage = 'This is a test error.'; + expect(error.message).toBe(expectedMessage); + }); + + it('should generate the correct message with internal documentation link', () => { + const error = new TestError(); + error.documentation = true; + const expectedMessage = + 'This is a test error.\n\nMore info: https://storybook.js.org/error/SB_TEST_CATEGORY_0123'; + expect(error.message).toBe(expectedMessage); + }); + + it('should generate the correct message with external documentation link', () => { + const error = new TestError(); + error.documentation = 'https://example.com/docs/test-error'; + const expectedMessage = + 'This is a test error.\n\nMore info: https://example.com/docs/test-error'; + expect(error.message).toBe(expectedMessage); + }); + + it('should have default documentation value of false', () => { + const error = new TestError(); + expect(error.documentation).toBe(false); + }); +}); diff --git a/code/lib/core-events/src/errors/storybook-error.ts b/code/lib/core-events/src/errors/storybook-error.ts new file mode 100644 index 000000000000..3a0abbf463f5 --- /dev/null +++ b/code/lib/core-events/src/errors/storybook-error.ts @@ -0,0 +1,58 @@ +export abstract class StorybookError extends Error { + /** + * Category of the error. Used to classify the type of error, e.g., 'PREVIEW_API'. + */ + abstract readonly category: string; + + /** + * Code representing the error. Used to uniquely identify the error, e.g., 1. + */ + abstract readonly code: number; + + /** + * A properly written error message template for this error. + * @see https://github.com/storybookjs/storybook/blob/next/code/lib/core-events/src/errors/README.md#how-to-write-a-proper-error-message + */ + abstract template(): string; + + /** + * Data associated with the error. Used to provide additional information in the error message or to be passed to telemetry. + */ + public readonly data = {}; + + /** + * Specifies the documentation for the error. + * - If `true`, links to a documentation page on the Storybook website (make sure it exists before enabling). + * - If a string, uses the provided URL for documentation (external or FAQ links). + * - If `false` (default), no documentation link is added. + */ + public documentation: boolean | string = false; + + /** + * Flag used to easily determine if the error originates from Storybook. + */ + readonly fromStorybook: true = true as const; + + /** + * Overrides the default `Error.name` property in the format: SB__. + */ + get name() { + const paddedCode = String(this.code).padStart(4, '0'); + return `SB_${this.category}_${paddedCode}` as `SB_${this['category']}_${string}`; + } + + /** + * Generates the error message along with additional documentation link (if applicable). + */ + get message() { + let page: string | undefined; + + if (this.documentation === true) { + page = `https://storybook.js.org/error/${this.name}`; + } else if (typeof this.documentation === 'string') { + page = this.documentation; + } + + return this.template() + (page != null ? `\n\nMore info: ${page}` : ''); + } +} diff --git a/code/lib/core-events/src/index.ts b/code/lib/core-events/src/index.ts index beec768961de..d8d6c41c01d3 100644 --- a/code/lib/core-events/src/index.ts +++ b/code/lib/core-events/src/index.ts @@ -69,6 +69,7 @@ enum events { RESULT_WHATS_NEW_DATA = 'resultWhatsNewData', SET_WHATS_NEW_CACHE = 'setWhatsNewCache', TOGGLE_WHATS_NEW_NOTIFICATIONS = 'toggleWhatsNewNotifications', + TELEMETRY_ERROR = 'telemetryError', } // Enables: `import Events from ...` @@ -120,9 +121,11 @@ export const { RESULT_WHATS_NEW_DATA, SET_WHATS_NEW_CACHE, TOGGLE_WHATS_NEW_NOTIFICATIONS, + TELEMETRY_ERROR, } = events; // Used to break out of the current render without showing a redbox +// eslint-disable-next-line local-rules/no-uncategorized-errors export const IGNORED_EXCEPTION = new Error('ignoredException'); export interface WhatsNewCache { diff --git a/code/lib/core-server/package.json b/code/lib/core-server/package.json index 11c591f9fd70..305d010bb28f 100644 --- a/code/lib/core-server/package.json +++ b/code/lib/core-server/package.json @@ -47,7 +47,8 @@ "public/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", @@ -89,7 +90,7 @@ "read-pkg-up": "^7.0.1", "semver": "^7.3.7", "serve-favicon": "^2.5.0", - "telejson": "^7.0.3", + "telejson": "^7.2.0", "tiny-invariant": "^1.3.1", "ts-dedent": "^2.0.0", "util": "^0.12.4", diff --git a/code/lib/core-server/src/presets/common-preset.ts b/code/lib/core-server/src/presets/common-preset.ts index 7e13ade9ddf0..36816316ccbf 100644 --- a/code/lib/core-server/src/presets/common-preset.ts +++ b/code/lib/core-server/src/presets/common-preset.ts @@ -25,6 +25,7 @@ import type { WhatsNewCache, WhatsNewData } from '@storybook/core-events'; import { REQUEST_WHATS_NEW_DATA, RESULT_WHATS_NEW_DATA, + TELEMETRY_ERROR, SET_WHATS_NEW_CACHE, TOGGLE_WHATS_NEW_NOTIFICATIONS, } from '@storybook/core-events'; @@ -329,5 +330,17 @@ export const experimental_serverChannel = async ( } ); + channel.on(TELEMETRY_ERROR, async (error) => { + const isTelemetryEnabled = coreOptions.disableTelemetry !== true; + + if (isTelemetryEnabled) { + await sendTelemetryError(error, 'browser', { + cliOptions: options, + presetOptions: { ...options, corePresets: [], overridePresets: [] }, + skipPrompt: true, + }); + } + }); + return channel; }; diff --git a/code/lib/core-server/src/utils/StoryIndexGenerator.ts b/code/lib/core-server/src/utils/StoryIndexGenerator.ts index 2f95114113ad..d2a195a31463 100644 --- a/code/lib/core-server/src/utils/StoryIndexGenerator.ts +++ b/code/lib/core-server/src/utils/StoryIndexGenerator.ts @@ -338,8 +338,7 @@ export class StoryIndexGenerator { autodocs === true || (autodocs === 'tag' && hasAutodocsTag) || isStoriesMdx; if (createDocEntry) { - const name = this.options.docs.defaultName; - invariant(name, 'expected a defaultName property in options.docs'); + const name = this.options.docs.defaultName ?? 'Docs'; const { metaId } = indexInputs[0]; const { title } = entries[0]; const tags = indexInputs[0].tags || []; @@ -407,8 +406,7 @@ export class StoryIndexGenerator { // a) it is a stories.mdx transpiled to CSF, OR // b) we have docs page enabled for this file if (componentTags.includes(STORIES_MDX_TAG) || autodocsOptedIn) { - const name = this.options.docs.defaultName; - invariant(name, 'expected a defaultName property in options.docs'); + const name = this.options.docs.defaultName ?? 'Docs'; invariant(csf.meta.title, 'expected a title property in csf.meta'); const id = toId(csf.meta.id || csf.meta.title, name); entries.unshift({ @@ -511,8 +509,7 @@ export class StoryIndexGenerator { title, "makeTitle created an undefined title. This happens when a specifier's doesn't have any matches in its fileName" ); - const { defaultName } = this.options.docs; - invariant(defaultName, 'expected a defaultName property in options.docs'); + const defaultName = this.options.docs.defaultName ?? 'Docs'; const name = result.name || diff --git a/code/lib/core-server/src/withTelemetry.ts b/code/lib/core-server/src/withTelemetry.ts index 706439b4bfd7..49b8c6c79699 100644 --- a/code/lib/core-server/src/withTelemetry.ts +++ b/code/lib/core-server/src/withTelemetry.ts @@ -85,6 +85,25 @@ export async function sendTelemetryError( error instanceof Error, 'The error passed to sendTelemetryError was not an Error, please only send Errors' ); + + let storybookErrorProperties = {}; + // if it's an UNCATEGORIZED error, it won't have a coded name, so we just pass the category and source + if ((error as any).category) { + const { category } = error as any; + storybookErrorProperties = { + category, + }; + } + + if ((error as any).fromStorybook) { + const { code, name } = error as any; + storybookErrorProperties = { + ...storybookErrorProperties, + code, + name, + }; + } + await telemetry( 'error', { @@ -92,6 +111,7 @@ export async function sendTelemetryError( precedingUpgrade, error: errorLevel === 'full' ? error : undefined, errorHash: oneWayHash(error.message), + ...storybookErrorProperties, }, { immediate: true, diff --git a/code/lib/core-webpack/package.json b/code/lib/core-webpack/package.json index 7758519e1414..3f1ddffc3a66 100644 --- a/code/lib/core-webpack/package.json +++ b/code/lib/core-webpack/package.json @@ -36,7 +36,8 @@ "templates/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/csf-plugin/package.json b/code/lib/csf-plugin/package.json index e4eb2f02a99a..bb2cbd8f9c37 100644 --- a/code/lib/csf-plugin/package.json +++ b/code/lib/csf-plugin/package.json @@ -36,7 +36,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/csf-tools/package.json b/code/lib/csf-tools/package.json index ba697fb1c766..ed12dbfb8c70 100644 --- a/code/lib/csf-tools/package.json +++ b/code/lib/csf-tools/package.json @@ -34,7 +34,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/docs-tools/package.json b/code/lib/docs-tools/package.json index 16f98bb651fa..49d36403f753 100644 --- a/code/lib/docs-tools/package.json +++ b/code/lib/docs-tools/package.json @@ -36,7 +36,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/instrumenter/package.json b/code/lib/instrumenter/package.json index 52e4a1cde104..22f218c85f73 100644 --- a/code/lib/instrumenter/package.json +++ b/code/lib/instrumenter/package.json @@ -36,7 +36,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/manager-api/package.json b/code/lib/manager-api/package.json index cd5660f56075..95ac20b8a0bb 100644 --- a/code/lib/manager-api/package.json +++ b/code/lib/manager-api/package.json @@ -35,7 +35,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", @@ -55,7 +56,7 @@ "memoizerific": "^1.11.3", "semver": "^7.3.7", "store2": "^2.14.2", - "telejson": "^7.0.3", + "telejson": "^7.2.0", "ts-dedent": "^2.0.0" }, "devDependencies": { diff --git a/code/lib/manager-api/src/index.tsx b/code/lib/manager-api/src/index.tsx index 52c2f3000986..ab247114e440 100644 --- a/code/lib/manager-api/src/index.tsx +++ b/code/lib/manager-api/src/index.tsx @@ -65,6 +65,7 @@ import * as version from './modules/versions'; import * as whatsnew from './modules/whatsnew'; import * as globals from './modules/globals'; +import type { ModuleFn } from './lib/types'; export * from './lib/shortcut'; @@ -76,14 +77,6 @@ export { ActiveTabs }; export const ManagerContext = createContext({ api: undefined, state: getInitialState({}) }); -export type ModuleArgs = RouterData & - API_ProviderData & { - mode?: 'production' | 'development'; - state: State; - fullAPI: API; - store: Store; - }; - export type State = layout.SubState & stories.SubState & refs.SubState & @@ -152,28 +145,10 @@ export const combineParameters = (...parameterSets: Parameters[]) => return undefined; }); -interface ModuleWithInit { - init: () => void | Promise; - api: APIType; - state: StateType; -} - -type ModuleWithoutInit = Omit< - ModuleWithInit, - 'init' ->; - -export type ModuleFn = ( - m: ModuleArgs, - options?: any -) => HasInit extends true - ? ModuleWithInit - : ModuleWithoutInit; - class ManagerProvider extends Component { api: API = {} as API; - modules: (ModuleWithInit | ModuleWithoutInit)[]; + modules: ReturnType[]; static displayName = 'Manager'; @@ -424,11 +399,13 @@ export function useSharedState(stateId: string, defaultState?: S) { addonStateCache[stateId] ? addonStateCache[stateId] : defaultState ); - if (api.getAddonState(stateId) && api.getAddonState(stateId) !== state) { - api.setAddonState(stateId, state).then((s) => { - addonStateCache[stateId] = s; - }); - } + useEffect(() => { + if (api.getAddonState(stateId) === undefined && api.getAddonState(stateId) !== state) { + api.setAddonState(stateId, state).then((s) => { + addonStateCache[stateId] = s; + }); + } + }, [api]); const setState = (s: S | API_StateMerger, options?: Options) => { const result = api.setAddonState(stateId, s, options); diff --git a/code/lib/manager-api/src/lib/stories.ts b/code/lib/manager-api/src/lib/stories.ts index 51823a7cfa6b..b48cbd51b877 100644 --- a/code/lib/manager-api/src/lib/stories.ts +++ b/code/lib/manager-api/src/lib/stories.ts @@ -23,7 +23,7 @@ import type { StoryIndexV2, } from '@storybook/types'; // eslint-disable-next-line import/no-cycle -import { type API, combineParameters } from '../index'; +import { type API, combineParameters, type State } from '../index'; import merge from './merge'; const TITLE_PATH_SEPARATOR = /\s*\/\s*/; @@ -45,12 +45,9 @@ export const denormalizeStoryParameters = ({ export const transformSetStoriesStoryDataToStoriesHash = ( data: SetStoriesStoryData, - { provider, docsOptions }: { provider: API_Provider; docsOptions: DocsOptions } + options: ToStoriesHashOptions ) => - transformStoryIndexToStoriesHash(transformSetStoriesStoryDataToPreparedStoryIndex(data), { - provider, - docsOptions, - }); + transformStoryIndexToStoriesHash(transformSetStoriesStoryDataToPreparedStoryIndex(data), options); const transformSetStoriesStoryDataToPreparedStoryIndex = ( stories: SetStoriesStoryData @@ -136,15 +133,16 @@ export const transformStoryIndexV3toV4 = (index: StoryIndexV3): API_PreparedStor }; }; +type ToStoriesHashOptions = { + provider: API_Provider; + docsOptions: DocsOptions; + filters: State['filters']; + status: State['status']; +}; + export const transformStoryIndexToStoriesHash = ( input: API_PreparedStoryIndex | StoryIndexV2 | StoryIndexV3, - { - provider, - docsOptions, - }: { - provider: API_Provider; - docsOptions: DocsOptions; - } + { provider, docsOptions, filters, status }: ToStoriesHashOptions ): API_IndexHash => { if (!input.v) { throw new Error('Composition: Missing stories.json version'); @@ -155,13 +153,25 @@ export const transformStoryIndexToStoriesHash = ( index = index.v === 3 ? transformStoryIndexV3toV4(index as any) : index; index = index as API_PreparedStoryIndex; - const entryValues = Object.values(index.entries); + const entryValues = Object.values(index.entries).filter((entry) => { + let result = true; + + Object.values(filters).forEach((filter) => { + if (result === false) { + return; + } + result = filter({ ...entry, status: status[entry.id] }); + }); + + return result; + }); + const { sidebar = {} } = provider.getConfig(); const { showRoots, collapsedRoots = [], renderLabel } = sidebar; const setShowRoots = typeof showRoots !== 'undefined'; - const storiesHashOutOfOrder = Object.values(entryValues).reduce((acc, item) => { + const storiesHashOutOfOrder = entryValues.reduce((acc, item) => { if (docsOptions.docsMode && item.type !== 'docs') { return acc; } diff --git a/code/lib/manager-api/src/lib/types.tsx b/code/lib/manager-api/src/lib/types.tsx new file mode 100644 index 000000000000..a195f514999e --- /dev/null +++ b/code/lib/manager-api/src/lib/types.tsx @@ -0,0 +1,22 @@ +import type { API_ProviderData } from '@storybook/types'; +import type { RouterData } from '@storybook/router'; + +import type { API, State } from '../index'; +import type Store from '../store'; + +export type ModuleFn = ( + m: ModuleArgs, + options?: any +) => { + init?: () => void | Promise; + api: APIType; + state: StateType; +}; + +export type ModuleArgs = RouterData & + API_ProviderData & { + mode?: 'production' | 'development'; + state: State; + fullAPI: API; + store: Store; + }; diff --git a/code/lib/manager-api/src/modules/addons.ts b/code/lib/manager-api/src/modules/addons.ts index 87027262d631..84fd51d7f206 100644 --- a/code/lib/manager-api/src/modules/addons.ts +++ b/code/lib/manager-api/src/modules/addons.ts @@ -7,7 +7,7 @@ import type { API_StateMerger, } from '@storybook/types'; import { Addon_TypesEnum } from '@storybook/types'; -import type { ModuleFn } from '../index'; +import type { ModuleFn } from '../lib/types'; import type { Options } from '../store'; export interface SubState { diff --git a/code/lib/manager-api/src/modules/channel.ts b/code/lib/manager-api/src/modules/channel.ts index e6c178ae32b4..c83c342c5253 100644 --- a/code/lib/manager-api/src/modules/channel.ts +++ b/code/lib/manager-api/src/modules/channel.ts @@ -3,7 +3,8 @@ import { STORIES_COLLAPSE_ALL, STORIES_EXPAND_ALL } from '@storybook/core-events import type { Listener } from '@storybook/channels'; import type { API_Provider } from '@storybook/types'; -import type { API, ModuleFn } from '../index'; +import type { API } from '../index'; +import type { ModuleFn } from '../lib/types'; export interface SubAPI { /** diff --git a/code/lib/manager-api/src/modules/globals.ts b/code/lib/manager-api/src/modules/globals.ts index 9b8d47069564..393deb58c4a2 100644 --- a/code/lib/manager-api/src/modules/globals.ts +++ b/code/lib/manager-api/src/modules/globals.ts @@ -3,7 +3,7 @@ import { logger } from '@storybook/client-logger'; import { dequal as deepEqual } from 'dequal'; import type { SetGlobalsPayload, Globals, GlobalTypes } from '@storybook/types'; -import type { ModuleFn } from '../index'; +import type { ModuleFn } from '../lib/types'; // eslint-disable-next-line import/no-cycle import { getEventMetadata } from '../lib/events'; @@ -32,7 +32,7 @@ export interface SubAPI { updateGlobals: (newGlobals: Globals) => void; } -export const init: ModuleFn = ({ store, fullAPI }) => { +export const init: ModuleFn = ({ store, fullAPI, provider }) => { const api: SubAPI = { getGlobals() { return store.getState().globals; @@ -42,7 +42,7 @@ export const init: ModuleFn = ({ store, fullAPI }) => { }, updateGlobals(newGlobals) { // Only emit the message to the local ref - fullAPI.emit(UPDATE_GLOBALS, { + provider.channel.emit(UPDATE_GLOBALS, { globals: newGlobals, options: { target: 'storybook-preview-iframe', @@ -62,8 +62,9 @@ export const init: ModuleFn = ({ store, fullAPI }) => { } }; - const initModule = () => { - fullAPI.on(GLOBALS_UPDATED, function handleGlobalsUpdated({ globals }: { globals: Globals }) { + provider.channel.on( + GLOBALS_UPDATED, + function handleGlobalsUpdated({ globals }: { globals: Globals }) { const { ref } = getEventMetadata(this, fullAPI); if (!ref) { @@ -73,10 +74,13 @@ export const init: ModuleFn = ({ store, fullAPI }) => { 'received a GLOBALS_UPDATED from a non-local ref. This is not currently supported.' ); } - }); + } + ); - // Emitted by the preview on initialization - fullAPI.on(SET_GLOBALS, function handleSetStories({ globals, globalTypes }: SetGlobalsPayload) { + // Emitted by the preview on initialization + provider.channel.on( + SET_GLOBALS, + function handleSetStories({ globals, globalTypes }: SetGlobalsPayload) { const { ref } = getEventMetadata(this, fullAPI); const currentGlobals = store.getState()?.globals; @@ -93,12 +97,11 @@ export const init: ModuleFn = ({ store, fullAPI }) => { ) { api.updateGlobals(currentGlobals); } - }); - }; + } + ); return { api, state, - init: initModule, }; }; diff --git a/code/lib/manager-api/src/modules/layout.ts b/code/lib/manager-api/src/modules/layout.ts index bbe43af9863d..f37e51e19942 100644 --- a/code/lib/manager-api/src/modules/layout.ts +++ b/code/lib/manager-api/src/modules/layout.ts @@ -7,7 +7,8 @@ import type { ThemeVars } from '@storybook/theming'; import type { API_Layout, API_PanelPositions, API_UI } from '@storybook/types'; import merge from '../lib/merge'; -import type { State, ModuleFn } from '../index'; +import type { State } from '../index'; +import type { ModuleFn } from '../lib/types'; const { document } = global; @@ -284,7 +285,7 @@ export const init: ModuleFn = ({ store, provider, singleStory, fullAPI }) => { state: merge(api.getInitialOptions(), persisted), init: () => { api.setOptions(merge(api.getInitialOptions(), persisted)); - fullAPI.on(SET_CONFIG, () => { + provider.channel.on(SET_CONFIG, () => { api.setOptions(merge(api.getInitialOptions(), persisted)); }); }, diff --git a/code/lib/manager-api/src/modules/notifications.ts b/code/lib/manager-api/src/modules/notifications.ts index 1f1059dc1939..83e95d3928ca 100644 --- a/code/lib/manager-api/src/modules/notifications.ts +++ b/code/lib/manager-api/src/modules/notifications.ts @@ -1,5 +1,5 @@ import type { API_Notification } from '@storybook/types'; -import type { ModuleFn } from '../index'; +import type { ModuleFn } from '../lib/types'; export interface SubState { notifications: API_Notification[]; diff --git a/code/lib/manager-api/src/modules/provider.ts b/code/lib/manager-api/src/modules/provider.ts index 272fc0d1839c..c150bf90bf8b 100644 --- a/code/lib/manager-api/src/modules/provider.ts +++ b/code/lib/manager-api/src/modules/provider.ts @@ -1,11 +1,11 @@ import type { API_IframeRenderer } from '@storybook/types'; -import type { ModuleFn } from '../index'; +import type { ModuleFn } from '../lib/types'; export interface SubAPI { renderPreview?: API_IframeRenderer; } -export const init: ModuleFn = ({ provider, fullAPI }) => { +export const init: ModuleFn = ({ provider, fullAPI }) => { return { api: provider.renderPreview ? { renderPreview: provider.renderPreview } : {}, state: {}, diff --git a/code/lib/manager-api/src/modules/refs.ts b/code/lib/manager-api/src/modules/refs.ts index 5fe2462dfbb3..02884665cf5a 100644 --- a/code/lib/manager-api/src/modules/refs.ts +++ b/code/lib/manager-api/src/modules/refs.ts @@ -15,7 +15,7 @@ import { transformStoryIndexToStoriesHash, } from '../lib/stories'; -import type { ModuleFn } from '../index'; +import type { ModuleFn } from '../lib/types'; const { location, fetch } = global; @@ -154,7 +154,7 @@ const map = ( return input; }; -export const init: ModuleFn = ( +export const init: ModuleFn = ( { store, provider, singleStory, docsOptions = {} }, { runCheck = true } = {} ) => { @@ -283,10 +283,15 @@ export const init: ModuleFn = ( if (setStoriesData) { index = transformSetStoriesStoryDataToStoriesHash( map(setStoriesData, ref, { storyMapper }), - { provider, docsOptions } + { provider, docsOptions, filters: {}, status: {} } ); } else if (storyIndex) { - index = transformStoryIndexToStoriesHash(storyIndex, { provider, docsOptions }); + index = transformStoryIndexToStoriesHash(storyIndex, { + provider, + docsOptions, + filters: {}, + status: {}, + }); } if (index) index = addRefIds(index, ref); diff --git a/code/lib/manager-api/src/modules/settings.ts b/code/lib/manager-api/src/modules/settings.ts index 2439a5954bcc..4c850f9aca1a 100644 --- a/code/lib/manager-api/src/modules/settings.ts +++ b/code/lib/manager-api/src/modules/settings.ts @@ -1,5 +1,5 @@ import type { API_Settings, StoryId } from '@storybook/types'; -import type { ModuleFn } from '../index'; +import type { ModuleFn } from '../lib/types'; export interface SubAPI { storeSelection: () => void; @@ -78,5 +78,8 @@ export const init: ModuleFn = ({ store, navigate, fullAPI }) = }, }; - return { state: { settings: { lastTrackedStoryId: null } }, api }; + return { + state: { settings: { lastTrackedStoryId: null } }, + api, + }; }; diff --git a/code/lib/manager-api/src/modules/shortcuts.ts b/code/lib/manager-api/src/modules/shortcuts.ts index 8dcf942f4bc7..be5592e98118 100644 --- a/code/lib/manager-api/src/modules/shortcuts.ts +++ b/code/lib/manager-api/src/modules/shortcuts.ts @@ -2,7 +2,7 @@ import { global } from '@storybook/global'; import { FORCE_REMOUNT, PREVIEW_KEYDOWN } from '@storybook/core-events'; -import type { ModuleFn } from '../index'; +import type { ModuleFn } from '../lib/types'; import type { KeyboardEventLike } from '../lib/shortcut'; import { shortcutMatchesShortcut, eventToShortcut } from '../lib/shortcut'; @@ -152,7 +152,7 @@ function focusInInput(event: KeyboardEvent) { return /input|textarea/i.test(target.tagName) || target.getAttribute('contenteditable') !== null; } -export const init: ModuleFn = ({ store, fullAPI }) => { +export const init: ModuleFn = ({ store, fullAPI, provider }) => { const api: SubAPI = { // Getting and setting shortcuts getShortcutKeys(): API_Shortcuts { @@ -397,13 +397,13 @@ export const init: ModuleFn = ({ store, fullAPI }) => { // Listen for keydown events in the manager document.addEventListener('keydown', (event: KeyboardEvent) => { if (!focusInInput(event)) { - fullAPI.handleKeydownEvent(event); + api.handleKeydownEvent(event); } }); // Also listen to keydown events sent over the channel - fullAPI.on(PREVIEW_KEYDOWN, (data: { event: KeyboardEventLike }) => { - fullAPI.handleKeydownEvent(data.event); + provider.channel.on(PREVIEW_KEYDOWN, (data: { event: KeyboardEventLike }) => { + api.handleKeydownEvent(data.event); }); }; diff --git a/code/lib/manager-api/src/modules/stories.ts b/code/lib/manager-api/src/modules/stories.ts index 78b8b086e106..c69e5e23cdb5 100644 --- a/code/lib/manager-api/src/modules/stories.ts +++ b/code/lib/manager-api/src/modules/stories.ts @@ -21,6 +21,7 @@ import type { API_ViewMode, API_StatusState, API_StatusUpdate, + API_FilterFunction, } from '@storybook/types'; import { PRELOAD_ENTRIES, @@ -39,6 +40,7 @@ import { STORY_MISSING, DOCS_PREPARED, SET_CURRENT_STORY, + SET_CONFIG, } from '@storybook/core-events'; import { logger } from '@storybook/client-logger'; @@ -53,7 +55,8 @@ import { addPreparedStories, } from '../lib/stories'; -import type { ComposedRef, ModuleFn } from '../index'; +import type { ComposedRef } from '../index'; +import type { ModuleFn } from '../lib/types'; const { FEATURES, fetch } = global; const STORY_INDEX_PATH = './index.json'; @@ -69,8 +72,10 @@ type DocsUpdate = Partial>; export interface SubState extends API_LoadedRefData { storyId: StoryId; + internal_index?: API_PreparedStoryIndex; viewMode: API_ViewMode; status: API_StatusState; + filters: Record; } export interface SubAPI { @@ -259,6 +264,14 @@ export interface SubAPI { * @returns {Promise} A promise that resolves when the status has been updated. */ experimental_updateStatus: (addonId: string, update: API_StatusUpdate) => Promise; + /** + * Updates the filtering of the index. + * + * @param {string} addonId - The ID of the addon to update. + * @param {API_FilterFunction} filterFunction - A function that returns a boolean based on the story, index and status. + * @returns {Promise} A promise that resolves when the state has been updated. + */ + experimental_setFilter: (addonId: string, filterFunction: API_FilterFunction) => Promise; } const removedOptions = ['enableShortcuts', 'theme', 'showRoots']; @@ -278,7 +291,7 @@ function removeRemovedOptions = Record = ({ +export const init: ModuleFn = ({ fullAPI, store, navigate, @@ -468,7 +481,7 @@ export const init: ModuleFn = ({ }, updateStoryArgs: (story, updatedArgs) => { const { id: storyId, refId } = story; - fullAPI.emit(UPDATE_STORY_ARGS, { + provider.channel.emit(UPDATE_STORY_ARGS, { storyId, updatedArgs, options: { target: refId }, @@ -476,7 +489,7 @@ export const init: ModuleFn = ({ }, resetStoryArgs: (story, argNames?: [string]) => { const { id: storyId, refId } = story; - fullAPI.emit(RESET_STORY_ARGS, { + provider.channel.emit(RESET_STORY_ARGS, { storyId, argNames, options: { target: refId }, @@ -495,7 +508,7 @@ export const init: ModuleFn = ({ return; } - await fullAPI.setIndex(storyIndex); + await api.setIndex(storyIndex); } catch (err) { await store.setState({ indexError: err }); } @@ -503,16 +516,19 @@ export const init: ModuleFn = ({ // The story index we receive on SET_INDEX is "prepared" in that it has parameters // The story index we receive on fetchStoryIndex is not, but all the prepared fields are optional // so we can cast one to the other easily enough - setIndex: async (storyIndex: API_PreparedStoryIndex) => { - const newHash = transformStoryIndexToStoriesHash(storyIndex, { + setIndex: async (input) => { + const { index: oldHash, status, filters } = store.getState(); + const newHash = transformStoryIndexToStoriesHash(input, { provider, docsOptions, + status, + filters, }); // Now we need to patch in the existing prepared stories - const oldHash = store.getState().index; + const output = addPreparedStories(newHash, oldHash); - await store.setState({ index: addPreparedStories(newHash, oldHash), indexError: undefined }); + await store.setState({ internal_index: input, index: output, indexError: undefined }); }, updateStory: async ( storyId: StoryId, @@ -556,7 +572,7 @@ export const init: ModuleFn = ({ await fullAPI.updateRef(refId, { index }); } }, - setPreviewInitialized: async (ref?: ComposedRef): Promise => { + setPreviewInitialized: async (ref) => { if (!ref) { store.setState({ previewInitialized: true }); } else { @@ -566,7 +582,7 @@ export const init: ModuleFn = ({ /* EXPERIMENTAL APIs */ experimental_updateStatus: async (id, update) => { - const { status } = store.getState(); + const { status, internal_index: index } = store.getState(); const newStatus = { ...status }; Object.entries(update).forEach(([storyId, value]) => { @@ -575,181 +591,197 @@ export const init: ModuleFn = ({ }); await store.setState({ status: newStatus }, { persistence: 'session' }); + await api.setIndex(index); + }, + experimental_setFilter: async (id, filterFunction) => { + const { internal_index: index } = store.getState(); + await store.setState({ filters: { ...store.getState().filters, [id]: filterFunction } }); + await api.setIndex(index); }, }; - const initModule = async () => { - // On initial load, the local iframe will select the first story (or other "selection specifier") - // and emit STORY_SPECIFIED with the id. We need to ensure we respond to this change. - fullAPI.on( - STORY_SPECIFIED, - function handler({ - storyId, - viewMode, - }: { - storyId: string; - viewMode: API_ViewMode; - [k: string]: any; - }) { - const { sourceType } = getEventMetadata(this, fullAPI); - - if (sourceType === 'local') { - const state = store.getState(); - const isCanvasRoute = - state.path === '/' || state.viewMode === 'story' || state.viewMode === 'docs'; - const stateHasSelection = state.viewMode && state.storyId; - const stateSelectionDifferent = state.viewMode !== viewMode || state.storyId !== storyId; - /** - * When storybook starts, we want to navigate to the first story. - * But there are a few exceptions: - * - If the current storyId and viewMode are already set/correct. - * - If the user has navigated away already. - * - If the user started storybook with a specific page-URL like "/settings/about" - */ - if (isCanvasRoute) { - if (stateHasSelection && stateSelectionDifferent) { - // The manager state is correct, the preview state is lagging behind - fullAPI.emit(SET_CURRENT_STORY, { storyId: state.storyId, viewMode: state.viewMode }); - } else if (stateSelectionDifferent) { - // The preview state is correct, the manager state is lagging behind - navigate(`/${viewMode}/${storyId}`); - } + // On initial load, the local iframe will select the first story (or other "selection specifier") + // and emit STORY_SPECIFIED with the id. We need to ensure we respond to this change. + provider.channel.on( + STORY_SPECIFIED, + function handler({ + storyId, + viewMode, + }: { + storyId: string; + viewMode: API_ViewMode; + [k: string]: any; + }) { + const { sourceType } = getEventMetadata(this, fullAPI); + + if (sourceType === 'local') { + const state = store.getState(); + const isCanvasRoute = + state.path === '/' || state.viewMode === 'story' || state.viewMode === 'docs'; + const stateHasSelection = state.viewMode && state.storyId; + const stateSelectionDifferent = state.viewMode !== viewMode || state.storyId !== storyId; + /** + * When storybook starts, we want to navigate to the first story. + * But there are a few exceptions: + * - If the current storyId and viewMode are already set/correct. + * - If the user has navigated away already. + * - If the user started storybook with a specific page-URL like "/settings/about" + */ + if (isCanvasRoute) { + if (stateHasSelection && stateSelectionDifferent) { + // The manager state is correct, the preview state is lagging behind + provider.channel.emit(SET_CURRENT_STORY, { + storyId: state.storyId, + viewMode: state.viewMode, + }); + } else if (stateSelectionDifferent) { + // The preview state is correct, the manager state is lagging behind + navigate(`/${viewMode}/${storyId}`); } } } - ); - - // The CURRENT_STORY_WAS_SET event is the best event to use to tell if a ref is ready. - // Until the ref has a selection, it will not render anything (e.g. while waiting for - // the preview.js file or the index to load). Once it has a selection, it will render its own - // preparing spinner. - fullAPI.on(CURRENT_STORY_WAS_SET, function handler() { - const { ref } = getEventMetadata(this, fullAPI); - fullAPI.setPreviewInitialized(ref); - }); + } + ); + + // The CURRENT_STORY_WAS_SET event is the best event to use to tell if a ref is ready. + // Until the ref has a selection, it will not render anything (e.g. while waiting for + // the preview.js file or the index to load). Once it has a selection, it will render its own + // preparing spinner. + provider.channel.on(CURRENT_STORY_WAS_SET, function handler() { + const { ref } = getEventMetadata(this, fullAPI); + api.setPreviewInitialized(ref); + }); - fullAPI.on(STORY_CHANGED, function handler() { - const { sourceType } = getEventMetadata(this, fullAPI); + provider.channel.on(STORY_CHANGED, function handler() { + const { sourceType } = getEventMetadata(this, fullAPI); - if (sourceType === 'local') { - const options = fullAPI.getCurrentParameter('options'); + if (sourceType === 'local') { + const options = api.getCurrentParameter('options'); - if (options) { - fullAPI.setOptions(removeRemovedOptions(options)); - } + if (options) { + fullAPI.setOptions(removeRemovedOptions(options)); } - }); + } + }); - fullAPI.on(STORY_PREPARED, function handler({ id, ...update }: StoryPreparedPayload) { - const { ref, sourceType } = getEventMetadata(this, fullAPI); - fullAPI.updateStory(id, { ...update, prepared: true }, ref); + provider.channel.on(STORY_PREPARED, function handler({ id, ...update }: StoryPreparedPayload) { + const { ref, sourceType } = getEventMetadata(this, fullAPI); + api.updateStory(id, { ...update, prepared: true }, ref); - if (!ref) { - if (!store.getState().hasCalledSetOptions) { - const { options } = update.parameters; - fullAPI.setOptions(removeRemovedOptions(options)); - store.setState({ hasCalledSetOptions: true }); - } + if (!ref) { + if (!store.getState().hasCalledSetOptions) { + const { options } = update.parameters; + fullAPI.setOptions(removeRemovedOptions(options)); + store.setState({ hasCalledSetOptions: true }); } + } - if (sourceType === 'local') { - const { storyId, index, refId } = store.getState(); - - // create a list of related stories to be preloaded - const toBePreloaded = Array.from( - new Set([ - api.findSiblingStoryId(storyId, index, 1, true), - api.findSiblingStoryId(storyId, index, -1, true), - ]) - ).filter(Boolean); - - fullAPI.emit(PRELOAD_ENTRIES, { - ids: toBePreloaded, - options: { target: refId }, - }); - } - }); + if (sourceType === 'local') { + const { storyId, index, refId } = store.getState(); - fullAPI.on(DOCS_PREPARED, function handler({ id, ...update }: DocsPreparedPayload) { - const { ref } = getEventMetadata(this, fullAPI); - fullAPI.updateStory(id, { ...update, prepared: true }, ref); - }); + // create a list of related stories to be preloaded + const toBePreloaded = Array.from( + new Set([ + api.findSiblingStoryId(storyId, index, 1, true), + api.findSiblingStoryId(storyId, index, -1, true), + ]) + ).filter(Boolean); - fullAPI.on(SET_INDEX, function handler(index: API_PreparedStoryIndex) { - const { ref } = getEventMetadata(this, fullAPI); + provider.channel.emit(PRELOAD_ENTRIES, { + ids: toBePreloaded, + options: { target: refId }, + }); + } + }); - if (!ref) { - fullAPI.setIndex(index); - const options = fullAPI.getCurrentParameter('options'); - fullAPI.setOptions(removeRemovedOptions(options)); - } else { - fullAPI.setRef(ref.id, { ...ref, storyIndex: index }, true); - } - }); + provider.channel.on(DOCS_PREPARED, function handler({ id, ...update }: DocsPreparedPayload) { + const { ref } = getEventMetadata(this, fullAPI); + api.updateStory(id, { ...update, prepared: true }, ref); + }); + + provider.channel.on(SET_INDEX, function handler(index: API_PreparedStoryIndex) { + const { ref } = getEventMetadata(this, fullAPI); + + if (!ref) { + api.setIndex(index); + const options = api.getCurrentParameter('options'); + fullAPI.setOptions(removeRemovedOptions(options)); + } else { + fullAPI.setRef(ref.id, { ...ref, storyIndex: index }, true); + } + }); + + // For composition back-compatibilty + provider.channel.on(SET_STORIES, function handler(data: SetStoriesPayload) { + const { ref } = getEventMetadata(this, fullAPI); + const setStoriesData = data.v ? denormalizeStoryParameters(data) : data.stories; + + if (!ref) { + throw new Error('Cannot call SET_STORIES for local frame'); + } else { + fullAPI.setRef(ref.id, { ...ref, setStoriesData }, true); + } + }); - // For composition back-compatibilty - fullAPI.on(SET_STORIES, function handler(data: SetStoriesPayload) { + provider.channel.on( + SELECT_STORY, + function handler({ + kind, + title = kind, + story, + name = story, + storyId, + ...rest + }: { + kind?: StoryKind; + title?: ComponentTitle; + story?: StoryName; + name?: StoryName; + storyId: string; + viewMode: API_ViewMode; + }) { const { ref } = getEventMetadata(this, fullAPI); - const setStoriesData = data.v ? denormalizeStoryParameters(data) : data.stories; if (!ref) { - throw new Error('Cannot call SET_STORIES for local frame'); + fullAPI.selectStory(storyId || title, name, rest); } else { - fullAPI.setRef(ref.id, { ...ref, setStoriesData }, true); + fullAPI.selectStory(storyId || title, name, { ...rest, ref: ref.id }); } - }); - - fullAPI.on( - SELECT_STORY, - function handler({ - kind, - title = kind, - story, - name = story, - storyId, - ...rest - }: { - kind?: StoryKind; - title?: ComponentTitle; - story?: StoryName; - name?: StoryName; - storyId: string; - viewMode: API_ViewMode; - }) { - const { ref } = getEventMetadata(this, fullAPI); - - if (!ref) { - fullAPI.selectStory(storyId || title, name, rest); - } else { - fullAPI.selectStory(storyId || title, name, { ...rest, ref: ref.id }); - } - } - ); - - fullAPI.on( - STORY_ARGS_UPDATED, - function handleStoryArgsUpdated({ storyId, args }: { storyId: StoryId; args: Args }) { - const { ref } = getEventMetadata(this, fullAPI); - fullAPI.updateStory(storyId, { args }, ref); - } - ); + } + ); - // When there's a preview error, we don't show it in the manager, but simply - fullAPI.on(CONFIG_ERROR, function handleConfigError(err) { + provider.channel.on( + STORY_ARGS_UPDATED, + function handleStoryArgsUpdated({ storyId, args }: { storyId: StoryId; args: Args }) { const { ref } = getEventMetadata(this, fullAPI); - fullAPI.setPreviewInitialized(ref); - }); + api.updateStory(storyId, { args }, ref); + } + ); - fullAPI.on(STORY_MISSING, function handleConfigError(err) { - const { ref } = getEventMetadata(this, fullAPI); - fullAPI.setPreviewInitialized(ref); - }); + // When there's a preview error, we don't show it in the manager, but simply + provider.channel.on(CONFIG_ERROR, function handleConfigError(err) { + const { ref } = getEventMetadata(this, fullAPI); + api.setPreviewInitialized(ref); + }); - if (FEATURES?.storyStoreV7) { - fullAPI.on(STORY_INDEX_INVALIDATED, () => fullAPI.fetchIndex()); - await fullAPI.fetchIndex(); + provider.channel.on(STORY_MISSING, function handleConfigError(err) { + const { ref } = getEventMetadata(this, fullAPI); + api.setPreviewInitialized(ref); + }); + + provider.channel.on(SET_CONFIG, () => { + const config = provider.getConfig(); + if (config?.sidebar?.filters) { + store.setState({ + filters: { + ...store.getState().filters, + ...config?.sidebar?.filters, + }, + }); } - }; + }); + + const config = provider.getConfig(); return { api, @@ -759,7 +791,13 @@ export const init: ModuleFn = ({ hasCalledSetOptions: false, previewInitialized: false, status: {}, + filters: config?.sidebar?.filters || {}, + }, + init: async () => { + if (FEATURES?.storyStoreV7) { + provider.channel.on(STORY_INDEX_INVALIDATED, () => api.fetchIndex()); + await api.fetchIndex(); + } }, - init: initModule, }; }; diff --git a/code/lib/manager-api/src/modules/url.ts b/code/lib/manager-api/src/modules/url.ts index b8e4dcf6ebf1..c6cfc5abbd80 100644 --- a/code/lib/manager-api/src/modules/url.ts +++ b/code/lib/manager-api/src/modules/url.ts @@ -11,7 +11,7 @@ import { dequal as deepEqual } from 'dequal'; import { global } from '@storybook/global'; import type { API_Layout, API_UI } from '@storybook/types'; -import type { ModuleArgs, ModuleFn } from '../index'; +import type { ModuleArgs, ModuleFn } from '../lib/types'; const { window: globalWindow } = global; @@ -116,7 +116,9 @@ export interface SubAPI { setQueryParams: (input: QueryParams) => void; } -export const init: ModuleFn = ({ store, navigate, state, provider, fullAPI, ...rest }) => { +export const init: ModuleFn = (moduleArgs) => { + const { store, navigate, provider, fullAPI } = moduleArgs; + const navigateTo = ( path: string, queryParams: Record = {}, @@ -153,7 +155,7 @@ export const init: ModuleFn = ({ store, navigate, state, provider, fullAPI, ...r }; if (!deepEqual(customQueryParams, update)) { store.setState({ customQueryParams: update }); - fullAPI.emit(UPDATE_QUERY_PARAMS, update); + provider.channel.emit(UPDATE_QUERY_PARAMS, update); } }, navigateUrl(url, options) { @@ -161,49 +163,48 @@ export const init: ModuleFn = ({ store, navigate, state, provider, fullAPI, ...r }, }; - const initModule = () => { - // Sets `args` parameter in URL, omitting any args that have their initial value or cannot be unserialized safely. - const updateArgsParam = () => { - const { path, queryParams, viewMode } = fullAPI.getUrlState(); - if (viewMode !== 'story') return; - - const currentStory = fullAPI.getCurrentStoryData(); - if (currentStory?.type !== 'story') return; - - const { args, initialArgs } = currentStory; - const argsString = buildArgsParam(initialArgs, args); - navigateTo(path, { ...queryParams, args: argsString }, { replace: true }); - api.setQueryParams({ args: argsString }); - }; - - fullAPI.on(SET_CURRENT_STORY, () => updateArgsParam()); - - let handleOrId: any; - fullAPI.on(STORY_ARGS_UPDATED, () => { - if ('requestIdleCallback' in globalWindow) { - if (handleOrId) globalWindow.cancelIdleCallback(handleOrId); - handleOrId = globalWindow.requestIdleCallback(updateArgsParam, { timeout: 1000 }); - } else { - if (handleOrId) clearTimeout(handleOrId); - setTimeout(updateArgsParam, 100); - } - }); - - fullAPI.on(GLOBALS_UPDATED, ({ globals, initialGlobals }) => { - const { path, queryParams } = fullAPI.getUrlState(); - const globalsString = buildArgsParam(initialGlobals, globals); - navigateTo(path, { ...queryParams, globals: globalsString }, { replace: true }); - api.setQueryParams({ globals: globalsString }); - }); - - fullAPI.on(NAVIGATE_URL, (url: string, options: NavigateOptions) => { - fullAPI.navigateUrl(url, options); - }); + /** + * Sets `args` parameter in URL, omitting any args that have their initial value or cannot be unserialized safely. + */ + const updateArgsParam = () => { + const { path, queryParams, viewMode } = api.getUrlState(); + if (viewMode !== 'story') return; + + const currentStory = fullAPI.getCurrentStoryData(); + if (currentStory?.type !== 'story') return; + + const { args, initialArgs } = currentStory; + const argsString = buildArgsParam(initialArgs, args); + navigateTo(path, { ...queryParams, args: argsString }, { replace: true }); + api.setQueryParams({ args: argsString }); }; + provider.channel.on(SET_CURRENT_STORY, () => updateArgsParam()); + + let handleOrId: any; + provider.channel.on(STORY_ARGS_UPDATED, () => { + if ('requestIdleCallback' in globalWindow) { + if (handleOrId) globalWindow.cancelIdleCallback(handleOrId); + handleOrId = globalWindow.requestIdleCallback(updateArgsParam, { timeout: 1000 }); + } else { + if (handleOrId) clearTimeout(handleOrId); + setTimeout(updateArgsParam, 100); + } + }); + + provider.channel.on(GLOBALS_UPDATED, ({ globals, initialGlobals }) => { + const { path, queryParams } = api.getUrlState(); + const globalsString = buildArgsParam(initialGlobals, globals); + navigateTo(path, { ...queryParams, globals: globalsString }, { replace: true }); + api.setQueryParams({ globals: globalsString }); + }); + + provider.channel.on(NAVIGATE_URL, (url: string, options: NavigateOptions) => { + api.navigateUrl(url, options); + }); + return { api, - state: initialUrlSupport({ store, navigate, state, provider, fullAPI, ...rest }), - init: initModule, + state: initialUrlSupport(moduleArgs), }; }; diff --git a/code/lib/manager-api/src/modules/versions.ts b/code/lib/manager-api/src/modules/versions.ts index 49ff24be9b1f..2d90a14fcd69 100644 --- a/code/lib/manager-api/src/modules/versions.ts +++ b/code/lib/manager-api/src/modules/versions.ts @@ -5,7 +5,7 @@ import memoize from 'memoizerific'; import type { API_UnknownEntries, API_Version, API_Versions } from '@storybook/types'; import { version as currentVersion } from '../version'; -import type { ModuleFn } from '../index'; +import type { ModuleFn } from '../lib/types'; const { VERSIONCHECK } = global; diff --git a/code/lib/manager-api/src/modules/whatsnew.ts b/code/lib/manager-api/src/modules/whatsnew.ts index 5890eaae6fc7..6ee90558bc7c 100644 --- a/code/lib/manager-api/src/modules/whatsnew.ts +++ b/code/lib/manager-api/src/modules/whatsnew.ts @@ -6,7 +6,7 @@ import { SET_WHATS_NEW_CACHE, TOGGLE_WHATS_NEW_NOTIFICATIONS, } from '@storybook/core-events'; -import type { ModuleFn } from '../index'; +import type { ModuleFn } from '../lib/types'; export type SubState = { whatsNewData?: WhatsNewData; @@ -20,7 +20,7 @@ export type SubAPI = { const WHATS_NEW_NOTIFICATION_ID = 'whats-new'; -export const init: ModuleFn = ({ fullAPI, store }) => { +export const init: ModuleFn = ({ fullAPI, store, provider }) => { const state: SubState = { whatsNewData: undefined, }; @@ -47,7 +47,7 @@ export const init: ModuleFn = ({ fullAPI, store }) => { ...state.whatsNewData, disableWhatsNewNotifications: !state.whatsNewData.disableWhatsNewNotifications, }); - fullAPI.emit(TOGGLE_WHATS_NEW_NOTIFICATIONS, { + provider.channel.emit(TOGGLE_WHATS_NEW_NOTIFICATIONS, { disableWhatsNewNotifications: state.whatsNewData.disableWhatsNewNotifications, }); } @@ -55,20 +55,24 @@ export const init: ModuleFn = ({ fullAPI, store }) => { }; function getLatestWhatsNewPost(): Promise { - fullAPI.emit(REQUEST_WHATS_NEW_DATA); + provider.channel.emit(REQUEST_WHATS_NEW_DATA); return new Promise((resolve) => - fullAPI.once(RESULT_WHATS_NEW_DATA, ({ data }: { data: WhatsNewData }) => resolve(data)) + provider.channel.once(RESULT_WHATS_NEW_DATA, ({ data }: { data: WhatsNewData }) => + resolve(data) + ) ); } function setWhatsNewCache(cache: WhatsNewCache): void { - fullAPI.emit(SET_WHATS_NEW_CACHE, cache); + provider.channel.emit(SET_WHATS_NEW_CACHE, cache); } const initModule = async () => { // The server channel doesn't exist in production, and we don't want to show what's new in production storybooks. - if (global.CONFIG_TYPE !== 'DEVELOPMENT') return; + if (global.CONFIG_TYPE !== 'DEVELOPMENT') { + return; + } const whatsNewData = await getLatestWhatsNewPost(); setWhatsNewState(whatsNewData); @@ -92,7 +96,9 @@ export const init: ModuleFn = ({ fullAPI, store }) => { }, icon: { name: 'hearthollow' }, onClear({ dismissed }) { - if (dismissed) setWhatsNewCache({ lastDismissedPost: whatsNewData.url }); + if (dismissed) { + setWhatsNewCache({ lastDismissedPost: whatsNewData.url }); + } }, }); } diff --git a/code/lib/manager-api/src/tests/globals.test.ts b/code/lib/manager-api/src/tests/globals.test.ts index babd449131b2..1f51a113935a 100644 --- a/code/lib/manager-api/src/tests/globals.test.ts +++ b/code/lib/manager-api/src/tests/globals.test.ts @@ -1,9 +1,10 @@ import { EventEmitter } from 'events'; import { SET_STORIES, SET_GLOBALS, UPDATE_GLOBALS, GLOBALS_UPDATED } from '@storybook/core-events'; -import type { ModuleArgs, API } from '../index'; +import type { API } from '../index'; import type { SubAPI } from '../modules/globals'; import { init as initModule } from '../modules/globals'; +import type { ModuleArgs } from '../lib/types'; const { logger } = require('@storybook/client-logger'); const { getEventMetadata } = require('../lib/events'); @@ -27,7 +28,8 @@ function createMockStore() { describe('globals API', () => { it('sets a sensible initialState', () => { const store = createMockStore(); - const { state } = initModule({ store } as unknown as ModuleArgs); + const channel = new EventEmitter(); + const { state } = initModule({ store, provider: { channel } } as unknown as ModuleArgs); expect(state).toEqual({ globals: {}, @@ -36,13 +38,15 @@ describe('globals API', () => { }); it('set global args on SET_GLOBALS', () => { - const api = Object.assign(new EventEmitter(), { findRef: jest.fn() }); + const channel = new EventEmitter(); const store = createMockStore(); - const { state, init } = initModule({ store, fullAPI: api } as unknown as ModuleArgs); + const { state } = initModule({ + store, + provider: { channel }, + } as unknown as ModuleArgs); store.setState(state); - init(); - api.emit(SET_GLOBALS, { + channel.emit(SET_GLOBALS, { globals: { a: 'b' }, globalTypes: { a: { type: { name: 'string' } } }, }); @@ -53,26 +57,34 @@ describe('globals API', () => { }); it('ignores SET_STORIES from other refs', () => { - const api = Object.assign(new EventEmitter(), { findRef: jest.fn() }); + const channel = new EventEmitter(); + const api = { findRef: jest.fn() }; const store = createMockStore(); - const { state, init } = initModule({ store, fullAPI: api } as unknown as ModuleArgs); + const { state } = initModule({ + store, + fullAPI: api, + provider: { channel }, + } as unknown as ModuleArgs); store.setState(state); - init(); getEventMetadata.mockReturnValueOnce({ sourceType: 'external', ref: { id: 'ref' } }); - api.emit(SET_STORIES, { globals: { a: 'b' } }); + channel.emit(SET_STORIES, { globals: { a: 'b' } }); expect(store.getState()).toEqual({ globals: {}, globalTypes: {} }); }); it('ignores SET_GLOBALS from other refs', () => { - const api = Object.assign(new EventEmitter(), { findRef: jest.fn() }); + const api = { findRef: jest.fn() }; + const channel = new EventEmitter(); const store = createMockStore(); - const { state, init } = initModule({ store, fullAPI: api } as unknown as ModuleArgs); + const { state } = initModule({ + store, + fullAPI: api, + provider: { channel }, + } as unknown as ModuleArgs); store.setState(state); - init(); getEventMetadata.mockReturnValueOnce({ sourceType: 'external', ref: { id: 'ref' } }); - api.emit(SET_GLOBALS, { + channel.emit(SET_GLOBALS, { globals: { a: 'b' }, globalTypes: { a: { type: { name: 'string' } } }, }); @@ -80,48 +92,56 @@ describe('globals API', () => { }); it('updates the state when the preview emits GLOBALS_UPDATED', () => { - const api = Object.assign(new EventEmitter(), { findRef: jest.fn() }); + const channel = new EventEmitter(); + const api = { findRef: jest.fn() }; const store = createMockStore(); - const { state, init } = initModule({ store, fullAPI: api } as unknown as ModuleArgs); + const { state } = initModule({ + store, + fullAPI: api, + provider: { channel }, + } as unknown as ModuleArgs); store.setState(state); - init(); - - api.emit(GLOBALS_UPDATED, { globals: { a: 'b' } }); + channel.emit(GLOBALS_UPDATED, { globals: { a: 'b' } }); expect(store.getState()).toEqual({ globals: { a: 'b' }, globalTypes: {} }); - api.emit(GLOBALS_UPDATED, { globals: { a: 'c' } }); + channel.emit(GLOBALS_UPDATED, { globals: { a: 'c' } }); expect(store.getState()).toEqual({ globals: { a: 'c' }, globalTypes: {} }); // SHOULD NOT merge global args - api.emit(GLOBALS_UPDATED, { globals: { d: 'e' } }); + channel.emit(GLOBALS_UPDATED, { globals: { d: 'e' } }); expect(store.getState()).toEqual({ globals: { d: 'e' }, globalTypes: {} }); }); it('ignores GLOBALS_UPDATED from other refs', () => { - const api = Object.assign(new EventEmitter(), { findRef: jest.fn() }); + const channel = new EventEmitter(); + const api = { findRef: jest.fn() }; const store = createMockStore(); - const { state, init } = initModule({ store, fullAPI: api } as unknown as ModuleArgs); + const { state } = initModule({ + store, + fullAPI: api, + provider: { channel }, + } as unknown as ModuleArgs); store.setState(state); - init(); - getEventMetadata.mockReturnValueOnce({ sourceType: 'external', ref: { id: 'ref' } }); logger.warn.mockClear(); - api.emit(GLOBALS_UPDATED, { globals: { a: 'b' } }); + channel.emit(GLOBALS_UPDATED, { globals: { a: 'b' } }); expect(store.getState()).toEqual({ globals: {}, globalTypes: {} }); expect(logger.warn).toHaveBeenCalled(); }); it('emits UPDATE_GLOBALS when updateGlobals is called', () => { - const fullAPI = { emit: jest.fn(), on: jest.fn() } as unknown as API; + const channel = new EventEmitter(); + const fullAPI = {} as unknown as API; const store = createMockStore(); - const { init, api } = initModule({ store, fullAPI } as unknown as ModuleArgs); - - init(); + const listener = jest.fn(); + channel.on(UPDATE_GLOBALS, listener); + const { api } = initModule({ store, fullAPI, provider: { channel } } as unknown as ModuleArgs); (api as SubAPI).updateGlobals({ a: 'b' }); - expect(fullAPI.emit).toHaveBeenCalledWith(UPDATE_GLOBALS, { + + expect(listener).toHaveBeenCalledWith({ globals: { a: 'b' }, options: { target: 'storybook-preview-iframe' }, }); diff --git a/code/lib/manager-api/src/tests/mockStoriesEntries.ts b/code/lib/manager-api/src/tests/mockStoriesEntries.ts new file mode 100644 index 000000000000..703b6e6efb76 --- /dev/null +++ b/code/lib/manager-api/src/tests/mockStoriesEntries.ts @@ -0,0 +1,129 @@ +import type { StoryIndex, API_PreparedStoryIndex } from '@storybook/types'; + +export const mockEntries: StoryIndex['entries'] = { + 'component-a--docs': { + type: 'docs', + id: 'component-a--docs', + title: 'Component A', + name: 'Docs', + importPath: './path/to/component-a.ts', + storiesImports: [], + }, + 'component-a--story-1': { + type: 'story', + id: 'component-a--story-1', + title: 'Component A', + name: 'Story 1', + importPath: './path/to/component-a.ts', + }, + 'component-a--story-2': { + type: 'story', + id: 'component-a--story-2', + title: 'Component A', + name: 'Story 2', + importPath: './path/to/component-a.ts', + }, + 'component-b--story-3': { + type: 'story', + id: 'component-b--story-3', + title: 'Component B', + name: 'Story 3', + importPath: './path/to/component-b.ts', + }, +}; +export const docsEntries: StoryIndex['entries'] = { + 'component-a--page': { + type: 'story', + id: 'component-a--page', + title: 'Component A', + name: 'Page', + importPath: './path/to/component-a.ts', + }, + 'component-a--story-2': { + type: 'story', + id: 'component-a--story-2', + title: 'Component A', + name: 'Story 2', + importPath: './path/to/component-a.ts', + }, + 'component-b-docs': { + type: 'docs', + id: 'component-b--docs', + title: 'Component B', + name: 'Docs', + importPath: './path/to/component-b.ts', + storiesImports: [], + tags: ['stories-mdx'], + }, + 'component-c--story-4': { + type: 'story', + id: 'component-c--story-4', + title: 'Component c', + name: 'Story 4', + importPath: './path/to/component-c.ts', + }, +}; +export const navigationEntries: StoryIndex['entries'] = { + 'a--1': { + type: 'story', + title: 'a', + name: '1', + id: 'a--1', + importPath: './a.ts', + }, + 'a--2': { + type: 'story', + title: 'a', + name: '2', + id: 'a--2', + importPath: './a.ts', + }, + 'b-c--1': { + type: 'story', + title: 'b/c', + name: '1', + id: 'b-c--1', + importPath: './b/c.ts', + }, + 'b-d--1': { + type: 'story', + title: 'b/d', + name: '1', + id: 'b-d--1', + importPath: './b/d.ts', + }, + 'b-d--2': { + type: 'story', + title: 'b/d', + name: '2', + id: 'b-d--2', + importPath: './b/d.ts', + }, + 'custom-id--1': { + type: 'story', + title: 'b/e', + name: '1', + id: 'custom-id--1', + importPath: './b/.ts', + }, +}; +export const preparedEntries: API_PreparedStoryIndex['entries'] = { + 'a--1': { + type: 'story', + title: 'a', + name: '1', + parameters: {}, + id: 'a--1', + args: { a: 'b' }, + importPath: './a.ts', + }, + 'b--1': { + type: 'story', + title: 'b', + name: '1', + parameters: {}, + id: 'b--1', + args: { x: 'y' }, + importPath: './b.ts', + }, +}; diff --git a/code/lib/manager-api/src/tests/stories.test.ts b/code/lib/manager-api/src/tests/stories.test.ts index 5427226865e5..0ee0c9be4ebb 100644 --- a/code/lib/manager-api/src/tests/stories.test.ts +++ b/code/lib/manager-api/src/tests/stories.test.ts @@ -16,28 +16,26 @@ import { import { EventEmitter } from 'events'; import { global } from '@storybook/global'; -import { Channel } from '@storybook/channels'; +import type { API_StoryEntry } from '@storybook/types'; +import { getEventMetadata as getEventMetadataOriginal } from '../lib/events'; -import type { API_StoryEntry, StoryIndex, API_PreparedStoryIndex } from '@storybook/types'; -import { getEventMetadata } from '../lib/events'; - -import type { SubAPI } from '../modules/stories'; import { init as initStories } from '../modules/stories'; import type Store from '../store'; -import type { ModuleArgs } from '..'; - -function mockChannel() { - const transport = { - setHandler: () => {}, - send: () => {}, - }; - - return new Channel({ transport }); -} +import type { API, State } from '..'; +import { mockEntries, docsEntries, preparedEntries, navigationEntries } from './mockStoriesEntries'; +import type { ModuleArgs } from '../lib/types'; const mockGetEntries = jest.fn(); +const fetch = global.fetch as jest.Mock>; +const getEventMetadata = getEventMetadataOriginal as unknown as jest.Mock< + ReturnType +>; + +const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); -jest.mock('../lib/events'); +jest.mock('../lib/events', () => ({ + getEventMetadata: jest.fn(() => ({ sourceType: 'local' })), +})); jest.mock('@storybook/global', () => ({ global: { ...globalThis, @@ -47,41 +45,7 @@ jest.mock('@storybook/global', () => ({ }, })); -const getEventMetadataMock = getEventMetadata as ReturnType; - -const mockEntries: StoryIndex['entries'] = { - 'component-a--docs': { - type: 'docs', - id: 'component-a--docs', - title: 'Component A', - name: 'Docs', - importPath: './path/to/component-a.ts', - storiesImports: [], - }, - 'component-a--story-1': { - type: 'story', - id: 'component-a--story-1', - title: 'Component A', - name: 'Story 1', - importPath: './path/to/component-a.ts', - }, - 'component-a--story-2': { - type: 'story', - id: 'component-a--story-2', - title: 'Component A', - name: 'Story 2', - importPath: './path/to/component-a.ts', - }, - 'component-b--story-3': { - type: 'story', - id: 'component-b--story-3', - title: 'Component B', - name: 'Story 3', - importPath: './path/to/component-b.ts', - }, -}; - -function createMockStore(initialState = {}) { +function createMockStore(initialState: Partial = {}) { let state = initialState; return { getState: jest.fn(() => state), @@ -91,40 +55,34 @@ function createMockStore(initialState = {}) { }), } as any as Store; } - -function initStoriesAndSetState({ store, ...options }: any) { - const { state, ...result } = initStories({ store, ...options } as any); - - store?.setState(state); - - return { state, ...result }; +function createMockProvider() { + return { + getConfig: jest.fn().mockReturnValue({}), + channel: new EventEmitter(), + }; +} +function createMockModuleArgs({ + fullAPI = {}, + initialState = {}, +}: { + fullAPI?: Partial>; + initialState?: Partial; +}) { + const navigate = jest.fn(); + const store = createMockStore({ filters: {}, status: {}, ...initialState }); + const provider = createMockProvider(); + + return { navigate, store, provider, fullAPI }; } - -const provider = { getConfig: jest.fn().mockReturnValue({}), serverChannel: mockChannel() }; - -beforeEach(() => { - provider.getConfig.mockReset().mockReturnValue({}); - provider.serverChannel = mockChannel(); - mockGetEntries.mockReset().mockReturnValue(mockEntries); - - (global.fetch as jest.Mock>).mockReset().mockReturnValue( - Promise.resolve({ - status: 200, - ok: true, - json: () => ({ v: 4, entries: mockGetEntries() }), - } as any as Response) - ); - - getEventMetadataMock.mockReturnValue({ sourceType: 'local' } as any); - getEventMetadataMock.mockReturnValue({ sourceType: 'local' } as any); -}); describe('stories API', () => { it('sets a sensible initialState', () => { - const { state } = initStoriesAndSetState({ + const moduleArgs = createMockModuleArgs({}); + const { state } = initStories({ + ...(moduleArgs as unknown as ModuleArgs), storyId: 'id', viewMode: 'story', - } as ModuleArgs); + }); expect(state).toEqual( expect.objectContaining({ @@ -138,16 +96,11 @@ describe('stories API', () => { describe('setIndex', () => { it('sets the initial set of stories in the stories hash', async () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter()); - - const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); - + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; api.setIndex({ v: 4, entries: mockEntries }); const { index } = store.getState(); - // We need exact key ordering, even if in theory JS doesn't guarantee it expect(Object.keys(index)).toEqual([ 'component-a', @@ -162,7 +115,6 @@ describe('stories API', () => { id: 'component-a', children: ['component-a--docs', 'component-a--story-1', 'component-a--story-2'], }); - expect(index['component-a--docs']).toMatchObject({ type: 'docs', id: 'component-a--docs', @@ -172,7 +124,6 @@ describe('stories API', () => { storiesImports: [], prepared: false, }); - expect(index['component-a--story-1']).toMatchObject({ type: 'story', id: 'component-a--story-1', @@ -185,15 +136,10 @@ describe('stories API', () => { (index['component-a--story-1'] as API_StoryEntry as API_StoryEntry).args ).toBeUndefined(); }); - it('trims whitespace of group/component names (which originate from the kind)', () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter()); - - const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); - + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; api.setIndex({ v: 4, entries: { @@ -207,7 +153,6 @@ describe('stories API', () => { }, }); const { index } = store.getState(); - // We need exact key ordering, even if in theory JS doesn't guarantee it expect(Object.keys(index)).toEqual([ 'design-system', @@ -228,15 +173,10 @@ describe('stories API', () => { name: ' My Story ', // story name is kept as-is, because it's set directly on the story }); }); - it('moves rootless stories to the front of the list', async () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter()); - - const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); - + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; api.setIndex({ v: 4, entries: { @@ -251,7 +191,6 @@ describe('stories API', () => { }, }); const { index } = store.getState(); - // We need exact key ordering, even if in theory JS doesn't guarantee it expect(Object.keys(index)).toEqual([ 'component-a', @@ -270,15 +209,10 @@ describe('stories API', () => { children: ['root-first'], }); }); - it('sets roots when showRoots = true', () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter()); - - const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); - + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store, provider } = moduleArgs; provider.getConfig.mockReturnValue({ sidebar: { showRoots: true } }); api.setIndex({ v: 4, @@ -292,9 +226,7 @@ describe('stories API', () => { }, }, }); - const { index } = store.getState(); - // We need exact key ordering, even if in theory JS doens't guarantee it expect(Object.keys(index)).toEqual(['a', 'a-b', 'a-b--1']); expect(index.a).toMatchObject({ @@ -316,15 +248,10 @@ describe('stories API', () => { title: 'a/b', }); }); - it('does not put bare stories into a root when showRoots = true', () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter()); - - const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); - + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store, provider } = moduleArgs; provider.getConfig.mockReturnValue({ sidebar: { showRoots: true } }); api.setIndex({ v: 4, @@ -338,9 +265,7 @@ describe('stories API', () => { }, }, }); - const { index } = store.getState(); - // We need exact key ordering, even if in theory JS doens't guarantee it expect(Object.keys(index)).toEqual(['a', 'a--1']); expect(index.a).toMatchObject({ @@ -356,17 +281,12 @@ describe('stories API', () => { name: '1', }); }); - // Stories can get out of order for a few reasons -- see reproductions on // https://github.com/storybookjs/storybook/issues/5518 it('does the right thing for out of order stories', async () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter()); - - const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); - + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store, provider } = moduleArgs; provider.getConfig.mockReturnValue({ sidebar: { showRoots: true } }); api.setIndex({ v: 4, @@ -376,9 +296,7 @@ describe('stories API', () => { 'a--2': { type: 'story', title: 'a', name: '2', id: 'a--2', importPath: './a.ts' }, }, }); - const { index } = store.getState(); - // We need exact key ordering, even if in theory JS doens't guarantee it expect(Object.keys(index)).toEqual(['a', 'a--1', 'a--2', 'b', 'b--1']); expect(index.a).toMatchObject({ @@ -386,23 +304,17 @@ describe('stories API', () => { id: 'a', children: ['a--1', 'a--2'], }); - expect(index.b).toMatchObject({ type: 'component', id: 'b', children: ['b--1'], }); }); - // Entries on the SET_STORIES event will be prepared it('handles properly prepared stories', async () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter(), {}); - - const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); - + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; api.setIndex({ v: 4, entries: { @@ -417,9 +329,7 @@ describe('stories API', () => { }, }, }); - const { index } = store.getState(); - expect(index['prepared--story']).toMatchObject({ type: 'story', id: 'prepared--story', @@ -431,21 +341,13 @@ describe('stories API', () => { args: { arg: 'exists' }, }); }); - it('retains prepared-ness of stories', async () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter(), { - setOptions: jest.fn(), - }); - - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); - init(); - + const fullAPI = { setOptions: jest.fn() }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store, provider } = moduleArgs; api.setIndex({ v: 4, entries: mockEntries }); - - fullAPI.emit(STORY_PREPARED, { + provider.channel.emit(STORY_PREPARED, { id: 'component-a--story-1', parameters: { a: 'b' }, args: { c: 'd' }, @@ -457,9 +359,7 @@ describe('stories API', () => { parameters: { a: 'b' }, args: { c: 'd' }, }); - api.setIndex({ v: 4, entries: mockEntries }); - // Let the promise/await chain resolve await new Promise((r) => setTimeout(r, 0)); expect(store.getState().index['component-a--story-1'] as API_StoryEntry).toMatchObject({ @@ -470,51 +370,13 @@ describe('stories API', () => { }); describe('docs entries', () => { - const docsEntries: StoryIndex['entries'] = { - 'component-a--page': { - type: 'story', - id: 'component-a--page', - title: 'Component A', - name: 'Page', - importPath: './path/to/component-a.ts', - }, - 'component-a--story-2': { - type: 'story', - id: 'component-a--story-2', - title: 'Component A', - name: 'Story 2', - importPath: './path/to/component-a.ts', - }, - 'component-b-docs': { - type: 'docs', - id: 'component-b--docs', - title: 'Component B', - name: 'Docs', - importPath: './path/to/component-b.ts', - storiesImports: [], - tags: ['stories-mdx'], - }, - 'component-c--story-4': { - type: 'story', - id: 'component-c--story-4', - title: 'Component c', - name: 'Story 4', - importPath: './path/to/component-c.ts', - }, - }; - it('handles docs entries', async () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter()); - - const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; api.setIndex({ v: 4, entries: docsEntries }); - const { index } = store.getState(); - // We need exact key ordering, even if in theory JS doesn't guarantee it expect(Object.keys(index)).toEqual([ 'component-a', @@ -530,26 +392,16 @@ describe('stories API', () => { expect(index['component-b--docs'].type).toBe('docs'); expect(index['component-c--story-4'].type).toBe('story'); }); - describe('when DOCS_MODE = true', () => { it('strips out story entries', async () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter()); - - const { api } = initStoriesAndSetState({ - store, - navigate, - provider, - fullAPI, + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories({ + ...(moduleArgs as unknown as ModuleArgs), docsOptions: { docsMode: true }, - } as any); - Object.assign(fullAPI, api); - + }); + const { store } = moduleArgs; api.setIndex({ v: 4, entries: docsEntries }); - const { index } = store.getState(); - expect(Object.keys(index)).toEqual(['component-b', 'component-b--docs']); }); }); @@ -558,269 +410,197 @@ describe('stories API', () => { describe('SET_INDEX event', () => { it('calls setIndex w/ the data', () => { - const fullAPI = Object.assign(new EventEmitter()); - const navigate = jest.fn(); - const store = createMockStore(); - - const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api, { - setIndex: jest.fn(), - setOptions: jest.fn(), - }); - init(); + const fullAPI = { setOptions: jest.fn() }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + initStories(moduleArgs as unknown as ModuleArgs); + const { store, provider } = moduleArgs; - fullAPI.emit(SET_INDEX, { v: 4, entries: mockEntries }); - - expect(fullAPI.setIndex).toHaveBeenCalled(); + provider.channel.emit(SET_INDEX, { v: 4, entries: mockEntries }); + expect(store.getState().index).toEqual( + expect.objectContaining({ + 'component-a': expect.any(Object), + 'component-a--docs': expect.any(Object), + 'component-a--story-1': expect.any(Object), + }) + ); }); - it('calls setOptions w/ first story parameter', () => { - const fullAPI = Object.assign(new EventEmitter()); - const navigate = jest.fn(); - const store = createMockStore(); - - const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api, { - setIndex: jest.fn(), - setOptions: jest.fn(), + const fullAPI = { setOptions: jest.fn() }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { provider } = moduleArgs; + + // HACK api to effectively mock getCurrentParameter + Object.assign(api, { getCurrentParameter: jest.fn().mockReturnValue('options'), }); - init(); - - fullAPI.emit(SET_INDEX, { v: 4, entries: mockEntries }); + provider.channel.emit(SET_INDEX, { v: 4, entries: mockEntries }); expect(fullAPI.setOptions).toHaveBeenCalledWith('options'); }); }); describe('fetchIndex', () => { it('deals with 500 errors', async () => { - const navigate = jest.fn(); - const store = createMockStore({}); - const fullAPI = Object.assign(new EventEmitter(), {}, {}); - - (global.fetch as jest.Mock>).mockReturnValue( + fetch.mockReturnValue( Promise.resolve({ status: 500, text: async () => new Error('sorting error'), } as any as Response) ); - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); + const moduleArgs = createMockModuleArgs({}); + const { init } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; await init(); const { indexError } = store.getState(); expect(indexError).toBeDefined(); }); - it('watches for the INVALIDATE event and re-fetches -- and resets the hash', async () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter(), { - setIndex: jest.fn(), - }); + fetch.mockReturnValue( + Promise.resolve({ + status: 200, + ok: true, + json: () => ({ + v: 4, + entries: { + 'component-a--story-1': { + type: 'story', + id: 'component-a--story-1', + title: 'Component A', + name: 'Story 1', + importPath: './path/to/component-a.ts', + }, + }, + }), + } as any as Response) + ); - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); + const moduleArgs = createMockModuleArgs({}); + const { init } = initStories(moduleArgs as unknown as ModuleArgs); + const { store, provider } = moduleArgs; - (global.fetch as jest.Mock>).mockClear(); await init(); - expect(global.fetch as jest.Mock>).toHaveBeenCalledTimes(1); - - (global.fetch as jest.Mock>).mockClear(); - mockGetEntries.mockReturnValueOnce({ - 'component-a--story-1': { - type: 'story', - id: 'component-a--story-1', - title: 'Component A', - name: 'Story 1', - importPath: './path/to/component-a.ts', - }, - }); - fullAPI.emit(STORY_INDEX_INVALIDATED); - expect(global.fetch).toHaveBeenCalledTimes(1); - // Let the promise/await chain resolve - await new Promise((r) => setTimeout(r, 0)); - const { index } = store.getState(); + expect(fetch).toHaveBeenCalledTimes(1); + provider.channel.emit(STORY_INDEX_INVALIDATED); + expect(global.fetch).toHaveBeenCalledTimes(2); + + // this side-effect is in an un-awaited promise. + await wait(16); + const { index } = store.getState(); expect(Object.keys(index)).toEqual(['component-a', 'component-a--story-1']); }); - it('clears 500 errors when invalidated', async () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter(), { - setIndex: jest.fn(), - }); - - (global.fetch as jest.Mock>).mockReturnValueOnce( + fetch.mockReturnValueOnce( Promise.resolve({ status: 500, text: async () => new Error('sorting error'), } as any as Response) ); - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); + const moduleArgs = createMockModuleArgs({}); + const { init } = initStories(moduleArgs as unknown as ModuleArgs); + const { store, provider } = moduleArgs; await init(); const { indexError } = store.getState(); expect(indexError).toBeDefined(); - (global.fetch as jest.Mock>).mockClear(); - mockGetEntries.mockReturnValueOnce({ - 'component-a--story-1': { - type: 'story', - id: 'component-a--story-1', - title: 'Component A', - name: 'Story 1', - importPath: './path/to/component-a.ts', - }, - }); - fullAPI.emit(STORY_INDEX_INVALIDATED); - expect(global.fetch).toHaveBeenCalledTimes(1); + fetch.mockReturnValueOnce( + Promise.resolve({ + status: 200, + ok: true, + json: () => ({ + v: 4, + entries: { + 'component-a--story-1': { + type: 'story', + id: 'component-a--story-1', + title: 'Component A', + name: 'Story 1', + importPath: './path/to/component-a.ts', + }, + }, + }), + } as any as Response) + ); + + provider.channel.emit(STORY_INDEX_INVALIDATED); + expect(global.fetch).toHaveBeenCalledTimes(2); + + // this side-effect is in an un-awaited promise. + await wait(16); - // Let the promise/await chain resolve - await new Promise((r) => setTimeout(r, 0)); const { index, indexError: newIndexError } = store.getState(); expect(newIndexError).not.toBeDefined(); - expect(Object.keys(index)).toEqual(['component-a', 'component-a--story-1']); }); }); describe('STORY_SPECIFIED event', () => { it('navigates to the story', async () => { - const navigate = jest.fn(); - const fullAPI = Object.assign(new EventEmitter(), { - isSettingsScreenActive() { - return false; - }, - }); - const store = createMockStore({ viewMode: 'story' }); - const { init, api } = initStoriesAndSetState({ - store, - navigate, - provider, - fullAPI, - viewMode: 'story', - } as any); - - Object.assign(fullAPI, api); - init(); - fullAPI.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' }); + const moduleArgs = createMockModuleArgs({ initialState: { path: '/' } }); + initStories(moduleArgs as unknown as ModuleArgs); + const { navigate, provider } = moduleArgs; + provider.channel.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' }); expect(navigate).toHaveBeenCalledWith('/story/a--1'); }); - it('DOES not navigate if the story was already selected', async () => { - const navigate = jest.fn(); - const fullAPI = Object.assign(new EventEmitter(), { - isSettingsScreenActive() { - return true; - }, - }); - const store = createMockStore({ viewMode: 'story', storyId: 'a--1' }); - const { api, init } = initStoriesAndSetState({ - store, - navigate, - provider, - fullAPI, - viewMode: 'story', - storyId: 'a--1', - } as any); - Object.assign(fullAPI, api); - init(); - - fullAPI.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' }); + const moduleArgs = createMockModuleArgs({ initialState: { path: '/story/a--1' } }); + initStories(moduleArgs as unknown as ModuleArgs); + const { navigate, provider } = moduleArgs; + provider.channel.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' }); expect(navigate).not.toHaveBeenCalled(); }); - it('DOES not navigate if a settings page was selected', async () => { - const navigate = jest.fn(); - const fullAPI = Object.assign(new EventEmitter(), { - isSettingsScreenActive() { - return true; - }, - }); - const store = createMockStore({ viewMode: 'settings', storyId: 'about' }); - const { api, init } = initStoriesAndSetState({ - store, - navigate, - provider, - fullAPI, - viewMode: 'settings', - storyId: 'about', - } as any); - Object.assign(fullAPI, api); - init(); - - fullAPI.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' }); + const moduleArgs = createMockModuleArgs({ initialState: { path: '/settings/about' } }); + initStories(moduleArgs as unknown as ModuleArgs); + const { navigate, provider } = moduleArgs; + provider.channel.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' }); expect(navigate).not.toHaveBeenCalled(); }); - it('DOES not navigate if a custom page was selected', async () => { - const navigate = jest.fn(); - const fullAPI = Object.assign(new EventEmitter(), { - isSettingsScreenActive() { - return true; - }, - }); - const store = createMockStore({ viewMode: 'custom', storyId: undefined }); - const { api, init } = initStoriesAndSetState({ - store, - navigate, - provider, - fullAPI, - viewMode: 'custom', - storyId: undefined, - } as any); - Object.assign(fullAPI, api); - init(); - - fullAPI.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' }); + const moduleArgs = createMockModuleArgs({ initialState: { path: '/custom/page' } }); + initStories(moduleArgs as unknown as ModuleArgs); + const { navigate, provider } = moduleArgs; + provider.channel.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' }); expect(navigate).not.toHaveBeenCalled(); }); }); describe('CURRENT_STORY_WAS_SET event', () => { it('sets previewInitialized', async () => { - const navigate = jest.fn(); - const fullAPI = Object.assign(new EventEmitter(), {}); - const store = createMockStore({}); - const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - - Object.assign(fullAPI, api); - await init(); - fullAPI.emit(CURRENT_STORY_WAS_SET, { id: 'a--1' }); + const moduleArgs = createMockModuleArgs({}); + initStories(moduleArgs as unknown as ModuleArgs); + const { store, provider } = moduleArgs; + provider.channel.emit(CURRENT_STORY_WAS_SET, { id: 'a--1' }); expect(store.getState().previewInitialized).toBe(true); }); - it('sets a ref to previewInitialized', async () => { - const navigate = jest.fn(); - const fullAPI = Object.assign(new EventEmitter(), { - updateRef: jest.fn(), - }); - const store = createMockStore(); - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - - Object.assign(fullAPI, api); + const fullAPI = { updateRef: jest.fn() }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + initStories(moduleArgs as unknown as ModuleArgs); + const { provider } = moduleArgs; + provider.channel.emit(CURRENT_STORY_WAS_SET, { id: 'a--1' }); - getEventMetadataMock.mockReturnValueOnce({ + getEventMetadata.mockReturnValueOnce({ sourceType: 'external', - ref: { id: 'refId', index: { 'a--1': { args: { a: 'b' } } } }, - } as any); - await init(); - fullAPI.emit(CURRENT_STORY_WAS_SET, { id: 'a--1' }); - + refId: 'refId', + source: '', + sourceLocation: '', + type: '', + ref: { id: 'refId', index: { 'a--1': { args: { a: 'b' } } } } as any, + }); + provider.channel.emit(CURRENT_STORY_WAS_SET, { id: 'a--1' }); expect(fullAPI.updateRef.mock.calls.length).toBe(1); - expect(fullAPI.updateRef.mock.calls[0][1]).toEqual({ previewInitialized: true, }); @@ -828,88 +608,53 @@ describe('stories API', () => { }); describe('args handling', () => { - const parameters = {}; - const preparedEntries: API_PreparedStoryIndex['entries'] = { - 'a--1': { - type: 'story', - title: 'a', - name: '1', - parameters, - id: 'a--1', - args: { a: 'b' }, - importPath: './a.ts', - }, - 'b--1': { - type: 'story', - title: 'b', - name: '1', - parameters, - id: 'b--1', - args: { x: 'y' }, - importPath: './b.ts', - }, - }; - it('changes args properly, per story when receiving STORY_ARGS_UPDATED', () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter(), { - updateRef: jest.fn(), - }); - - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - - const { setIndex } = Object.assign(fullAPI, api); - setIndex({ v: 4, entries: preparedEntries }); + const fullAPI = { updateRef: jest.fn() }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { provider, store } = moduleArgs; + api.setIndex({ v: 4, entries: preparedEntries }); const { index } = store.getState(); expect((index['a--1'] as API_StoryEntry).args).toEqual({ a: 'b' }); expect((index['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' }); - - init(); - fullAPI.emit(STORY_ARGS_UPDATED, { storyId: 'a--1', args: { foo: 'bar' } }); - + provider.channel.emit(STORY_ARGS_UPDATED, { storyId: 'a--1', args: { foo: 'bar' } }); const { index: changedIndex } = store.getState(); expect((changedIndex['a--1'] as API_StoryEntry).args).toEqual({ foo: 'bar' }); expect((changedIndex['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' }); }); - it('changes reffed args properly, per story when receiving STORY_ARGS_UPDATED', () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = new EventEmitter(); + const fullAPI = { updateRef: jest.fn() }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + initStories(moduleArgs as unknown as ModuleArgs); + const { provider } = moduleArgs; - const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api, { - updateRef: jest.fn(), - }); - - init(); - getEventMetadataMock.mockReturnValueOnce({ + getEventMetadata.mockReturnValueOnce({ sourceType: 'external', - ref: { id: 'refId', index: { 'a--1': { args: { a: 'b' } } } }, - } as any); - fullAPI.emit(STORY_ARGS_UPDATED, { storyId: 'a--1', args: { foo: 'bar' } }); - expect((fullAPI as any).updateRef).toHaveBeenCalledWith('refId', { + refId: 'refId', + source: '', + sourceLocation: '', + type: '', + ref: { id: 'refId', index: { 'a--1': { args: { a: 'b' } } } } as any, + }); + provider.channel.emit(STORY_ARGS_UPDATED, { storyId: 'a--1', args: { foo: 'bar' } }); + expect(fullAPI.updateRef).toHaveBeenCalledWith('refId', { index: { 'a--1': { args: { foo: 'bar' } } }, }); }); - it('updateStoryArgs emits UPDATE_STORY_ARGS to the local frame and does not change anything', () => { - const navigate = jest.fn(); - const emit = jest.fn(); - const on = jest.fn(); - const fullAPI = { emit, on }; - const store = createMockStore(); - - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - const { setIndex } = Object.assign(fullAPI, api); - setIndex({ v: 4, entries: preparedEntries }); + const fullAPI = { updateRef: jest.fn() }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { provider, store } = moduleArgs; - init(); + const listener = jest.fn(); + provider.channel.on(UPDATE_STORY_ARGS, listener); + api.setIndex({ v: 4, entries: preparedEntries }); api.updateStoryArgs({ id: 'a--1' } as API_StoryEntry, { foo: 'bar' }); - expect(emit).toHaveBeenCalledWith(UPDATE_STORY_ARGS, { + + expect(listener).toHaveBeenCalledWith({ storyId: 'a--1', updatedArgs: { foo: 'bar' }, options: { @@ -921,23 +666,18 @@ describe('stories API', () => { expect((index['a--1'] as API_StoryEntry).args).toEqual({ a: 'b' }); expect((index['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' }); }); - it('updateStoryArgs emits UPDATE_STORY_ARGS to the right frame', () => { - const navigate = jest.fn(); - const emit = jest.fn(); - const on = jest.fn(); - const fullAPI = { emit, on }; - const store = createMockStore(); - - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - - const { setIndex } = Object.assign(fullAPI, api); - setIndex({ v: 4, entries: preparedEntries }); + const fullAPI = { updateRef: jest.fn() }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { provider } = moduleArgs; - init(); + const listener = jest.fn(); + provider.channel.on(UPDATE_STORY_ARGS, listener); + api.setIndex({ v: 4, entries: preparedEntries }); api.updateStoryArgs({ id: 'a--1', refId: 'refId' } as API_StoryEntry, { foo: 'bar' }); - expect(emit).toHaveBeenCalledWith(UPDATE_STORY_ARGS, { + expect(listener).toHaveBeenCalledWith({ storyId: 'a--1', updatedArgs: { foo: 'bar' }, options: { @@ -945,22 +685,18 @@ describe('stories API', () => { }, }); }); - it('refId to the local frame and does not change anything', () => { - const navigate = jest.fn(); - const emit = jest.fn(); - const on = jest.fn(); - const fullAPI = { emit, on }; - const store = createMockStore(); - - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - - const { setIndex } = Object.assign(fullAPI, api); - setIndex({ v: 4, entries: preparedEntries }); - init(); - + const fullAPI = { updateRef: jest.fn() }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { provider, store } = moduleArgs; + const listener = jest.fn(); + provider.channel.on(RESET_STORY_ARGS, listener); + + api.setIndex({ v: 4, entries: preparedEntries }); api.resetStoryArgs({ id: 'a--1' } as API_StoryEntry, ['foo']); - expect(emit).toHaveBeenCalledWith(RESET_STORY_ARGS, { + + expect(listener).toHaveBeenCalledWith({ storyId: 'a--1', argNames: ['foo'], options: { @@ -972,22 +708,18 @@ describe('stories API', () => { expect((index['a--1'] as API_StoryEntry).args).toEqual({ a: 'b' }); expect((index['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' }); }); - it('resetStoryArgs emits RESET_STORY_ARGS to the right frame', () => { - const navigate = jest.fn(); - const emit = jest.fn(); - const on = jest.fn(); - const fullAPI = { emit, on }; - const store = createMockStore(); - - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); + const fullAPI = { updateRef: jest.fn() }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { provider } = moduleArgs; - const { setIndex } = Object.assign(fullAPI, api); - setIndex({ v: 4, entries: preparedEntries }); - init(); + const listener = jest.fn(); + provider.channel.on(RESET_STORY_ARGS, listener); + api.setIndex({ v: 4, entries: preparedEntries }); api.resetStoryArgs({ id: 'a--1', refId: 'refId' } as API_StoryEntry, ['foo']); - expect(emit).toHaveBeenCalledWith(RESET_STORY_ARGS, { + expect(listener).toHaveBeenCalledWith({ storyId: 'a--1', argNames: ['foo'], options: { @@ -997,268 +729,156 @@ describe('stories API', () => { }); }); - const navigationEntries: StoryIndex['entries'] = { - 'a--1': { - type: 'story', - title: 'a', - name: '1', - id: 'a--1', - importPath: './a.ts', - }, - 'a--2': { - type: 'story', - title: 'a', - name: '2', - id: 'a--2', - importPath: './a.ts', - }, - 'b-c--1': { - type: 'story', - title: 'b/c', - name: '1', - id: 'b-c--1', - importPath: './b/c.ts', - }, - 'b-d--1': { - type: 'story', - title: 'b/d', - name: '1', - id: 'b-d--1', - importPath: './b/d.ts', - }, - 'b-d--2': { - type: 'story', - title: 'b/d', - name: '2', - id: 'b-d--2', - importPath: './b/d.ts', - }, - 'custom-id--1': { - type: 'story', - title: 'b/e', - name: '1', - id: 'custom-id--1', - importPath: './b/.ts', - }, - }; - describe('jumpToStory', () => { it('works forward', () => { - const navigate = jest.fn(); - const store = createMockStore(); + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; - const { - api: { setIndex, jumpToStory }, - } = initStoriesAndSetState({ - store, - storyId: 'a--1', - viewMode: 'story', - navigate, - provider, - } as any); - setIndex({ v: 4, entries: navigationEntries }); + api.setIndex({ v: 4, entries: navigationEntries }); + api.jumpToStory(1); - jumpToStory(1); expect(navigate).toHaveBeenCalledWith('/story/a--2'); }); - it('works backwards', () => { - const navigate = jest.fn(); - const store = createMockStore(); - - const { - api: { setIndex, jumpToStory }, - } = initStoriesAndSetState({ - store, - storyId: 'a--2', - viewMode: 'story', - navigate, - provider, - } as any); - setIndex({ v: 4, entries: navigationEntries }); + const initialState = { path: '/story/a--2', storyId: 'a--2', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; + + api.setIndex({ v: 4, entries: navigationEntries }); + api.jumpToStory(-1); - jumpToStory(-1); expect(navigate).toHaveBeenCalledWith('/story/a--1'); }); - it('does nothing if you are at the last story and go forward', () => { - const navigate = jest.fn(); - const store = createMockStore(); - - const { - api: { setIndex, jumpToStory }, - } = initStoriesAndSetState({ - store, + const initialState = { + path: '/story/custom-id--1', storyId: 'custom-id--1', viewMode: 'story', - navigate, - provider, - } as any); - setIndex({ v: 4, entries: navigationEntries }); + }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; - jumpToStory(1); + api.setIndex({ v: 4, entries: navigationEntries }); + api.jumpToStory(1); expect(navigate).not.toHaveBeenCalled(); }); - it('does nothing if you are at the first story and go backward', () => { - const navigate = jest.fn(); - const store = createMockStore(); - - const { - api: { setIndex, jumpToStory }, - } = initStoriesAndSetState({ - store, - storyId: 'a--1', - viewMode: 'story', - navigate, - provider, - } as any); - setIndex({ v: 4, entries: navigationEntries }); + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; - jumpToStory(-1); + api.setIndex({ v: 4, entries: navigationEntries }); + api.jumpToStory(-1); expect(navigate).not.toHaveBeenCalled(); }); - it('does nothing if you have not selected a story', () => { - const navigate = jest.fn(); - const store = createMockStore(); - - const { - api: { setIndex, jumpToStory }, - } = initStoriesAndSetState({ store, navigate, provider } as any); - setIndex({ v: 4, entries: navigationEntries }); - - jumpToStory(1); + // @ts-expect-error (storyId type is maybe wrong?) + const initialState = { path: '/story', storyId: undefined, viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; + + api.setIndex({ v: 4, entries: navigationEntries }); + api.jumpToStory(1); expect(navigate).not.toHaveBeenCalled(); }); }); describe('findSiblingStoryId', () => { it('works forward', () => { - const navigate = jest.fn(); - const store = createMockStore(); + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; - const storyId = 'a--1'; - const { - api: { setIndex, findSiblingStoryId }, - } = initStoriesAndSetState({ store, navigate, storyId, viewMode: 'story', provider } as any); - setIndex({ v: 4, entries: navigationEntries }); - - const result = findSiblingStoryId(storyId, store.getState().index, 1, false); + api.setIndex({ v: 4, entries: navigationEntries }); + const result = api.findSiblingStoryId('a--1', store.getState().index, 1, false); expect(result).toBe('a--2'); }); it('works forward toSiblingGroup', () => { - const navigate = jest.fn(); - const store = createMockStore(); - - const storyId = 'a--1'; - const { - api: { setIndex, findSiblingStoryId }, - } = initStoriesAndSetState({ store, navigate, storyId, viewMode: 'story', provider } as any); - setIndex({ v: 4, entries: navigationEntries }); + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; - const result = findSiblingStoryId(storyId, store.getState().index, 1, true); + api.setIndex({ v: 4, entries: navigationEntries }); + const result = api.findSiblingStoryId('a--1', store.getState().index, 1, true); expect(result).toBe('b-c--1'); }); }); describe('jumpToComponent', () => { it('works forward', () => { - const navigate = jest.fn(); - const store = createMockStore(); - - const { - api: { setIndex, jumpToComponent }, - } = initStoriesAndSetState({ - store, - navigate, - storyId: 'a--1', - viewMode: 'story', - provider, - } as any); - setIndex({ v: 4, entries: navigationEntries }); + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; - jumpToComponent(1); + api.setIndex({ v: 4, entries: navigationEntries }); + api.jumpToComponent(1); expect(navigate).toHaveBeenCalledWith('/story/b-c--1'); }); - it('works backwards', () => { - const navigate = jest.fn(); - const store = createMockStore(); - - const { - api: { setIndex, jumpToComponent }, - } = initStoriesAndSetState({ - store, - navigate, + const initialState = { + path: '/story/b-c--1', storyId: 'b-c--1', viewMode: 'story', - provider, - } as any); - setIndex({ v: 4, entries: navigationEntries }); + }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; - jumpToComponent(-1); + api.setIndex({ v: 4, entries: navigationEntries }); + api.jumpToComponent(-1); expect(navigate).toHaveBeenCalledWith('/story/a--1'); }); - it('does nothing if you are in the last component and go forward', () => { - const navigate = jest.fn(); - const store = createMockStore(); - - const { - api: { setIndex, jumpToComponent }, - } = initStoriesAndSetState({ - store, - navigate, + const initialState = { + path: '/story/custom-id--1', storyId: 'custom-id--1', viewMode: 'story', - provider, - } as any); - setIndex({ v: 4, entries: navigationEntries }); + }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; - jumpToComponent(1); + api.setIndex({ v: 4, entries: navigationEntries }); + api.jumpToComponent(1); expect(navigate).not.toHaveBeenCalled(); }); - it('does nothing if you are at the first component and go backward', () => { - const navigate = jest.fn(); - const store = createMockStore(); - - const { - api: { setIndex, jumpToComponent }, - } = initStoriesAndSetState({ - store, - navigate, - storyId: 'a--2', - viewMode: 'story', - provider, - } as any); - setIndex({ v: 4, entries: navigationEntries }); + const initialState = { path: '/story/a--2', storyId: 'a--2', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; - jumpToComponent(-1); + api.setIndex({ v: 4, entries: navigationEntries }); + api.jumpToComponent(-1); expect(navigate).not.toHaveBeenCalled(); }); }); - describe('selectStory', () => { it('navigates', () => { - const navigate = jest.fn(); - const store = createMockStore({ storyId: 'a--1', viewMode: 'story' }); - const { - api: { setIndex, selectStory }, - } = initStoriesAndSetState({ store, navigate, provider } as any); - setIndex({ v: 4, entries: navigationEntries }); - - selectStory('a--2'); + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; + + api.setIndex({ v: 4, entries: navigationEntries }); + api.selectStory('a--2'); expect(navigate).toHaveBeenCalledWith('/story/a--2'); }); - it('sets view mode to docs if doc-level component is selected', () => { - const navigate = jest.fn(); - const store = createMockStore({ storyId: 'a--1', viewMode: 'docs' }); - const { - api: { setIndex, selectStory }, - } = initStoriesAndSetState({ store, navigate, provider } as any); - setIndex({ + const initialState = { path: '/docs/a--1', storyId: 'a--1', viewMode: 'docs' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; + + api.setIndex({ v: 4, entries: { ...navigationEntries, @@ -1272,194 +892,129 @@ describe('stories API', () => { }, }, }); - - selectStory('intro'); + api.selectStory('intro'); expect(navigate).toHaveBeenCalledWith('/docs/intro--docs'); }); - - describe('legacy api', () => { + describe('deprecated api', () => { it('allows navigating to a combination of title + name', () => { - const navigate = jest.fn(); - const store = createMockStore(); - const { - api: { setIndex, selectStory }, - } = initStoriesAndSetState({ - store, - storyId: 'a--1', - viewMode: 'story', - navigate, - provider, - } as any); - setIndex({ v: 4, entries: navigationEntries }); - - selectStory('a', '2'); + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; + + api.setIndex({ v: 4, entries: navigationEntries }); + api.selectStory('a', '2'); expect(navigate).toHaveBeenCalledWith('/story/a--2'); }); - it('allows navigating to a given name (in the current component)', () => { - const navigate = jest.fn(); - const store = createMockStore(); - const { - api: { setIndex, selectStory }, - } = initStoriesAndSetState({ - store, - storyId: 'a--1', - viewMode: 'story', - navigate, - provider, - } as any); - setIndex({ v: 4, entries: navigationEntries }); - - selectStory(undefined, '2'); + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; + + api.setIndex({ v: 4, entries: navigationEntries }); + api.selectStory(undefined, '2'); expect(navigate).toHaveBeenCalledWith('/story/a--2'); }); }); - it('allows navigating away from the settings pages', () => { - const navigate = jest.fn(); - const store = createMockStore({ storyId: 'a--1', viewMode: 'settings' }); - const { - api: { setIndex, selectStory }, - } = initStoriesAndSetState({ store, navigate, provider } as any); - setIndex({ v: 4, entries: navigationEntries }); - - selectStory('a--2'); + const initialState = { path: '/settings/a--1', storyId: 'a--1', viewMode: 'settings' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; + + api.setIndex({ v: 4, entries: navigationEntries }); + api.selectStory('a--2'); expect(navigate).toHaveBeenCalledWith('/story/a--2'); }); - it('allows navigating to first story in component on call by component id', () => { - const navigate = jest.fn(); - const store = createMockStore(); - const { - api: { setIndex, selectStory }, - } = initStoriesAndSetState({ - store, - storyId: 'a--1', - viewMode: 'story', - navigate, - provider, - } as any); - setIndex({ v: 4, entries: navigationEntries }); + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; - selectStory('a'); + api.setIndex({ v: 4, entries: navigationEntries }); + api.selectStory('a'); expect(navigate).toHaveBeenCalledWith('/story/a--1'); }); - it('allows navigating to first story in group on call by group id', () => { - const navigate = jest.fn(); - const store = createMockStore(); - const { - api: { setIndex, selectStory }, - } = initStoriesAndSetState({ - store, - storyId: 'a--1', - viewMode: 'story', - navigate, - provider, - } as any); - setIndex({ v: 4, entries: navigationEntries }); + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; - selectStory('b'); + api.setIndex({ v: 4, entries: navigationEntries }); + api.selectStory('b'); expect(navigate).toHaveBeenCalledWith('/story/b-c--1'); }); - it('allows navigating to first story in component on call by title', () => { - const navigate = jest.fn(); - const store = createMockStore(); - const { - api: { setIndex, selectStory }, - } = initStoriesAndSetState({ - store, - storyId: 'a--1', - viewMode: 'story', - navigate, - provider, - } as any); - setIndex({ v: 4, entries: navigationEntries }); + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; - selectStory('A'); + api.setIndex({ v: 4, entries: navigationEntries }); + api.selectStory('A'); expect(navigate).toHaveBeenCalledWith('/story/a--1'); }); - it('allows navigating to the first story of the current component if passed nothing', () => { - const navigate = jest.fn(); - const store = createMockStore(); - const { - api: { setIndex, selectStory }, - } = initStoriesAndSetState({ - store, - storyId: 'a--2', - viewMode: 'story', - navigate, - provider, - } as any); - setIndex({ v: 4, entries: navigationEntries }); + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; - selectStory(); + api.setIndex({ v: 4, entries: navigationEntries }); + api.selectStory(); expect(navigate).toHaveBeenCalledWith('/story/a--1'); }); - describe('component permalinks', () => { it('allows navigating to kind/storyname (legacy api)', () => { - const navigate = jest.fn(); - const store = createMockStore(); - - const { - api: { selectStory, setIndex }, - } = initStoriesAndSetState({ store, navigate, provider } as any); - setIndex({ v: 4, entries: navigationEntries }); + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; - selectStory('b/e', '1'); + api.setIndex({ v: 4, entries: navigationEntries }); + api.selectStory('b/e', '1'); expect(navigate).toHaveBeenCalledWith('/story/custom-id--1'); }); - it('allows navigating to component permalink/storyname (legacy api)', () => { - const navigate = jest.fn(); - const store = createMockStore(); - - const { - api: { selectStory, setIndex }, - } = initStoriesAndSetState({ store, navigate, provider } as any); - setIndex({ v: 4, entries: navigationEntries }); + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; - selectStory('custom-id', '1'); + api.setIndex({ v: 4, entries: navigationEntries }); + api.selectStory('custom-id', '1'); expect(navigate).toHaveBeenCalledWith('/story/custom-id--1'); }); - it('allows navigating to first story in kind on call by kind', () => { - const navigate = jest.fn(); - const store = createMockStore(); + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { navigate } = moduleArgs; - const { - api: { selectStory, setIndex }, - } = initStoriesAndSetState({ store, navigate, provider } as any); - setIndex({ v: 4, entries: navigationEntries }); - - selectStory('b/e'); + api.setIndex({ v: 4, entries: navigationEntries }); + api.selectStory('b/e'); expect(navigate).toHaveBeenCalledWith('/story/custom-id--1'); }); }); }); - describe('STORY_PREPARED', () => { it('prepares the story', async () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter(), { - setStories: jest.fn(), - setOptions: jest.fn(), - }); + const fullAPI = { setOptions: jest.fn() }; + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState, fullAPI }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { provider, store } = moduleArgs; - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); + api.setIndex({ v: 4, entries: mockEntries }); - await init(); - fullAPI.emit(STORY_PREPARED, { + provider.channel.emit(STORY_PREPARED, { id: 'component-a--story-1', parameters: { a: 'b' }, args: { c: 'd' }, }); - const { index } = store.getState(); expect(index['component-a--story-1']).toMatchObject({ type: 'story', @@ -1472,54 +1027,42 @@ describe('stories API', () => { args: { c: 'd' }, }); }); - it('sets options the first time it is called', async () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter(), { - setStories: jest.fn(), - setOptions: jest.fn(), - }); + const fullAPI = { setOptions: jest.fn() }; + const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' }; + const moduleArgs = createMockModuleArgs({ initialState, fullAPI }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { provider } = moduleArgs; - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); + api.setIndex({ v: 4, entries: mockEntries }); - await init(); - fullAPI.emit(STORY_PREPARED, { + provider.channel.emit(STORY_PREPARED, { id: 'component-a--story-1', parameters: { options: 'options' }, }); - expect(fullAPI.setOptions).toHaveBeenCalledWith('options'); fullAPI.setOptions.mockClear(); - fullAPI.emit(STORY_PREPARED, { + + provider.channel.emit(STORY_PREPARED, { id: 'component-a--story-1', parameters: { options: 'options2' }, }); - expect(fullAPI.setOptions).not.toHaveBeenCalled(); }); }); - describe('DOCS_PREPARED', () => { it('prepares the docs entry', async () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter(), { - setStories: jest.fn(), - setOptions: jest.fn(), - }); + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { provider, store } = moduleArgs; - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); + api.setIndex({ v: 4, entries: mockEntries }); - await init(); - fullAPI.emit(DOCS_PREPARED, { + provider.channel.emit(DOCS_PREPARED, { id: 'component-a--docs', parameters: { a: 'b' }, }); - const { index } = store.getState(); expect(index['component-a--docs']).toMatchObject({ type: 'docs', @@ -1532,104 +1075,75 @@ describe('stories API', () => { }); }); }); - describe('CONFIG_ERROR', () => { it('sets previewInitialized to true, local', async () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter(), {}); + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { provider, store } = moduleArgs; - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); - - await init(); - - fullAPI.emit(CONFIG_ERROR, { message: 'Failed to run configure' }); + api.setIndex({ v: 4, entries: mockEntries }); + provider.channel.emit(CONFIG_ERROR, { message: 'Failed to run configure' }); const { previewInitialized } = store.getState(); expect(previewInitialized).toBe(true); }); - it('sets previewInitialized to true, ref', async () => { - const navigate = jest.fn(); - const fullAPI = Object.assign(new EventEmitter(), { - updateRef: jest.fn(), - }); - const store = createMockStore(); - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); + const fullAPI = { updateRef: jest.fn() }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { provider } = moduleArgs; - Object.assign(fullAPI, api); + api.setIndex({ v: 4, entries: mockEntries }); - getEventMetadataMock.mockReturnValueOnce({ + getEventMetadata.mockReturnValueOnce({ sourceType: 'external', ref: { id: 'refId', stories: { 'a--1': { args: { a: 'b' } } } }, } as any); - await init(); - fullAPI.emit(CONFIG_ERROR, { message: 'Failed to run configure' }); - + provider.channel.emit(CONFIG_ERROR, { message: 'Failed to run configure' }); expect(fullAPI.updateRef.mock.calls.length).toBe(1); expect(fullAPI.updateRef.mock.calls[0][1]).toEqual({ previewInitialized: true, }); }); }); - describe('STORY_MISSING', () => { it('sets previewInitialized to true, local', async () => { - const navigate = jest.fn(); - const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter(), {}); - - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); - - await init(); - - fullAPI.emit(STORY_MISSING, { message: 'Failed to run configure' }); + const moduleArgs = createMockModuleArgs({}); + initStories(moduleArgs as unknown as ModuleArgs); + const { provider, store } = moduleArgs; + provider.channel.emit(STORY_MISSING, { message: 'Failed to run configure' }); const { previewInitialized } = store.getState(); expect(previewInitialized).toBe(true); }); - it('sets previewInitialized to true, ref', async () => { - const navigate = jest.fn(); - const fullAPI = Object.assign(new EventEmitter(), { - updateRef: jest.fn(), - }); - const store = createMockStore(); - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - - Object.assign(fullAPI, api); + const fullAPI = { updateRef: jest.fn() }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + initStories(moduleArgs as unknown as ModuleArgs); + const { provider } = moduleArgs; - getEventMetadataMock.mockReturnValueOnce({ + getEventMetadata.mockReturnValueOnce({ sourceType: 'external', ref: { id: 'refId', stories: { 'a--1': { args: { a: 'b' } } } }, } as any); - await init(); - fullAPI.emit(STORY_MISSING, { message: 'Failed to run configure' }); - + provider.channel.emit(STORY_MISSING, { message: 'Failed to run configure' }); expect(fullAPI.updateRef.mock.calls.length).toBe(1); expect(fullAPI.updateRef.mock.calls[0][1]).toEqual({ previewInitialized: true, }); }); }); - describe('v2 SET_STORIES event', () => { it('normalizes parameters and calls setRef for external stories', () => { - const fullAPI = Object.assign(new EventEmitter(), {}); - const navigate = jest.fn(); - const store = createMockStore(); - - const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - const finalAPI = Object.assign(fullAPI, api, { - setIndex: jest.fn(), + const fullAPI = { findRef: jest.fn(), setRef: jest.fn(), - }); - init(); + }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + initStories(moduleArgs as unknown as ModuleArgs); + const { provider, store } = moduleArgs; - getEventMetadataMock.mockReturnValueOnce({ + getEventMetadata.mockReturnValueOnce({ sourceType: 'external', ref: { id: 'ref' }, } as any); @@ -1639,10 +1153,9 @@ describe('stories API', () => { kindParameters: { a: { kind: 'kind' } }, stories: { 'a--1': { kind: 'a', parameters: { story: 'story' } } }, }; - finalAPI.emit(SET_STORIES, setStoriesPayload); - - expect(finalAPI.setIndex).not.toHaveBeenCalled(); - expect(finalAPI.setRef).toHaveBeenCalledWith( + provider.channel.emit(SET_STORIES, setStoriesPayload); + expect(store.getState().index).toBeUndefined(); + expect(fullAPI.setRef).toHaveBeenCalledWith( 'ref', { id: 'ref', @@ -1656,28 +1169,23 @@ describe('stories API', () => { }); describe('legacy (v1) SET_STORIES event', () => { it('calls setRef with stories', () => { - const fullAPI = Object.assign(new EventEmitter()); - const navigate = jest.fn(); - const store = createMockStore(); - - const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api, { - setIndex: jest.fn(), + const fullAPI = { findRef: jest.fn(), setRef: jest.fn(), - }); - init(); + }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + initStories(moduleArgs as unknown as ModuleArgs); + const { provider, store } = moduleArgs; - getEventMetadataMock.mockReturnValueOnce({ + getEventMetadata.mockReturnValueOnce({ sourceType: 'external', ref: { id: 'ref' }, } as any); const setStoriesPayload = { stories: { 'a--1': {} }, }; - fullAPI.emit(SET_STORIES, setStoriesPayload); - - expect(fullAPI.setIndex).not.toHaveBeenCalled(); + provider.channel.emit(SET_STORIES, setStoriesPayload); + expect(store.getState().index).toBeUndefined(); expect(fullAPI.setRef).toHaveBeenCalledWith( 'ref', { @@ -1690,13 +1198,10 @@ describe('stories API', () => { ); }); }); - describe('experimental_updateStatus', () => { it('is included in the initial state', () => { - const { state } = initStoriesAndSetState({ - storyId: 'id', - viewMode: 'story', - } as ModuleArgs); + const moduleArgs = createMockModuleArgs({}); + const { state } = initStories(moduleArgs as unknown as ModuleArgs); expect(state).toEqual( expect.objectContaining({ @@ -1704,24 +1209,15 @@ describe('stories API', () => { }) ); }); - it('updates a story', async () => { - const fullAPI = Object.assign(new EventEmitter()); - const navigate = jest.fn(); - const store = createMockStore(); - - const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; - const API: SubAPI = Object.assign(fullAPI, api, { - setIndex: jest.fn(), - findRef: jest.fn(), - setRef: jest.fn(), - }); - - await init(); + await api.setIndex({ v: 4, entries: mockEntries }); await expect( - API.experimental_updateStatus('a-addon-id', { + api.experimental_updateStatus('a-addon-id', { 'a-story-id': { status: 'pending', title: 'an addon title', @@ -1729,7 +1225,6 @@ describe('stories API', () => { }, }) ).resolves.not.toThrow(); - expect(store.getState().status).toMatchInlineSnapshot(` Object { "a-story-id": Object { @@ -1742,24 +1237,15 @@ describe('stories API', () => { } `); }); - it('updates multiple stories', async () => { - const fullAPI = Object.assign(new EventEmitter()); - const navigate = jest.fn(); - const store = createMockStore(); - - const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; - const API: SubAPI = Object.assign(fullAPI, api, { - setIndex: jest.fn(), - findRef: jest.fn(), - setRef: jest.fn(), - }); - - await init(); + await api.setIndex({ v: 4, entries: mockEntries }); await expect( - API.experimental_updateStatus('a-addon-id', { + api.experimental_updateStatus('a-addon-id', { 'a-story-id': { status: 'pending', title: 'an addon title', @@ -1768,7 +1254,6 @@ describe('stories API', () => { 'another-story-id': { status: 'success', title: 'a addon title', description: '' }, }) ).resolves.not.toThrow(); - expect(store.getState().status).toMatchInlineSnapshot(` Object { "a-story-id": Object { @@ -1789,4 +1274,220 @@ describe('stories API', () => { `); }); }); + describe('experimental_setFilter', () => { + it('is included in the initial state', async () => { + const moduleArgs = createMockModuleArgs({}); + const { state, api } = initStories(moduleArgs as unknown as ModuleArgs); + + await api.setIndex({ v: 4, entries: mockEntries }); + + expect(state).toEqual( + expect.objectContaining({ + filters: {}, + }) + ); + }); + it('updates state', async () => { + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; + + await api.setIndex({ v: 4, entries: mockEntries }); + + api.experimental_setFilter('myCustomFilter', () => true); + + expect(store.getState()).toEqual( + expect.objectContaining({ + filters: { + myCustomFilter: expect.any(Function), + }, + }) + ); + }); + + it('can filter', async () => { + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; + + await api.setIndex({ v: 4, entries: navigationEntries }); + await api.experimental_setFilter('myCustomFilter', (item) => item.id.startsWith('a')); + + const { index } = store.getState(); + + expect(index).toMatchInlineSnapshot(` + Object { + "a": Object { + "children": Array [ + "a--1", + "a--2", + ], + "depth": 0, + "id": "a", + "isComponent": true, + "isLeaf": false, + "isRoot": false, + "name": "a", + "parent": undefined, + "renderLabel": undefined, + "type": "component", + }, + "a--1": Object { + "depth": 1, + "id": "a--1", + "importPath": "./a.ts", + "isComponent": false, + "isLeaf": true, + "isRoot": false, + "kind": "a", + "name": "1", + "parent": "a", + "prepared": false, + "renderLabel": undefined, + "title": "a", + "type": "story", + }, + "a--2": Object { + "depth": 1, + "id": "a--2", + "importPath": "./a.ts", + "isComponent": false, + "isLeaf": true, + "isRoot": false, + "kind": "a", + "name": "2", + "parent": "a", + "prepared": false, + "renderLabel": undefined, + "title": "a", + "type": "story", + }, + } + `); + }); + + it('can filter on status', async () => { + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; + + await api.setIndex({ v: 4, entries: navigationEntries }); + await api.experimental_setFilter( + 'myCustomFilter', + (item) => + item.status !== undefined && + Object.values(item.status).some((v) => v.status === 'pending') + ); + + // empty, because there are no stories with status + expect(store.getState().index).toMatchInlineSnapshot(`Object {}`); + + // setting status should update the index + await api.experimental_updateStatus('a-addon-id', { + 'a--1': { + status: 'pending', + title: 'an addon title', + description: 'an addon description', + }, + 'a--2': { status: 'success', title: 'a addon title', description: '' }, + }); + + expect(store.getState().index).toMatchInlineSnapshot(` + Object { + "a": Object { + "children": Array [ + "a--1", + ], + "depth": 0, + "id": "a", + "isComponent": true, + "isLeaf": false, + "isRoot": false, + "name": "a", + "parent": undefined, + "renderLabel": undefined, + "type": "component", + }, + "a--1": Object { + "depth": 1, + "id": "a--1", + "importPath": "./a.ts", + "isComponent": false, + "isLeaf": true, + "isRoot": false, + "kind": "a", + "name": "1", + "parent": "a", + "prepared": false, + "renderLabel": undefined, + "title": "a", + "type": "story", + }, + } + `); + }); + + it('persists filter when index is updated', async () => { + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; + + await api.setIndex({ v: 4, entries: navigationEntries }); + await api.experimental_setFilter('myCustomFilter', (item) => item.id.startsWith('a')); + + await api.setIndex({ v: 4, entries: navigationEntries }); + + const { index } = store.getState(); + + expect(index).toMatchInlineSnapshot(` + Object { + "a": Object { + "children": Array [ + "a--1", + "a--2", + ], + "depth": 0, + "id": "a", + "isComponent": true, + "isLeaf": false, + "isRoot": false, + "name": "a", + "parent": undefined, + "renderLabel": undefined, + "type": "component", + }, + "a--1": Object { + "depth": 1, + "id": "a--1", + "importPath": "./a.ts", + "isComponent": false, + "isLeaf": true, + "isRoot": false, + "kind": "a", + "name": "1", + "parent": "a", + "prepared": false, + "renderLabel": undefined, + "title": "a", + "type": "story", + }, + "a--2": Object { + "depth": 1, + "id": "a--2", + "importPath": "./a.ts", + "isComponent": false, + "isLeaf": true, + "isRoot": false, + "kind": "a", + "name": "2", + "parent": "a", + "prepared": false, + "renderLabel": undefined, + "title": "a", + "type": "story", + }, + } + `); + }); + }); }); diff --git a/code/lib/manager-api/src/tests/url.test.js b/code/lib/manager-api/src/tests/url.test.js index c269331b1c77..33cf4a1872c1 100644 --- a/code/lib/manager-api/src/tests/url.test.js +++ b/code/lib/manager-api/src/tests/url.test.js @@ -2,6 +2,7 @@ import qs from 'qs'; import { SET_CURRENT_STORY, GLOBALS_UPDATED, UPDATE_QUERY_PARAMS } from '@storybook/core-events'; +import EventEmitter from 'events'; import { init as initURL } from '../modules/url'; jest.mock('@storybook/client-logger'); @@ -17,7 +18,7 @@ describe('initial state', () => { const { state: { layout }, - } = initURL({ navigate, state: { location } }); + } = initURL({ navigate, state: { location }, provider: { channel: new EventEmitter() } }); expect(layout).toEqual({ isFullscreen: true }); }); @@ -28,7 +29,7 @@ describe('initial state', () => { const { state: { layout }, - } = initURL({ navigate, state: { location } }); + } = initURL({ navigate, state: { location }, provider: { channel: new EventEmitter() } }); expect(layout).toEqual({ showNav: false }); }); @@ -39,7 +40,7 @@ describe('initial state', () => { const { state: { ui }, - } = initURL({ navigate, state: { location } }); + } = initURL({ navigate, state: { location }, provider: { channel: new EventEmitter() } }); expect(ui).toEqual({ enableShortcuts: false }); }); @@ -50,7 +51,7 @@ describe('initial state', () => { const { state: { layout }, - } = initURL({ navigate, state: { location } }); + } = initURL({ navigate, state: { location }, provider: { channel: new EventEmitter() } }); expect(layout).toEqual({ panelPosition: 'bottom' }); }); @@ -61,7 +62,7 @@ describe('initial state', () => { const { state: { layout }, - } = initURL({ navigate, state: { location } }); + } = initURL({ navigate, state: { location }, provider: { channel: new EventEmitter() } }); expect(layout).toEqual({ panelPosition: 'right' }); }); @@ -72,7 +73,7 @@ describe('initial state', () => { const { state: { layout }, - } = initURL({ navigate, state: { location } }); + } = initURL({ navigate, state: { location }, provider: { channel: new EventEmitter() } }); expect(layout).toEqual({ showPanel: false }); }); @@ -88,18 +89,23 @@ describe('queryParams', () => { }, getState: () => state, }; - const fullAPI = { emit: jest.fn() }; + const channel = new EventEmitter(); const { api } = initURL({ state: { location: { search: '' } }, navigate: jest.fn(), store, - fullAPI, + provider: { channel }, }); + const listener = jest.fn(); + + channel.on(UPDATE_QUERY_PARAMS, listener); + api.setQueryParams({ foo: 'bar' }); expect(api.getQueryParam('foo')).toEqual('bar'); - expect(fullAPI.emit).toHaveBeenCalledWith(UPDATE_QUERY_PARAMS, { foo: 'bar' }); + + expect(listener).toHaveBeenCalledWith({ foo: 'bar' }); }); }); @@ -120,14 +126,6 @@ describe('initModule', () => { }); const fullAPI = { - callbacks: {}, - on(event, fn) { - this.callbacks[event] = this.callbacks[event] || []; - this.callbacks[event].push(fn); - }, - emit(event, ...args) { - this.callbacks[event]?.forEach((cb) => cb(...args)); - }, showReleaseNotesOnLaunch: jest.fn(), }; @@ -140,19 +138,22 @@ describe('initModule', () => { store.setState(storyState('test--story')); const navigate = jest.fn(); - - const { api, init } = initURL({ store, state: { location: {} }, navigate, fullAPI }); - Object.assign(fullAPI, api, { - getCurrentStoryData: () => ({ - type: 'story', - args: { a: 1, b: 2 }, - initialArgs: { a: 1, b: 1 }, - isLeaf: true, + const channel = new EventEmitter(); + initURL({ + store, + provider: { channel }, + state: { location: {} }, + navigate, + fullAPI: Object.assign(fullAPI, { + getCurrentStoryData: () => ({ + type: 'story', + args: { a: 1, b: 2 }, + initialArgs: { a: 1, b: 1 }, + isLeaf: true, + }), }), }); - init(); - - fullAPI.emit(SET_CURRENT_STORY); + channel.emit(SET_CURRENT_STORY); expect(navigate).toHaveBeenCalledWith( '/story/test--story&args=b:2', expect.objectContaining({ replace: true }) @@ -164,12 +165,10 @@ describe('initModule', () => { store.setState(storyState('test--story')); const navigate = jest.fn(); + const channel = new EventEmitter(); + initURL({ store, provider: { channel }, state: { location: {} }, navigate, fullAPI }); - const { api, init } = initURL({ store, state: { location: {} }, navigate, fullAPI }); - Object.assign(fullAPI, api); - init(); - - fullAPI.emit(GLOBALS_UPDATED, { globals: { a: 2 }, initialGlobals: { a: 1, b: 1 } }); + channel.emit(GLOBALS_UPDATED, { globals: { a: 2 }, initialGlobals: { a: 1, b: 1 } }); expect(navigate).toHaveBeenCalledWith( '/story/test--story&globals=a:2;b:!undefined', expect.objectContaining({ replace: true }) @@ -180,20 +179,24 @@ describe('initModule', () => { it('adds url params alphabetically', async () => { store.setState({ ...storyState('test--story'), customQueryParams: { full: 1 } }); const navigate = jest.fn(); - - const { api, init } = initURL({ store, state: { location: {} }, navigate, fullAPI }); - Object.assign(fullAPI, api, { - getCurrentStoryData: () => ({ type: 'story', args: { a: 1 }, isLeaf: true }), + const channel = new EventEmitter(); + const { api } = initURL({ + store, + provider: { channel }, + state: { location: {} }, + navigate, + fullAPI: Object.assign(fullAPI, { + getCurrentStoryData: () => ({ type: 'story', args: { a: 1 }, isLeaf: true }), + }), }); - init(); - fullAPI.emit(GLOBALS_UPDATED, { globals: { g: 2 } }); + channel.emit(GLOBALS_UPDATED, { globals: { g: 2 } }); expect(navigate).toHaveBeenCalledWith( '/story/test--story&full=1&globals=g:2', expect.objectContaining({ replace: true }) ); - fullAPI.emit(SET_CURRENT_STORY); + channel.emit(SET_CURRENT_STORY); expect(navigate).toHaveBeenCalledWith( '/story/test--story&args=a:1&full=1&globals=g:2', expect.objectContaining({ replace: true }) diff --git a/code/lib/node-logger/package.json b/code/lib/node-logger/package.json index 91d9bd320f57..0870a33674a3 100644 --- a/code/lib/node-logger/package.json +++ b/code/lib/node-logger/package.json @@ -34,7 +34,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/postinstall/package.json b/code/lib/postinstall/package.json index b99ab6d71c52..49c70de632b4 100644 --- a/code/lib/postinstall/package.json +++ b/code/lib/postinstall/package.json @@ -37,7 +37,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/preview-api/package.json b/code/lib/preview-api/package.json index d3b2eb8f8733..b7e9fa5add43 100644 --- a/code/lib/preview-api/package.json +++ b/code/lib/preview-api/package.json @@ -60,7 +60,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/preview-api/src/modules/store/StoryIndexStore.test.ts b/code/lib/preview-api/src/modules/store/StoryIndexStore.test.ts index a898ecd1b06c..c906d7a21a75 100644 --- a/code/lib/preview-api/src/modules/store/StoryIndexStore.test.ts +++ b/code/lib/preview-api/src/modules/store/StoryIndexStore.test.ts @@ -154,7 +154,7 @@ describe('StoryIndexStore', () => { const store = new StoryIndexStore(storyIndex); expect(() => store.storyIdToEntry('random')).toThrow( - /Couldn't find story matching 'random'/ + /Couldn't find story matching id 'random'/ ); }); }); diff --git a/code/lib/preview-api/src/modules/store/StoryIndexStore.ts b/code/lib/preview-api/src/modules/store/StoryIndexStore.ts index 8731f3d495e1..03e8a129b9bf 100644 --- a/code/lib/preview-api/src/modules/store/StoryIndexStore.ts +++ b/code/lib/preview-api/src/modules/store/StoryIndexStore.ts @@ -1,4 +1,3 @@ -import { dedent } from 'ts-dedent'; import type { IndexEntry, Path, @@ -8,6 +7,7 @@ import type { ComponentTitle, } from '@storybook/types'; import memoize from 'memoizerific'; +import { MissingStoryAfterHmrError } from '@storybook/core-events/preview-errors'; export type StorySpecifier = StoryId | { name: StoryName; title: ComponentTitle } | '*'; @@ -49,11 +49,7 @@ export class StoryIndexStore { storyIdToEntry(storyId: StoryId): IndexEntry { const storyEntry = this.entries[storyId]; if (!storyEntry) { - throw new Error(dedent`Couldn't find story matching '${storyId}' after HMR. - - Did you remove it from your CSF file? - - Are you sure a story with that id exists? - - Please check your entries field of your main.js config. - - Also check the browser console and terminal for error messages.`); + throw new MissingStoryAfterHmrError({ storyId }); } return storyEntry; diff --git a/code/lib/preview-api/src/typings.d.ts b/code/lib/preview-api/src/typings.d.ts index fb9194834b96..bedefed4b9a8 100644 --- a/code/lib/preview-api/src/typings.d.ts +++ b/code/lib/preview-api/src/typings.d.ts @@ -24,6 +24,7 @@ declare var __STORYBOOK_PREVIEW__: import('./modules/preview-web/PreviewWeb').Pr declare var __STORYBOOK_STORY_STORE__: any; declare var STORYBOOK_HOOKS_CONTEXT: any; declare var LOGLEVEL: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined; +declare var sendTelemetryError: (error: any) => void; declare module 'ansi-to-html'; declare class AnsiToHtml { diff --git a/code/lib/preview/package.json b/code/lib/preview/package.json index 436af73290fb..7d386ea80aae 100644 --- a/code/lib/preview/package.json +++ b/code/lib/preview/package.json @@ -48,7 +48,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", @@ -58,6 +59,7 @@ "@storybook/channels": "workspace:*", "@storybook/client-logger": "workspace:*", "@storybook/core-events": "workspace:*", + "@storybook/global": "^5.0.0", "@storybook/preview-api": "workspace:*", "typescript": "~4.9.3" }, diff --git a/code/lib/preview/src/runtime.ts b/code/lib/preview/src/runtime.ts index 5e890a133a55..7785e42df8c9 100644 --- a/code/lib/preview/src/runtime.ts +++ b/code/lib/preview/src/runtime.ts @@ -1,3 +1,6 @@ +import { TELEMETRY_ERROR } from '@storybook/core-events'; +import { global } from '@storybook/global'; + import { values } from './globals/runtime'; import { globals } from './globals/types'; @@ -5,5 +8,23 @@ const getKeys = Object.keys as (obj: T) => Array; // Apply all the globals getKeys(globals).forEach((key) => { - (globalThis as any)[globals[key]] = values[key]; + (global as any)[globals[key]] = values[key]; +}); + +global.sendTelemetryError = (error: any) => { + const channel = global.__STORYBOOK_ADDONS_CHANNEL__; + channel.emit(TELEMETRY_ERROR, error); +}; + +// handle all uncaught StorybookError at the root of the application and log to telemetry if applicable +global.addEventListener('error', (args: any) => { + const error = args.error || args; + if (error.fromStorybook) { + global.sendTelemetryError(error); + } +}); +global.addEventListener('unhandledrejection', ({ reason }: any) => { + if (reason.fromStorybook) { + global.sendTelemetryError(reason); + } }); diff --git a/code/lib/preview/src/typings.d.ts b/code/lib/preview/src/typings.d.ts index bfd9e55123ff..a816c261fa72 100644 --- a/code/lib/preview/src/typings.d.ts +++ b/code/lib/preview/src/typings.d.ts @@ -1 +1,5 @@ +/* eslint-disable @typescript-eslint/naming-convention */ declare var LOGLEVEL: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined; + +declare var __STORYBOOK_ADDONS_CHANNEL__: any; +declare var sendTelemetryError: (error: any) => void; diff --git a/code/lib/react-dom-shim/package.json b/code/lib/react-dom-shim/package.json index 5eb83c154d23..d2ae7e06a70e 100644 --- a/code/lib/react-dom-shim/package.json +++ b/code/lib/react-dom-shim/package.json @@ -46,7 +46,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/router/package.json b/code/lib/router/package.json index 22de4497fbcd..742df75b4d4a 100644 --- a/code/lib/router/package.json +++ b/code/lib/router/package.json @@ -41,7 +41,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/source-loader/package.json b/code/lib/source-loader/package.json index 69696ba896f9..0f53d7cd35e3 100644 --- a/code/lib/source-loader/package.json +++ b/code/lib/source-loader/package.json @@ -37,7 +37,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/telemetry/package.json b/code/lib/telemetry/package.json index 2d217c439ffe..8f85a012b16a 100644 --- a/code/lib/telemetry/package.json +++ b/code/lib/telemetry/package.json @@ -36,7 +36,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/telemetry/src/types.ts b/code/lib/telemetry/src/types.ts index ba3af37719c0..35266814dff7 100644 --- a/code/lib/telemetry/src/types.ts +++ b/code/lib/telemetry/src/types.ts @@ -9,6 +9,7 @@ export type EventType = | 'build' | 'upgrade' | 'init' + | 'browser' | 'canceled' | 'error' | 'error-metadata' diff --git a/code/lib/theming/package.json b/code/lib/theming/package.json index 0353a3fb2762..1f44927fdf1d 100644 --- a/code/lib/theming/package.json +++ b/code/lib/theming/package.json @@ -41,7 +41,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/types/package.json b/code/lib/types/package.json index 68b73cf8419b..9ee90eea32ee 100644 --- a/code/lib/types/package.json +++ b/code/lib/types/package.json @@ -36,7 +36,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/lib/types/src/modules/addons.ts b/code/lib/types/src/modules/addons.ts index f55efcd29983..7c6a7987a2e3 100644 --- a/code/lib/types/src/modules/addons.ts +++ b/code/lib/types/src/modules/addons.ts @@ -10,6 +10,7 @@ import type { } from 'react'; import type { RenderData as RouterData } from '../../../router/src/types'; import type { ThemeVars } from '../../../theming/src/types'; +import type { API_SidebarOptions } from './api'; import type { Args, ArgsStoryFn as ArgsStoryFnForFramework, @@ -477,6 +478,7 @@ export interface Addon_Config { toolbar?: { [id: string]: Addon_ToolbarConfig; }; + sidebar?: API_SidebarOptions; [key: string]: any; } diff --git a/code/lib/types/src/modules/api-stories.ts b/code/lib/types/src/modules/api-stories.ts index 414f1384d761..3df42dd812ec 100644 --- a/code/lib/types/src/modules/api-stories.ts +++ b/code/lib/types/src/modules/api-stories.ts @@ -130,7 +130,7 @@ export interface API_IndexHash { } // We used to received a bit more data over the channel on the SET_STORIES event, including // the full parameters for each story. -type API_PreparedIndexEntry = IndexEntry & { +export type API_PreparedIndexEntry = IndexEntry & { parameters?: Parameters; argTypes?: ArgTypes; args?: Args; @@ -184,3 +184,7 @@ export interface API_StatusObject { export type API_StatusState = Record>; export type API_StatusUpdate = Record; + +export type API_FilterFunction = ( + item: API_PreparedIndexEntry & { status: Record } +) => boolean; diff --git a/code/lib/types/src/modules/api.ts b/code/lib/types/src/modules/api.ts index 762166b48fee..1fbaf0bba9bd 100644 --- a/code/lib/types/src/modules/api.ts +++ b/code/lib/types/src/modules/api.ts @@ -4,7 +4,7 @@ import type { RenderData } from '../../../router/src/types'; import type { Channel } from '../../../channels/src'; import type { ThemeVars } from '../../../theming/src/types'; import type { DocsOptions } from './core-common'; -import type { API_HashEntry, API_IndexHash } from './api-stories'; +import type { API_FilterFunction, API_HashEntry, API_IndexHash } from './api-stories'; import type { SetStoriesStory, SetStoriesStoryData } from './channelApi'; import type { Addon_BaseType, Addon_Collection, Addon_RenderOptions, Addon_Type } from './addons'; import type { StoryIndex } from './indexer'; @@ -112,6 +112,7 @@ export type API_ActiveTabsType = 'sidebar' | 'canvas' | 'addons'; export interface API_SidebarOptions { showRoots?: boolean; + filters?: Record; collapsedRoots?: string[]; renderLabel?: (item: API_HashEntry) => any; } diff --git a/code/package.json b/code/package.json index fea83a390a7d..d533bef95490 100644 --- a/code/package.json +++ b/code/package.json @@ -225,6 +225,7 @@ "eslint": "^8.28.0", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.26.0", + "eslint-plugin-local-rules": "portal:../scripts/eslint-plugin-local-rules", "eslint-plugin-react": "^7.31.10", "eslint-plugin-storybook": "^0.6.6", "fs-extra": "^11.1.0", @@ -326,5 +327,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "7.4.0-alpha.1" } diff --git a/code/presets/create-react-app/package.json b/code/presets/create-react-app/package.json index 3cf55e5f42cc..e55935c0173e 100644 --- a/code/presets/create-react-app/package.json +++ b/code/presets/create-react-app/package.json @@ -41,7 +41,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/presets/html-webpack/package.json b/code/presets/html-webpack/package.json index 3081c900af9e..02b3a3a1fb75 100644 --- a/code/presets/html-webpack/package.json +++ b/code/presets/html-webpack/package.json @@ -41,7 +41,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/presets/preact-webpack/package.json b/code/presets/preact-webpack/package.json index 9a7138652934..59c0a2c2b7a2 100644 --- a/code/presets/preact-webpack/package.json +++ b/code/presets/preact-webpack/package.json @@ -41,7 +41,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/presets/react-webpack/package.json b/code/presets/react-webpack/package.json index bc54d05d5f2c..80128c0a7697 100644 --- a/code/presets/react-webpack/package.json +++ b/code/presets/react-webpack/package.json @@ -56,7 +56,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/presets/server-webpack/package.json b/code/presets/server-webpack/package.json index 928f3f034e7c..b7d8b5b1ba5f 100644 --- a/code/presets/server-webpack/package.json +++ b/code/presets/server-webpack/package.json @@ -46,7 +46,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/presets/svelte-webpack/package.json b/code/presets/svelte-webpack/package.json index b3ce5cffea9a..2e01104e0e1b 100644 --- a/code/presets/svelte-webpack/package.json +++ b/code/presets/svelte-webpack/package.json @@ -56,7 +56,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/presets/vue-webpack/package.json b/code/presets/vue-webpack/package.json index 61cb5b8df5ac..0d416acc071f 100644 --- a/code/presets/vue-webpack/package.json +++ b/code/presets/vue-webpack/package.json @@ -51,7 +51,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/presets/vue3-webpack/package.json b/code/presets/vue3-webpack/package.json index cb8ccb055bed..b0698c46efa0 100644 --- a/code/presets/vue3-webpack/package.json +++ b/code/presets/vue3-webpack/package.json @@ -51,7 +51,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/presets/web-components-webpack/package.json b/code/presets/web-components-webpack/package.json index ab4328602caa..ff0e9ac7d830 100644 --- a/code/presets/web-components-webpack/package.json +++ b/code/presets/web-components-webpack/package.json @@ -44,7 +44,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/renderers/html/package.json b/code/renderers/html/package.json index 1bff3b40d439..cb58d3272450 100644 --- a/code/renderers/html/package.json +++ b/code/renderers/html/package.json @@ -38,10 +38,11 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", + "template/cli/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/renderers/preact/package.json b/code/renderers/preact/package.json index 815a7ef6cc11..8493c73e9792 100644 --- a/code/renderers/preact/package.json +++ b/code/renderers/preact/package.json @@ -38,10 +38,11 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", + "template/cli/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/renderers/react/package.json b/code/renderers/react/package.json index 69cb4117ff77..df67f8ea3845 100644 --- a/code/renderers/react/package.json +++ b/code/renderers/react/package.json @@ -42,10 +42,11 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", + "template/cli/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/renderers/server/package.json b/code/renderers/server/package.json index f53a45fac8f9..74df9790f49b 100644 --- a/code/renderers/server/package.json +++ b/code/renderers/server/package.json @@ -43,10 +43,11 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", + "template/cli/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/renderers/svelte/package.json b/code/renderers/svelte/package.json index d0017068e9cc..cb9d4df6509d 100644 --- a/code/renderers/svelte/package.json +++ b/code/renderers/svelte/package.json @@ -42,10 +42,11 @@ "files": [ "dist/**/*", "templates/**/*", - "template/**/*", + "template/cli/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "svelte-check", diff --git a/code/renderers/vue/package.json b/code/renderers/vue/package.json index 178410ddfb9a..4333bcc45055 100644 --- a/code/renderers/vue/package.json +++ b/code/renderers/vue/package.json @@ -38,10 +38,11 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", + "template/cli/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "vue-tsc --noEmit", diff --git a/code/renderers/vue3/package.json b/code/renderers/vue3/package.json index 3fd73ead43eb..df621810ebc2 100644 --- a/code/renderers/vue3/package.json +++ b/code/renderers/vue3/package.json @@ -38,10 +38,11 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", + "template/cli/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "vue-tsc --noEmit", diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index fc04b6defbdb..5c1e9e51b0e5 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -106,16 +106,16 @@ function mapSlots( ?.bindings?.map((b) => b.name) .join(','); - if (typeof slot === 'function') { + if (typeof slot === 'string') { + slotContent = slot; + } else if (typeof slot === 'function') { slotContent = generateExpression(slot); - } - if (isVNode(slot)) { + } else if (isVNode(slot)) { slotContent = generateComponentSource(slot); - } - - if (typeof slot === 'object' && !isVNode(slot)) { + } else if (typeof slot === 'object' && !isVNode(slot)) { slotContent = JSON.stringify(slot); } + const bindingsString = scropedArgs ? `="{${scropedArgs}}"` : ''; slotContent = slot ? `` : ``; diff --git a/code/renderers/vue3/src/globals.ts b/code/renderers/vue3/src/globals.ts index 58d62a43b4b6..adb3949bd3e1 100644 --- a/code/renderers/vue3/src/globals.ts +++ b/code/renderers/vue3/src/globals.ts @@ -1,5 +1,10 @@ import { global } from '@storybook/global'; +import type { App } from 'vue'; +import type { StoryContext } from './public-types'; const { window: globalWindow } = global; globalWindow.STORYBOOK_ENV = 'vue3'; +globalWindow.PLUGINS_SETUP_FUNCTIONS ||= new Set< + (app: App, context: StoryContext) => unknown +>(); diff --git a/code/renderers/vue3/src/index.ts b/code/renderers/vue3/src/index.ts index 0c37ede8d826..6987cefb0c8e 100644 --- a/code/renderers/vue3/src/index.ts +++ b/code/renderers/vue3/src/index.ts @@ -6,4 +6,10 @@ export * from './public-api'; export * from './public-types'; // optimization: stop HMR propagation in webpack -if (typeof module !== 'undefined') module?.hot?.decline(); +try { + if (module?.hot?.decline) { + module.hot.decline(); + } +} catch (e) { + /* do nothing */ +} diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index a8873bb67772..cf27f67357c8 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,3 +1,4 @@ +/* eslint-disable local-rules/no-uncategorized-errors */ /* eslint-disable no-param-reassign */ import type { App } from 'vue'; import { createApp, h, reactive, isVNode, isReactive } from 'vue'; @@ -16,18 +17,17 @@ export const render: ArgsStoryFn = (props, context) => { return () => h(Component, props, getSlots(props, context)); }; -// set of setup functions that will be called when story is created -const setupFunctions = new Set<(app: App, storyContext?: StoryContext) => void>(); -/** add a setup function to set that will be call when story is created a d - * - * @param fn - */ -export const setup = (fn: (app: App, storyContext?: StoryContext) => void) => { - setupFunctions.add(fn); +export const setup = (fn: (app: App, storyContext?: StoryContext) => unknown) => { + globalThis.PLUGINS_SETUP_FUNCTIONS ??= new Set(); + globalThis.PLUGINS_SETUP_FUNCTIONS.add(fn); }; -const runSetupFunctions = (app: App, storyContext: StoryContext) => { - setupFunctions.forEach((fn) => fn(app, storyContext)); +const runSetupFunctions = async ( + app: App, + storyContext: StoryContext +): Promise => { + if (globalThis && globalThis.PLUGINS_SETUP_FUNCTIONS) + await Promise.all([...globalThis.PLUGINS_SETUP_FUNCTIONS].map((fn) => fn(app, storyContext))); }; const map = new Map< @@ -38,7 +38,7 @@ const map = new Map< } >(); -export function renderToCanvas( +export async function renderToCanvas( { storyFn, forceRemount, showMain, showException, storyContext, id }: RenderContext, canvasElement: VueRenderer['canvasElement'] ) { @@ -80,7 +80,7 @@ export function renderToCanvas( }); vueApp.config.errorHandler = (e: unknown) => showException(e as Error); - runSetupFunctions(vueApp, storyContext); + await runSetupFunctions(vueApp, storyContext); vueApp.mount(canvasElement); showMain(); diff --git a/code/renderers/vue3/src/types.ts b/code/renderers/vue3/src/types.ts index 8526a6035165..1094f6780625 100644 --- a/code/renderers/vue3/src/types.ts +++ b/code/renderers/vue3/src/types.ts @@ -1,5 +1,5 @@ -import type { StoryContext as StoryContextBase, WebRenderer } from '@storybook/types'; -import type { ConcreteComponent } from 'vue'; +import { type StoryContext as StoryContextBase, type WebRenderer } from '@storybook/types'; +import type { App, ConcreteComponent } from 'vue'; export type { RenderContext } from '@storybook/types'; @@ -14,6 +14,8 @@ export type StoryFnVueReturnType = ConcreteComponent; export type StoryContext = StoryContextBase; +export type StorybookVueApp = { vueApp: App; storyContext: StoryContext }; + /** * @deprecated Use `VueRenderer` instead. */ diff --git a/code/renderers/vue3/src/typings.d.ts b/code/renderers/vue3/src/typings.d.ts index f4beceae1d63..d477e85e2a29 100644 --- a/code/renderers/vue3/src/typings.d.ts +++ b/code/renderers/vue3/src/typings.d.ts @@ -1 +1,2 @@ declare var STORYBOOK_ENV: 'vue3'; +declare var PLUGINS_SETUP_FUNCTIONS = new Set<(app, context) => unknown>(); diff --git a/code/renderers/web-components/package.json b/code/renderers/web-components/package.json index 300e25d46f33..c0659a9c57fd 100644 --- a/code/renderers/web-components/package.json +++ b/code/renderers/web-components/package.json @@ -41,10 +41,11 @@ "types": "dist/index.d.ts", "files": [ "dist/**/*", - "template/**/*", + "template/cli/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/ui/.storybook/main.ts b/code/ui/.storybook/main.ts index b21c8b0f9413..5f979898a5c2 100644 --- a/code/ui/.storybook/main.ts +++ b/code/ui/.storybook/main.ts @@ -6,16 +6,12 @@ import type { StorybookConfig } from '../../frameworks/react-vite'; const isBlocksOnly = process.env.STORYBOOK_BLOCKS_ONLY === 'true'; const allStories = [ - { - directory: '../components/src/new', - titlePrefix: '@core-ui', - }, { directory: '../manager/src', titlePrefix: '@manager', }, { - directory: '../components/src/legacy', + directory: '../components/src/components', titlePrefix: '@components', }, { diff --git a/code/ui/.storybook/manager.tsx b/code/ui/.storybook/manager.tsx index 775a1f63c8ed..64691f64f4a3 100644 --- a/code/ui/.storybook/manager.tsx +++ b/code/ui/.storybook/manager.tsx @@ -1,7 +1,5 @@ import { addons, types } from '@storybook/manager-api'; -import { IconButton, Icons } from '@storybook/components'; import startCase from 'lodash/startCase.js'; -import React, { Fragment } from 'react'; addons.setConfig({ sidebar: { diff --git a/code/ui/blocks/package.json b/code/ui/blocks/package.json index 51c2d5855531..e077d15ec27c 100644 --- a/code/ui/blocks/package.json +++ b/code/ui/blocks/package.json @@ -36,7 +36,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", @@ -62,7 +63,7 @@ "memoizerific": "^1.11.3", "polished": "^4.2.2", "react-colorful": "^5.1.2", - "telejson": "^7.0.3", + "telejson": "^7.2.0", "tocbot": "^4.20.1", "ts-dedent": "^2.0.0", "util-deprecate": "^1.0.2" diff --git a/code/ui/blocks/src/blocks/mdx.tsx b/code/ui/blocks/src/blocks/mdx.tsx index eae97528ecb4..3c1c69f87fc2 100644 --- a/code/ui/blocks/src/blocks/mdx.tsx +++ b/code/ui/blocks/src/blocks/mdx.tsx @@ -1,8 +1,7 @@ import type { FC, MouseEvent, PropsWithChildren, SyntheticEvent } from 'react'; import React, { useContext } from 'react'; import { NAVIGATE_URL } from '@storybook/core-events'; -import { Code, components, nameSpaceClassNames } from '@storybook/components'; -import { Icon } from '@storybook/components/experimental'; +import { Code, components, Icons, nameSpaceClassNames } from '@storybook/components'; import { global } from '@storybook/global'; import { styled } from '@storybook/theming'; import { Source } from '../components'; @@ -191,7 +190,7 @@ const HeaderWithOcticonAnchor: FC - + {children} diff --git a/code/ui/blocks/src/components/ArgsTable/ArgControl.tsx b/code/ui/blocks/src/components/ArgsTable/ArgControl.tsx index 7a1061f14e7a..be9b7e1c8a46 100644 --- a/code/ui/blocks/src/components/ArgsTable/ArgControl.tsx +++ b/code/ui/blocks/src/components/ArgsTable/ArgControl.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react'; import React, { useCallback, useState, useEffect } from 'react'; -import { Link } from '@storybook/components/experimental'; +import { Link } from '@storybook/components'; import { BooleanControl, ColorControl, diff --git a/code/ui/blocks/src/components/ArgsTable/ArgsTable.stories.tsx b/code/ui/blocks/src/components/ArgsTable/ArgsTable.stories.tsx index d575ddda9162..39ed63dc6299 100644 --- a/code/ui/blocks/src/components/ArgsTable/ArgsTable.stories.tsx +++ b/code/ui/blocks/src/components/ArgsTable/ArgsTable.stories.tsx @@ -1,17 +1,22 @@ import React from 'react'; import { action } from '@storybook/addon-actions'; import { styled } from '@storybook/theming'; +import type { Meta, StoryObj } from '@storybook/react'; import { ArgsTable, ArgsTableError } from './ArgsTable'; import * as ArgRow from './ArgRow.stories'; -export default { +const meta = { component: ArgsTable, title: 'Components/ArgsTable/ArgsTable', args: { updateArgs: action('updateArgs'), resetArgs: action('resetArgs'), }, -}; +} satisfies Meta; + +export default meta; +type Story = StoryObj; + const propsSection = { category: 'props ', }; @@ -145,6 +150,16 @@ export const Empty = { }, }; +export const EmptyInsideAddonPanel: Story = { + args: { + isLoading: false, + inAddonPanel: true, + }, + parameters: { + layout: 'centered', + }, +}; + export const WithDefaultExpandedArgs = { args: { rows: { diff --git a/code/ui/blocks/src/components/ArgsTable/ArgsTable.tsx b/code/ui/blocks/src/components/ArgsTable/ArgsTable.tsx index a528c292e25c..120cc7a21368 100644 --- a/code/ui/blocks/src/components/ArgsTable/ArgsTable.tsx +++ b/code/ui/blocks/src/components/ArgsTable/ArgsTable.tsx @@ -5,8 +5,7 @@ import { styled } from '@storybook/theming'; import { transparentize } from 'polished'; import { includeConditionalArg } from '@storybook/csf'; import { once } from '@storybook/client-logger'; -import { IconButton, ResetWrapper } from '@storybook/components'; -import { Icon, Link } from '@storybook/components/experimental'; +import { IconButton, Icons, ResetWrapper, Link } from '@storybook/components'; import { ArgRow } from './ArgRow'; import { SectionRow } from './SectionRow'; @@ -211,7 +210,7 @@ interface ArgsTableErrorProps { } export interface ArgsTableLoadingProps { - isLoading: true; + isLoading: boolean; } export type ArgsTableProps = ArgsTableOptionProps & @@ -380,7 +379,7 @@ export const ArgsTable: FC = (props) => { Control{' '} {!isLoading && resetArgs && ( resetArgs()} title="Reset controls"> - + )} diff --git a/code/ui/blocks/src/components/ArgsTable/Empty.tsx b/code/ui/blocks/src/components/ArgsTable/Empty.tsx index ea8e3c2f17fc..7fa211bffc0e 100644 --- a/code/ui/blocks/src/components/ArgsTable/Empty.tsx +++ b/code/ui/blocks/src/components/ArgsTable/Empty.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react'; import React, { useEffect, useState } from 'react'; import { styled } from '@storybook/theming'; -import { Link } from '@storybook/components/experimental'; +import { Icons, Link } from '@storybook/components'; interface EmptyProps { inAddonPanel?: boolean; @@ -42,10 +42,11 @@ const Description = styled.div(({ theme }) => ({ color: theme.textMutedColor, })); -const Links = styled.div({ +const Links = styled.div(({ theme }) => ({ display: 'flex', + fontSize: theme.typography.size.s2 - 1, gap: 25, -}); +})); const Divider = styled.div(({ theme }) => ({ width: 1, @@ -85,8 +86,8 @@ export const Empty: FC = ({ inAddonPanel }) => { {inAddonPanel && ( <> - - Watch 5m video + + Watch 5m video void; @@ -34,7 +33,7 @@ const Zoom: FC = ({ zoom, resetZoom }) => ( }} title="Zoom in" > - + = ({ zoom, resetZoom }) => ( }} title="Zoom out" > - + = ({ zoom, resetZoom }) => ( }} title="Reset zoom" > - + ); diff --git a/code/ui/blocks/src/controls/Object.tsx b/code/ui/blocks/src/controls/Object.tsx index 8cd8c8bb7bea..11c51f8f6392 100644 --- a/code/ui/blocks/src/controls/Object.tsx +++ b/code/ui/blocks/src/controls/Object.tsx @@ -4,7 +4,6 @@ import type { ComponentProps, SyntheticEvent, FC, FocusEvent } from 'react'; import React, { useCallback, useMemo, useState, useEffect, useRef } from 'react'; import { styled, useTheme, type Theme } from '@storybook/theming'; import { Form, Icons, type IconsProps, IconButton } from '@storybook/components'; -import { Icon } from '@storybook/components/experimental'; import { JsonTree, getObjectType } from './react-editable-json-tree'; import { getControlId, getControlSetterButtonId } from './helpers'; import type { ControlProps, ObjectValue, ObjectConfig } from './types'; @@ -295,7 +294,7 @@ export const ObjectControl: FC = ({ name, value, onChange }) => { setShowRaw((v) => !v); }} > - {showRaw ? : } + RAW )} diff --git a/code/ui/blocks/src/controls/options/Select.tsx b/code/ui/blocks/src/controls/options/Select.tsx index 43c47cfc3405..f033c131bd71 100644 --- a/code/ui/blocks/src/controls/options/Select.tsx +++ b/code/ui/blocks/src/controls/options/Select.tsx @@ -3,7 +3,8 @@ import React from 'react'; import { styled } from '@storybook/theming'; import type { CSSObject } from '@storybook/theming'; import { logger } from '@storybook/client-logger'; -import { Icon } from '@storybook/components/experimental'; +import { Icons } from '@storybook/components'; + import type { ControlProps, OptionsSelection, NormalizedOptionsConfig } from '../types'; import { selectedKey, selectedKeys, selectedValues } from './helpers'; @@ -72,17 +73,21 @@ const SelectWrapper = styled.span(({ theme }) => ({ position: 'relative', verticalAlign: 'top', width: '100%', -})); -const SelectChevronDown = styled.div(({ theme }) => ({ - position: 'absolute', - zIndex: 1, - pointerEvents: 'none', - height: '12px', - marginTop: '-6px', - right: '12px', - top: '50%', - color: theme.textMutedColor, + svg: { + position: 'absolute', + zIndex: 1, + pointerEvents: 'none', + height: '12px', + marginTop: '-6px', + right: '12px', + top: '50%', + fill: theme.textMutedColor, + + path: { + fill: theme.textMutedColor, + }, + }, })); type SelectConfig = NormalizedOptionsConfig & { isMulti: boolean }; @@ -99,9 +104,7 @@ const SingleSelect: FC = ({ name, value, options, onChange }) => { return ( - - - +