diff --git a/.circleci/config.yml b/.circleci/config.yml index b011deeaefab..5e7cc4a22d24 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -183,7 +183,7 @@ jobs: git diff --exit-code - report-workflow-on-failure - cancel-workflow-on-failure - script-unit-tests: + script-checks: executor: sb_node_16_browsers steps: - git-shallow-clone/checkout_advanced: @@ -191,7 +191,17 @@ jobs: - attach_workspace: at: . - run: - name: Test + name: Check parallelism count + command: | + cd scripts + yarn get-template --check + - run: + name: Type check + command: | + cd scripts + yarn check + - run: + name: Run tests command: | cd scripts yarn test --coverage --ci @@ -479,7 +489,7 @@ workflows: - unit-tests: requires: - build - - script-unit-tests: + - script-checks: requires: - build - chromatic-internal-storybooks: @@ -489,11 +499,11 @@ workflows: requires: - unit-tests - create-sandboxes: - parallelism: 11 + parallelism: 12 requires: - build - build-sandboxes: - parallelism: 11 + parallelism: 12 requires: - create-sandboxes - chromatic-sandboxes: @@ -513,7 +523,7 @@ workflows: requires: - build-sandboxes - bench: - parallelism: 2 + parallelism: 3 requires: - build-sandboxes # TODO: reenable once we find out the source of flakyness @@ -535,7 +545,7 @@ workflows: - unit-tests: requires: - build - - script-unit-tests: + - script-checks: requires: - build - chromatic-internal-storybooks: @@ -545,11 +555,11 @@ workflows: requires: - unit-tests - create-sandboxes: - parallelism: 20 + parallelism: 21 requires: - build - build-sandboxes: - parallelism: 20 + parallelism: 21 requires: - create-sandboxes - chromatic-sandboxes: @@ -569,7 +579,7 @@ workflows: requires: - build-sandboxes - bench: - parallelism: 2 + parallelism: 3 requires: - build-sandboxes # TODO: reenable once we find out the source of flakyness @@ -592,21 +602,21 @@ workflows: - unit-tests: requires: - build - - script-unit-tests: + - script-checks: requires: - build - chromatic-internal-storybooks: requires: - build - create-sandboxes: - parallelism: 34 + parallelism: 35 requires: - build # - smoke-test-sandboxes: # disabled for now # requires: # - create-sandboxes - build-sandboxes: - parallelism: 34 + parallelism: 35 requires: - create-sandboxes - chromatic-sandboxes: 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/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3a6eeebd8248..18e23c174162 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -176,30 +176,9 @@ jobs: git commit -m "Update $VERSION_FILE for v${{ steps.version.outputs.current-version }}" git push origin main - # TODO: this is currently disabled, because we may have a better strategy that we want to try out manually first before comitting to it: - # - create a branch "release-" from HEAD of main - # - git push --force origin ${{ steps.target.outputs.target }}:main - # - ... this will keep the "main" history in the new release branch, and then overwrite main's history with next's - - # Sync next-release to main if it is not a prerelease, and this release is from next-release - # This happens when eg. next has been tracking 7.1.0-alpha.X, and now we want to release 7.1.0 - # This will keep next-release, next and main all tracking v7.1.0 - # See "Alternative merge strategies" in https://stackoverflow.com/a/36321787 - # - name: Sync next-release to main - # if: steps.publish-needed.outputs.published == 'false' && steps.target.outputs.target == 'next' && !steps.is-prerelease.outputs.prerelease - # working-directory: . - # run: | - # git fetch origin next-release - # git checkout next-release - # git pull - # git fetch origin main - # git checkout main - # git pull - # git merge --no-commit -s ours next-release - # git rm -rf . - # git checkout next-release -- . - # git commit -m "Sync next-release to main" - # git push origin main + - name: Overwrite main with next + if: steps.target.outputs.target == 'next' && steps.is-prerelease.outputs.prerelease == 'false' + run: git push --force origin next:main - name: Report job failure to Discord if: failure() diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cd63f3ea6e1..6f4d663ad990 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +## 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)! + +## 7.3.0 + +- ✨ Indexer: Introduce new experimental `indexer` API - #23691, thanks [@JReinhold](https://github.com/jreinhold)! +- ✨ CLI: Update postinstall to look for addon script - [#23791](https://github.com/storybookjs/storybook/pull/23791), thanks [@Integrayshaun](https://github.com/Integrayshaun)! +- ✨ Server: Add support for tags - #23660, thanks [@JReinhold](https://github.com/jreinhold)! +- πŸ› CSF-Tools: Remove prettier from printConfig - [#23766](https://github.com/storybookjs/storybook/pull/23766), thanks [@kasperpeulen](https://github.com/kasperpeulen)! +- πŸ› Build: Support Chrome 100, Safari 15 and Firefox 91 - [#23800](https://github.com/storybookjs/storybook/pull/23800), thanks [@kasperpeulen](https://github.com/kasperpeulen)! +- πŸ› Vue3: Don't automatically assign values to all slots - [#23697](https://github.com/storybookjs/storybook/pull/23697), thanks [@kasperpeulen](https://github.com/kasperpeulen)! +- πŸ› Core: Fix `composeStories` typings - [#23577](https://github.com/storybookjs/storybook/pull/23577), thanks [@yannbf](https://github.com/yannbf)! +- πŸ› WebpackBuilder: Remove need for `react` as peerDependency - [#23496](https://github.com/storybookjs/storybook/pull/23496), thanks [@ndelangen](https://github.com/ndelangen)! +- πŸ”§ Addon-docs, Core, Server: Use new `indexer` API - #23660, thanks [@JReinhold](https://github.com/jreinhold)! +- πŸ”§ Core-server: Improve internal types - #23632, thanks [@JReinhold](https://github.com/jreinhold)! +- πŸ”§ UI: Improve Link component - [#23767](https://github.com/storybookjs/storybook/pull/23767), thanks [@cdedreuille](https://github.com/cdedreuille)! +- πŸ”§ UI: Improve new `Button` component - [#23765](https://github.com/storybookjs/storybook/pull/23765), thanks [@cdedreuille](https://github.com/cdedreuille)! +- πŸ”§ UI: Update Button types to allow for no children on iconOnly buttons - [#23735](https://github.com/storybookjs/storybook/pull/23735), thanks [@cdedreuille](https://github.com/cdedreuille)! +- πŸ”§ UI: Upgrade Icon component - [#23680](https://github.com/storybookjs/storybook/pull/23680), thanks [@cdedreuille](https://github.com/cdedreuille)! +- πŸ”§ Addons: Deprecate key in addon render function as it is not available anymore - [#23792](https://github.com/storybookjs/storybook/pull/23792), thanks [@kasperpeulen](https://github.com/kasperpeulen)! +- πŸ”§ UI: Update IconButton and add new Toolbar component - [#23795](https://github.com/storybookjs/storybook/pull/23795), thanks [@cdedreuille](https://github.com/cdedreuille)! + ## 7.2.3 - Build: Support Chrome 100, Safari 15 and Firefox 91 - [#23800](https://github.com/storybookjs/storybook/pull/23800), thanks [@kasperpeulen](https://github.com/kasperpeulen)! diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 1ccadcfa2d1a..071fd1a3d01a 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,7 @@ +## 7.4.0-alpha.0 + +- Index: Fix `*.story.*` CSF indexing - [#23852](https://github.com/storybookjs/storybook/pull/23852), thanks [@shilman](https://github.com/shilman)! + ## 7.3.0-alpha.0 - Addons: Deprecate key in addon render function as it is not available anymore - [#23792](https://github.com/storybookjs/storybook/pull/23792), thanks [@kasperpeulen](https://github.com/kasperpeulen)! 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/MIGRATION.md b/MIGRATION.md index 6fa7d0c5d300..5022eefb59bd 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1669,7 +1669,7 @@ If you're using `storiesOf` and want to restore the previous behavior, you can a ```js module.exports = { webpackFinal: (config) => { - config.modules.rules.push({ + config.module.rules.push({ test: /\.stories\.[tj]sx?$/, use: [ { 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 2c2156d4d51c..36feadcbe854 100644 --- a/code/addons/a11y/package.json +++ b/code/addons/a11y/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-a11y", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Test component compliance with web accessibility standards", "keywords": [ "a11y", @@ -56,7 +56,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/actions/package.json b/code/addons/actions/package.json index 1009187922aa..bff07d8c6329 100644 --- a/code/addons/actions/package.json +++ b/code/addons/actions/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-actions", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Get UI feedback when an action is performed on an interactive element", "keywords": [ "storybook", @@ -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 ecf7fdadef90..01dfed29e3b0 100644 --- a/code/addons/backgrounds/package.json +++ b/code/addons/backgrounds/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-backgrounds", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Switch backgrounds to view components in different settings", "keywords": [ "addon", @@ -69,7 +69,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/controls/package.json b/code/addons/controls/package.json index 641e9fd86da3..5a3369812b85 100644 --- a/code/addons/controls/package.json +++ b/code/addons/controls/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-controls", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Interact with component inputs dynamically in the Storybook UI", "keywords": [ "addon", @@ -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 cdf462ff29b4..52289b84f552 100644 --- a/code/addons/docs/package.json +++ b/code/addons/docs/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-docs", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Document component usage and properties in Markdown", "keywords": [ "addon", @@ -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 fba1b841ba5f..e28df802153a 100644 --- a/code/addons/essentials/package.json +++ b/code/addons/essentials/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-essentials", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Curated addons to bring out the best of Storybook", "keywords": [ "addon", @@ -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 b168369ddc3d..bf07ea3c8599 100644 --- a/code/addons/gfm/package.json +++ b/code/addons/gfm/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-mdx-gfm", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "GitHub Flavored Markdown in Storybook", "keywords": [ "addon", @@ -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/package.json b/code/addons/highlight/package.json index 008affa52747..5653ea2600b5 100644 --- a/code/addons/highlight/package.json +++ b/code/addons/highlight/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-highlight", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Highlight DOM nodes within your stories", "keywords": [ "storybook-addons", @@ -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 bbfff8cc38ea..9e62cb8521f7 100644 --- a/code/addons/interactions/package.json +++ b/code/addons/interactions/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-interactions", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Automate, test and debug user interactions", "keywords": [ "storybook-addons", @@ -65,7 +65,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/jest/package.json b/code/addons/jest/package.json index 933a83e769cc..25718a3e86b9 100644 --- a/code/addons/jest/package.json +++ b/code/addons/jest/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-jest", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "React storybook addon that show component jest report", "keywords": [ "addon", @@ -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 1f499df1d5d5..0e1d2d9d3ef8 100644 --- a/code/addons/links/package.json +++ b/code/addons/links/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-links", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Link stories together to build demos and prototypes with your UI components", "keywords": [ "addon", @@ -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 22987c6919e8..ad39e0801b02 100644 --- a/code/addons/measure/package.json +++ b/code/addons/measure/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-measure", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Inspect layouts by visualizing the box model", "keywords": [ "storybook-addons", @@ -68,7 +68,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/outline/package.json b/code/addons/outline/package.json index 5337a3d49a62..ec168cdfdd17 100644 --- a/code/addons/outline/package.json +++ b/code/addons/outline/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-outline", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Outline all elements with CSS to help with layout placement and alignment", "keywords": [ "storybook-addons", @@ -71,7 +71,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/storyshots-core/package.json b/code/addons/storyshots-core/package.json index ba7e5f6d72c6..f3d0a1862bcd 100644 --- a/code/addons/storyshots-core/package.json +++ b/code/addons/storyshots-core/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-storyshots", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Take a code snapshot of every story automatically with Jest", "keywords": [ "addon", @@ -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 ea1cad78b38a..a8239e7f3604 100644 --- a/code/addons/storyshots-puppeteer/package.json +++ b/code/addons/storyshots-puppeteer/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-storyshots-puppeteer", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Image snapshots addition to StoryShots based on puppeteer", "keywords": [ "addon", @@ -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 7c6f153c185d..abc36836fb9e 100644 --- a/code/addons/storysource/package.json +++ b/code/addons/storysource/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-storysource", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "View a story’s source code to see how it works and paste into your app", "keywords": [ "addon", @@ -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 6424cdefb365..916b3a433bdd 100644 --- a/code/addons/themes/package.json +++ b/code/addons/themes/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-themes", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Switch between multiple themes for you components in Storybook", "keywords": [ "css", @@ -65,7 +65,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/addons/toolbars/package.json b/code/addons/toolbars/package.json index 1653015f22c0..45fcc8b940de 100644 --- a/code/addons/toolbars/package.json +++ b/code/addons/toolbars/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-toolbars", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Create your own toolbar items that control story rendering", "keywords": [ "addon", @@ -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 768a60f46eec..33cebf97e9f5 100644 --- a/code/addons/viewport/package.json +++ b/code/addons/viewport/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-viewport", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Build responsive components by adjusting Storybook’s viewport size and orientation", "keywords": [ "addon", @@ -66,7 +66,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/builders/builder-manager/package.json b/code/builders/builder-manager/package.json index 9621433b4535..71991bc45cb1 100644 --- a/code/builders/builder-manager/package.json +++ b/code/builders/builder-manager/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/builder-manager", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook manager builder", "keywords": [ "storybook" @@ -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 d352946844d3..8c0e606d56bc 100644 --- a/code/builders/builder-vite/package.json +++ b/code/builders/builder-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/builder-vite", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "A plugin to run and build Storybooks with Vite", "homepage": "https://github.com/storybookjs/storybook/tree/next/code/builders/builder-vite/#readme", "bugs": { @@ -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 e2483f016420..9d31c809a045 100644 --- a/code/builders/builder-webpack5/package.json +++ b/code/builders/builder-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/builder-webpack5", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" @@ -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..c99a1df1da81 100644 --- a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts +++ b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts @@ -36,29 +36,19 @@ const storybookPaths: Record = { `@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 06697059c818..628855e4405f 100644 --- a/code/deprecated/addons/package.json +++ b/code/deprecated/addons/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addons", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook addons store", "keywords": [ "storybook" @@ -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 a2111537a5f2..a721b95682d2 100644 --- a/code/deprecated/channel-postmessage/package.json +++ b/code/deprecated/channel-postmessage/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/channel-postmessage", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "", "keywords": [ "storybook" @@ -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 7addbe019f74..8f5093e32b21 100644 --- a/code/deprecated/channel-websocket/package.json +++ b/code/deprecated/channel-websocket/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/channel-websocket", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "", "keywords": [ "storybook" @@ -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 d79ceee1add1..a3f609e785d9 100644 --- a/code/deprecated/client-api/package.json +++ b/code/deprecated/client-api/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/client-api", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook Client API", "keywords": [ "storybook" @@ -35,7 +35,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/deprecated/core-client/package.json b/code/deprecated/core-client/package.json index 06daaaac3182..616241039520 100644 --- a/code/deprecated/core-client/package.json +++ b/code/deprecated/core-client/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core-client", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" diff --git a/code/deprecated/manager-api-shim/package.json b/code/deprecated/manager-api-shim/package.json index 1d34e8029668..c2661e9789bc 100644 --- a/code/deprecated/manager-api-shim/package.json +++ b/code/deprecated/manager-api-shim/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/api", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook Manager API (facade)", "keywords": [ "storybook" @@ -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 4872094db318..8278714652f6 100644 --- a/code/deprecated/preview-web/package.json +++ b/code/deprecated/preview-web/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preview-web", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "", "keywords": [ "storybook" @@ -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 ad9629d74176..eb2e62685049 100644 --- a/code/deprecated/store/package.json +++ b/code/deprecated/store/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/store", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "", "keywords": [ "storybook" @@ -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 dc50f5351acb..575f53bdb903 100644 --- a/code/frameworks/angular/package.json +++ b/code/frameworks/angular/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/angular", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Angular: Develop Angular components in isolation with hot reloading.", "keywords": [ "storybook", @@ -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 20b21ce09871..21385a1979db 100644 --- a/code/frameworks/ember/package.json +++ b/code/frameworks/ember/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/ember", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Ember: Develop Ember Component in isolation with Hot Reloading.", "homepage": "https://github.com/storybookjs/storybook/tree/next/code/frameworks/ember", "bugs": { @@ -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 7e8c76aa0e2a..52312c98fe89 100644 --- a/code/frameworks/html-vite/package.json +++ b/code/frameworks/html-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/html-vite", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for HTML and Vite: Develop HTML in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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 1765450e25da..d0a5bd15342f 100644 --- a/code/frameworks/html-webpack5/package.json +++ b/code/frameworks/html-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/html-webpack5", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for HTML: View HTML snippets in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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/README.md b/code/frameworks/nextjs/README.md index d15ad4d4a98a..4d9ea8542470 100644 --- a/code/frameworks/nextjs/README.md +++ b/code/frameworks/nextjs/README.md @@ -827,10 +827,15 @@ Below is an example of how to add svgr support to Storybook with this framework. export default { // ... webpackFinal: async (config) => { + config.module = config.module || {}; + config.module.rules = config.module.rules || []; + // This modifies the existing image rule to exclude .svg files // since you want to handle those files with @svgr/webpack - const imageRule = config.module.rules.find((rule) => rule.test.test('.svg')); - imageRule.exclude = /\.svg$/; + const imageRule = config.module.rules.find((rule) => rule?.['test']?.test('.svg')); + if (imageRule) { + imageRule['exclude'] = /\.svg$/; + } // Configure .svg files to be loaded with @svgr/webpack config.module.rules.push({ diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json index 5f4c763aef7e..14ec26a04924 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/nextjs", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Next.js", "keywords": [ "storybook", @@ -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 e90f99778a57..a01c073a4995 100644 --- a/code/frameworks/preact-vite/package.json +++ b/code/frameworks/preact-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preact-vite", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Preact and Vite: Develop Preact components in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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 a52089ba02a6..b79235490989 100644 --- a/code/frameworks/preact-webpack5/package.json +++ b/code/frameworks/preact-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preact-webpack5", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Preact: Develop Preact Component in isolation.", "keywords": [ "storybook" @@ -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 41438c647e63..e9e8cd60b902 100644 --- a/code/frameworks/react-vite/package.json +++ b/code/frameworks/react-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react-vite", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for React and Vite: Develop React components in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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 9944c493b052..4a7d7f299227 100644 --- a/code/frameworks/react-webpack5/package.json +++ b/code/frameworks/react-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react-webpack5", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for React: Develop React Component in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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 08a300586dba..00a2b306cd3b 100644 --- a/code/frameworks/server-webpack5/package.json +++ b/code/frameworks/server-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/server-webpack5", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Server: View HTML snippets from a server in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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 45aeb946c3d9..d409f5a5a24f 100644 --- a/code/frameworks/svelte-vite/package.json +++ b/code/frameworks/svelte-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/svelte-vite", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Svelte and Vite: Develop Svelte components in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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 9950459dd654..ec397d079f4e 100644 --- a/code/frameworks/svelte-webpack5/package.json +++ b/code/frameworks/svelte-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/svelte-webpack5", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Svelte: Develop Svelte Component in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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/README.md b/code/frameworks/sveltekit/README.md index 0777e8fe9ed5..467643ee1ae5 100644 --- a/code/frameworks/sveltekit/README.md +++ b/code/frameworks/sveltekit/README.md @@ -26,10 +26,10 @@ However SvelteKit has some [Kit-specific modules](https://kit.svelte.dev/docs/mo | **Module** | **Status** | **Note** | | ---------------------------------------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | [`$app/environment`](https://kit.svelte.dev/docs/modules#$app-environment) | βœ… Supported | `version` is always empty in Storybook. | -| [`$app/forms`](https://kit.svelte.dev/docs/modules#$app-forms) | ⏳ Planned for 7.1 | Will use mocks. Tracked in [#20999](https://github.com/storybookjs/storybook/issues/20999) | -| [`$app/navigation`](https://kit.svelte.dev/docs/modules#$app-navigation) | ⏳ Planned for 7.1 | Will use mocks. Tracked in [#20999](https://github.com/storybookjs/storybook/issues/20999) | +| [`$app/forms`](https://kit.svelte.dev/docs/modules#$app-forms) | ⏳ Future | Will use mocks. Tracked in [#20999](https://github.com/storybookjs/storybook/issues/20999) | +| [`$app/navigation`](https://kit.svelte.dev/docs/modules#$app-navigation) | ⏳ Future | Will use mocks. Tracked in [#20999](https://github.com/storybookjs/storybook/issues/20999) | | [`$app/paths`](https://kit.svelte.dev/docs/modules#$app-paths) | βœ… Supported | Requires SvelteKit 1.4.0 or newer | -| [`$app/stores`](https://kit.svelte.dev/docs/modules#$app-stores) | βœ… Supported | Mocks planned for 7.1, so you can set different store values per story. | +| [`$app/stores`](https://kit.svelte.dev/docs/modules#$app-stores) | βœ… Supported | Mocks planned, so you can set different store values per story. | | [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private) | β›” Not supported | They are meant to only be available server-side, and Storybook renders all components on the client. | | [`$env/dynamic/public`](https://kit.svelte.dev/docs/modules#$env-dynamic-public) | 🚧 Partially supported | Only supported in development mode. Storybook is built as a static app with no server-side API so cannot dynamically serve content. | | [`$env/static/private`](https://kit.svelte.dev/docs/modules#$env-static-private) | β›” Not supported | They are meant to only be available server-side, and Storybook renders all components on the client. | diff --git a/code/frameworks/sveltekit/package.json b/code/frameworks/sveltekit/package.json index eec331b0123a..340b6637b3ae 100644 --- a/code/frameworks/sveltekit/package.json +++ b/code/frameworks/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/sveltekit", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for SvelteKit", "keywords": [ "storybook", @@ -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 fd7e4a10dec4..bfaababee6dc 100644 --- a/code/frameworks/vue-vite/package.json +++ b/code/frameworks/vue-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/vue-vite", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Vue2 and Vite: Develop Vue2 Components in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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 41c6eb7a7b42..ee6d015aae60 100644 --- a/code/frameworks/vue-webpack5/package.json +++ b/code/frameworks/vue-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/vue-webpack5", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Vue: Develop Vue Component in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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 1fd2d7fb589d..950443767edd 100644 --- a/code/frameworks/vue3-vite/package.json +++ b/code/frameworks/vue3-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/vue3-vite", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Vue3 and Vite: Develop Vue3 components in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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 2b7b4b70a8c1..5e7f95b842a6 100644 --- a/code/frameworks/vue3-webpack5/package.json +++ b/code/frameworks/vue3-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/vue3-webpack5", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Vue 3: Develop Vue 3 Components in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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 5dd6cd701498..e9f8ad02fbf6 100644 --- a/code/frameworks/web-components-vite/package.json +++ b/code/frameworks/web-components-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/web-components-vite", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for web-components and Vite: Develop Web Components in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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 653f4dca96ed..21a934770346 100644 --- a/code/frameworks/web-components-webpack5/package.json +++ b/code/frameworks/web-components-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/web-components-webpack5", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for web-components: View web components snippets in isolation with Hot Reloading.", "keywords": [ "lit", @@ -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 7f72f07e241a..6b0ffae6aba5 100644 --- a/code/lib/channels/package.json +++ b/code/lib/channels/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/channels", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "", "keywords": [ "storybook" @@ -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-sb/package.json b/code/lib/cli-sb/package.json index ed53324f49e8..7af2c0760424 100644 --- a/code/lib/cli-sb/package.json +++ b/code/lib/cli-sb/package.json @@ -1,6 +1,6 @@ { "name": "sb", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook CLI", "keywords": [ "storybook" diff --git a/code/lib/cli-storybook/package.json b/code/lib/cli-storybook/package.json index 59c370a3b391..ef12a9829dae 100644 --- a/code/lib/cli-storybook/package.json +++ b/code/lib/cli-storybook/package.json @@ -1,6 +1,6 @@ { "name": "storybook", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook CLI", "keywords": [ "storybook" diff --git a/code/lib/cli/package.json b/code/lib/cli/package.json index 9efc9cebf234..64c1f850a2d5 100644 --- a/code/lib/cli/package.json +++ b/code/lib/cli/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/cli", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook's CLI - easiest method of adding storybook to your projects", "keywords": [ "cli", @@ -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/sandbox-templates.ts b/code/lib/cli/src/sandbox-templates.ts index 5b19a3d8d4ad..95aa9bf029a3 100644 --- a/code/lib/cli/src/sandbox-templates.ts +++ b/code/lib/cli/src/sandbox-templates.ts @@ -62,6 +62,7 @@ export type Template = { modifications?: { skipTemplateStories?: boolean; mainConfig?: Partial; + disableDocs?: boolean; }; /** * Flag to indicate that this template is a secondary template, which is used mainly to test rather specific features. @@ -526,6 +527,16 @@ const benchTemplates = { }, skipTasks: ['e2e-tests-dev', 'test-runner', 'test-runner-dev', 'e2e-tests', 'chromatic'], }, + 'bench/react-vite-default-ts-nodocs': { + ...baseTemplates['react-vite/default-ts'], + name: 'Bench (react-vite/default-ts, no docs)', + isInternal: true, + modifications: { + skipTemplateStories: true, + disableDocs: true, + }, + skipTasks: ['e2e-tests-dev', 'test-runner', 'test-runner-dev', 'e2e-tests', 'chromatic'], + }, } satisfies Record<`bench/${string}`, Template & { isInternal: true }>; export const allTemplates: Record = { @@ -546,6 +557,7 @@ export const normal: TemplateKey[] = [ 'nextjs/default-ts', 'bench/react-vite-default-ts', 'bench/react-webpack-18-ts', + 'bench/react-vite-default-ts-nodocs', ]; export const merged: TemplateKey[] = [ ...normal, 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/cli/src/versions.ts b/code/lib/cli/src/versions.ts index 5bef17983941..097d7a86bd5e 100644 --- a/code/lib/cli/src/versions.ts +++ b/code/lib/cli/src/versions.ts @@ -1,97 +1,97 @@ // auto generated file, do not edit export default { - '@storybook/addon-a11y': '7.3.0-alpha.0', - '@storybook/addon-actions': '7.3.0-alpha.0', - '@storybook/addon-backgrounds': '7.3.0-alpha.0', - '@storybook/addon-controls': '7.3.0-alpha.0', - '@storybook/addon-docs': '7.3.0-alpha.0', - '@storybook/addon-essentials': '7.3.0-alpha.0', - '@storybook/addon-highlight': '7.3.0-alpha.0', - '@storybook/addon-interactions': '7.3.0-alpha.0', - '@storybook/addon-jest': '7.3.0-alpha.0', - '@storybook/addon-links': '7.3.0-alpha.0', - '@storybook/addon-mdx-gfm': '7.3.0-alpha.0', - '@storybook/addon-measure': '7.3.0-alpha.0', - '@storybook/addon-outline': '7.3.0-alpha.0', - '@storybook/addon-themes': '7.3.0-alpha.0', - '@storybook/addon-storyshots': '7.3.0-alpha.0', - '@storybook/addon-storyshots-puppeteer': '7.3.0-alpha.0', - '@storybook/addon-storysource': '7.3.0-alpha.0', - '@storybook/addon-toolbars': '7.3.0-alpha.0', - '@storybook/addon-viewport': '7.3.0-alpha.0', - '@storybook/addons': '7.3.0-alpha.0', - '@storybook/angular': '7.3.0-alpha.0', - '@storybook/api': '7.3.0-alpha.0', - '@storybook/blocks': '7.3.0-alpha.0', - '@storybook/builder-manager': '7.3.0-alpha.0', - '@storybook/builder-vite': '7.3.0-alpha.0', - '@storybook/builder-webpack5': '7.3.0-alpha.0', - '@storybook/channel-postmessage': '7.3.0-alpha.0', - '@storybook/channel-websocket': '7.3.0-alpha.0', - '@storybook/channels': '7.3.0-alpha.0', - '@storybook/cli': '7.3.0-alpha.0', - '@storybook/client-api': '7.3.0-alpha.0', - '@storybook/client-logger': '7.3.0-alpha.0', - '@storybook/codemod': '7.3.0-alpha.0', - '@storybook/components': '7.3.0-alpha.0', - '@storybook/core-client': '7.3.0-alpha.0', - '@storybook/core-common': '7.3.0-alpha.0', - '@storybook/core-events': '7.3.0-alpha.0', - '@storybook/core-server': '7.3.0-alpha.0', - '@storybook/core-webpack': '7.3.0-alpha.0', - '@storybook/csf-plugin': '7.3.0-alpha.0', - '@storybook/csf-tools': '7.3.0-alpha.0', - '@storybook/docs-tools': '7.3.0-alpha.0', - '@storybook/ember': '7.3.0-alpha.0', - '@storybook/html': '7.3.0-alpha.0', - '@storybook/html-vite': '7.3.0-alpha.0', - '@storybook/html-webpack5': '7.3.0-alpha.0', - '@storybook/instrumenter': '7.3.0-alpha.0', - '@storybook/manager': '7.3.0-alpha.0', - '@storybook/manager-api': '7.3.0-alpha.0', - '@storybook/nextjs': '7.3.0-alpha.0', - '@storybook/node-logger': '7.3.0-alpha.0', - '@storybook/postinstall': '7.3.0-alpha.0', - '@storybook/preact': '7.3.0-alpha.0', - '@storybook/preact-vite': '7.3.0-alpha.0', - '@storybook/preact-webpack5': '7.3.0-alpha.0', - '@storybook/preset-create-react-app': '7.3.0-alpha.0', - '@storybook/preset-html-webpack': '7.3.0-alpha.0', - '@storybook/preset-preact-webpack': '7.3.0-alpha.0', - '@storybook/preset-react-webpack': '7.3.0-alpha.0', - '@storybook/preset-server-webpack': '7.3.0-alpha.0', - '@storybook/preset-svelte-webpack': '7.3.0-alpha.0', - '@storybook/preset-vue-webpack': '7.3.0-alpha.0', - '@storybook/preset-vue3-webpack': '7.3.0-alpha.0', - '@storybook/preset-web-components-webpack': '7.3.0-alpha.0', - '@storybook/preview': '7.3.0-alpha.0', - '@storybook/preview-api': '7.3.0-alpha.0', - '@storybook/preview-web': '7.3.0-alpha.0', - '@storybook/react': '7.3.0-alpha.0', - '@storybook/react-dom-shim': '7.3.0-alpha.0', - '@storybook/react-vite': '7.3.0-alpha.0', - '@storybook/react-webpack5': '7.3.0-alpha.0', - '@storybook/router': '7.3.0-alpha.0', - '@storybook/server': '7.3.0-alpha.0', - '@storybook/server-webpack5': '7.3.0-alpha.0', - '@storybook/source-loader': '7.3.0-alpha.0', - '@storybook/store': '7.3.0-alpha.0', - '@storybook/svelte': '7.3.0-alpha.0', - '@storybook/svelte-vite': '7.3.0-alpha.0', - '@storybook/svelte-webpack5': '7.3.0-alpha.0', - '@storybook/sveltekit': '7.3.0-alpha.0', - '@storybook/telemetry': '7.3.0-alpha.0', - '@storybook/theming': '7.3.0-alpha.0', - '@storybook/types': '7.3.0-alpha.0', - '@storybook/vue': '7.3.0-alpha.0', - '@storybook/vue-vite': '7.3.0-alpha.0', - '@storybook/vue-webpack5': '7.3.0-alpha.0', - '@storybook/vue3': '7.3.0-alpha.0', - '@storybook/vue3-vite': '7.3.0-alpha.0', - '@storybook/vue3-webpack5': '7.3.0-alpha.0', - '@storybook/web-components': '7.3.0-alpha.0', - '@storybook/web-components-vite': '7.3.0-alpha.0', - '@storybook/web-components-webpack5': '7.3.0-alpha.0', - sb: '7.3.0-alpha.0', - storybook: '7.3.0-alpha.0', + '@storybook/addon-a11y': '7.4.0-alpha.0', + '@storybook/addon-actions': '7.4.0-alpha.0', + '@storybook/addon-backgrounds': '7.4.0-alpha.0', + '@storybook/addon-controls': '7.4.0-alpha.0', + '@storybook/addon-docs': '7.4.0-alpha.0', + '@storybook/addon-essentials': '7.4.0-alpha.0', + '@storybook/addon-highlight': '7.4.0-alpha.0', + '@storybook/addon-interactions': '7.4.0-alpha.0', + '@storybook/addon-jest': '7.4.0-alpha.0', + '@storybook/addon-links': '7.4.0-alpha.0', + '@storybook/addon-mdx-gfm': '7.4.0-alpha.0', + '@storybook/addon-measure': '7.4.0-alpha.0', + '@storybook/addon-outline': '7.4.0-alpha.0', + '@storybook/addon-themes': '7.4.0-alpha.0', + '@storybook/addon-storyshots': '7.4.0-alpha.0', + '@storybook/addon-storyshots-puppeteer': '7.4.0-alpha.0', + '@storybook/addon-storysource': '7.4.0-alpha.0', + '@storybook/addon-toolbars': '7.4.0-alpha.0', + '@storybook/addon-viewport': '7.4.0-alpha.0', + '@storybook/addons': '7.4.0-alpha.0', + '@storybook/angular': '7.4.0-alpha.0', + '@storybook/api': '7.4.0-alpha.0', + '@storybook/blocks': '7.4.0-alpha.0', + '@storybook/builder-manager': '7.4.0-alpha.0', + '@storybook/builder-vite': '7.4.0-alpha.0', + '@storybook/builder-webpack5': '7.4.0-alpha.0', + '@storybook/channel-postmessage': '7.4.0-alpha.0', + '@storybook/channel-websocket': '7.4.0-alpha.0', + '@storybook/channels': '7.4.0-alpha.0', + '@storybook/cli': '7.4.0-alpha.0', + '@storybook/client-api': '7.4.0-alpha.0', + '@storybook/client-logger': '7.4.0-alpha.0', + '@storybook/codemod': '7.4.0-alpha.0', + '@storybook/components': '7.4.0-alpha.0', + '@storybook/core-client': '7.4.0-alpha.0', + '@storybook/core-common': '7.4.0-alpha.0', + '@storybook/core-events': '7.4.0-alpha.0', + '@storybook/core-server': '7.4.0-alpha.0', + '@storybook/core-webpack': '7.4.0-alpha.0', + '@storybook/csf-plugin': '7.4.0-alpha.0', + '@storybook/csf-tools': '7.4.0-alpha.0', + '@storybook/docs-tools': '7.4.0-alpha.0', + '@storybook/ember': '7.4.0-alpha.0', + '@storybook/html': '7.4.0-alpha.0', + '@storybook/html-vite': '7.4.0-alpha.0', + '@storybook/html-webpack5': '7.4.0-alpha.0', + '@storybook/instrumenter': '7.4.0-alpha.0', + '@storybook/manager': '7.4.0-alpha.0', + '@storybook/manager-api': '7.4.0-alpha.0', + '@storybook/nextjs': '7.4.0-alpha.0', + '@storybook/node-logger': '7.4.0-alpha.0', + '@storybook/postinstall': '7.4.0-alpha.0', + '@storybook/preact': '7.4.0-alpha.0', + '@storybook/preact-vite': '7.4.0-alpha.0', + '@storybook/preact-webpack5': '7.4.0-alpha.0', + '@storybook/preset-create-react-app': '7.4.0-alpha.0', + '@storybook/preset-html-webpack': '7.4.0-alpha.0', + '@storybook/preset-preact-webpack': '7.4.0-alpha.0', + '@storybook/preset-react-webpack': '7.4.0-alpha.0', + '@storybook/preset-server-webpack': '7.4.0-alpha.0', + '@storybook/preset-svelte-webpack': '7.4.0-alpha.0', + '@storybook/preset-vue-webpack': '7.4.0-alpha.0', + '@storybook/preset-vue3-webpack': '7.4.0-alpha.0', + '@storybook/preset-web-components-webpack': '7.4.0-alpha.0', + '@storybook/preview': '7.4.0-alpha.0', + '@storybook/preview-api': '7.4.0-alpha.0', + '@storybook/preview-web': '7.4.0-alpha.0', + '@storybook/react': '7.4.0-alpha.0', + '@storybook/react-dom-shim': '7.4.0-alpha.0', + '@storybook/react-vite': '7.4.0-alpha.0', + '@storybook/react-webpack5': '7.4.0-alpha.0', + '@storybook/router': '7.4.0-alpha.0', + '@storybook/server': '7.4.0-alpha.0', + '@storybook/server-webpack5': '7.4.0-alpha.0', + '@storybook/source-loader': '7.4.0-alpha.0', + '@storybook/store': '7.4.0-alpha.0', + '@storybook/svelte': '7.4.0-alpha.0', + '@storybook/svelte-vite': '7.4.0-alpha.0', + '@storybook/svelte-webpack5': '7.4.0-alpha.0', + '@storybook/sveltekit': '7.4.0-alpha.0', + '@storybook/telemetry': '7.4.0-alpha.0', + '@storybook/theming': '7.4.0-alpha.0', + '@storybook/types': '7.4.0-alpha.0', + '@storybook/vue': '7.4.0-alpha.0', + '@storybook/vue-vite': '7.4.0-alpha.0', + '@storybook/vue-webpack5': '7.4.0-alpha.0', + '@storybook/vue3': '7.4.0-alpha.0', + '@storybook/vue3-vite': '7.4.0-alpha.0', + '@storybook/vue3-webpack5': '7.4.0-alpha.0', + '@storybook/web-components': '7.4.0-alpha.0', + '@storybook/web-components-vite': '7.4.0-alpha.0', + '@storybook/web-components-webpack5': '7.4.0-alpha.0', + sb: '7.4.0-alpha.0', + storybook: '7.4.0-alpha.0', }; diff --git a/code/lib/client-logger/package.json b/code/lib/client-logger/package.json index a65564ec40b9..4b2d17703c6f 100644 --- a/code/lib/client-logger/package.json +++ b/code/lib/client-logger/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/client-logger", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "", "keywords": [ "storybook" @@ -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 f3222cae0007..083d26b82457 100644 --- a/code/lib/codemod/package.json +++ b/code/lib/codemod/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/codemod", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "A collection of codemod scripts written with JSCodeshift", "keywords": [ "storybook" @@ -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 7c9b9b78e8eb..39d967f2bff8 100644 --- a/code/lib/core-common/package.json +++ b/code/lib/core-common/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core-common", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" @@ -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 fdbe8a12a89f..b98966eda27a 100644 --- a/code/lib/core-events/package.json +++ b/code/lib/core-events/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core-events", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Event names used in storybook core", "keywords": [ "storybook" @@ -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 571923980407..305d010bb28f 100644 --- a/code/lib/core-server/package.json +++ b/code/lib/core-server/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core-server", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" @@ -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 fcb4b5681571..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'; @@ -195,7 +196,7 @@ export const features = async ( }); export const csfIndexer: Indexer = { - test: /\.stories\.(m?js|ts)x?$/, + test: /\.(stories|story)\.(m?js|ts)x?$/, index: async (fileName, options) => (await readCsf(fileName, options)).parse().indexInputs, }; @@ -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.test.ts b/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts index 7110743b7003..b1097c168a86 100644 --- a/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts +++ b/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts @@ -99,6 +99,36 @@ describe('StoryIndexGenerator', () => { `); }); }); + describe('single file .story specifier', () => { + it('extracts stories from the right files', async () => { + const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry( + './src/F.story.ts', + options + ); + + const generator = new StoryIndexGenerator([specifier], options); + await generator.initialize(); + + expect(await generator.getIndex()).toMatchInlineSnapshot(` + Object { + "entries": Object { + "f--story-one": Object { + "id": "f--story-one", + "importPath": "./src/F.story.ts", + "name": "Story One", + "tags": Array [ + "autodocs", + "story", + ], + "title": "F", + "type": "story", + }, + }, + "v": 4, + } + `); + }); + }); describe('non-recursive specifier', () => { it('extracts stories from the right files', async () => { const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry( 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/utils/__mockdata__/src/F.story.ts b/code/lib/core-server/src/utils/__mockdata__/src/F.story.ts new file mode 100644 index 000000000000..bb14d42c7112 --- /dev/null +++ b/code/lib/core-server/src/utils/__mockdata__/src/F.story.ts @@ -0,0 +1,7 @@ +const component = {}; +export default { + component, + tags: ['autodocs'], +}; + +export const StoryOne = {}; 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 0e0a5eaadc29..3f1ddffc3a66 100644 --- a/code/lib/core-webpack/package.json +++ b/code/lib/core-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core-webpack", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" @@ -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 b1dad9ee786e..bb2cbd8f9c37 100644 --- a/code/lib/csf-plugin/package.json +++ b/code/lib/csf-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/csf-plugin", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Enrich CSF files via static analysis", "keywords": [ "storybook" @@ -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 de1e835e7360..ed12dbfb8c70 100644 --- a/code/lib/csf-tools/package.json +++ b/code/lib/csf-tools/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/csf-tools", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Parse and manipulate CSF and Storybook config files", "keywords": [ "storybook" @@ -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 1bf9432eadee..49d36403f753 100644 --- a/code/lib/docs-tools/package.json +++ b/code/lib/docs-tools/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/docs-tools", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Shared utility functions for frameworks to implement docs", "keywords": [ "storybook" @@ -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 e72d73f10729..22f218c85f73 100644 --- a/code/lib/instrumenter/package.json +++ b/code/lib/instrumenter/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/instrumenter", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "", "keywords": [ "storybook" @@ -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 40b6bd44ea96..95ac20b8a0bb 100644 --- a/code/lib/manager-api/package.json +++ b/code/lib/manager-api/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/manager-api", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Core Storybook Manager API & Context", "keywords": [ "storybook" @@ -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..688b4a2d6837 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'; 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..3ca68f09be89 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 } = {} ) => { 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..b295af6730db 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'; @@ -71,6 +74,7 @@ export interface SubState extends API_LoadedRefData { storyId: StoryId; viewMode: API_ViewMode; status: API_StatusState; + filters: Record; } export interface SubAPI { @@ -259,6 +263,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 +290,7 @@ function removeRemovedOptions = Record = ({ +export const init: ModuleFn = ({ fullAPI, store, navigate, @@ -468,7 +480,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 +488,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 +507,7 @@ export const init: ModuleFn = ({ return; } - await fullAPI.setIndex(storyIndex); + await api.setIndex(storyIndex); } catch (err) { await store.setState({ indexError: err }); } @@ -503,7 +515,7 @@ 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) => { + setIndex: async (storyIndex) => { const newHash = transformStoryIndexToStoriesHash(storyIndex, { provider, docsOptions, @@ -556,7 +568,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 { @@ -576,180 +588,193 @@ export const init: ModuleFn = ({ await store.setState({ status: newStatus }, { persistence: 'session' }); }, + experimental_setFilter: async (id, filterFunction) => { + await store.setState({ filters: { ...store.getState().filters, [id]: filterFunction } }); + }, }; - 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 +784,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..b9f7687f4526 100644 --- a/code/lib/manager-api/src/tests/stories.test.ts +++ b/code/lib/manager-api/src/tests/stories.test.ts @@ -16,28 +16,28 @@ import { import { EventEmitter } from 'events'; import { global } from '@storybook/global'; -import { Channel } from '@storybook/channels'; +import type { API_IndexHash, 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: () => {}, - }; +import type { API, State } from '..'; +import { mockEntries, docsEntries, preparedEntries, navigationEntries } from './mockStoriesEntries'; +import type { ModuleArgs } from '../lib/types'; - return new Channel({ transport }); -} +import { getAncestorIds } from '../../../../ui/manager/src/utils/tree'; const mockGetEntries = jest.fn(); +const fetch = global.fetch as jest.Mock>; +const getEventMetadata = getEventMetadataOriginal as unknown as jest.Mock< + ReturnType +>; -jest.mock('../lib/events'); +const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +jest.mock('../lib/events', () => ({ + getEventMetadata: jest.fn(() => ({ sourceType: 'local' })), +})); jest.mock('@storybook/global', () => ({ global: { ...globalThis, @@ -47,41 +47,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 +57,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(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 +98,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 +117,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 +126,6 @@ describe('stories API', () => { storiesImports: [], prepared: false, }); - expect(index['component-a--story-1']).toMatchObject({ type: 'story', id: 'component-a--story-1', @@ -185,15 +138,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 +155,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 +175,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 +193,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 +211,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 +228,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 +250,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 +267,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 +283,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 +298,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 +306,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 +331,7 @@ describe('stories API', () => { }, }, }); - const { index } = store.getState(); - expect(index['prepared--story']).toMatchObject({ type: 'story', id: 'prepared--story', @@ -431,21 +343,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 +361,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 +372,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 +394,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 +412,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 +610,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 { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api, { - updateRef: jest.fn(), - }); + const fullAPI = { updateRef: jest.fn() }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + initStories(moduleArgs as unknown as ModuleArgs); + const { provider } = moduleArgs; - 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 fullAPI = { updateRef: jest.fn() }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { provider, store } = moduleArgs; - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - const { setIndex } = Object.assign(fullAPI, api); - setIndex({ v: 4, entries: preparedEntries }); - - 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 +668,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 fullAPI = { updateRef: jest.fn() }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { provider } = moduleArgs; - const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - - const { setIndex } = Object.assign(fullAPI, api); - setIndex({ v: 4, entries: preparedEntries }); - - 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 +687,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 +710,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 +731,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 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; - const { - api: { setIndex, jumpToStory }, - } = initStoriesAndSetState({ - store, - storyId: 'a--2', - 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--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 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, jumpToComponent }, - } = initStoriesAndSetState({ - store, - navigate, - storyId: 'a--1', - viewMode: 'story', - provider, - } as any); - setIndex({ v: 4, entries: navigationEntries }); - - 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 }); + 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--2'); + 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 +894,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 }); + 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; - selectStory('a--2'); + 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 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', '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 { - 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'); + 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 +1029,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 +1077,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 { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - Object.assign(fullAPI, api); + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { provider, store } = moduleArgs; - 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); + const fullAPI = { updateRef: jest.fn() }; + const moduleArgs = createMockModuleArgs({ fullAPI }); + initStories(moduleArgs as unknown as ModuleArgs); + const { provider } = moduleArgs; - Object.assign(fullAPI, api); - - 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 +1155,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 +1171,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,47 +1200,33 @@ describe('stories API', () => { ); }); }); +}); +describe('experimental_updateStatus', () => { + it('is included in the initial state', () => { + const moduleArgs = createMockModuleArgs({}); + const { state } = initStories(moduleArgs as unknown as ModuleArgs); - describe('experimental_updateStatus', () => { - it('is included in the initial state', () => { - const { state } = initStoriesAndSetState({ - storyId: 'id', - viewMode: 'story', - } as ModuleArgs); - - expect(state).toEqual( - expect.objectContaining({ - status: {}, - }) - ); - }); - - 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 API: SubAPI = Object.assign(fullAPI, api, { - setIndex: jest.fn(), - findRef: jest.fn(), - setRef: jest.fn(), - }); - - await init(); - - await expect( - API.experimental_updateStatus('a-addon-id', { - 'a-story-id': { - status: 'pending', - title: 'an addon title', - description: 'an addon description', - }, - }) - ).resolves.not.toThrow(); - - expect(store.getState().status).toMatchInlineSnapshot(` + expect(state).toEqual( + expect.objectContaining({ + status: {}, + }) + ); + }); + it('updates a story', async () => { + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; + + await expect( + api.experimental_updateStatus('a-addon-id', { + 'a-story-id': { + status: 'pending', + title: 'an addon title', + description: 'an addon description', + }, + }) + ).resolves.not.toThrow(); + expect(store.getState().status).toMatchInlineSnapshot(` Object { "a-story-id": Object { "a-addon-id": Object { @@ -1741,35 +1237,23 @@ 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 API: SubAPI = Object.assign(fullAPI, api, { - setIndex: jest.fn(), - findRef: jest.fn(), - setRef: jest.fn(), - }); - - await init(); - - await expect( - API.experimental_updateStatus('a-addon-id', { - 'a-story-id': { - status: 'pending', - title: 'an addon title', - description: 'an addon description', - }, - 'another-story-id': { status: 'success', title: 'a addon title', description: '' }, - }) - ).resolves.not.toThrow(); - - expect(store.getState().status).toMatchInlineSnapshot(` + }); + it('updates multiple stories', async () => { + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; + + await expect( + api.experimental_updateStatus('a-addon-id', { + 'a-story-id': { + status: 'pending', + title: 'an addon title', + description: 'an addon description', + }, + 'another-story-id': { status: 'success', title: 'a addon title', description: '' }, + }) + ).resolves.not.toThrow(); + expect(store.getState().status).toMatchInlineSnapshot(` Object { "a-story-id": Object { "a-addon-id": Object { @@ -1787,6 +1271,132 @@ describe('stories API', () => { }, } `); + }); + describe('experimental_setFilter', () => { + it('is included in the initial state', () => { + const moduleArgs = createMockModuleArgs({}); + const { state } = initStories(moduleArgs as unknown as ModuleArgs); + + expect(state).toEqual( + expect.objectContaining({ + filters: {}, + }) + ); + }); + it('updates state', () => { + const moduleArgs = createMockModuleArgs({}); + const { api } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; + + api.experimental_setFilter('myCustomFilter', () => true); + + expect(store.getState()).toEqual( + expect.objectContaining({ + filters: { + myCustomFilter: expect.any(Function), + }, + }) + ); + }); + + it('can filter', () => { + const moduleArgs = createMockModuleArgs({}); + const { + api, + state: { status }, + } = initStories(moduleArgs as unknown as ModuleArgs); + const { store } = moduleArgs; + + /** + * This function is a copy of the one in the containers/sidebar.ts file inside of ui/manager + * I'm hoping we can eventually merge this 2 packages so there's no odd looking import and no re-implementation. + */ + const applyFilters = (originalIndex: API_IndexHash) => { + if (!originalIndex) { + return originalIndex; + } + + const filtered = new Set(); + Object.values(originalIndex).forEach((item) => { + if (item.type === 'story' || item.type === 'docs') { + let result = true; + + Object.values(filters).forEach((filter) => { + if (result === true) { + result = filter({ ...item, status: status[item.id] }); + } + }); + + if (result) { + filtered.add(item.id); + getAncestorIds(originalIndex, item.id).forEach((id) => { + filtered.add(id); + }); + } + } + }); + + return Object.fromEntries( + Object.entries(originalIndex).filter(([key]) => filtered.has(key)) + ); + }; + + api.experimental_setFilter('myCustomFilter', (item) => item.id.startsWith('a')); + api.setIndex({ v: 4, entries: navigationEntries }); + + const { index, filters } = store.getState(); + + const filtered = applyFilters(index); + + expect(filtered).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/manager-api/src/version.ts b/code/lib/manager-api/src/version.ts index a5a65afa751e..70107dd59789 100644 --- a/code/lib/manager-api/src/version.ts +++ b/code/lib/manager-api/src/version.ts @@ -1 +1 @@ -export const version = '7.3.0-alpha.0'; +export const version = '7.4.0-alpha.0'; diff --git a/code/lib/node-logger/package.json b/code/lib/node-logger/package.json index 497b2a521760..0870a33674a3 100644 --- a/code/lib/node-logger/package.json +++ b/code/lib/node-logger/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/node-logger", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "", "keywords": [ "storybook" @@ -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 dc58f48394ac..49c70de632b4 100644 --- a/code/lib/postinstall/package.json +++ b/code/lib/postinstall/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/postinstall", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook addons postinstall utilities", "keywords": [ "api", @@ -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 966ea6360c45..b7e9fa5add43 100644 --- a/code/lib/preview-api/package.json +++ b/code/lib/preview-api/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preview-api", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "", "keywords": [ "storybook" @@ -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/modules/store/csf/testing-utils/index.ts b/code/lib/preview-api/src/modules/store/csf/testing-utils/index.ts index 40fcddcb6e35..10468737e6dc 100644 --- a/code/lib/preview-api/src/modules/store/csf/testing-utils/index.ts +++ b/code/lib/preview-api/src/modules/store/csf/testing-utils/index.ts @@ -10,7 +10,7 @@ import type { Store_CSFExports, StoryContext, Parameters, - PreparedStoryFn, + ComposedStoryFn, } from '@storybook/types'; import { HooksContext } from '../../../addons'; @@ -36,7 +36,7 @@ export function composeStory = GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS as ProjectAnnotations, defaultConfig: ProjectAnnotations = {}, exportsName?: string -): PreparedStoryFn> { +): ComposedStoryFn> { if (storyAnnotations === undefined) { throw new Error('Expected a story but received undefined.'); } @@ -73,22 +73,25 @@ export function composeStory) => { - const context: Partial = { - ...story, - hooks: new HooksContext(), - globals: defaultGlobals, - args: { ...story.initialArgs, ...extraArgs }, - }; - - return story.unboundStoryFn(prepareContext(context as StoryContext)); - }; - - composedStory.storyName = storyName; - composedStory.args = story.initialArgs as Partial; - composedStory.play = story.playFunction as ComposedStoryPlayFn>; - composedStory.parameters = story.parameters as Parameters; - composedStory.id = story.id; + const composedStory: ComposedStoryFn> = Object.assign( + (extraArgs?: Partial) => { + const context: Partial = { + ...story, + hooks: new HooksContext(), + globals: defaultGlobals, + args: { ...story.initialArgs, ...extraArgs }, + }; + + return story.unboundStoryFn(prepareContext(context as StoryContext)); + }, + { + storyName, + args: story.initialArgs as Partial, + play: story.playFunction as ComposedStoryPlayFn>, + parameters: story.parameters as Parameters, + id: story.id, + } + ); return composedStory; } 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 0445b0d24547..7d386ea80aae 100644 --- a/code/lib/preview/package.json +++ b/code/lib/preview/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preview", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "", "keywords": [ "storybook" @@ -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 6db51a1302a0..d2ae7e06a70e 100644 --- a/code/lib/react-dom-shim/package.json +++ b/code/lib/react-dom-shim/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react-dom-shim", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "", "keywords": [ "storybook" @@ -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 175b765ac2be..742df75b4d4a 100644 --- a/code/lib/router/package.json +++ b/code/lib/router/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/router", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Core Storybook Router", "keywords": [ "storybook" @@ -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 e86bb6e5c358..0f53d7cd35e3 100644 --- a/code/lib/source-loader/package.json +++ b/code/lib/source-loader/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/source-loader", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Source loader", "keywords": [ "lib", @@ -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 b0941ef96519..8f85a012b16a 100644 --- a/code/lib/telemetry/package.json +++ b/code/lib/telemetry/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/telemetry", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Telemetry logging for crash reports and usage statistics", "keywords": [ "storybook" @@ -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 1ca54183a75b..1f44927fdf1d 100644 --- a/code/lib/theming/package.json +++ b/code/lib/theming/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/theming", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Core Storybook Components", "keywords": [ "storybook" @@ -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 e64f21ee68b7..9ee90eea32ee 100644 --- a/code/lib/types/package.json +++ b/code/lib/types/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/types", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Core Storybook TS Types", "keywords": [ "storybook" @@ -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..fd0f3ca31d04 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_IndexHash[keyof API_IndexHash] & { 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/lib/types/src/modules/composedStory.ts b/code/lib/types/src/modules/composedStory.ts index ce31138bac20..f02a30187a38 100644 --- a/code/lib/types/src/modules/composedStory.ts +++ b/code/lib/types/src/modules/composedStory.ts @@ -8,8 +8,8 @@ import type { ComponentAnnotations, Parameters, StoryAnnotations, + StoryAnnotationsOrFn, StoryContext, - StoryFn, } from './csf'; import type { ProjectAnnotations } from './story'; @@ -22,6 +22,15 @@ export type Store_CSFExports) // or PrimaryButton() + * PrimaryButton.play({ canvasElement: container }) + */ export type ComposedStoryPlayContext = Partial< StoryContext & Pick, 'canvasElement'> >; @@ -30,40 +39,52 @@ export type ComposedStoryPlayFn ) => Promise | void; -export type PreparedStoryFn = AnnotatedStoryFn< - TRenderer, - TArgs -> & { play: ComposedStoryPlayFn; args: TArgs; id: StoryId }; - -export type ComposedStory = - | StoryFn - | StoryAnnotations; +/** + * A story function with partial args, used internally by composeStory + */ +export type PartialArgsStoryFn = ( + args?: TArgs +) => (TRenderer & { + T: TArgs; +})['storyResult']; /** - * T represents the whole ES module of a stories file. K of T means named exports (basically the Story type) - * 1. pick the keys K of T that have properties that are Story - * 2. infer the actual prop type for each Story - * 3. reconstruct Story with Partial. Story -> Story> + * A story that got recomposed for portable stories, containing all the necessary data to be rendered in external environments + */ +export type ComposedStoryFn< + TRenderer extends Renderer = Renderer, + TArgs = Args +> = PartialArgsStoryFn & { + play: ComposedStoryPlayFn; + args: TArgs; + id: StoryId; + storyName: string; + parameters: Parameters; +}; +/** + * Based on a module of stories, it returns all stories within it, filtering non-stories + * Each story will have partial props, as their props should be handled when composing stories */ export type StoriesWithPartialProps = { - // @TODO once we can use Typescript 4.0 do this to exclude nonStory exports: - // replace [K in keyof TModule] with [K in keyof TModule as TModule[K] extends ComposedStory ? K : never] - [K in keyof TModule]: TModule[K] extends ComposedStory - ? PreparedStoryFn> + // T represents the whole ES module of a stories file. K of T means named exports (basically the Story type) + // 1. pick the keys K of T that have properties that are Story + // 2. infer the actual prop type for each Story + // 3. reconstruct Story with Partial. Story -> Story> + [K in keyof TModule as TModule[K] extends StoryAnnotationsOrFn + ? K + : never]: TModule[K] extends StoryAnnotationsOrFn + ? ComposedStoryFn> : unknown; }; +/** + * Type used for integrators of portable stories, as reference when creating their own composeStory function + */ export interface ComposeStoryFn { ( storyAnnotations: AnnotatedStoryFn | StoryAnnotations, componentAnnotations: ComponentAnnotations, projectAnnotations: ProjectAnnotations, exportsName?: string - ): { - (extraArgs: Partial): TRenderer['storyResult']; - storyName: string; - args: Args; - play: ComposedStoryPlayFn; - parameters: Parameters; - }; + ): ComposedStoryFn; } diff --git a/code/package.json b/code/package.json index 86f31753abae..3ae4a217a371 100644 --- a/code/package.json +++ b/code/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/root", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "private": true, "description": "Storybook root", "homepage": "https://storybook.js.org/", @@ -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", diff --git a/code/presets/create-react-app/package.json b/code/presets/create-react-app/package.json index 55886997a704..e55935c0173e 100644 --- a/code/presets/create-react-app/package.json +++ b/code/presets/create-react-app/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preset-create-react-app", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Create React App preset", "keywords": [ "storybook" @@ -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 e682ca0f08bf..02b3a3a1fb75 100644 --- a/code/presets/html-webpack/package.json +++ b/code/presets/html-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preset-html-webpack", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for HTML: View HTML snippets in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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 6806ab325e0f..59c0a2c2b7a2 100644 --- a/code/presets/preact-webpack/package.json +++ b/code/presets/preact-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preset-preact-webpack", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Preact: Develop Preact Component in isolation.", "keywords": [ "storybook" @@ -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 208dee7257bb..80128c0a7697 100644 --- a/code/presets/react-webpack/package.json +++ b/code/presets/react-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preset-react-webpack", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for React: Develop React Component in isolation with Hot Reloading", "keywords": [ "storybook" @@ -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 a7fb771923a9..b7d8b5b1ba5f 100644 --- a/code/presets/server-webpack/package.json +++ b/code/presets/server-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preset-server-webpack", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Server: View HTML snippets from a server in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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 3a91d6c27fe3..2e01104e0e1b 100644 --- a/code/presets/svelte-webpack/package.json +++ b/code/presets/svelte-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preset-svelte-webpack", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Svelte: Develop Svelte Component in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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 c8a5f6cc40e5..0d416acc071f 100644 --- a/code/presets/vue-webpack/package.json +++ b/code/presets/vue-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preset-vue-webpack", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Vue: Develop Vue Component in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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 e3c87d969fc8..b0698c46efa0 100644 --- a/code/presets/vue3-webpack/package.json +++ b/code/presets/vue3-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preset-vue3-webpack", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for Vue 3: Develop Vue 3 Components in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -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 08378a729ca4..ff0e9ac7d830 100644 --- a/code/presets/web-components-webpack/package.json +++ b/code/presets/web-components-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preset-web-components-webpack", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook for web-components: View web components snippets in isolation with Hot Reloading.", "keywords": [ "lit", @@ -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 e1e0ed5e4563..cb58d3272450 100644 --- a/code/renderers/html/package.json +++ b/code/renderers/html/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/html", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook HTML renderer", "keywords": [ "storybook" @@ -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 63e0a26a508e..8493c73e9792 100644 --- a/code/renderers/preact/package.json +++ b/code/renderers/preact/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preact", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook Preact renderer", "keywords": [ "storybook" @@ -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 f2e06a9cdd20..df67f8ea3845 100644 --- a/code/renderers/react/package.json +++ b/code/renderers/react/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook React renderer", "keywords": [ "storybook" @@ -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/react/src/testing-api.ts b/code/renderers/react/src/testing-api.ts index da061e64ef04..545147fab2db 100644 --- a/code/renderers/react/src/testing-api.ts +++ b/code/renderers/react/src/testing-api.ts @@ -6,7 +6,7 @@ import { import type { Args, ProjectAnnotations, - ComposedStory, + StoryAnnotationsOrFn, Store_CSFExports, StoriesWithPartialProps, } from '@storybook/types'; @@ -81,13 +81,13 @@ const defaultProjectAnnotations: ProjectAnnotations = { * @param [exportsName] - in case your story does not contain a name and you want it to have a name. */ export function composeStory( - story: ComposedStory, + story: StoryAnnotationsOrFn, componentAnnotations: Meta, projectAnnotations?: ProjectAnnotations, exportsName?: string ) { return originalComposeStory( - story as ComposedStory, + story as StoryAnnotationsOrFn, componentAnnotations, projectAnnotations, defaultProjectAnnotations, diff --git a/code/renderers/server/package.json b/code/renderers/server/package.json index 4eea18ccfef7..74df9790f49b 100644 --- a/code/renderers/server/package.json +++ b/code/renderers/server/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/server", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook Server renderer", "keywords": [ "storybook" @@ -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 a17bbe33040e..cb9d4df6509d 100644 --- a/code/renderers/svelte/package.json +++ b/code/renderers/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/svelte", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook Svelte renderer", "keywords": [ "storybook" @@ -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 a9275946b25c..4333bcc45055 100644 --- a/code/renderers/vue/package.json +++ b/code/renderers/vue/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/vue", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook Vue renderer", "keywords": [ "storybook" @@ -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 b8905045274d..df621810ebc2 100644 --- a/code/renderers/vue3/package.json +++ b/code/renderers/vue3/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/vue3", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook Vue 3 renderer", "keywords": [ "storybook" @@ -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/web-components/package.json b/code/renderers/web-components/package.json index e84d54f7cb4d..c0659a9c57fd 100644 --- a/code/renderers/web-components/package.json +++ b/code/renderers/web-components/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/web-components", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook web-components renderer", "keywords": [ "lit", @@ -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/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 f8086dff4690..e077d15ec27c 100644 --- a/code/ui/blocks/package.json +++ b/code/ui/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/blocks", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Storybook Doc Blocks", "keywords": [ "storybook" @@ -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/components/package.json b/code/ui/components/package.json index e819b577a0e5..65b38724e688 100644 --- a/code/ui/components/package.json +++ b/code/ui/components/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/components", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Core Storybook Components", "keywords": [ "storybook" @@ -60,7 +60,8 @@ "dist/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/ui/manager/package.json b/code/ui/manager/package.json index 89bded8e03d2..96191e437764 100644 --- a/code/ui/manager/package.json +++ b/code/ui/manager/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/manager", - "version": "7.3.0-alpha.0", + "version": "7.4.0-alpha.0", "description": "Core Storybook UI", "keywords": [ "storybook" @@ -43,7 +43,8 @@ "static/**/*", "README.md", "*.js", - "*.d.ts" + "*.d.ts", + "!src/**/*" ], "scripts": { "check": "../../../scripts/prepare/check.ts", diff --git a/code/ui/manager/src/containers/sidebar.tsx b/code/ui/manager/src/containers/sidebar.tsx index c4beb5bda4c4..a97f929bf246 100755 --- a/code/ui/manager/src/containers/sidebar.tsx +++ b/code/ui/manager/src/containers/sidebar.tsx @@ -1,10 +1,11 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import type { Combo, StoriesHash } from '@storybook/manager-api'; import { Consumer } from '@storybook/manager-api'; import { Sidebar as SidebarComponent } from '../components/sidebar/Sidebar'; import { useMenu } from './menu'; +import { getAncestorIds } from '../utils/tree'; export type Item = StoriesHash[keyof StoriesHash]; @@ -16,11 +17,12 @@ const Sidebar = React.memo(function Sideber() { storyId, refId, layout: { showToolbar, isFullscreen, showPanel, showNav }, - index, + index: originalIndex, status, indexError, previewInitialized, refs, + filters, } = state; const menu = useMenu( @@ -36,6 +38,34 @@ const Sidebar = React.memo(function Sideber() { const whatsNewNotificationsEnabled = state.whatsNewData?.status === 'SUCCESS' && !state.disableWhatsNewNotifications; + const index = useMemo(() => { + if (!originalIndex) { + return originalIndex; + } + + const filtered = new Set(); + Object.values(originalIndex).forEach((item) => { + if (item.type === 'story' || item.type === 'docs') { + let result = true; + + Object.values(filters).forEach((filter) => { + if (result === true) { + result = filter({ ...item, status: status[item.id] }); + } + }); + + if (result) { + filtered.add(item.id); + getAncestorIds(originalIndex, item.id).forEach((id) => { + filtered.add(id); + }); + } + } + }); + + return Object.fromEntries(Object.entries(originalIndex).filter(([key]) => filtered.has(key))); + }, [originalIndex, filters, status]); + return { title: name, url, diff --git a/code/ui/manager/src/globals/exports.ts b/code/ui/manager/src/globals/exports.ts index 793165aaf103..8eb900860c6e 100644 --- a/code/ui/manager/src/globals/exports.ts +++ b/code/ui/manager/src/globals/exports.ts @@ -172,6 +172,7 @@ export default { 'STORY_SPECIFIED', 'STORY_THREW_EXCEPTION', 'STORY_UNCHANGED', + 'TELEMETRY_ERROR', 'TOGGLE_WHATS_NEW_NOTIFICATIONS', 'UPDATE_GLOBALS', 'UPDATE_QUERY_PARAMS', diff --git a/code/ui/manager/src/index.tsx b/code/ui/manager/src/index.tsx index 8bf921c005da..2836846be5b1 100644 --- a/code/ui/manager/src/index.tsx +++ b/code/ui/manager/src/index.tsx @@ -7,6 +7,7 @@ import { Location, LocationProvider, useNavigate } from '@storybook/router'; import { Provider as ManagerProvider, types } from '@storybook/manager-api'; import type { Combo } from '@storybook/manager-api'; import { ThemeProvider, ensure as ensureTheme } from '@storybook/theming'; +import { ProviderDoesNotExtendBaseProviderError } from '@storybook/core-events/manager-errors'; import { HelmetProvider } from 'react-helmet-async'; @@ -83,7 +84,7 @@ const Main: FC<{ provider: Provider }> = ({ provider }) => { export function renderStorybookUI(domNode: HTMLElement, provider: Provider) { if (!(provider instanceof Provider)) { - throw new Error('provider is not extended from the base Provider'); + throw new ProviderDoesNotExtendBaseProviderError(); } ReactDOM.render(, domNode); diff --git a/code/ui/manager/src/runtime.ts b/code/ui/manager/src/runtime.ts index bb1691be334c..dd29b9a45223 100644 --- a/code/ui/manager/src/runtime.ts +++ b/code/ui/manager/src/runtime.ts @@ -1,3 +1,5 @@ +/* eslint-disable local-rules/no-uncategorized-errors */ + import { global } from '@storybook/global'; import type { Channel } from '@storybook/channels'; @@ -5,7 +7,8 @@ import type { AddonStore } from '@storybook/manager-api'; import { addons } from '@storybook/manager-api'; import type { Addon_Types, Addon_Config } from '@storybook/types'; import { createBrowserChannel } from '@storybook/channels'; -import { CHANNEL_CREATED } from '@storybook/core-events'; +import { CHANNEL_CREATED, TELEMETRY_ERROR } from '@storybook/core-events'; +import { UncaughtManagerError } from '@storybook/core-events/manager-errors'; import Provider from './provider'; import { renderStorybookUI } from './index'; @@ -35,6 +38,7 @@ class ReactProvider extends Provider { this.addons = addons; this.channel = channel; + global.__STORYBOOK_ADDONS_CHANNEL__ = channel; if (FEATURES?.storyStoreV7 && CONFIG_TYPE === 'DEVELOPMENT') { this.serverChannel = this.channel; @@ -55,12 +59,51 @@ class ReactProvider extends Provider { } } -const { document } = global; - -const rootEl = document.getElementById('root'); -renderStorybookUI(rootEl, new ReactProvider()); - // Apply all the globals Object.keys(Keys).forEach((key: keyof typeof Keys) => { global[Keys[key]] = values[key]; }); + +function preprocessError( + originalError: Error & { + fromStorybook?: boolean; + category?: string; + target?: any; + currentTarget?: any; + srcElement?: any; + } +) { + let error = originalError; + + if (!originalError.fromStorybook) { + error = new UncaughtManagerError(originalError); + } + + // DOM manipulation errors and other similar errors are not serializable as they contain + // circular references to the window object. If that's the case, we make a simplified copy + if (error.target === window || error.currentTarget === window || error.srcElement === window) { + error = new Error(originalError.message); + error.name = originalError.name || error.name; + error.category = originalError.category; + } + + return error; +} + +global.sendTelemetryError = (error) => { + const channel = global.__STORYBOOK_ADDONS_CHANNEL__; + channel.emit(TELEMETRY_ERROR, preprocessError(error)); +}; + +// handle all uncaught errors at the root of the application and log to telemetry +global.addEventListener('error', (args) => { + const error = args.error || args; + global.sendTelemetryError(error); +}); +global.addEventListener('unhandledrejection', ({ reason }) => { + global.sendTelemetryError(reason); +}); + +const { document } = global; +const rootEl = document.getElementById('root'); +renderStorybookUI(rootEl, new ReactProvider()); diff --git a/code/ui/manager/src/typings.d.ts b/code/ui/manager/src/typings.d.ts index f46c49b91852..2ff3df07e63e 100644 --- a/code/ui/manager/src/typings.d.ts +++ b/code/ui/manager/src/typings.d.ts @@ -25,3 +25,5 @@ declare var __STORYBOOKTHEMING__: any; declare var __STORYBOOKAPI__: any; declare var __STORYBOOKADDONS__: any; declare var __STORYBOOKCLIENTLOGGER__: any; +declare var __STORYBOOK_ADDONS_CHANNEL__: any; +declare var sendTelemetryError: (error: any) => void; diff --git a/code/yarn.lock b/code/yarn.lock index 05b7c110550b..545d48a58ab0 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -463,7 +463,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.0, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.19.6, @babel/core@npm:^7.20.12, @babel/core@npm:^7.22.0, @babel/core@npm:^7.22.1, @babel/core@npm:^7.22.9, @babel/core@npm:^7.3.4, @babel/core@npm:^7.7.5": +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.0, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.19.6, @babel/core@npm:^7.20.12, @babel/core@npm:^7.22.1, @babel/core@npm:^7.22.9, @babel/core@npm:^7.3.4, @babel/core@npm:^7.7.5": version: 7.22.9 resolution: "@babel/core@npm:7.22.9" dependencies: @@ -5822,7 +5822,7 @@ __metadata: 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 typescript: ~4.9.3 uuid: ^9.0.0 @@ -6395,7 +6395,7 @@ __metadata: jest-specific-snapshot: ^8.0.0 read-pkg-up: ^7.0.1 semver: ^7.3.7 - telejson: ^7.0.3 + telejson: ^7.2.0 tmp: ^0.2.1 ts-dedent: ^2.0.0 tsconfig-paths-webpack-plugin: ^4.0.1 @@ -6501,7 +6501,7 @@ __metadata: 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 @@ -6584,15 +6584,23 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/builder-webpack5@workspace:builders/builder-webpack5" 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/pretty-hrtime": ^1.0.0 @@ -6627,6 +6635,9 @@ __metadata: webpack-dev-middleware: ^6.1.1 webpack-hot-middleware: ^2.25.1 webpack-virtual-modules: ^0.5.0 + 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 @@ -6661,7 +6672,7 @@ __metadata: "@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 typescript: ~4.9.3 languageName: unknown @@ -6861,6 +6872,7 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/core-events@workspace:lib/core-events" dependencies: + ts-dedent: ^2.0.0 typescript: ~4.9.3 languageName: unknown linkType: soft @@ -6915,7 +6927,7 @@ __metadata: semver: ^7.3.7 serve-favicon: ^2.5.0 slash: ^5.0.0 - telejson: ^7.0.3 + telejson: ^7.2.0 tiny-invariant: ^1.3.1 ts-dedent: ^2.0.0 typescript: ~4.9.3 @@ -7195,7 +7207,7 @@ __metadata: qs: ^6.10.0 semver: ^7.3.7 store2: ^2.14.2 - telejson: ^7.0.3 + telejson: ^7.2.0 ts-dedent: ^2.0.0 typescript: ~4.9.3 peerDependencies: @@ -7621,6 +7633,7 @@ __metadata: "@storybook/channels": "workspace:*" "@storybook/client-logger": "workspace:*" "@storybook/core-events": "workspace:*" + "@storybook/global": ^5.0.0 "@storybook/preview-api": "workspace:*" typescript: ~4.9.3 languageName: unknown @@ -7877,6 +7890,7 @@ __metadata: 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 @@ -15912,6 +15926,12 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-local-rules@portal:../scripts/eslint-plugin-local-rules::locator=%40storybook%2Froot%40workspace%3A.": + version: 0.0.0-use.local + resolution: "eslint-plugin-local-rules@portal:../scripts/eslint-plugin-local-rules::locator=%40storybook%2Froot%40workspace%3A." + languageName: node + linkType: soft + "eslint-plugin-prettier@npm:^3.4.0": version: 3.4.1 resolution: "eslint-plugin-prettier@npm:3.4.1" @@ -29608,12 +29628,12 @@ __metadata: languageName: node linkType: hard -"telejson@npm:^7.0.3": - version: 7.1.0 - resolution: "telejson@npm:7.1.0" +"telejson@npm:^7.2.0": + version: 7.2.0 + resolution: "telejson@npm:7.2.0" dependencies: memoizerific: ^1.11.3 - checksum: dc9a185d0e00d947c0eaa229bfb993aab61a3ba79282ae409768fc8ae66d236e89a64ebe291f9ea6ed5e05396e0be52a7542ea32b6c1321b20440f28c7828edc + checksum: d26e6cc93e54bfdcdb207b49905508c5db45862e811a2e2193a735409e47b14530e1c19351618a3e03ad2fd4ffc3759364fcd72851aba2df0300fab574b6151c languageName: node linkType: hard diff --git a/docs/essentials/controls.md b/docs/essentials/controls.md index 5266c0760f02..cc0f937ec97b 100644 --- a/docs/essentials/controls.md +++ b/docs/essentials/controls.md @@ -123,26 +123,26 @@ If you haven't used the CLI to setup the configuration, or if you want to define ## Fully custom args -Until now, we only used auto-generated controls based on the component we're writing stories for. If we are writing [complex stories](../writing-stories/stories-for-multiple-components.md), we may want to add controls for args that aren’t part of the component. +Until now, we only used auto-generated controls based on the component we're writing stories for. If we are writing [complex stories](../writing-stories/stories-for-multiple-components.md), we may want to add controls for args that aren’t part of the component. For example, here's how you could use a `footer` arg to populate a child component: diff --git a/docs/snippets/angular/page-story-slots.ts.mdx b/docs/snippets/angular/page-story-slots.ts.mdx index 2b79a6caa3e3..63ac7f3309cc 100644 --- a/docs/snippets/angular/page-story-slots.ts.mdx +++ b/docs/snippets/angular/page-story-slots.ts.mdx @@ -5,26 +5,23 @@ import type { Meta, StoryObj } from '@storybook/angular'; import { Page } from './page.component'; -const meta: Meta = { - component: Page, -}; - -export default meta; -type Story = StoryObj; +type PagePropsAndCustomArgs = Page & { footer?: string }; -/* - *πŸ‘‡ Render functions are a framework specific feature to allow you control on how the component renders. - * See https://storybook.js.org/docs/angular/api/csf - * to learn how to use render functions. - */ -export const CustomFooter: Story = { - render: (args) => ({ +const meta: Meta = { + component: Page, + render: ({ footer, ...args }) => ({ props: args, template: ` - ${args.footer} + ${footer} `, }), +}; +export default meta; + +type Story = StoryObj; + +export const CustomFooter: Story = { args: { footer: 'Built with Storybook', }, diff --git a/docs/snippets/angular/table-story-fully-customize-controls.ts.mdx b/docs/snippets/angular/table-story-fully-customize-controls.ts.mdx deleted file mode 100644 index 387ca9506c93..000000000000 --- a/docs/snippets/angular/table-story-fully-customize-controls.ts.mdx +++ /dev/null @@ -1,39 +0,0 @@ -```ts -// Table.stories.ts - -import type { Meta, StoryObj } from '@storybook/angular'; - -import { Table } from './Table.component'; - -const meta: Meta = { - component: Table, -}; - -export default meta; -type Story = StoryObj
; - -export const Numeric: Story = { - render: (args) => ({ - props: args, - template: ` -
- - - - - -
- {{data[i][j]}} -
- `, - }), - args: { - data: [ - [1, 2, 3], - [4, 5, 6], - ], - //πŸ‘‡ The remaining args get passed to the `Table` component - size: 'large', - }, -}; -``` diff --git a/docs/snippets/angular/typed-csf-file.ts.mdx b/docs/snippets/angular/typed-csf-file.ts.mdx new file mode 100644 index 000000000000..6ac8b473a93b --- /dev/null +++ b/docs/snippets/angular/typed-csf-file.ts.mdx @@ -0,0 +1,22 @@ +```ts +// Button.stories.ts + +import type { Meta, StoryObj } from '@storybook/angular'; + +import { Button } from './button.component'; + +const meta: Meta + + +``` + +The same setup works with Svelte stories files too, providing both type safety and autocompletion. + + diff --git a/docs/writing-tests/test-runner.md b/docs/writing-tests/test-runner.md index 22b68d2b1627..4ef6b07315ab 100644 --- a/docs/writing-tests/test-runner.md +++ b/docs/writing-tests/test-runner.md @@ -79,23 +79,31 @@ Test runner offers zero-config support for Storybook. However, you can run `test The test-runner is powered by [Jest](https://jestjs.io/) and accepts a subset of its [CLI options](https://jestjs.io/docs/cli) (for example, `--watch`, `--maxWorkers`). If you're already using any of those flags in your project, you should be able to migrate them into Storybook's test-runner without any issues. Listed below are all the available flags and examples of using them. -| Options | Description | -| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `--help` | Output usage information
`test-storybook --help` | +| Options | Description | +| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--help` | Output usage information
`test-storybook --help` | | `-s`, `--stories-json` | Run in stories json mode. Automatically detected (requires a compatible Storybook)
`test-storybook --stories-json` | | `--no-stories-json` | Disables stories json mode
`test-storybook --no-stories-json` | -| `-c`, `--config-dir [dir-name]` | Directory where to load Storybook configurations from
`test-storybook -c .storybook` | -| `--watch` | Run in watch mode
`test-storybook --watch` | -| `--url` | Define the URL to run tests in. Useful for custom Storybook URLs
`test-storybook --url http://the-storybook-url-here.com` | -| `--browsers` | Define browsers to run tests in. One or multiple of: chromium, firefox, webkit
`test-storybook --browsers firefox chromium` | -| `--maxWorkers [amount]` | Specifies the maximum number of workers the worker-pool will spawn for running tests
`test-storybook --maxWorkers=2` | -| `--no-cache` | Disable the cache
`test-storybook --no-cache` | -| `--clearCache` | Deletes the Jest cache directory and then exits without running tests
`test-storybook --clearCache` | -| `--verbose` | Display individual test results with the test suite hierarchy
`test-storybook --verbose` | -| `-u`, `--updateSnapshot` | Use this flag to re-record every snapshot that fails during this test run
`test-storybook -u` | -| `--eject` | Creates a local configuration file to override defaults of the test-runner
`test-storybook --eject` | -| `--coverage` | Runs [coverage tests](./test-coverage.md) on your stories and components
`test-storybook --coverage` | -| `--shard [index/count]` | Requires CI. Splits the test suite execution into multiple machines
`test-storybook --shard=1/8` | +| `-c`, `--config-dir [dir-name]` | Directory where to load Storybook configurations from
`test-storybook -c .storybook` | +| `--watch` | Run in watch mode
`test-storybook --watch` | +| `--watchAll` | Watch files for changes and rerun all tests when something changes.
`test-storybook --watchAll` | +| `--coverage` | Runs [coverage tests](./test-coverage.md) on your stories and components
`test-storybook --coverage` | +| `--coverageDirectory` | Directory where to write coverage report output
`test-storybook --coverage --coverageDirectory coverage/ui/storybook` | +| `--url` | Define the URL to run tests in. Useful for custom Storybook URLs
`test-storybook --url http://the-storybook-url-here.com` | +| `--browsers` | Define browsers to run tests in. One or multiple of: chromium, firefox, webkit
`test-storybook --browsers firefox chromium` | +| `--maxWorkers [amount]` | Specifies the maximum number of workers the worker-pool will spawn for running tests
`test-storybook --maxWorkers=2` | +| `--no-cache` | Disable the cache
`test-storybook --no-cache` | +| `--clearCache` | Deletes the Jest cache directory and then exits without running tests
`test-storybook --clearCache` | +| `--verbose` | Display individual test results with the test suite hierarchy
`test-storybook --verbose` | +| `-u`, `--updateSnapshot` | Use this flag to re-record every snapshot that fails during this test run
`test-storybook -u` | +| `--eject` | Creates a local configuration file to override defaults of the test-runner
`test-storybook --eject` | +| `--json` | Prints the test results in JSON. This mode will send all other test output and user messages to stderr.
`test-storybook --json` | +| `--outputFile` | Write test results to a file when the --json option is also specified.
`test-storybook --json --outputFile results.json` | +| `--junit` | Indicates that test information should be reported in a junit file.
`test-storybook --**junit**` | +| `--ci` | Instead of the regular behavior of storing a new snapshot automatically, it will fail the test and require Jest to be run with `--updateSnapshot`.
`test-storybook --ci` | +| `--shard [index/count]` | Requires CI. Splits the test suite execution into multiple machines
`test-storybook --shard=1/8` | +| `--failOnConsole` | Makes tests fail on browser console errors
`test-storybook --failOnConsole` | + @@ -190,11 +198,12 @@ The test-runner renders a story and executes its [play function](../writing-stor The test-runner exports test hooks that can be overridden globally to enable use cases like visual or DOM snapshots. These hooks give you access to the test lifecycle _before_ and _after_ the story is rendered. Listed below are the available hooks and an overview of how to use them. -| Hook | Description | -| ------------ | ----------------------------------------------------------------------------- | -| `setup` | Executes once before all the tests run
`setup() {}` | -| `preRender` | Executes before a story is rendered
`async preRender(page, context) {}` | -| `postRender` | Executes after the story is rendered
`async postRender(page, context) {}` | +| Hook | Description | +| ------------ | -------------------------------------------------------------------------------------------------- | +| `prepare` | Prepares the browser for tests
`async prepare({ page, browserContext, testRunnerConfig }() {}` | +| `setup` | Executes once before all the tests run
`setup() {}` | +| `preRender` | Executes before a story is rendered
`async preRender(page, context) {}` | +| `postRender` | Executes after the story is rendered
`async postRender(page, context) {}` |
diff --git a/scripts/bench/browse.ts b/scripts/bench/browse.ts index d101f27e8c47..c063fed1ff8d 100644 --- a/scripts/bench/browse.ts +++ b/scripts/bench/browse.ts @@ -10,7 +10,7 @@ interface Result { mdxVisible?: number; } -export const browse = async (url: string) => { +export const browse = async (url: string, { disableDocs }: { disableDocs?: boolean }) => { const result: Result = {}; /* Heat up time for playwright and the builder @@ -21,13 +21,13 @@ export const browse = async (url: string) => { * We instantiate a new browser for each run to avoid any caching happening in the browser itself */ const x = await benchStory(url); - await benchAutodocs(url); + if (!disableDocs) await benchAutodocs(url); result.storyVisibleUncached = x.storyVisible; - Object.assign(result, await benchMDX(url)); + if (!disableDocs) Object.assign(result, await benchMDX(url)); Object.assign(result, await benchStory(url)); - Object.assign(result, await benchAutodocs(url)); + if (!disableDocs) Object.assign(result, await benchAutodocs(url)); return result; }; diff --git a/scripts/eslint-plugin-local-rules/README.md b/scripts/eslint-plugin-local-rules/README.md new file mode 100644 index 000000000000..86ca125d559d --- /dev/null +++ b/scripts/eslint-plugin-local-rules/README.md @@ -0,0 +1,12 @@ +## ESLint plugin local rules + +This package serves as a local ESLint plugin to be used in the monorepo and help maintainers keep certain code standards. + +### Development + +If you're fixing a rule or creating a new one, make sure to: + +1. Make your code changes +2. Rerun yarn install in the `code` directory. It's necessary to update the module reference +3. Update the necessary `.eslintrc.js` files (if you are adding a new rule) +4. Restart the ESLint server in your IDE diff --git a/scripts/eslint-plugin-local-rules/index.js b/scripts/eslint-plugin-local-rules/index.js new file mode 100644 index 000000000000..3e3e116b7527 --- /dev/null +++ b/scripts/eslint-plugin-local-rules/index.js @@ -0,0 +1,7 @@ +/* eslint-disable global-require */ +module.exports = { + rules: { + 'no-uncategorized-errors': require('./no-uncategorized-errors'), + 'no-duplicated-error-codes': require('./no-duplicated-error-codes'), + }, +}; diff --git a/scripts/eslint-plugin-local-rules/no-duplicated-error-codes.js b/scripts/eslint-plugin-local-rules/no-duplicated-error-codes.js new file mode 100644 index 000000000000..53968edb572c --- /dev/null +++ b/scripts/eslint-plugin-local-rules/no-duplicated-error-codes.js @@ -0,0 +1,49 @@ +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Ensure unique error codes per category in the same file', + category: 'Best Practices', + recommended: true, + }, + fixable: null, + }, + create(context) { + const errorClasses = {}; + + return { + ClassDeclaration(node) { + if (node.superClass.name === 'StorybookError') { + const categoryProperty = node.body.body.find((prop) => { + return prop.type === 'PropertyDefinition' && prop.key.name === 'category'; + }); + + const codeProperty = node.body.body.find((prop) => { + return prop.type === 'PropertyDefinition' && prop.key.name === 'code'; + }); + + if (categoryProperty && categoryProperty.value.type === 'MemberExpression') { + const categoryName = categoryProperty.value.property.name; + + if (codeProperty && codeProperty.value.type === 'Literal') { + const errorCode = codeProperty.value.value; + + if (!errorClasses[categoryName]) { + errorClasses[categoryName] = new Set(); + } + + if (errorClasses[categoryName].has(errorCode)) { + context.report({ + node: codeProperty, + message: `Duplicate error code '${errorCode}' in category '${categoryName}'.`, + }); + } else { + errorClasses[categoryName].add(errorCode); + } + } + } + } + }, + }; + }, +}; diff --git a/scripts/eslint-plugin-local-rules/no-uncategorized-errors.js b/scripts/eslint-plugin-local-rules/no-uncategorized-errors.js new file mode 100644 index 000000000000..ce91cccf1040 --- /dev/null +++ b/scripts/eslint-plugin-local-rules/no-uncategorized-errors.js @@ -0,0 +1,24 @@ +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Disallow usage of the Error JavaScript class.', + category: 'Best Practices', + recommended: true, + url: 'https://github.com/storybookjs/storybook/blob/next/code/lib/core-events/src/errors/README.md', + }, + }, + create(context) { + return { + NewExpression(node) { + if (node.callee.name === 'Error') { + context.report({ + node, + message: + 'Avoid using a generic Error class. Use a categorized StorybookError class instead. See the docs πŸ‘‰', + }); + } + }, + }; + }, +}; diff --git a/scripts/eslint-plugin-local-rules/package.json b/scripts/eslint-plugin-local-rules/package.json new file mode 100644 index 000000000000..d61a7e099db9 --- /dev/null +++ b/scripts/eslint-plugin-local-rules/package.json @@ -0,0 +1,8 @@ +{ + "name": "eslint-plugin-local-rules", + "version": "1.0.0", + "description": "", + "license": "ISC", + "author": "", + "main": "index.js" +} diff --git a/scripts/event-log-checker.ts b/scripts/event-log-checker.ts index bc2056998664..e1f6d82bd533 100644 --- a/scripts/event-log-checker.ts +++ b/scripts/event-log-checker.ts @@ -70,10 +70,11 @@ async function run() { 8, `Expected 8 stories but received ${exampleStoryCount} instead.` ); + const expectedDocsCount = template.modifications?.disableDocs ? 0 : 3; assert.equal( exampleDocsCount, - 3, - `Expected 3 docs entries but received ${exampleDocsCount} instead.` + expectedDocsCount, + `Expected ${expectedDocsCount} docs entries but received ${exampleDocsCount} instead.` ); }); } diff --git a/scripts/event-log-collector.ts b/scripts/event-log-collector.ts index 160120428553..f3ad10700887 100644 --- a/scripts/event-log-collector.ts +++ b/scripts/event-log-collector.ts @@ -12,7 +12,7 @@ server.post('/event-log', (req, res) => { res.end('OK'); }); -server.get('/event-log', (req, res) => { +server.get('/event-log', (_req, res) => { console.log(`Sending ${events.length} events`); res.json(events); }); diff --git a/scripts/get-template.ts b/scripts/get-template.ts index 2968d8a9c6ce..c56418f7a0b4 100644 --- a/scripts/get-template.ts +++ b/scripts/get-template.ts @@ -1,7 +1,9 @@ import { readdir } from 'fs/promises'; -import { pathExists } from 'fs-extra'; +import { pathExists, readFile } from 'fs-extra'; import { program } from 'commander'; import dedent from 'ts-dedent'; +import chalk from 'chalk'; +import yaml from 'yaml'; import { allTemplates, templatesByCadence, @@ -63,30 +65,40 @@ export async function getTemplate( } templates to run for the "${scriptName}" task: ${potentialTemplateKeys.map((v) => `- ${v}`).join('\n')} - ${await getParallelismSummary(cadence)} + ${await checkParallelism(cadence)} `); } return potentialTemplateKeys[index]; } -const tasks = [ - 'sandbox', - 'build', - 'chromatic', - 'e2e-tests', - 'e2e-tests-dev', - 'test-runner', +const tasksMap = { + sandbox: 'create-sandboxes', + build: 'build-sandboxes', + chromatic: 'chromatic-sandboxes', + 'e2e-tests': 'e2e-production', + 'e2e-tests-dev': 'e2e-dev', + 'test-runner': 'test-runner-production', // 'test-runner-dev', TODO: bring this back when the task is enabled again - 'bench', -]; + bench: 'bench', +} as const; + +type TaskKey = keyof typeof tasksMap; + +const tasks = Object.keys(tasksMap) as TaskKey[]; + +const CONFIG_YML_FILE = '../.circleci/config.yml'; + +async function checkParallelism(cadence?: Cadence, scriptName?: TaskKey) { + const configYml = await readFile(CONFIG_YML_FILE, 'utf-8'); + const data = yaml.parse(configYml); -async function getParallelismSummary(cadence?: Cadence, scriptName?: string) { let potentialTemplateKeys: TemplateKey[] = []; const cadences = cadence ? [cadence] : (Object.keys(templatesByCadence) as Cadence[]); const scripts = scriptName ? [scriptName] : tasks; const summary = []; - summary.push('These are the values you should have in .circleci/config.yml:'); + let isIncorrect = false; + cadences.forEach((cad) => { summary.push(`\n${cad}`); const cadenceTemplates = Object.entries(allTemplates).filter(([key]) => @@ -102,31 +114,56 @@ async function getParallelismSummary(cadence?: Cadence, scriptName?: string) { !currentTemplate.skipTasks?.includes(script as SkippableTask) ); }); - if (templateKeysPerScript.length > 0) { - summary.push( - `-- ${script} - parallelism: ${templateKeysPerScript.length}${ - templateKeysPerScript.length === 2 ? ' (default)' : '' - }` - ); + const workflowJobsRaw: (string | { [key: string]: any })[] = data.workflows[cad].jobs; + const workflowJobs = workflowJobsRaw + .filter((item) => typeof item === 'object' && item !== null) + .reduce((result, item) => Object.assign(result, item), {}) as Record; + + if (templateKeysPerScript.length > 0 && workflowJobs[tasksMap[script]]) { + const currentParallelism = workflowJobs[tasksMap[script]].parallelism || 2; + const newParallelism = templateKeysPerScript.length; + + if (newParallelism !== currentParallelism) { + summary.push( + `-- ❌ ${tasksMap[script]} - parallelism: ${currentParallelism} ${chalk.bgRed( + `(should be ${newParallelism})` + )}` + ); + isIncorrect = true; + } else { + summary.push( + `-- βœ… ${tasksMap[script]} - parallelism: ${templateKeysPerScript.length}${ + templateKeysPerScript.length === 2 ? ' (default)' : '' + }` + ); + } } else { summary.push(`-- ${script} - this script is fully skipped for this cadence.`); } }); }); - return summary.concat('\n').join('\n'); + if (isIncorrect) { + summary.unshift( + 'The parellism count is incorrect for some jobs in .circleci/config.yml, you have to update them:' + ); + throw new Error(summary.concat('\n').join('\n')); + } else { + summary.unshift('βœ… The parallelism count is correct for all jobs in .circleci/config.yml:'); + console.log(summary.concat('\n').join('\n')); + } } -type RunOptions = { cadence?: Cadence; task?: string; debug: boolean }; -async function run({ cadence, task, debug }: RunOptions) { - if (debug) { +type RunOptions = { cadence?: Cadence; task?: TaskKey; check: boolean }; +async function run({ cadence, task, check }: RunOptions) { + if (check) { if (task && !tasks.includes(task)) { throw new Error( dedent`The "${task}" task you provided is not valid. Valid tasks (found in .circleci/config.yml) are: ${tasks.map((v) => `- ${v}`).join('\n')}` ); } - console.log(await getParallelismSummary(cadence as Cadence, task)); + await checkParallelism(cadence as Cadence, task); return; } @@ -147,7 +184,11 @@ if (require.main === module) { .description('Retrieve the template to run for a given cadence and task') .option('--cadence ', 'Which cadence you want to run the script for') .option('--task ', 'Which task you want to run the script for') - .option('--debug', 'Whether to list the parallelism counts for tasks by cadence', false); + .option( + '--check', + 'Throws an error when the parallelism counts for tasks are incorrect', + false + ); program.parse(process.argv); diff --git a/scripts/package.json b/scripts/package.json index d551f204baef..eff165cd7f76 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -3,6 +3,7 @@ "version": "7.0.0-alpha.16", "private": true, "scripts": { + "check": "./prepare/check-scripts.ts", "docs:prettier:check": "cd ../docs && prettier --check ./snippets", "docs:prettier:write": "cd ../docs && prettier --write ./snippets", "get-report-message": "ts-node --swc ./get-report-message.ts", @@ -175,6 +176,7 @@ "slash": "^3.0.0", "sort-package-json": "^2.0.0", "tempy": "^1.0.0", + "tiny-invariant": "^1.3.1", "trash": "^7.0.0", "ts-dedent": "^2.0.0", "ts-node": "^10.9.1", @@ -185,6 +187,7 @@ "uuid": "^9.0.0", "wait-on": "^7.0.1", "window-size": "^1.1.1", + "yaml": "^2.3.1", "zod": "^3.21.4" }, "optionalDependencies": { diff --git a/scripts/prepare/check-scripts.ts b/scripts/prepare/check-scripts.ts new file mode 100755 index 000000000000..86418acd93ba --- /dev/null +++ b/scripts/prepare/check-scripts.ts @@ -0,0 +1,75 @@ +#!/usr/bin/env ./node_modules/.bin/ts-node-script + +import { join } from 'path'; +import * as ts from 'typescript'; + +const run = async ({ cwd }: { cwd: string }) => { + const { options, fileNames } = getTSFilesAndConfig('tsconfig.json'); + const { program, host } = getTSProgramAndHost(fileNames, options); + + const tsDiagnostics = getTSDiagnostics(program, cwd, host); + if (tsDiagnostics.length > 0) { + console.log(tsDiagnostics); + process.exit(1); + } else { + console.log('no type errors'); + } + + // TODO, add more package checks here, like: + // - check for missing dependencies/peerDependencies + // - check for unused exports + + console.log('done'); +}; + +run({ cwd: process.cwd() }).catch((err: unknown) => { + // We can't let the stack try to print, it crashes in a way that sets the exit code to 0. + // Seems to have something to do with running JSON.parse() on binary / base64 encoded sourcemaps + // in @cspotcode/source-map-support + if (err instanceof Error) { + console.error(err.message); + } + process.exit(1); +}); + +function getTSDiagnostics(program: ts.Program, cwd: string, host: ts.CompilerHost): any { + return ts.formatDiagnosticsWithColorAndContext( + ts.getPreEmitDiagnostics(program).filter((d) => d.file.fileName.startsWith(cwd)), + host + ); +} + +function getTSProgramAndHost(fileNames: string[], options: ts.CompilerOptions) { + const program = ts.createProgram({ + rootNames: fileNames, + options: { + module: ts.ModuleKind.CommonJS, + ...options, + declaration: false, + noEmit: true, + }, + }); + + const host = ts.createCompilerHost(program.getCompilerOptions()); + return { program, host }; +} + +function getTSFilesAndConfig(tsconfigPath: string) { + const content = ts.readJsonConfigFile(tsconfigPath, ts.sys.readFile); + return ts.parseJsonSourceFileConfigFileContent( + content, + { + useCaseSensitiveFileNames: true, + readDirectory: ts.sys.readDirectory, + fileExists: ts.sys.fileExists, + readFile: ts.sys.readFile, + }, + process.cwd(), + { + noEmit: true, + outDir: join(process.cwd(), 'types'), + target: ts.ScriptTarget.ES2022, + declaration: false, + } + ); +} diff --git a/scripts/prepare/esm-bundle.ts b/scripts/prepare/esm-bundle.ts index 33a7c8314bae..08e039ad6dd2 100755 --- a/scripts/prepare/esm-bundle.ts +++ b/scripts/prepare/esm-bundle.ts @@ -13,7 +13,8 @@ import { exec } from '../utils/exec'; /* TYPES */ type BundlerConfig = { - entries: string[]; + browserEntries: string[]; + nodeEntries: string[]; externals: string[]; pre: string; post: string; diff --git a/scripts/release/pick-patches.ts b/scripts/release/pick-patches.ts index bbd90fb415e5..82f1fe2b4a1a 100644 --- a/scripts/release/pick-patches.ts +++ b/scripts/release/pick-patches.ts @@ -6,6 +6,7 @@ import ora from 'ora'; import { setOutput } from '@actions/core'; import { git } from './utils/git-client'; import { getUnpickedPRs } from './utils/github-client'; +import invariant from 'tiny-invariant'; program.name('pick-patches').description('Cherry pick patch PRs back to main'); @@ -57,6 +58,7 @@ export const run = async (_: unknown) => { await git.raw(['cherry-pick', '-m', '1', '--keep-redundant-commits', '-x', pr.mergeCommit]); prSpinner.succeed(`Picked: ${formatPR(pr)}`); } catch (pickError) { + invariant(pickError instanceof Error); prSpinner.fail(`Failed to automatically pick: ${formatPR(pr)}`); logger.error(pickError.message); const abort = ora(`Aborting cherry pick for merge commit: ${pr.mergeCommit}`).start(); @@ -64,8 +66,9 @@ export const run = async (_: unknown) => { await git.raw(['cherry-pick', '--abort']); abort.stop(); } catch (abortError) { + invariant(abortError instanceof Error); abort.warn(`Failed to abort cherry pick (${pr.mergeCommit})`); - logger.error(pickError.message); + logger.error(abortError.message); } failedCherryPicks.push(pr.mergeCommit); prSpinner.info( diff --git a/scripts/release/utils/get-github-info.ts b/scripts/release/utils/get-github-info.ts index 65508bcc05ae..6bd7126aec04 100644 --- a/scripts/release/utils/get-github-info.ts +++ b/scripts/release/utils/get-github-info.ts @@ -97,7 +97,7 @@ function makeQuery(repos: ReposWithCommitsAndPRsToFetch) { // getReleaseLine will be called a large number of times but it'll be called at the same time // so instead of doing a bunch of network requests, we can do a single one. const GHDataLoader = new DataLoader( - async (requests: RequestData[]) => { + async (requests: readonly RequestData[]) => { if (!process.env.GH_TOKEN) { throw new Error( 'Please create a GitHub personal access token at https://github.com/settings/tokens/new with `read:user` and `repo:status` permissions and add it as the GH_TOKEN environment variable' diff --git a/scripts/release/version.ts b/scripts/release/version.ts index 1636b248b454..3b34d288a8c7 100644 --- a/scripts/release/version.ts +++ b/scripts/release/version.ts @@ -141,12 +141,10 @@ const bumpVersionSources = async (currentVersion: string, nextVersion: string) = const bumpAllPackageJsons = async ({ packages, - currentVersion, nextVersion, verbose, }: { packages: Workspace[]; - currentVersion: string; nextVersion: string; verbose?: boolean; }) => { @@ -279,7 +277,7 @@ export const run = async (options: unknown) => { await bumpCodeVersion(nextVersion); await bumpVersionSources(currentVersion, nextVersion); - await bumpAllPackageJsons({ packages, currentVersion, nextVersion, verbose }); + await bumpAllPackageJsons({ packages, nextVersion, verbose }); console.log(`⬆️ Updating lock file with ${chalk.blue('yarn install --mode=update-lockfile')}`); await execaCommand(`yarn install --mode=update-lockfile`, { diff --git a/scripts/sandbox/utils/git.ts b/scripts/sandbox/utils/git.ts index e4261083c9a5..84fe02f25cdb 100644 --- a/scripts/sandbox/utils/git.ts +++ b/scripts/sandbox/utils/git.ts @@ -1,4 +1,6 @@ import fetch from 'node-fetch'; +import invariant from 'tiny-invariant'; + import { execaCommand } from '../../utils/exec'; // eslint-disable-next-line import/no-cycle import { logger } from '../publish'; @@ -27,9 +29,9 @@ const getTheLastCommitHashThatUpdatedTheSandboxRepo = async (branch: string) => `Could not find the last commit hash in the following commit message: "${latestCommitMessage}".\nDid someone manually push to the sandboxes repo?` ); } - return lastCommitHash; } catch (error) { + invariant(error instanceof Error); if (!error.message.includes('Did someone manually push to the sandboxes repo')) { logger.error( `⚠️ Error getting latest commit message of ${owner}/${repo} on branch ${branch}: ${error.message}` @@ -84,6 +86,7 @@ export async function commitAllToGit({ cwd, branch }: { cwd: string; branch: str ].join('\n'); gitCommitCommand = `git commit -m "${commitTitle}" -m "${commitBody}"`; } catch (err) { + invariant(err instanceof Error); logger.log( `⚠️ Falling back to a simpler commit message because of an error while trying to get the previous commit hash: ${err.message}` ); @@ -95,6 +98,7 @@ export async function commitAllToGit({ cwd, branch }: { cwd: string; branch: str cwd, }); } catch (e) { + invariant(e instanceof Error); if (e.message.includes('nothing to commit')) { logger.log( `🀷 Git found no changes between previous versions so there is nothing to commit. Skipping publish!` diff --git a/scripts/sandbox/utils/template.ts b/scripts/sandbox/utils/template.ts index 4cc3723827f0..10fdef474eb1 100644 --- a/scripts/sandbox/utils/template.ts +++ b/scripts/sandbox/utils/template.ts @@ -33,20 +33,17 @@ export async function getTemplatesData(branch: string) { > >; - const templatesData = Object.keys(sandboxTemplates).reduce( - (acc, curr: keyof typeof sandboxTemplates) => { - const [dirName, templateName] = curr.split('/'); - const groupName = - dirName === 'cra' ? 'CRA' : dirName.slice(0, 1).toUpperCase() + dirName.slice(1); - const generatorData = sandboxTemplates[curr]; - acc[groupName] = acc[groupName] || {}; - acc[groupName][templateName] = { - ...generatorData, - stackblitzUrl: getStackblitzUrl(curr, branch), - }; - return acc; - }, - {} - ); + const templatesData = Object.keys(sandboxTemplates).reduce((acc, curr) => { + const [dirName, templateName] = curr.split('/'); + const groupName = + dirName === 'cra' ? 'CRA' : dirName.slice(0, 1).toUpperCase() + dirName.slice(1); + const generatorData = sandboxTemplates[curr as keyof typeof sandboxTemplates]; + acc[groupName] = acc[groupName] || {}; + acc[groupName][templateName] = { + ...generatorData, + stackblitzUrl: getStackblitzUrl(curr, branch), + }; + return acc; + }, {}); return templatesData; } diff --git a/scripts/task.ts b/scripts/task.ts index 66314121768a..9e18d1f618c2 100644 --- a/scripts/task.ts +++ b/scripts/task.ts @@ -35,6 +35,7 @@ import { } from '../code/lib/cli/src/sandbox-templates'; import { version } from '../code/package.json'; +import invariant from 'tiny-invariant'; const sandboxDir = process.env.SANDBOX_ROOT || SANDBOX_DIRECTORY; @@ -73,7 +74,7 @@ export type Task = { /** * Is this task already "ready", and potentially not required? */ - ready: (details: TemplateDetails, options: PassedOptionValues) => MaybePromise; + ready: (details: TemplateDetails, options?: PassedOptionValues) => MaybePromise; /** * Run the task */ @@ -176,6 +177,11 @@ export const options = createOptions({ description: 'Do not include template stories and their addons', promptType: false, }, + disableDocs: { + type: 'boolean', + description: 'Disable addon-docs from essentials', + promptType: false, + }, }); type PassedOptionValues = Omit, 'task' | 'startFrom' | 'junit'>; @@ -304,7 +310,10 @@ async function runTask(task: Task, details: TemplateDetails, optionValues: Passe try { let updatedOptions = optionValues; if (details.template?.modifications?.skipTemplateStories) { - updatedOptions = { ...optionValues, skipTemplateStories: true }; + updatedOptions = { ...updatedOptions, skipTemplateStories: true }; + } + if (details.template?.modifications?.disableDocs) { + updatedOptions = { ...updatedOptions, disableDocs: true }; } const controller = await task.run(details, updatedOptions); @@ -312,6 +321,7 @@ async function runTask(task: Task, details: TemplateDetails, optionValues: Passe return controller; } catch (err) { + invariant(err instanceof Error); const hasJunitFile = await pathExists(junitFilename); // If there's a non-test related error (junit report has not been reported already), we report the general failure in a junit report if (junitFilename && !hasJunitFile) { @@ -458,6 +468,7 @@ async function run() { }); if (controller) controllers.push(controller); } catch (err) { + invariant(err instanceof Error); logger.error(`Error running task ${getTaskKey(task)}:`); // If it is the last task, we don't need to log the full trace if (task === finalTask) { diff --git a/scripts/tasks/bench.ts b/scripts/tasks/bench.ts index e1b1fec753ea..25325e1090e0 100644 --- a/scripts/tasks/bench.ts +++ b/scripts/tasks/bench.ts @@ -16,6 +16,7 @@ export const bench: Task = { async run(details, options) { const controllers: AbortController[] = []; try { + const { disableDocs } = options; const { browse } = await import('../bench/browse'); const { saveBench, loadBench } = await import('../bench/utils'); const { default: prettyBytes } = await dynamicImport('pretty-bytes'); @@ -26,7 +27,7 @@ export const bench: Task = { throw new Error('dev: controller is null'); } controllers.push(devController); - const devBrowseResult = await browse(`http://localhost:${devPort}`); + const devBrowseResult = await browse(`http://localhost:${devPort}`, { disableDocs }); devController.abort(); const serveController = await serve.run(details, { ...options, debug: false }); @@ -34,7 +35,7 @@ export const bench: Task = { throw new Error('serve: controller is null'); } controllers.push(serveController); - const buildBrowseResult = await browse(`http://localhost:${servePort}`); + const buildBrowseResult = await browse(`http://localhost:${servePort}`, { disableDocs }); serveController.abort(); await saveBench( diff --git a/scripts/tasks/sandbox-parts.ts b/scripts/tasks/sandbox-parts.ts index d08360a22ebe..f0aaa39d825d 100644 --- a/scripts/tasks/sandbox-parts.ts +++ b/scripts/tasks/sandbox-parts.ts @@ -283,13 +283,13 @@ function updateStoriesField(mainConfig: ConfigFile, isJs: boolean) { } // Add a stories field entry for the passed symlink -function addStoriesEntry(mainConfig: ConfigFile, path: string) { +function addStoriesEntry(mainConfig: ConfigFile, path: string, disableDocs: boolean) { const stories = mainConfig.getFieldValue(['stories']) as string[]; const entry = { directory: slash(join('../template-stories', path)), titlePrefix: slash(path), - files: '**/*.@(mdx|stories.@(js|jsx|ts|tsx))', + files: disableDocs ? '**/*.stories.@(js|jsx|ts|tsx)' : '**/*.@(mdx|stories.@(js|jsx|ts|tsx))', }; mainConfig.setFieldValue(['stories'], [...stories, entry]); @@ -302,7 +302,12 @@ function getStoriesFolderWithVariant(variant?: string, folder = 'stories') { // packageDir is eg 'renderers/react', 'addons/actions' async function linkPackageStories( packageDir: string, - { mainConfig, cwd, linkInDir }: { mainConfig: ConfigFile; cwd: string; linkInDir?: string }, + { + mainConfig, + cwd, + linkInDir, + disableDocs, + }: { mainConfig: ConfigFile; cwd: string; linkInDir?: string; disableDocs: boolean }, variant?: string ) { const storiesFolderName = variant ? getStoriesFolderWithVariant(variant) : 'stories'; @@ -320,7 +325,7 @@ async function linkPackageStories( await ensureSymlink(source, target); if (!linkInDir) { - addStoriesEntry(mainConfig, packageDir); + addStoriesEntry(mainConfig, packageDir, disableDocs); } // Add `previewAnnotation` entries of the form @@ -373,7 +378,7 @@ async function addExtraDependencies({ export const addStories: Task['run'] = async ( { sandboxDir, template, key }, - { addon: extraAddons, dryRun, debug } + { addon: extraAddons, dryRun, debug, disableDocs } ) => { logger.log('πŸ’ƒ adding stories'); const cwd = sandboxDir; @@ -409,6 +414,7 @@ export const addStories: Task['run'] = async ( mainConfig, cwd, linkInDir: resolve(cwd, storiesPath), + disableDocs, }); if ( @@ -422,6 +428,7 @@ export const addStories: Task['run'] = async ( mainConfig, cwd, linkInDir: resolve(cwd, storiesPath), + disableDocs, }, sandboxSpecificStoriesFolder ); @@ -439,6 +446,7 @@ export const addStories: Task['run'] = async ( mainConfig, cwd, linkInDir: resolve(cwd, storiesPath), + disableDocs, }); } @@ -453,6 +461,7 @@ export const addStories: Task['run'] = async ( mainConfig, cwd, linkInDir: resolve(cwd, storiesPath), + disableDocs, }, sandboxSpecificStoriesFolder ); @@ -465,6 +474,7 @@ export const addStories: Task['run'] = async ( await linkPackageStories(await workspacePath('core package', '@storybook/preview-api'), { mainConfig, cwd, + disableDocs, }); } @@ -475,7 +485,10 @@ export const addStories: Task['run'] = async ( if (!match) return acc; const suffix = match[1]; if (suffix === 'essentials') { - return [...acc, ...essentialsAddons]; + const essentials = disableDocs + ? essentialsAddons.filter((a) => a !== 'docs') + : essentialsAddons; + return [...acc, ...essentials]; } return [...acc, suffix]; }, @@ -494,7 +507,7 @@ export const addStories: Task['run'] = async ( if (isCoreRenderer) { const existingStories = await filterExistsInCodeDir(addonDirs, join('template', 'stories')); for (const packageDir of existingStories) { - await linkPackageStories(packageDir, { mainConfig, cwd }); + await linkPackageStories(packageDir, { mainConfig, cwd, disableDocs }); } // Add some extra settings (see above for what these do) @@ -509,7 +522,7 @@ export const addStories: Task['run'] = async ( await writeConfig(mainConfig); }; -export const extendMain: Task['run'] = async ({ template, sandboxDir }) => { +export const extendMain: Task['run'] = async ({ template, sandboxDir }, { disableDocs }) => { logger.log('πŸ“ Extending main.js'); const mainConfig = await readMainConfig({ cwd: sandboxDir }); const templateConfig = template.modifications?.mainConfig || {}; @@ -527,6 +540,23 @@ export const extendMain: Task['run'] = async ({ template, sandboxDir }) => { Object.entries(configToAdd).forEach(([field, value]) => mainConfig.setFieldValue([field], value)); + // Simulate Storybook Lite + if (disableDocs) { + const addons = mainConfig.getFieldValue(['addons']); + const addonsNoDocs = addons.map((addon: any) => + addon !== '@storybook/addon-essentials' ? addon : { name: addon, options: { docs: false } } + ); + mainConfig.setFieldValue(['addons'], addonsNoDocs); + + // remove the docs options so that docs tags are ignored + mainConfig.setFieldValue(['docs'], {}); + mainConfig.setFieldValue(['typescript'], { reactDocgen: false }); + + let updatedStories = mainConfig.getFieldValue(['stories']) as string[]; + updatedStories = updatedStories.filter((specifier) => !specifier.endsWith('.mdx')); + mainConfig.setFieldValue(['stories'], updatedStories); + } + if (template.expected.builder === '@storybook/builder-vite') setSandboxViteFinal(mainConfig); await writeConfig(mainConfig); }; diff --git a/scripts/tasks/test-runner-build.ts b/scripts/tasks/test-runner-build.ts index 546bb6c5aa38..b57d4c803233 100644 --- a/scripts/tasks/test-runner-build.ts +++ b/scripts/tasks/test-runner-build.ts @@ -12,7 +12,12 @@ export const testRunnerBuild: Task & { port: number } = { }, async run({ sandboxDir, junitFilename, template }, { dryRun, debug }) { const execOptions = { cwd: sandboxDir }; - const flags = [`--url http://localhost:${this.port}`, '--junit', '--maxWorkers=2']; + const flags = [ + `--url http://localhost:${this.port}`, + '--junit', + '--maxWorkers=2', + '--failOnConsole', + ]; // index-json mode is only supported in ssv7 if (template.modifications?.mainConfig?.features?.storyStoreV7 !== false) { diff --git a/scripts/ts-to-ts49.ts b/scripts/ts-to-ts49.ts index a73eaabbd2be..869d71bfb9a2 100644 --- a/scripts/ts-to-ts49.ts +++ b/scripts/ts-to-ts49.ts @@ -7,6 +7,7 @@ import * as recast from 'recast'; import type Babel from '@babel/core'; import type { File } from '@babel/types'; import * as t from '@babel/types'; +import invariant from 'tiny-invariant'; const files = glob.sync('**/*.ts.mdx', { absolute: true, @@ -74,6 +75,7 @@ for (const [, file] of files.entries()) { console.log('changed', file); } } catch (e) { + invariant(e instanceof Error); console.error(e.message); } } diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json index 591055d2ab92..c82e14a95108 100644 --- a/scripts/tsconfig.json +++ b/scripts/tsconfig.json @@ -8,14 +8,14 @@ "moduleResolution": "Node", "target": "ES2020", "module": "CommonJS", - "skipLibCheck": false, + "skipLibCheck": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "isolatedModules": true, "strictBindCallApply": true, "lib": ["dom", "esnext"], "types": ["node", "jest"], - "strict": false, + "strict": true, "strictNullChecks": false, "forceConsistentCasingInFileNames": true, "noUnusedLocals": true, diff --git a/scripts/utils/exec.ts b/scripts/utils/exec.ts index 6280f65ff9cf..74a886189420 100644 --- a/scripts/utils/exec.ts +++ b/scripts/utils/exec.ts @@ -67,7 +67,7 @@ export const exec = async ( } } } catch (err) { - if (!err.killed) { + if (!(typeof err === 'object' && 'killed' in err && err.killed)) { logger.error(chalk.red(`An error occurred while executing: \`${command}\``)); logger.log(`${errorMessage}\n`); } diff --git a/scripts/utils/options.test.ts b/scripts/utils/options.test.ts index ccbc6a058ec3..c183db207f2c 100644 --- a/scripts/utils/options.test.ts +++ b/scripts/utils/options.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from '@jest/globals'; import { createCommand } from 'commander'; -import type { MaybeOptionValues, OptionValues } from './options'; import { areOptionsSatisfied, createOptions, getCommand, getOptions } from './options'; const allOptions = createOptions({ @@ -35,17 +34,6 @@ const allOptions = createOptions({ }, }); -// TS "tests" -// deepscan-disable-next-line -function test(mv: MaybeOptionValues, v: OptionValues) { - console.log(mv.first, mv.second, mv.third, mv.fourth, mv.fifth, mv.sixth); - // @ts-expect-error as it's not allowed - console.log(mv.seventh); - console.log(v.first, v.second, v.third, v.fourth, v.fifth, v.sixth); - // @ts-expect-error as it's not allowed - console.log(v.seventh); -} - describe('getOptions', () => { it('deals with boolean options', () => { expect(getOptions(createCommand(), allOptions, ['command', 'name', '--first'])).toMatchObject({ @@ -71,7 +59,6 @@ describe('getOptions', () => { }); it('deals with string options', () => { - const r = getOptions(createCommand(), allOptions, ['command', 'name', '--third', 'one']); expect( getOptions(createCommand(), allOptions, ['command', 'name', '--third', 'one']) ).toMatchObject({ diff --git a/scripts/yarn.lock b/scripts/yarn.lock index 21dc69364812..be6fe9587929 100644 --- a/scripts/yarn.lock +++ b/scripts/yarn.lock @@ -3033,6 +3033,7 @@ __metadata: slash: ^3.0.0 sort-package-json: ^2.0.0 tempy: ^1.0.0 + tiny-invariant: ^1.3.1 trash: ^7.0.0 ts-dedent: ^2.0.0 ts-loader: ^9.4.2 @@ -3046,6 +3047,7 @@ __metadata: verdaccio-auth-memory: ^10.2.0 wait-on: ^7.0.1 window-size: ^1.1.1 + yaml: ^2.3.1 zod: ^3.21.4 dependenciesMeta: "@verdaccio/types": @@ -15617,6 +15619,13 @@ __metadata: languageName: node linkType: hard +"tiny-invariant@npm:^1.3.1": + version: 1.3.1 + resolution: "tiny-invariant@npm:1.3.1" + checksum: 5b87c1d52847d9452b60d0dcb77011b459044e0361ca8253bfe7b43d6288106e12af926adb709a6fc28900e3864349b91dad9a4ac93c39aa15f360b26c2ff4db + languageName: node + linkType: hard + "tmp@npm:~0.2.1": version: 0.2.1 resolution: "tmp@npm:0.2.1" @@ -17359,7 +17368,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.0.0": +"yaml@npm:^2.0.0, yaml@npm:^2.3.1": version: 2.3.1 resolution: "yaml@npm:2.3.1" checksum: ed4c21a907fb1cd60a25177612fa46d95064a144623d269199817908475fe85bef20fb17406e3bdc175351b6488056a6f84beb7836e8c262646546a0220188e3