diff --git a/.circleci/config.yml b/.circleci/config.yml index b212c825ddc2..0d3340694846 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -706,22 +706,22 @@ workflows: 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: - parallelism: 31 + parallelism: 32 requires: - build-sandboxes - e2e-production: - parallelism: 29 + parallelism: 30 requires: - build-sandboxes - e2e-dev: @@ -729,7 +729,7 @@ workflows: requires: - create-sandboxes - test-runner-production: - parallelism: 29 + parallelism: 30 requires: - build-sandboxes diff --git a/.github/workflows/generate-sandboxes-main.yml b/.github/workflows/generate-sandboxes-main.yml deleted file mode 100644 index cc17376c57de..000000000000 --- a/.github/workflows/generate-sandboxes-main.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Generate and push sandboxes (main) - -on: - schedule: - - cron: '2 2 */1 * *' - workflow_dispatch: - # To test fixes on push rather than wait for the scheduling, do the following: - # 1. Uncomment the lines below and add your branch. - # push: - # branches: - # - - # 2. change the "ref" value to in the actions/checkout step below. - # 3. πŸ‘‰ DON'T FORGET TO UNDO THE VALUES BACK TO `main` BEFORE YOU MERGE YOUR CHANGES! - -jobs: - generate: - runs-on: ubuntu-latest - env: - YARN_ENABLE_IMMUTABLE_INSTALLS: false - CLEANUP_SANDBOX_NODE_MODULES: true - steps: - - uses: actions/checkout@v3 - with: - ref: main - - uses: actions/setup-node@v3 - with: - node-version-file: '.nvmrc' - - name: Setup git user - run: | - git config --global user.name "Storybook Bot" - git config --global user.email "bot@storybook.js.org" - - name: Install dependencies - run: | - cd ./scripts - node --experimental-modules ./check-dependencies.js - cd .. - - name: Compile Storybook libraries - run: yarn task --task compile --start-from=auto --no-link - - name: Publishing to local registry - run: yarn local-registry --publish - working-directory: ./code - - name: Running local registry - run: yarn local-registry --open & - working-directory: ./code - - name: Wait for registry - run: yarn wait-on tcp:127.0.0.1:6001 - working-directory: ./code - - name: Generate - run: yarn generate-sandboxes --local-registry - working-directory: ./code - - name: Publish - run: yarn publish-sandboxes --remote=https://storybook-bot:${{ secrets.PAT_STORYBOOK_BOT}}@github.com/storybookjs/sandboxes.git --push --branch=main - working-directory: ./code - - name: The job has failed - if: ${{ failure() || cancelled() }} - env: - DISCORD_WEBHOOK: ${{ secrets.DISCORD_MONITORING_URL }} - uses: Ilshidur/action-discord@master - with: - args: 'The generation of sandboxes in the **main** branch has failed. [View Run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})' diff --git a/.github/workflows/generate-sandboxes-next.yml b/.github/workflows/generate-sandboxes-next.yml deleted file mode 100644 index 2c0b592d024f..000000000000 --- a/.github/workflows/generate-sandboxes-next.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Generate and push sandboxes (next) - -on: - schedule: - - cron: '2 2 */1 * *' - workflow_dispatch: - # To test fixes on push rather than wait for the scheduling, do the following: - # 1. Uncomment the lines below and add your branch. - # push: - # branches: - # - - # 2. change the "ref" value to in the actions/checkout step below. - # 3. πŸ‘‰ DON'T FORGET TO UNDO THE VALUES BACK TO `next` BEFORE YOU MERGE YOUR CHANGES! - -jobs: - generate: - runs-on: ubuntu-latest - env: - YARN_ENABLE_IMMUTABLE_INSTALLS: false - CLEANUP_SANDBOX_NODE_MODULES: true - steps: - - uses: actions/checkout@v3 - with: - ref: next - - uses: actions/setup-node@v3 - with: - node-version-file: '.nvmrc' - - name: Setup git user - run: | - git config --global user.name "Storybook Bot" - git config --global user.email "bot@storybook.js.org" - - name: Install dependencies - run: | - cd ./scripts - node --experimental-modules ./check-dependencies.js - cd .. - - name: Compile Storybook libraries - run: yarn task --task compile --start-from=auto --no-link - - name: Publishing to local registry - run: yarn local-registry --publish - working-directory: ./code - - name: Running local registry - run: yarn local-registry --open & - working-directory: ./code - - name: Wait for registry - run: yarn wait-on tcp:127.0.0.1:6001 - working-directory: ./code - - name: Generate - run: yarn generate-sandboxes --local-registry --debug - working-directory: ./code - - name: Publish - run: yarn publish-sandboxes --remote=https://storybook-bot:${{ secrets.PAT_STORYBOOK_BOT}}@github.com/storybookjs/sandboxes.git --push --branch=next - working-directory: ./code - - name: The job has failed - if: ${{ failure() || cancelled() }} - env: - DISCORD_WEBHOOK: ${{ secrets.DISCORD_MONITORING_URL }} - uses: Ilshidur/action-discord@master - with: - args: 'The generation of sandboxes in the **next** branch has failed. [View Run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})' diff --git a/.github/workflows/generate-sandboxes.yml b/.github/workflows/generate-sandboxes.yml new file mode 100644 index 000000000000..88dc9e21bb41 --- /dev/null +++ b/.github/workflows/generate-sandboxes.yml @@ -0,0 +1,127 @@ +name: Generate and publish sandboxes + +on: + schedule: + - cron: "2 2 */1 * *" + workflow_dispatch: + # To test fixes on push rather than wait for the scheduling, do the following: + # 1. Uncomment the lines below and add your branch. + # push: + # branches: + # - + # 2. Change the "ref" value to in the actions/checkout step below. + # 3. Comment out the whole "generate-main" job starting at line 77 + # 4. πŸ‘‰ DON'T FORGET TO UNDO THE STEPS BEFORE YOU MERGE YOUR CHANGES! + +env: + YARN_ENABLE_IMMUTABLE_INSTALLS: "false" + CLEANUP_SANDBOX_NODE_MODULES: "true" + +defaults: + run: + working-directory: ./code + +jobs: + generate-next: + name: Generate to next + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: next + + - uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + + - name: Setup git user + run: | + git config --global user.name "storybook-bot" + git config --global user.email "32066757+storybook-bot@users.noreply.github.com" + + - name: Install dependencies + working-directory: ./scripts + run: node --experimental-modules ./check-dependencies.js + + - name: Compile Storybook libraries + run: yarn task --task compile --start-from=auto --no-link + + - name: Publish to local registry + run: yarn local-registry --publish + + - name: Run local registry + run: yarn local-registry --open & + + - name: Wait for registry + run: yarn wait-on tcp:127.0.0.1:6001 + + - name: Generate + id: generate + run: yarn generate-sandboxes --local-registry + + - name: Publish + # publish sandboxes even if the generation fails, as some sandboxes might have been generated successfully + if: ${{ !cancelled() }} + run: yarn publish-sandboxes --remote=https://storybook-bot:${{ secrets.PAT_STORYBOOK_BOT }}@github.com/storybookjs/sandboxes.git --push --branch=next + + - name: Report failure to Discord + if: failure() + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_MONITORING_URL }} + uses: Ilshidur/action-discord@master + with: + args: | + The generation of some or all sandboxes on the **next** branch has failed. + [See the job summary for details](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + generate-main: + name: Generate to main + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: main + + - uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + + - name: Setup git user + run: | + git config --global user.name "storybook-bot" + git config --global user.email "32066757+storybook-bot@users.noreply.github.com" + + - name: Install dependencies + working-directory: ./scripts + run: node --experimental-modules ./check-dependencies.js + + - name: Compile Storybook libraries + run: yarn task --task compile --start-from=auto --no-link + + - name: Publish to local registry + run: yarn local-registry --publish + + - name: Run local registry + run: yarn local-registry --open & + + - name: Wait for registry + run: yarn wait-on tcp:127.0.0.1:6001 + + - name: Generate + id: generate + run: yarn generate-sandboxes --local-registry + + - name: Publish + # publish sandboxes even if the generation fails, as some sandboxes might have been generated successfully + if: ${{ !cancelled() }} + run: yarn publish-sandboxes --remote=https://storybook-bot:${{ secrets.PAT_STORYBOOK_BOT }}@github.com/storybookjs/sandboxes.git --push --branch=main + + - name: Report failure to Discord + if: failure() + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_MONITORING_URL }} + uses: Ilshidur/action-discord@master + with: + args: | + The generation of some or all sandboxes on the **main** branch has failed. + [See the job summary for details](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index bd3066ba9728..6e9e1eb4ba44 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,9 @@ +## 8.0.0-rc.1 + +- CLI: Fix addon compatibility check error reporting in storybook dev - [#26258](https://github.com/storybookjs/storybook/pull/26258), thanks [@yannbf](https://github.com/yannbf)! +- Onboarding: Fix manager dist reference - [#26282](https://github.com/storybookjs/storybook/pull/26282), thanks [@shilman](https://github.com/shilman)! +- ReactVite: Docgen ignore un-parsable files - [#26254](https://github.com/storybookjs/storybook/pull/26254), thanks [@ndelangen](https://github.com/ndelangen)! + ## 8.0.0-rc.0 Bumping 8.0.0-beta.6 to 8.0.0-rc.0. Please refer to the changelogs of previous beta releases. diff --git a/MIGRATION.md b/MIGRATION.md index 202962c2cf7f..5919ec501c42 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,7 +1,10 @@

Migration

- [From version 7.x to 8.0.0](#from-version-7x-to-800) - - [Type change in `composeStories` API](#type-change-in-composestories-api) + - [Portable stories](#portable-stories) + - [Project annotations are now merged instead of overwritten in composeStory](#project-annotations-are-now-merged-instead-of-overwritten-in-composestory) + - [Type change in `composeStories` API](#type-change-in-composestories-api) + - [DOM structure changed in portable stories](#dom-structure-changed-in-portable-stories) - [Tab addons are now routed to a query parameter](#tab-addons-are-now-routed-to-a-query-parameter) - [Default keyboard shortcuts changed](#default-keyboard-shortcuts-changed) - [Manager addons are now rendered with React 18](#manager-addons-are-now-rendered-with-react-18) @@ -86,17 +89,17 @@ - [Tab addons cannot manually route, Tool addons can filter their visibility via tabId](#tab-addons-cannot-manually-route-tool-addons-can-filter-their-visibility-via-tabid) - [Removed `config` preset](#removed-config-preset-1) - [From version 7.5.0 to 7.6.0](#from-version-750-to-760) - - [CommonJS with Vite is deprecated](#commonjs-with-vite-is-deprecated) - - [Using implicit actions during rendering is deprecated](#using-implicit-actions-during-rendering-is-deprecated) - - [typescript.skipBabel deprecated](#typescriptskipbabel-deprecated) - - [Primary doc block accepts of prop](#primary-doc-block-accepts-of-prop) - - [Addons no longer need a peer dependency on React](#addons-no-longer-need-a-peer-dependency-on-react) + - [CommonJS with Vite is deprecated](#commonjs-with-vite-is-deprecated) + - [Using implicit actions during rendering is deprecated](#using-implicit-actions-during-rendering-is-deprecated) + - [typescript.skipBabel deprecated](#typescriptskipbabel-deprecated) + - [Primary doc block accepts of prop](#primary-doc-block-accepts-of-prop) + - [Addons no longer need a peer dependency on React](#addons-no-longer-need-a-peer-dependency-on-react) - [From version 7.4.0 to 7.5.0](#from-version-740-to-750) - - [`storyStoreV6` and `storiesOf` is deprecated](#storystorev6-and-storiesof-is-deprecated) - - [`storyIndexers` is replaced with `experimental_indexers`](#storyindexers-is-replaced-with-experimental_indexers) + - [`storyStoreV6` and `storiesOf` is deprecated](#storystorev6-and-storiesof-is-deprecated) + - [`storyIndexers` is replaced with `experimental_indexers`](#storyindexers-is-replaced-with-experimental_indexers) - [From version 7.0.0 to 7.2.0](#from-version-700-to-720) - - [Addon API is more type-strict](#addon-api-is-more-type-strict) - - [Addon-controls hideNoControlsWarning parameter is deprecated](#addon-controls-hidenocontrolswarning-parameter-is-deprecated) + - [Addon API is more type-strict](#addon-api-is-more-type-strict) + - [Addon-controls hideNoControlsWarning parameter is deprecated](#addon-controls-hidenocontrolswarning-parameter-is-deprecated) - [From version 6.5.x to 7.0.0](#from-version-65x-to-700) - [7.0 breaking changes](#70-breaking-changes) - [Dropped support for Node 15 and below](#dropped-support-for-node-15-and-below) @@ -122,7 +125,7 @@ - [Deploying build artifacts](#deploying-build-artifacts) - [Dropped support for file URLs](#dropped-support-for-file-urls) - [Serving with nginx](#serving-with-nginx) - - [Ignore story files from node\_modules](#ignore-story-files-from-node_modules) + - [Ignore story files from node_modules](#ignore-story-files-from-node_modules) - [7.0 Core changes](#70-core-changes) - [7.0 feature flags removed](#70-feature-flags-removed) - [Story context is prepared before for supporting fine grained updates](#story-context-is-prepared-before-for-supporting-fine-grained-updates) @@ -136,7 +139,7 @@ - [Addon-interactions: Interactions debugger is now default](#addon-interactions-interactions-debugger-is-now-default) - [7.0 Vite changes](#70-vite-changes) - [Vite builder uses Vite config automatically](#vite-builder-uses-vite-config-automatically) - - [Vite cache moved to node\_modules/.cache/.vite-storybook](#vite-cache-moved-to-node_modulescachevite-storybook) + - [Vite cache moved to node_modules/.cache/.vite-storybook](#vite-cache-moved-to-node_modulescachevite-storybook) - [7.0 Webpack changes](#70-webpack-changes) - [Webpack4 support discontinued](#webpack4-support-discontinued) - [Babel mode v7 exclusively](#babel-mode-v7-exclusively) @@ -186,7 +189,7 @@ - [Dropped addon-docs manual babel configuration](#dropped-addon-docs-manual-babel-configuration) - [Dropped addon-docs manual configuration](#dropped-addon-docs-manual-configuration) - [Autoplay in docs](#autoplay-in-docs) - - [Removed STORYBOOK\_REACT\_CLASSES global](#removed-storybook_react_classes-global) + - [Removed STORYBOOK_REACT_CLASSES global](#removed-storybook_react_classes-global) - [7.0 Deprecations and default changes](#70-deprecations-and-default-changes) - [storyStoreV7 enabled by default](#storystorev7-enabled-by-default) - [`Story` type deprecated](#story-type-deprecated) @@ -401,7 +404,24 @@ ## From version 7.x to 8.0.0 -### Type change in `composeStories` API +### Portable stories + +#### Project annotations are now merged instead of overwritten in composeStory + +When passing project annotations overrides via `composeStory` such as: + +```tsx +const projectAnnotationOverrides = { parameters: { foo: "bar" } }; +const Primary = composeStory( + stories.Primary, + stories, + projectAnnotationOverrides +); +``` + +they are now merged with the annotations passed via `setProjectAnnotations` rather than completely overwriting them. This was seen as a bug and it's now fixed. If you have a use case where you really need this, please open an issue to elaborate. + +#### Type change in `composeStories` API There is a TypeScript type change in the `play` function returned from `composeStories` or `composeStory` in `@storybook/react` or `@storybook/vue3`, where before it was always defined, now it is potentially undefined. This means that you might have to make a small change in your code, such as: @@ -418,6 +438,35 @@ await Primary.play!(...) // if you want a runtime error when the play function d There are plans to make the type of the play function be inferred based on your imported story's play function in a near future, so the types will be 100% accurate. +#### DOM structure changed in portable stories + +The portable stories API now adds a wrapper to your stories with a unique id based on your story id, such as: + +```html +
+ +
+``` + +This means that if you take DOM snapshots of your stories, they will be affected and you will have to update them. + +The id calculation is based on different heuristics based on your Meta title and Story name. When using `composeStories`, the id can be inferred automatically. However, when using `composeStory` and your story does not explicitly have a `storyName` property, the story name can't be inferred automatically. As a result, its name will be "Unnamed Story", resulting in a wrapper id like `"#storybook-story-button--unnamed-story"`. If the id matters to you and you want to fix it, you have to specify the `exportsName` property like so: + +```ts +test("snapshots the story with custom id", () => { + const Primary = composeStory( + stories.Primary, + stories.default, + undefined, + // If you do not want the `unnamed-story` id, you have to pass the name of the story as a parameter + "Primary" + ); + + const { baseElement } = render(); + expect(baseElement).toMatchSnapshot(); +}); +``` + ### Tab addons are now routed to a query parameter The URL of a tab used to be: `http://localhost:6006/?path=/my-addon-tab/my-story`. @@ -556,7 +605,6 @@ This means https://github.com/IanVS/vite-plugin-turbosnap is no longer necessary Now that both Vite and Webpack support the `preview-stats.json` file, the flag has been renamed. The old flag will continue to work. - ### Implicit actions can not be used during rendering (for example in the play function) In Storybook 7, we inferred if the component accepts any action props, diff --git a/code/addons/a11y/package.json b/code/addons/a11y/package.json index e7057ee37c7d..ebae68256c87 100644 --- a/code/addons/a11y/package.json +++ b/code/addons/a11y/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-a11y", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Test component compliance with web accessibility standards", "keywords": [ "a11y", diff --git a/code/addons/actions/package.json b/code/addons/actions/package.json index c1a55b717379..bc89e2f5e5c7 100644 --- a/code/addons/actions/package.json +++ b/code/addons/actions/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-actions", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Get UI feedback when an action is performed on an interactive element", "keywords": [ "storybook", diff --git a/code/addons/backgrounds/package.json b/code/addons/backgrounds/package.json index 1d9cba4d0338..4fbdfc5c027c 100644 --- a/code/addons/backgrounds/package.json +++ b/code/addons/backgrounds/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-backgrounds", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Switch backgrounds to view components in different settings", "keywords": [ "addon", diff --git a/code/addons/controls/package.json b/code/addons/controls/package.json index edf0f1d8aca9..588947d02c70 100644 --- a/code/addons/controls/package.json +++ b/code/addons/controls/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-controls", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Interact with component inputs dynamically in the Storybook UI", "keywords": [ "addon", diff --git a/code/addons/docs/package.json b/code/addons/docs/package.json index afe403d9e813..a159c03063b6 100644 --- a/code/addons/docs/package.json +++ b/code/addons/docs/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-docs", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Document component usage and properties in Markdown", "keywords": [ "addon", diff --git a/code/addons/essentials/package.json b/code/addons/essentials/package.json index c792492511e3..61dbf507bbb1 100644 --- a/code/addons/essentials/package.json +++ b/code/addons/essentials/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-essentials", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Curated addons to bring out the best of Storybook", "keywords": [ "addon", diff --git a/code/addons/gfm/package.json b/code/addons/gfm/package.json index f618ecf6effd..5491ba3a58e4 100644 --- a/code/addons/gfm/package.json +++ b/code/addons/gfm/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-mdx-gfm", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "GitHub Flavored Markdown in Storybook", "keywords": [ "addon", diff --git a/code/addons/highlight/package.json b/code/addons/highlight/package.json index 5347386529e5..28f331bf4dc8 100644 --- a/code/addons/highlight/package.json +++ b/code/addons/highlight/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-highlight", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Highlight DOM nodes within your stories", "keywords": [ "storybook-addons", diff --git a/code/addons/interactions/package.json b/code/addons/interactions/package.json index b05d3c512d42..daece4716036 100644 --- a/code/addons/interactions/package.json +++ b/code/addons/interactions/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-interactions", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Automate, test and debug user interactions", "keywords": [ "storybook-addons", diff --git a/code/addons/jest/package.json b/code/addons/jest/package.json index 28c2c343acba..3b194968932c 100644 --- a/code/addons/jest/package.json +++ b/code/addons/jest/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-jest", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "React storybook addon that show component jest report", "keywords": [ "addon", diff --git a/code/addons/links/package.json b/code/addons/links/package.json index 9cfea47205a5..d1777a65aa4d 100644 --- a/code/addons/links/package.json +++ b/code/addons/links/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-links", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Link stories together to build demos and prototypes with your UI components", "keywords": [ "addon", diff --git a/code/addons/measure/package.json b/code/addons/measure/package.json index c33c07fd2bb0..2d23660bdac2 100644 --- a/code/addons/measure/package.json +++ b/code/addons/measure/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-measure", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Inspect layouts by visualizing the box model", "keywords": [ "storybook-addons", diff --git a/code/addons/onboarding/package.json b/code/addons/onboarding/package.json index cef855f5c780..057221c7df4e 100644 --- a/code/addons/onboarding/package.json +++ b/code/addons/onboarding/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-onboarding", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook Addon Onboarding - Introduces a new onboarding experience", "keywords": [ "storybook-addons", diff --git a/code/addons/onboarding/preset.js b/code/addons/onboarding/preset.js index 2c7ea670f4a6..87f1602c2f26 100644 --- a/code/addons/onboarding/preset.js +++ b/code/addons/onboarding/preset.js @@ -1,5 +1,5 @@ function managerEntries(entry = []) { - return [...entry, require.resolve('./dist/manager.mjs')]; + return [...entry, require.resolve('./dist/manager.js')]; } module.exports = { diff --git a/code/addons/outline/package.json b/code/addons/outline/package.json index 5afe3e8023c9..a0a1ff61ab03 100644 --- a/code/addons/outline/package.json +++ b/code/addons/outline/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-outline", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Outline all elements with CSS to help with layout placement and alignment", "keywords": [ "storybook-addons", diff --git a/code/addons/storysource/package.json b/code/addons/storysource/package.json index a24b842f279e..c9fc3680601d 100644 --- a/code/addons/storysource/package.json +++ b/code/addons/storysource/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-storysource", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "View a story’s source code to see how it works and paste into your app", "keywords": [ "addon", diff --git a/code/addons/themes/package.json b/code/addons/themes/package.json index 6af4ca76727a..c28b3426d2e1 100644 --- a/code/addons/themes/package.json +++ b/code/addons/themes/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-themes", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Switch between multiple themes for you components in Storybook", "keywords": [ "css", diff --git a/code/addons/toolbars/package.json b/code/addons/toolbars/package.json index 047f7e002a34..f9aa8edb217e 100644 --- a/code/addons/toolbars/package.json +++ b/code/addons/toolbars/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-toolbars", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Create your own toolbar items that control story rendering", "keywords": [ "addon", diff --git a/code/addons/viewport/package.json b/code/addons/viewport/package.json index 42f34b72834f..55459bde0a95 100644 --- a/code/addons/viewport/package.json +++ b/code/addons/viewport/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-viewport", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Build responsive components by adjusting Storybook’s viewport size and orientation", "keywords": [ "addon", diff --git a/code/builders/builder-manager/package.json b/code/builders/builder-manager/package.json index 2733705ef1cd..56b28e9c4fbf 100644 --- a/code/builders/builder-manager/package.json +++ b/code/builders/builder-manager/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/builder-manager", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook manager builder", "keywords": [ "storybook" diff --git a/code/builders/builder-vite/package.json b/code/builders/builder-vite/package.json index f4ed850b8034..b1dd337500cb 100644 --- a/code/builders/builder-vite/package.json +++ b/code/builders/builder-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/builder-vite", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "A plugin to run and build Storybooks with Vite", "homepage": "https://github.com/storybookjs/storybook/tree/next/code/builders/builder-vite/#readme", "bugs": { diff --git a/code/builders/builder-webpack5/package.json b/code/builders/builder-webpack5/package.json index e42617eed5d2..09e2717e316a 100644 --- a/code/builders/builder-webpack5/package.json +++ b/code/builders/builder-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/builder-webpack5", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" diff --git a/code/frameworks/angular/package.json b/code/frameworks/angular/package.json index de20986e3618..b58cb37738cf 100644 --- a/code/frameworks/angular/package.json +++ b/code/frameworks/angular/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/angular", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for Angular: Develop Angular components in isolation with hot reloading.", "keywords": [ "storybook", diff --git a/code/frameworks/ember/package.json b/code/frameworks/ember/package.json index 70c68121930b..f279db33b3ed 100644 --- a/code/frameworks/ember/package.json +++ b/code/frameworks/ember/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/ember", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for Ember: Develop Ember Component in isolation with Hot Reloading.", "homepage": "https://github.com/storybookjs/storybook/tree/next/code/frameworks/ember", "bugs": { diff --git a/code/frameworks/html-vite/package.json b/code/frameworks/html-vite/package.json index dd35f7ee8983..c4004391a219 100644 --- a/code/frameworks/html-vite/package.json +++ b/code/frameworks/html-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/html-vite", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for HTML and Vite: Develop HTML in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/html-webpack5/package.json b/code/frameworks/html-webpack5/package.json index fa8386b9071f..81cc4e41b756 100644 --- a/code/frameworks/html-webpack5/package.json +++ b/code/frameworks/html-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/html-webpack5", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for HTML: View HTML snippets in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json index 42c3eb78d060..73b56bd55c4b 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/nextjs", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for Next.js", "keywords": [ "storybook", diff --git a/code/frameworks/preact-vite/package.json b/code/frameworks/preact-vite/package.json index 0b5819bd04f5..e059321937b6 100644 --- a/code/frameworks/preact-vite/package.json +++ b/code/frameworks/preact-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preact-vite", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for Preact and Vite: Develop Preact components in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/preact-webpack5/package.json b/code/frameworks/preact-webpack5/package.json index b58ce7ede06a..1bfffc99ab65 100644 --- a/code/frameworks/preact-webpack5/package.json +++ b/code/frameworks/preact-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preact-webpack5", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for Preact: Develop Preact Component in isolation.", "keywords": [ "storybook" diff --git a/code/frameworks/react-vite/package.json b/code/frameworks/react-vite/package.json index 1518d1f76df9..5673d4caf9dd 100644 --- a/code/frameworks/react-vite/package.json +++ b/code/frameworks/react-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react-vite", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for React and Vite: Develop React components in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -50,9 +50,13 @@ "@joshwooding/vite-plugin-react-docgen-typescript": "0.3.0", "@rollup/pluginutils": "^5.0.2", "@storybook/builder-vite": "workspace:*", + "@storybook/node-logger": "workspace:*", "@storybook/react": "workspace:*", + "find-up": "^5.0.0", "magic-string": "^0.30.0", - "react-docgen": "^7.0.0" + "react-docgen": "^7.0.0", + "resolve": "^1.22.8", + "tsconfig-paths": "^4.2.0" }, "devDependencies": { "@types/node": "^18.0.0", diff --git a/code/frameworks/react-vite/src/plugins/docgen-resolver.ts b/code/frameworks/react-vite/src/plugins/docgen-resolver.ts new file mode 100644 index 000000000000..ab0fda807cf8 --- /dev/null +++ b/code/frameworks/react-vite/src/plugins/docgen-resolver.ts @@ -0,0 +1,74 @@ +import { extname } from 'path'; +import resolve from 'resolve'; + +export class ReactDocgenResolveError extends Error { + // the magic string that react-docgen uses to check if a module is ignored + readonly code = 'MODULE_NOT_FOUND'; + + constructor(filename: string) { + super(`'${filename}' was ignored by react-docgen.`); + } +} + +/* The below code was copied from: + * https://github.com/reactjs/react-docgen/blob/df2daa8b6f0af693ecc3c4dc49f2246f60552bcb/packages/react-docgen/src/importer/makeFsImporter.ts#L14-L63 + * because it wasn't exported from the react-docgen package. + * watch out: when updating this code, also update the code in code/presets/react-webpack/src/loaders/docgen-resolver.ts + */ + +// These extensions are sorted by priority +// resolve() will check for files in the order these extensions are sorted +export const RESOLVE_EXTENSIONS = [ + '.js', + '.cts', // These were originally not in the code, I added them + '.mts', // These were originally not in the code, I added them + '.ctsx', // These were originally not in the code, I added them + '.mtsx', // These were originally not in the code, I added them + '.ts', + '.tsx', + '.mjs', + '.cjs', + '.mts', + '.cts', + '.jsx', +]; + +export function defaultLookupModule(filename: string, basedir: string): string { + const resolveOptions = { + basedir, + extensions: RESOLVE_EXTENSIONS, + // we do not need to check core modules as we cannot import them anyway + includeCoreModules: false, + }; + + try { + return resolve.sync(filename, resolveOptions); + } catch (error) { + const ext = extname(filename); + let newFilename: string; + + // if we try to import a JavaScript file it might be that we are actually pointing to + // a TypeScript file. This can happen in ES modules as TypeScript requires to import other + // TypeScript files with .js extensions + // https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions + switch (ext) { + case '.js': + case '.mjs': + case '.cjs': + newFilename = `${filename.slice(0, -2)}ts`; + break; + + case '.jsx': + newFilename = `${filename.slice(0, -3)}tsx`; + break; + default: + throw error; + } + + return resolve.sync(newFilename, { + ...resolveOptions, + // we already know that there is an extension at this point, so no need to check other extensions + extensions: [], + }); + } +} diff --git a/code/frameworks/react-vite/src/plugins/react-docgen.test.ts b/code/frameworks/react-vite/src/plugins/react-docgen.test.ts new file mode 100644 index 000000000000..1fed686198b9 --- /dev/null +++ b/code/frameworks/react-vite/src/plugins/react-docgen.test.ts @@ -0,0 +1,52 @@ +import { getReactDocgenImporter } from './react-docgen'; +import { describe, it, expect, vi } from 'vitest'; + +const reactDocgenMock = vi.hoisted(() => { + return { + makeFsImporter: vi.fn().mockImplementation((fn) => fn), + }; +}); + +const reactDocgenResolverMock = vi.hoisted(() => { + return { + defaultLookupModule: vi.fn(), + }; +}); + +vi.mock('./docgen-resolver', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + defaultLookupModule: reactDocgenResolverMock.defaultLookupModule, + }; +}); + +vi.mock('react-docgen', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + makeFsImporter: reactDocgenMock.makeFsImporter, + }; +}); + +describe('getReactDocgenImporter function', () => { + it('should not map the request if a tsconfig path mapping is not available', () => { + const filename = './src/components/Button.tsx'; + const basedir = '/src'; + const imported = getReactDocgenImporter(undefined); + reactDocgenResolverMock.defaultLookupModule.mockImplementation((filen: string) => filen); + const result = (imported as any)(filename, basedir); + expect(result).toBe(filename); + }); + + it('should map the request', () => { + const mappedFile = './mapped-file.tsx'; + const matchPath = vi.fn().mockReturnValue(mappedFile); + const filename = './src/components/Button.tsx'; + const basedir = '/src'; + const imported = getReactDocgenImporter(matchPath); + reactDocgenResolverMock.defaultLookupModule.mockImplementation((filen: string) => filen); + const result = (imported as any)(filename, basedir); + expect(result).toBe(mappedFile); + }); +}); diff --git a/code/frameworks/react-vite/src/plugins/react-docgen.ts b/code/frameworks/react-vite/src/plugins/react-docgen.ts index 5be555fbd081..c59861e4ff43 100644 --- a/code/frameworks/react-vite/src/plugins/react-docgen.ts +++ b/code/frameworks/react-vite/src/plugins/react-docgen.ts @@ -6,18 +6,25 @@ import { parse, builtinHandlers as docgenHandlers, builtinResolvers as docgenResolver, - builtinImporters as docgenImporters, + makeFsImporter, } from 'react-docgen'; import MagicString from 'magic-string'; import type { PluginOption } from 'vite'; +import * as TsconfigPaths from 'tsconfig-paths'; +import findUp from 'find-up'; import actualNameHandler from './docgen-handlers/actualNameHandler'; +import { + RESOLVE_EXTENSIONS, + ReactDocgenResolveError, + defaultLookupModule, +} from './docgen-resolver'; +import { logger } from '@storybook/node-logger'; type DocObj = Documentation & { actualName: string }; // TODO: None of these are able to be overridden, so `default` is aspirational here. const defaultHandlers = Object.values(docgenHandlers).map((handler) => handler); const defaultResolver = new docgenResolver.FindExportedDefinitionsResolver(); -const defaultImporter = docgenImporters.fsImporter; const handlers = [...defaultHandlers, actualNameHandler]; type Options = { @@ -25,25 +32,40 @@ type Options = { exclude?: string | RegExp | (string | RegExp)[]; }; -export function reactDocgen({ +export async function reactDocgen({ include = /\.(mjs|tsx?|jsx?)$/, exclude = [/node_modules\/.*/], -}: Options = {}): PluginOption { +}: Options = {}): Promise { const cwd = process.cwd(); const filter = createFilter(include, exclude); + const tsconfigPath = await findUp('tsconfig.json', { cwd }); + const tsconfig = TsconfigPaths.loadConfig(tsconfigPath); + + let matchPath: TsconfigPaths.MatchPath | undefined; + + if (tsconfig.resultType === 'success') { + logger.info('Using tsconfig paths for react-docgen'); + matchPath = TsconfigPaths.createMatchPath(tsconfig.absoluteBaseUrl, tsconfig.paths, [ + 'browser', + 'module', + 'main', + ]); + } + return { name: 'storybook:react-docgen-plugin', enforce: 'pre', async transform(src: string, id: string) { - const relPath = path.relative(cwd, id); - if (!filter(relPath)) return; + if (!filter(path.relative(cwd, id))) { + return; + } try { const docgenResults = parse(src, { resolver: defaultResolver, handlers, - importer: defaultImporter, + importer: getReactDocgenImporter(matchPath), filename: id, }) as DocObj[]; const s = new MagicString(src); @@ -70,3 +92,24 @@ export function reactDocgen({ }, }; } + +export function getReactDocgenImporter(matchPath: TsconfigPaths.MatchPath | undefined) { + return makeFsImporter((filename, basedir) => { + const mappedFilenameByPaths = (() => { + if (matchPath) { + const match = matchPath(filename); + return match || filename; + } else { + return filename; + } + })(); + + const result = defaultLookupModule(mappedFilenameByPaths, basedir); + + if (RESOLVE_EXTENSIONS.find((ext) => result.endsWith(ext))) { + return result; + } + + throw new ReactDocgenResolveError(filename); + }); +} diff --git a/code/frameworks/react-vite/src/preset.ts b/code/frameworks/react-vite/src/preset.ts index 35a83a306ce0..cbdde0f61a07 100644 --- a/code/frameworks/react-vite/src/preset.ts +++ b/code/frameworks/react-vite/src/preset.ts @@ -43,7 +43,7 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config, { presets // Needs to run before the react plugin, so add to the front plugins.unshift( // If react-docgen is specified, use it for everything, otherwise only use it for non-typescript files - reactDocgen({ + await reactDocgen({ include: reactDocgenOption === 'react-docgen' ? /\.(mjs|tsx?|jsx?)$/ : /\.(mjs|jsx?)$/, }) ); diff --git a/code/frameworks/react-webpack5/package.json b/code/frameworks/react-webpack5/package.json index f91e1ecb1b1a..448905c2a899 100644 --- a/code/frameworks/react-webpack5/package.json +++ b/code/frameworks/react-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react-webpack5", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for React: Develop React Component in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/server-webpack5/package.json b/code/frameworks/server-webpack5/package.json index 6ea2a36ce6a8..8b00bc35433d 100644 --- a/code/frameworks/server-webpack5/package.json +++ b/code/frameworks/server-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/server-webpack5", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for Server: View HTML snippets from a server in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/svelte-vite/package.json b/code/frameworks/svelte-vite/package.json index 17c1e07bd414..be8d6b0c228e 100644 --- a/code/frameworks/svelte-vite/package.json +++ b/code/frameworks/svelte-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/svelte-vite", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for Svelte and Vite: Develop Svelte components in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -58,13 +58,13 @@ "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.1", "@types/node": "^18.0.0", - "svelte": "^5.0.0-next.28", + "svelte": "^5.0.0-next.65", "typescript": "^5.3.2", "vite": "^4.0.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^2.0.0 || ^3.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.16", + "svelte": "^4.0.0 || ^5.0.0-next.65", "vite": "^4.0.0 || ^5.0.0" }, "engines": { diff --git a/code/frameworks/svelte-webpack5/package.json b/code/frameworks/svelte-webpack5/package.json index dbb884b0ed44..3979794e33b1 100644 --- a/code/frameworks/svelte-webpack5/package.json +++ b/code/frameworks/svelte-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/svelte-webpack5", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for Svelte: Develop Svelte Component in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -58,7 +58,7 @@ "typescript": "^5.3.2" }, "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.16", + "svelte": "^4.0.0 || ^5.0.0-next.65", "svelte-loader": "*" }, "engines": { diff --git a/code/frameworks/svelte-webpack5/src/typings.d.ts b/code/frameworks/svelte-webpack5/src/typings.d.ts index 48e7ba6228fd..153f6ec1f9ce 100644 --- a/code/frameworks/svelte-webpack5/src/typings.d.ts +++ b/code/frameworks/svelte-webpack5/src/typings.d.ts @@ -1,3 +1,2 @@ -declare module '@storybook/svelte/templates/SlotDecorator.svelte'; -declare module '@storybook/svelte/templates/PreviewRender.svelte'; -declare module '@storybook/svelte/templates/HOC.svelte'; +declare module '@storybook/svelte/internal/SlotDecorator.svelte'; +declare module '@storybook/svelte/internal/PreviewRender.svelte'; diff --git a/code/frameworks/sveltekit/package.json b/code/frameworks/sveltekit/package.json index e6f043e0b74c..8dd860c61d21 100644 --- a/code/frameworks/sveltekit/package.json +++ b/code/frameworks/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/sveltekit", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for SvelteKit", "keywords": [ "storybook", @@ -64,7 +64,7 @@ "vite": "^4.0.0" }, "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.16", + "svelte": "^4.0.0 || ^5.0.0-next.65", "vite": "^4.0.0 || ^5.0.0" }, "engines": { diff --git a/code/frameworks/vue3-vite/package.json b/code/frameworks/vue3-vite/package.json index 60fd9c7e67e0..ad452ade8d46 100644 --- a/code/frameworks/vue3-vite/package.json +++ b/code/frameworks/vue3-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/vue3-vite", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for Vue3 and Vite: Develop Vue3 components in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/vue3-webpack5/package.json b/code/frameworks/vue3-webpack5/package.json index 1774e6739735..6b6504531dbf 100644 --- a/code/frameworks/vue3-webpack5/package.json +++ b/code/frameworks/vue3-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/vue3-webpack5", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for Vue 3: Develop Vue 3 Components in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/web-components-vite/package.json b/code/frameworks/web-components-vite/package.json index 4c274ca128fc..c4151a92ca89 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": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for web-components and Vite: Develop Web Components in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/web-components-webpack5/package.json b/code/frameworks/web-components-webpack5/package.json index 83e6f47fc808..7e0ab8ec6130 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": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for web-components: View web components snippets in isolation with Hot Reloading.", "keywords": [ "lit", diff --git a/code/lib/channels/package.json b/code/lib/channels/package.json index 4157290b916e..6abecbb236fc 100644 --- a/code/lib/channels/package.json +++ b/code/lib/channels/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/channels", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "", "keywords": [ "storybook" diff --git a/code/lib/cli-sb/package.json b/code/lib/cli-sb/package.json index 589235ac4c83..fb2741dc33a2 100644 --- a/code/lib/cli-sb/package.json +++ b/code/lib/cli-sb/package.json @@ -1,6 +1,6 @@ { "name": "sb", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook CLI", "keywords": [ "storybook" diff --git a/code/lib/cli-storybook/package.json b/code/lib/cli-storybook/package.json index 14dff4e67c38..f4e2319a57cb 100644 --- a/code/lib/cli-storybook/package.json +++ b/code/lib/cli-storybook/package.json @@ -1,6 +1,6 @@ { "name": "storybook", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook CLI", "keywords": [ "storybook" diff --git a/code/lib/cli/bin/index.js b/code/lib/cli/bin/index.js index 7131e95a311d..af7869a6e043 100755 --- a/code/lib/cli/bin/index.js +++ b/code/lib/cli/bin/index.js @@ -1,8 +1,8 @@ #!/usr/bin/env node -const majorNodeVersion = parseInt(process.version.toString().replace('v', '').split('.')[0], 10); -if (majorNodeVersion < 16) { - console.error('To run storybook you need to have node 16 or higher'); +const majorNodeVersion = parseInt(process.versions.node, 10); +if (majorNodeVersion < 18) { + console.error('To run Storybook you need to have Node.js 18 or higher'); process.exit(1); } diff --git a/code/lib/cli/package.json b/code/lib/cli/package.json index ef6648b7bdac..ed8b9f90bcf6 100644 --- a/code/lib/cli/package.json +++ b/code/lib/cli/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/cli", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook's CLI - install, dev, build, upgrade, and more", "keywords": [ "cli", diff --git a/code/lib/cli/src/add.test.ts b/code/lib/cli/src/add.test.ts new file mode 100644 index 000000000000..3025da275e49 --- /dev/null +++ b/code/lib/cli/src/add.test.ts @@ -0,0 +1,148 @@ +import { describe, expect, test, vi } from 'vitest'; +import { add, getVersionSpecifier } from './add'; + +const MockedConfig = vi.hoisted(() => { + return { + appendValueToArray: vi.fn(), + }; +}); +const MockedPackageManager = vi.hoisted(() => { + return { + retrievePackageJson: vi.fn(() => ({})), + latestVersion: vi.fn(() => '1.0.0'), + addDependencies: vi.fn(() => {}), + type: 'npm', + }; +}); +const MockedPostInstall = vi.hoisted(() => { + return { + postinstallAddon: vi.fn(), + }; +}); +const MockedConsole = { + log: vi.fn(), + warn: vi.fn(), + error: vi.fn(), +} as any as Console; + +vi.mock('@storybook/csf-tools', () => { + return { + readConfig: vi.fn(() => MockedConfig), + writeConfig: vi.fn(), + }; +}); +vi.mock('./postinstallAddon', () => { + return MockedPostInstall; +}); +vi.mock('@storybook/core-common', () => { + return { + getStorybookInfo: vi.fn(() => ({ mainConfig: {}, configDir: '' })), + serverRequire: vi.fn(() => ({})), + JsPackageManagerFactory: { + getPackageManager: vi.fn(() => MockedPackageManager), + }, + getCoercedStorybookVersion: vi.fn(() => '8.0.0'), + versions: { + '@storybook/addon-docs': '^8.0.0', + }, + }; +}); + +describe('getVersionSpecifier', (it) => { + test.each([ + ['@storybook/addon-docs', ['@storybook/addon-docs', undefined]], + ['@storybook/addon-docs@7.0.1', ['@storybook/addon-docs', '7.0.1']], + ['@storybook/addon-docs@7.0.1-beta.1', ['@storybook/addon-docs', '7.0.1-beta.1']], + ['@storybook/addon-docs@~7.0.1-beta.1', ['@storybook/addon-docs', '~7.0.1-beta.1']], + ['@storybook/addon-docs@^7.0.1-beta.1', ['@storybook/addon-docs', '^7.0.1-beta.1']], + ['@storybook/addon-docs@next', ['@storybook/addon-docs', 'next']], + ])('%s => %s', (input, expected) => { + const result = getVersionSpecifier(input); + expect(result[0]).toEqual(expected[0]); + expect(result[1]).toEqual(expected[1]); + }); +}); + +describe('add', () => { + const testData = [ + { input: 'aa', expected: 'aa@^1.0.0' }, // resolves to the latest version + { input: 'aa@4', expected: 'aa@^4' }, + { input: 'aa@4.1.0', expected: 'aa@^4.1.0' }, + { input: 'aa@^4', expected: 'aa@^4' }, + { input: 'aa@~4', expected: 'aa@~4' }, + { input: 'aa@4.1.0-alpha.1', expected: 'aa@^4.1.0-alpha.1' }, + { input: 'aa@next', expected: 'aa@next' }, + + { input: '@org/aa', expected: '@org/aa@^1.0.0' }, + { input: '@org/aa@4', expected: '@org/aa@^4' }, + { input: '@org/aa@4.1.0', expected: '@org/aa@^4.1.0' }, + { input: '@org/aa@4.1.0-alpha.1', expected: '@org/aa@^4.1.0-alpha.1' }, + { input: '@org/aa@next', expected: '@org/aa@next' }, + + { input: '@storybook/addon-docs@~4', expected: '@storybook/addon-docs@~4' }, + { input: '@storybook/addon-docs@next', expected: '@storybook/addon-docs@next' }, + { input: '@storybook/addon-docs', expected: '@storybook/addon-docs@^8.0.0' }, // takes it from the versions file + ]; + + test.each(testData)('$input', async ({ input, expected }) => { + const [packageName] = getVersionSpecifier(input); + + await add(input, { packageManager: 'npm', skipPostinstall: true }, MockedConsole); + + expect(MockedConfig.appendValueToArray).toHaveBeenCalledWith( + expect.arrayContaining(['addons']), + packageName + ); + + expect(MockedPackageManager.addDependencies).toHaveBeenCalledWith( + { installAsDevDependencies: true }, + [expected] + ); + }); +}); + +describe('add (extra)', () => { + test('not warning when installing the correct version of storybook', async () => { + await add( + '@storybook/addon-docs', + { packageManager: 'npm', skipPostinstall: true }, + MockedConsole + ); + + expect(MockedConsole.warn).not.toHaveBeenCalledWith( + expect.stringContaining(`is not the same as the version of Storybook you are using.`) + ); + }); + test('not warning when installing unrelated package', async () => { + await add('aa', { packageManager: 'npm', skipPostinstall: true }, MockedConsole); + + expect(MockedConsole.warn).not.toHaveBeenCalledWith( + expect.stringContaining(`is not the same as the version of Storybook you are using.`) + ); + }); + test('warning when installing a core addon mismatching version of storybook', async () => { + await add( + '@storybook/addon-docs@2.0.0', + { packageManager: 'npm', skipPostinstall: true }, + MockedConsole + ); + + expect(MockedConsole.warn).toHaveBeenCalledWith( + expect.stringContaining( + `The version of @storybook/addon-docs you are installing is not the same as the version of Storybook you are using. This may lead to unexpected behavior.` + ) + ); + }); + + test('postInstall', async () => { + await add( + '@storybook/addon-docs', + { packageManager: 'npm', skipPostinstall: false }, + MockedConsole + ); + + expect(MockedPostInstall.postinstallAddon).toHaveBeenCalledWith('@storybook/addon-docs', { + packageManager: 'npm', + }); + }); +}); diff --git a/code/lib/cli/src/add.ts b/code/lib/cli/src/add.ts index 0321ec966fd7..19a4b552fcc0 100644 --- a/code/lib/cli/src/add.ts +++ b/code/lib/cli/src/add.ts @@ -1,44 +1,32 @@ import { getStorybookInfo, serverRequire, - getCoercedStorybookVersion, - isCorePackage, JsPackageManagerFactory, + getCoercedStorybookVersion, type PackageManagerName, + versions, } from '@storybook/core-common'; import { readConfig, writeConfig } from '@storybook/csf-tools'; import { isAbsolute, join } from 'path'; import SemVer from 'semver'; import dedent from 'ts-dedent'; +import { postinstallAddon } from './postinstallAddon'; -const logger = console; - -interface PostinstallOptions { +export interface PostinstallOptions { packageManager: PackageManagerName; } -const postinstallAddon = async (addonName: string, options: PostinstallOptions) => { - try { - const modulePath = require.resolve(`${addonName}/postinstall`, { paths: [process.cwd()] }); - - const postinstall = require(modulePath); - - try { - logger.log(`Running postinstall script for ${addonName}`); - await postinstall(options); - } catch (e) { - logger.error(`Error running postinstall script for ${addonName}`); - logger.error(e); - } - } catch (e) { - // no postinstall script - } -}; - -const getVersionSpecifier = (addon: string) => { - const groups = /^(...*)@(.*)$/.exec(addon); +/** + * Extract the addon name and version specifier from the input string + * @param addon - the input string + * @returns [addonName, versionSpecifier] + * @example + * getVersionSpecifier('@storybook/addon-docs@7.0.1') => ['@storybook/addon-docs', '7.0.1'] + */ +export const getVersionSpecifier = (addon: string) => { + const groups = /^(@{0,1}[^@]+)(?:@(.+))?$/.exec(addon); if (groups) { - return [groups[0], groups[2]] as const; + return [groups[1], groups[2]] as const; } return [addon, undefined] as const; }; @@ -58,6 +46,8 @@ const checkInstalled = (addonName: string, main: any) => { return !!existingAddon; }; +const isCoreAddon = (addonName: string) => Object.hasOwn(versions, addonName); + /** * Install the given addon package and add it to main.js * @@ -71,9 +61,11 @@ const checkInstalled = (addonName: string, main: any) => { */ export async function add( addon: string, - options: { packageManager: PackageManagerName; skipPostinstall: boolean } + options: { packageManager: PackageManagerName; skipPostinstall: boolean }, + logger = console ) { const { packageManager: pkgMgr } = options; + const [addonName, inputVersion] = getVersionSpecifier(addon); const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); const packageJson = await packageManager.retrievePackageJson(); @@ -85,43 +77,52 @@ export async function add( `); } - if (checkInstalled(addon, requireMain(configDir))) { - throw new Error(dedent` - Addon ${addon} is already installed; we skipped adding it to your ${mainConfig}. - `); - } - - const [addonName, versionSpecifier] = getVersionSpecifier(addon); - if (!mainConfig) { logger.error('Unable to find storybook main.js config'); return; } + + if (checkInstalled(addonName, requireMain(configDir))) { + throw new Error(dedent` + Addon ${addonName} is already installed; we skipped adding it to your ${mainConfig}. + `); + } + const main = await readConfig(mainConfig); logger.log(`Verifying ${addonName}`); - const latestVersion = await packageManager.latestVersion(addonName); - if (!latestVersion) { - logger.error(`Unknown addon ${addonName}`); - } - // add to package.json - const isStorybookAddon = addonName.startsWith('@storybook/'); - const isAddonFromCore = isCorePackage(addonName); const storybookVersion = await getCoercedStorybookVersion(packageManager); - const version = versionSpecifier || (isAddonFromCore ? storybookVersion : latestVersion); - const addonWithVersion = SemVer.valid(version) + let version = inputVersion; + + if (!version && isCoreAddon(addonName) && storybookVersion) { + version = storybookVersion; + } + if (!version) { + version = await packageManager.latestVersion(addonName); + } + + if (isCoreAddon(addonName) && version !== storybookVersion) { + logger.warn( + `The version of ${addonName} you are installing is not the same as the version of Storybook you are using. This may lead to unexpected behavior.` + ); + } + + const addonWithVersion = isValidVersion(version) ? `${addonName}@^${version}` : `${addonName}@${version}`; + logger.log(`Installing ${addonWithVersion}`); await packageManager.addDependencies({ installAsDevDependencies: true }, [addonWithVersion]); - // add to main.js logger.log(`Adding '${addon}' to main.js addons field.`); main.appendValueToArray(['addons'], addonName); await writeConfig(main); - if (!options.skipPostinstall && isStorybookAddon) { + if (!options.skipPostinstall && isCoreAddon(addonName)) { await postinstallAddon(addonName, { packageManager: packageManager.type }); } } +function isValidVersion(version: string) { + return SemVer.valid(version) || version.match(/^\d+$/); +} diff --git a/code/lib/cli/src/automigrate/fixes/addon-postcss.ts b/code/lib/cli/src/automigrate/fixes/addon-postcss.ts index 7978e545a1c5..f8d134183c0b 100644 --- a/code/lib/cli/src/automigrate/fixes/addon-postcss.ts +++ b/code/lib/cli/src/automigrate/fixes/addon-postcss.ts @@ -29,7 +29,7 @@ export const addonPostCSS: Fix = { return dedent` ${chalk.bold( 'Attention' - )}: We've detected that you're using the following package which are incompatible with Storybook 8 and beyond: + )}: We've detected that you're using the following package which is incompatible with Storybook 8 and beyond: - ${chalk.cyan(`@storybook/addon-postcss`)} diff --git a/code/lib/cli/src/automigrate/fixes/addons-api.test.ts b/code/lib/cli/src/automigrate/fixes/addons-api.test.ts new file mode 100644 index 000000000000..2bae14386d3b --- /dev/null +++ b/code/lib/cli/src/automigrate/fixes/addons-api.test.ts @@ -0,0 +1,44 @@ +import { addonsAPI } from './addons-api'; +import type { StorybookConfig } from '@storybook/types'; +import type { JsPackageManager } from '@storybook/core-common'; +import { expect, describe, it } from 'vitest'; + +const checkAddonsAPI = async ({ + packageManager, + mainConfig = {}, + storybookVersion = '7.0.0', +}: { + packageManager?: Partial; + mainConfig?: Partial; + storybookVersion?: string; +}) => { + return addonsAPI.check({ + packageManager: packageManager as any, + storybookVersion, + mainConfig: mainConfig as any, + }); +}; + +describe('check function', () => { + it('should return { usesAddonsAPI: true } if @storybook/addons is installed', async () => { + await expect( + checkAddonsAPI({ + packageManager: { + getAllDependencies: async () => ({ + '@storybook/addons': '6.0.0', + }), + }, + }) + ).resolves.toEqual({ usesAddonsAPI: true }); + }); + + it('should return null if @storybook/addons is not installed', async () => { + await expect( + checkAddonsAPI({ + packageManager: { + getAllDependencies: async () => ({}), + }, + }) + ).resolves.toBeNull(); + }); +}); diff --git a/code/lib/cli/src/automigrate/fixes/addons-api.ts b/code/lib/cli/src/automigrate/fixes/addons-api.ts new file mode 100644 index 000000000000..f193898aa82f --- /dev/null +++ b/code/lib/cli/src/automigrate/fixes/addons-api.ts @@ -0,0 +1,45 @@ +import chalk from 'chalk'; +import { dedent } from 'ts-dedent'; +import type { Fix } from '../types'; + +interface AddonsAPIRunOptions { + usesAddonsAPI: boolean; +} + +export const addonsAPI: Fix = { + id: 'addons-api', + + versionRange: ['<8', '>=8'], + + promptType: 'notification', + + async check({ packageManager }) { + const allDependencies = await packageManager.getAllDependencies(); + const usesAddonsAPI = !!allDependencies['@storybook/addons']; + + if (!usesAddonsAPI) { + return null; + } + + return { usesAddonsAPI: true }; + }, + + prompt() { + return dedent` + ${chalk.bold( + 'Attention' + )}: We've detected that you're using the following package which is removed in Storybook 8 and beyond: + + - ${chalk.cyan(`@storybook/addons`)} + + This package has been deprecated and replaced with ${chalk.cyan( + `@storybook/preview-api` + )} and ${chalk.cyan(`@storybook/manager-api`)}. + + You can find more information about the new addons API in the migration guide: + ${chalk.yellow( + 'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#new-addons-api' + )} + `; + }, +}; diff --git a/code/lib/cli/src/automigrate/fixes/index.ts b/code/lib/cli/src/automigrate/fixes/index.ts index 91ba9a27927f..17b3d4942be1 100644 --- a/code/lib/cli/src/automigrate/fixes/index.ts +++ b/code/lib/cli/src/automigrate/fixes/index.ts @@ -23,12 +23,14 @@ import { storyshotsMigration } from './storyshots-migration'; import { removeArgtypesRegex } from './remove-argtypes-regex'; import { webpack5CompilerSetup } from './webpack5-compiler-setup'; import { removeJestTestingLibrary } from './remove-jest-testing-library'; +import { addonsAPI } from './addons-api'; import { mdx1to3 } from './mdx-1-to-3'; import { addonPostCSS } from './addon-postcss'; export * from '../types'; export const allFixes: Fix[] = [ + addonsAPI, newFrameworks, cra5, webpack5, diff --git a/code/lib/cli/src/automigrate/fixes/vite-config-file.ts b/code/lib/cli/src/automigrate/fixes/vite-config-file.ts index 6325b71ad062..6a8dd9ec0e02 100644 --- a/code/lib/cli/src/automigrate/fixes/vite-config-file.ts +++ b/code/lib/cli/src/automigrate/fixes/vite-config-file.ts @@ -4,6 +4,7 @@ import findUp from 'find-up'; import { getFrameworkPackageName } from '../helpers/mainConfigFile'; import { frameworkToRenderer } from '../../helpers'; import { frameworkPackages } from '@storybook/core-common'; +import path from 'path'; interface ViteConfigFileRunOptions { plugins: string[]; @@ -15,13 +16,13 @@ export const viteConfigFile = { versionRange: ['<8.0.0-beta.3', '>=8.0.0-beta.3'], - async check({ mainConfig, packageManager }) { - let isViteConfigFileFound = !!(await findUp([ - 'vite.config.js', - 'vite.config.mjs', - 'vite.config.cjs', - 'vite.config.ts', - ])); + promptType: 'notification', + + async check({ mainConfig, packageManager, mainConfigPath }) { + let isViteConfigFileFound = !!(await findUp( + ['vite.config.js', 'vite.config.mjs', 'vite.config.cjs', 'vite.config.ts'], + { cwd: mainConfigPath ? path.join(mainConfigPath, '..') : process.cwd() } + )); const rendererToVitePluginMap: Record = { preact: '@preact/preset-vite', diff --git a/code/lib/cli/src/automigrate/index.ts b/code/lib/cli/src/automigrate/index.ts index b0864a7071c6..8a84476b5e93 100644 --- a/code/lib/cli/src/automigrate/index.ts +++ b/code/lib/cli/src/automigrate/index.ts @@ -111,6 +111,7 @@ export const automigrate = async ({ renderer: rendererPackage, skipInstall, hideMigrationSummary = false, + isUpgrade, }: AutofixOptions): Promise<{ fixResults: Record; preCheckFailure?: PreCheckFailure; @@ -142,6 +143,7 @@ export const automigrate = async ({ mainConfigPath, storybookVersion, beforeVersion, + isUpgrade, dryRun, yes, }); diff --git a/code/lib/cli/src/automigrate/types.ts b/code/lib/cli/src/automigrate/types.ts index 36b4bac18c47..d8cc9f06af3e 100644 --- a/code/lib/cli/src/automigrate/types.ts +++ b/code/lib/cli/src/automigrate/types.ts @@ -27,9 +27,8 @@ export interface RunOptions { */ export type Prompt = 'auto' | 'manual' | 'notification'; -export interface Fix { +type BaseFix = { id: string; - promptType?: Prompt | ((result: ResultType) => Promise | Prompt); /** * The from/to version range of Storybook that this fix applies to. The strings are semver ranges. * The versionRange will only be checked if the automigration is part of an upgrade. @@ -38,8 +37,23 @@ export interface Fix { versionRange: [from: string, to: string]; check: (options: CheckOptions) => Promise; prompt: (result: ResultType) => string; - run?: (options: RunOptions) => Promise; -} +}; + +type PromptType = + | T + | ((result: ResultType) => Promise | Prompt); + +export type Fix = ( + | { + promptType?: PromptType; + run: (options: RunOptions) => Promise; + } + | { + promptType: PromptType; + run?: never; + } +) & + BaseFix; export type FixId = string; diff --git a/code/lib/cli/src/doctor/getIncompatibleAddons.ts b/code/lib/cli/src/doctor/getIncompatibleAddons.ts deleted file mode 100644 index e46297e94087..000000000000 --- a/code/lib/cli/src/doctor/getIncompatibleAddons.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { StorybookConfig } from '@storybook/types'; -import type { SemVer } from 'semver'; -import semver from 'semver'; -import { getAddonNames } from '../automigrate/helpers/mainConfigFile'; -import { JsPackageManagerFactory } from '@storybook/core-common'; - -export const getIncompatibleAddons = async ( - mainConfig: StorybookConfig, - packageManager = JsPackageManagerFactory.getPackageManager() -) => { - // TODO: Keep this up to date with https://github.com/storybookjs/storybook/issues/20529 in case more addons get added - const incompatibleList = { - '@storybook/addon-knobs': '6.4.0', - '@storybook/addon-postcss': '2.0.0', - 'storybook-addon-next-router': '4.0.2', - 'storybook-addon-outline': '1.4.2', // (deprecated) - '@storybook/addon-info': '5.3.21', - 'storybook-addon-designs': '6.3.1', - 'storybook-addon-next': '1.7.0', // (deprecated)' - 'storybook-docs-toc': '1.7.0', - '@storybook/addon-google-analytics': '6.2.9', - 'storybook-addon-pseudo-states': '1.15.5', - 'storybook-dark-mode': '2.1.1', - 'storybook-addon-gatsby': '0.0.5', - '@etchteam/storybook-addon-css-variables-theme': '1.4.0', - '@storybook/addon-cssresources': '6.2.9', - 'storybook-addon-grid': '0.3.1', - 'storybook-multilevel-sort': '1.2.0', - 'storybook-addon-i18next': '1.3.0', - 'storybook-source-link': '2.0.8', - 'babel-plugin-storybook-csf-title': '2.1.0', - '@urql/storybook-addon': '2.0.1', - 'storybook-addon-intl': '2.4.1', - 'storybook-addon-mock': '3.2.0', - '@chakra-ui/storybook-addon': '4.0.16', - 'storybook-mobile-addon': '1.0.2', - '@storybook/addon-queryparams': '6.2.9', - }; - - const addons = getAddonNames(mainConfig).filter( - (addon): addon is keyof typeof incompatibleList => addon in incompatibleList - ); - - const dependencies = await packageManager.getAllDependencies(); - const storybookPackages = Object.keys(dependencies).filter((dep) => dep.includes('storybook')); - - const packagesToCheck = [...new Set([...addons, ...storybookPackages])]; - - const addonVersions = await Promise.all( - packagesToCheck.map( - async (addon) => - ({ - name: addon, - version: await packageManager.getPackageVersion(addon), - }) as { name: keyof typeof incompatibleList; version: string } - ) - ); - - if (addonVersions.length === 0) { - return []; - } - - const incompatibleAddons: { name: string; version: string }[] = []; - addonVersions.forEach(({ name, version: installedVersion }) => { - if (installedVersion === null) { - return; - } - - const addonVersion = incompatibleList[name]; - try { - if ( - semver.lte(semver.coerce(installedVersion) as SemVer, semver.coerce(addonVersion) as SemVer) - ) { - incompatibleAddons.push({ name, version: installedVersion }); - } - } catch (err) { - // we tried our best but if we can't compare, we just no-op for that addon - } - }); - - return incompatibleAddons; -}; diff --git a/code/lib/cli/src/doctor/getIncompatibleStorybookPackages.ts b/code/lib/cli/src/doctor/getIncompatibleStorybookPackages.ts index d481fae93c82..d27694136074 100644 --- a/code/lib/cli/src/doctor/getIncompatibleStorybookPackages.ts +++ b/code/lib/cli/src/doctor/getIncompatibleStorybookPackages.ts @@ -88,7 +88,7 @@ export const getIncompatibleStorybookPackages = async ( const allDeps = await packageManager.getAllDependencies(); const storybookLikeDeps = Object.keys(allDeps).filter((dep) => dep.includes('storybook')); - if (storybookLikeDeps.length === 0) { + if (storybookLikeDeps.length === 0 && !context.skipErrors) { throw new Error('No Storybook dependencies found in the package.json'); } diff --git a/code/lib/cli/src/postinstallAddon.ts b/code/lib/cli/src/postinstallAddon.ts new file mode 100644 index 000000000000..50719c29e29c --- /dev/null +++ b/code/lib/cli/src/postinstallAddon.ts @@ -0,0 +1,19 @@ +import type { PostinstallOptions } from './add'; + +export const postinstallAddon = async (addonName: string, options: PostinstallOptions) => { + try { + const modulePath = require.resolve(`${addonName}/postinstall`, { paths: [process.cwd()] }); + + const postinstall = require(modulePath); + + try { + console.log(`Running postinstall script for ${addonName}`); + await postinstall(options); + } catch (e) { + console.error(`Error running postinstall script for ${addonName}`); + console.error(e); + } + } catch (e) { + // no postinstall script + } +}; diff --git a/code/lib/cli/src/sandbox-templates.ts b/code/lib/cli/src/sandbox-templates.ts index e1395b81156f..182e49798148 100644 --- a/code/lib/cli/src/sandbox-templates.ts +++ b/code/lib/cli/src/sandbox-templates.ts @@ -626,6 +626,7 @@ export const daily: TemplateKey[] = [ 'vue-cli/default-js', 'lit-vite/default-js', 'svelte-kit/skeleton-js', + 'svelte-kit/prerelease-ts', 'svelte-vite/default-js', 'nextjs/13-ts', 'nextjs/default-js', diff --git a/code/lib/client-logger/package.json b/code/lib/client-logger/package.json index ce0c6e8651de..f5f6f32ed843 100644 --- a/code/lib/client-logger/package.json +++ b/code/lib/client-logger/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/client-logger", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "", "keywords": [ "storybook" diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json index 51e18e91ef1e..cdb05de5da4d 100644 --- a/code/lib/codemod/package.json +++ b/code/lib/codemod/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/codemod", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "A collection of codemod scripts written with JSCodeshift", "keywords": [ "storybook" @@ -67,7 +67,7 @@ "jscodeshift": "^0.15.1", "lodash": "^4.17.21", "prettier": "^3.1.1", - "recast": "^0.23.1", + "recast": "^0.23.5", "tiny-invariant": "^1.3.1" }, "devDependencies": { diff --git a/code/lib/core-common/package.json b/code/lib/core-common/package.json index 9f74856a0af9..be5bac6b4cf8 100644 --- a/code/lib/core-common/package.json +++ b/code/lib/core-common/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core-common", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" diff --git a/code/lib/core-common/src/versions.ts b/code/lib/core-common/src/versions.ts index eb304c7d984a..346f3d272c65 100644 --- a/code/lib/core-common/src/versions.ts +++ b/code/lib/core-common/src/versions.ts @@ -1,83 +1,83 @@ // auto generated file, do not edit export default { - '@storybook/addon-a11y': '8.0.0-rc.0', - '@storybook/addon-actions': '8.0.0-rc.0', - '@storybook/addon-backgrounds': '8.0.0-rc.0', - '@storybook/addon-controls': '8.0.0-rc.0', - '@storybook/addon-docs': '8.0.0-rc.0', - '@storybook/addon-essentials': '8.0.0-rc.0', - '@storybook/addon-highlight': '8.0.0-rc.0', - '@storybook/addon-interactions': '8.0.0-rc.0', - '@storybook/addon-jest': '8.0.0-rc.0', - '@storybook/addon-links': '8.0.0-rc.0', - '@storybook/addon-mdx-gfm': '8.0.0-rc.0', - '@storybook/addon-measure': '8.0.0-rc.0', - '@storybook/addon-onboarding': '8.0.0-rc.0', - '@storybook/addon-outline': '8.0.0-rc.0', - '@storybook/addon-storysource': '8.0.0-rc.0', - '@storybook/addon-themes': '8.0.0-rc.0', - '@storybook/addon-toolbars': '8.0.0-rc.0', - '@storybook/addon-viewport': '8.0.0-rc.0', - '@storybook/angular': '8.0.0-rc.0', - '@storybook/blocks': '8.0.0-rc.0', - '@storybook/builder-manager': '8.0.0-rc.0', - '@storybook/builder-vite': '8.0.0-rc.0', - '@storybook/builder-webpack5': '8.0.0-rc.0', - '@storybook/channels': '8.0.0-rc.0', - '@storybook/cli': '8.0.0-rc.0', - '@storybook/client-logger': '8.0.0-rc.0', - '@storybook/codemod': '8.0.0-rc.0', - '@storybook/components': '8.0.0-rc.0', - '@storybook/core-common': '8.0.0-rc.0', - '@storybook/core-events': '8.0.0-rc.0', - '@storybook/core-server': '8.0.0-rc.0', - '@storybook/core-webpack': '8.0.0-rc.0', - '@storybook/csf-plugin': '8.0.0-rc.0', - '@storybook/csf-tools': '8.0.0-rc.0', - '@storybook/docs-tools': '8.0.0-rc.0', - '@storybook/ember': '8.0.0-rc.0', - '@storybook/html': '8.0.0-rc.0', - '@storybook/html-vite': '8.0.0-rc.0', - '@storybook/html-webpack5': '8.0.0-rc.0', - '@storybook/instrumenter': '8.0.0-rc.0', - '@storybook/manager': '8.0.0-rc.0', - '@storybook/manager-api': '8.0.0-rc.0', - '@storybook/nextjs': '8.0.0-rc.0', - '@storybook/node-logger': '8.0.0-rc.0', - '@storybook/preact': '8.0.0-rc.0', - '@storybook/preact-vite': '8.0.0-rc.0', - '@storybook/preact-webpack5': '8.0.0-rc.0', - '@storybook/preset-create-react-app': '8.0.0-rc.0', - '@storybook/preset-html-webpack': '8.0.0-rc.0', - '@storybook/preset-preact-webpack': '8.0.0-rc.0', - '@storybook/preset-react-webpack': '8.0.0-rc.0', - '@storybook/preset-server-webpack': '8.0.0-rc.0', - '@storybook/preset-svelte-webpack': '8.0.0-rc.0', - '@storybook/preset-vue3-webpack': '8.0.0-rc.0', - '@storybook/preview': '8.0.0-rc.0', - '@storybook/preview-api': '8.0.0-rc.0', - '@storybook/react': '8.0.0-rc.0', - '@storybook/react-dom-shim': '8.0.0-rc.0', - '@storybook/react-vite': '8.0.0-rc.0', - '@storybook/react-webpack5': '8.0.0-rc.0', - '@storybook/router': '8.0.0-rc.0', - '@storybook/server': '8.0.0-rc.0', - '@storybook/server-webpack5': '8.0.0-rc.0', - '@storybook/source-loader': '8.0.0-rc.0', - '@storybook/svelte': '8.0.0-rc.0', - '@storybook/svelte-vite': '8.0.0-rc.0', - '@storybook/svelte-webpack5': '8.0.0-rc.0', - '@storybook/sveltekit': '8.0.0-rc.0', - '@storybook/telemetry': '8.0.0-rc.0', - '@storybook/test': '8.0.0-rc.0', - '@storybook/theming': '8.0.0-rc.0', - '@storybook/types': '8.0.0-rc.0', - '@storybook/vue3': '8.0.0-rc.0', - '@storybook/vue3-vite': '8.0.0-rc.0', - '@storybook/vue3-webpack5': '8.0.0-rc.0', - '@storybook/web-components': '8.0.0-rc.0', - '@storybook/web-components-vite': '8.0.0-rc.0', - '@storybook/web-components-webpack5': '8.0.0-rc.0', - sb: '8.0.0-rc.0', - storybook: '8.0.0-rc.0', + '@storybook/addon-a11y': '8.0.0-rc.1', + '@storybook/addon-actions': '8.0.0-rc.1', + '@storybook/addon-backgrounds': '8.0.0-rc.1', + '@storybook/addon-controls': '8.0.0-rc.1', + '@storybook/addon-docs': '8.0.0-rc.1', + '@storybook/addon-essentials': '8.0.0-rc.1', + '@storybook/addon-highlight': '8.0.0-rc.1', + '@storybook/addon-interactions': '8.0.0-rc.1', + '@storybook/addon-jest': '8.0.0-rc.1', + '@storybook/addon-links': '8.0.0-rc.1', + '@storybook/addon-mdx-gfm': '8.0.0-rc.1', + '@storybook/addon-measure': '8.0.0-rc.1', + '@storybook/addon-onboarding': '8.0.0-rc.1', + '@storybook/addon-outline': '8.0.0-rc.1', + '@storybook/addon-storysource': '8.0.0-rc.1', + '@storybook/addon-themes': '8.0.0-rc.1', + '@storybook/addon-toolbars': '8.0.0-rc.1', + '@storybook/addon-viewport': '8.0.0-rc.1', + '@storybook/angular': '8.0.0-rc.1', + '@storybook/blocks': '8.0.0-rc.1', + '@storybook/builder-manager': '8.0.0-rc.1', + '@storybook/builder-vite': '8.0.0-rc.1', + '@storybook/builder-webpack5': '8.0.0-rc.1', + '@storybook/channels': '8.0.0-rc.1', + '@storybook/cli': '8.0.0-rc.1', + '@storybook/client-logger': '8.0.0-rc.1', + '@storybook/codemod': '8.0.0-rc.1', + '@storybook/components': '8.0.0-rc.1', + '@storybook/core-common': '8.0.0-rc.1', + '@storybook/core-events': '8.0.0-rc.1', + '@storybook/core-server': '8.0.0-rc.1', + '@storybook/core-webpack': '8.0.0-rc.1', + '@storybook/csf-plugin': '8.0.0-rc.1', + '@storybook/csf-tools': '8.0.0-rc.1', + '@storybook/docs-tools': '8.0.0-rc.1', + '@storybook/ember': '8.0.0-rc.1', + '@storybook/html': '8.0.0-rc.1', + '@storybook/html-vite': '8.0.0-rc.1', + '@storybook/html-webpack5': '8.0.0-rc.1', + '@storybook/instrumenter': '8.0.0-rc.1', + '@storybook/manager': '8.0.0-rc.1', + '@storybook/manager-api': '8.0.0-rc.1', + '@storybook/nextjs': '8.0.0-rc.1', + '@storybook/node-logger': '8.0.0-rc.1', + '@storybook/preact': '8.0.0-rc.1', + '@storybook/preact-vite': '8.0.0-rc.1', + '@storybook/preact-webpack5': '8.0.0-rc.1', + '@storybook/preset-create-react-app': '8.0.0-rc.1', + '@storybook/preset-html-webpack': '8.0.0-rc.1', + '@storybook/preset-preact-webpack': '8.0.0-rc.1', + '@storybook/preset-react-webpack': '8.0.0-rc.1', + '@storybook/preset-server-webpack': '8.0.0-rc.1', + '@storybook/preset-svelte-webpack': '8.0.0-rc.1', + '@storybook/preset-vue3-webpack': '8.0.0-rc.1', + '@storybook/preview': '8.0.0-rc.1', + '@storybook/preview-api': '8.0.0-rc.1', + '@storybook/react': '8.0.0-rc.1', + '@storybook/react-dom-shim': '8.0.0-rc.1', + '@storybook/react-vite': '8.0.0-rc.1', + '@storybook/react-webpack5': '8.0.0-rc.1', + '@storybook/router': '8.0.0-rc.1', + '@storybook/server': '8.0.0-rc.1', + '@storybook/server-webpack5': '8.0.0-rc.1', + '@storybook/source-loader': '8.0.0-rc.1', + '@storybook/svelte': '8.0.0-rc.1', + '@storybook/svelte-vite': '8.0.0-rc.1', + '@storybook/svelte-webpack5': '8.0.0-rc.1', + '@storybook/sveltekit': '8.0.0-rc.1', + '@storybook/telemetry': '8.0.0-rc.1', + '@storybook/test': '8.0.0-rc.1', + '@storybook/theming': '8.0.0-rc.1', + '@storybook/types': '8.0.0-rc.1', + '@storybook/vue3': '8.0.0-rc.1', + '@storybook/vue3-vite': '8.0.0-rc.1', + '@storybook/vue3-webpack5': '8.0.0-rc.1', + '@storybook/web-components': '8.0.0-rc.1', + '@storybook/web-components-vite': '8.0.0-rc.1', + '@storybook/web-components-webpack5': '8.0.0-rc.1', + sb: '8.0.0-rc.1', + storybook: '8.0.0-rc.1', }; diff --git a/code/lib/core-events/package.json b/code/lib/core-events/package.json index 8d2fcc7de97a..03a63f346e27 100644 --- a/code/lib/core-events/package.json +++ b/code/lib/core-events/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core-events", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Event names used in storybook core", "keywords": [ "storybook" diff --git a/code/lib/core-server/package.json b/code/lib/core-server/package.json index d00c78ec22b6..663facee0635 100644 --- a/code/lib/core-server/package.json +++ b/code/lib/core-server/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core-server", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" diff --git a/code/lib/core-webpack/package.json b/code/lib/core-webpack/package.json index f9675ac7f7a6..46f38e226ea1 100644 --- a/code/lib/core-webpack/package.json +++ b/code/lib/core-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core-webpack", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" diff --git a/code/lib/csf-plugin/package.json b/code/lib/csf-plugin/package.json index de3f29bc395a..c447831f6657 100644 --- a/code/lib/csf-plugin/package.json +++ b/code/lib/csf-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/csf-plugin", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Enrich CSF files via static analysis", "keywords": [ "storybook" diff --git a/code/lib/csf-tools/package.json b/code/lib/csf-tools/package.json index 908f50b5816f..9004d3e3beae 100644 --- a/code/lib/csf-tools/package.json +++ b/code/lib/csf-tools/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/csf-tools", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Parse and manipulate CSF and Storybook config files", "keywords": [ "storybook" @@ -49,7 +49,7 @@ "@storybook/csf": "^0.1.2", "@storybook/types": "workspace:*", "fs-extra": "^11.1.0", - "recast": "^0.23.1", + "recast": "^0.23.5", "ts-dedent": "^2.0.0" }, "devDependencies": { diff --git a/code/lib/docs-tools/package.json b/code/lib/docs-tools/package.json index 6dd8e6b0378c..06e103124ee1 100644 --- a/code/lib/docs-tools/package.json +++ b/code/lib/docs-tools/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/docs-tools", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Shared utility functions for frameworks to implement docs", "keywords": [ "storybook" diff --git a/code/lib/instrumenter/package.json b/code/lib/instrumenter/package.json index 7776051f6845..32c7c06686a8 100644 --- a/code/lib/instrumenter/package.json +++ b/code/lib/instrumenter/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/instrumenter", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "", "keywords": [ "storybook" diff --git a/code/lib/manager-api/package.json b/code/lib/manager-api/package.json index d4a46f9b543e..a790395da7a3 100644 --- a/code/lib/manager-api/package.json +++ b/code/lib/manager-api/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/manager-api", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Core Storybook Manager API & Context", "keywords": [ "storybook" diff --git a/code/lib/manager-api/src/version.ts b/code/lib/manager-api/src/version.ts index 277e71b47e44..156fd9c6a94a 100644 --- a/code/lib/manager-api/src/version.ts +++ b/code/lib/manager-api/src/version.ts @@ -1 +1 @@ -export const version = '8.0.0-rc.0'; +export const version = '8.0.0-rc.1'; diff --git a/code/lib/node-logger/package.json b/code/lib/node-logger/package.json index 53221fcadf30..12672e9c0278 100644 --- a/code/lib/node-logger/package.json +++ b/code/lib/node-logger/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/node-logger", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "", "keywords": [ "storybook" diff --git a/code/lib/preview-api/package.json b/code/lib/preview-api/package.json index 2ce58b60d381..75d64c9a87b8 100644 --- a/code/lib/preview-api/package.json +++ b/code/lib/preview-api/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preview-api", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "", "keywords": [ "storybook" diff --git a/code/lib/preview-api/src/index.ts b/code/lib/preview-api/src/index.ts index e47cdaa0a0dd..63d45114dc23 100644 --- a/code/lib/preview-api/src/index.ts +++ b/code/lib/preview-api/src/index.ts @@ -56,6 +56,7 @@ export { filterArgTypes, sanitizeStoryContextUpdate, setProjectAnnotations, + getPortableStoryWrapperId, inferControls, userOrAutoTitleFromSpecifier, userOrAutoTitle, diff --git a/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts b/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts index af775f27360b..dbca4640b05b 100644 --- a/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts +++ b/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts @@ -1,10 +1,18 @@ // @vitest-environment node import { describe, expect, vi, it } from 'vitest'; -import { composeStory, composeStories } from './portable-stories'; +import type { + ComponentAnnotations as Meta, + StoryAnnotationsOrFn as Story, + Store_CSFExports, +} from '@storybook/types'; + +import { composeStory, composeStories, setProjectAnnotations } from './portable-stories'; + +type StoriesModule = Store_CSFExports & Record; // Most integration tests for this functionality are located under renderers/react describe('composeStory', () => { - const meta = { + const meta: Meta = { title: 'Button', parameters: { firstAddon: true, @@ -15,13 +23,26 @@ describe('composeStory', () => { }, }; - it('should return story with composed args and parameters', () => { - const Story = () => {}; - Story.args = { primary: true }; - Story.parameters = { + it('should return story with composed annotations from story, meta and project', () => { + const decoratorFromProjectAnnotations = vi.fn((StoryFn) => StoryFn()); + const decoratorFromStoryAnnotations = vi.fn((StoryFn) => StoryFn()); + setProjectAnnotations([ + { + parameters: { injected: true }, + globalTypes: { + locale: { defaultValue: 'en' }, + }, + decorators: [decoratorFromProjectAnnotations], + }, + ]); + + const Story: Story = { + render: () => {}, + args: { primary: true }, parameters: { secondAddon: true, }, + decorators: [decoratorFromStoryAnnotations], }; const composedStory = composeStory(Story, meta); @@ -29,11 +50,16 @@ describe('composeStory', () => { expect(composedStory.parameters).toEqual( expect.objectContaining({ ...Story.parameters, ...meta.parameters }) ); + + composedStory(); + + expect(decoratorFromProjectAnnotations).toHaveBeenCalledOnce(); + expect(decoratorFromStoryAnnotations).toHaveBeenCalledOnce(); }); it('should compose with a play function', async () => { const spy = vi.fn(); - const Story = () => {}; + const Story: Story = () => {}; Story.args = { primary: true, }; @@ -53,6 +79,80 @@ describe('composeStory', () => { ); }); + it('should merge parameters with correct precedence in all combinations', async () => { + const storyAnnotations = { render: () => {} }; + const metaAnnotations: Meta = { parameters: { label: 'meta' } }; + const projectAnnotations: Meta = { parameters: { label: 'projectOverrides' } }; + + const storyPrecedence = composeStory( + { ...storyAnnotations, parameters: { label: 'story' } }, + metaAnnotations, + projectAnnotations + ); + expect(storyPrecedence.parameters.label).toEqual('story'); + + const metaPrecedence = composeStory(storyAnnotations, metaAnnotations, projectAnnotations); + expect(metaPrecedence.parameters.label).toEqual('meta'); + + const projectPrecedence = composeStory(storyAnnotations, {}, projectAnnotations); + expect(projectPrecedence.parameters.label).toEqual('projectOverrides'); + + setProjectAnnotations({ parameters: { label: 'setProjectAnnotationsOverrides' } }); + const setProjectAnnotationsPrecedence = composeStory(storyAnnotations, {}, {}); + expect(setProjectAnnotationsPrecedence.parameters.label).toEqual( + 'setProjectAnnotationsOverrides' + ); + }); + + it('should call and compose loaders data', async () => { + const loadSpy = vi.fn(); + const args = { story: 'story' }; + const LoaderStory: Story = { + args, + loaders: [ + async (context) => { + loadSpy(); + expect(context.args).toEqual(args); + return { + foo: 'bar', + }; + }, + ], + render: (_args, { loaded }) => { + expect(loaded).toEqual({ foo: 'bar' }); + }, + }; + + const composedStory = composeStory(LoaderStory, {}); + await composedStory.load(); + composedStory(); + expect(loadSpy).toHaveBeenCalled(); + }); + + it('should work with spies set up in loaders', async () => { + const spyFn = vi.fn(); + + const Story: Story = { + args: { + spyFn, + }, + loaders: [ + async () => { + spyFn.mockReturnValue('mockedData'); + }, + ], + render: (args) => { + const data = args.spyFn(); + expect(data).toBe('mockedData'); + }, + }; + + const composedStory = composeStory(Story, {}); + await composedStory.load(); + composedStory(); + expect(spyFn).toHaveBeenCalled(); + }); + it('should throw an error if Story is undefined', () => { expect(() => { // @ts-expect-error (invalid input) @@ -62,7 +162,7 @@ describe('composeStory', () => { describe('Id of the story', () => { it('is exposed correctly when composeStories is used', () => { - const module = { + const module: StoriesModule = { default: { title: 'Example/Button', }, @@ -72,7 +172,7 @@ describe('composeStory', () => { expect(Primary.id).toBe('example-button--csf-3-primary'); }); it('is exposed correctly when composeStory is used and exportsName is passed', () => { - const module = { + const module: StoriesModule = { default: { title: 'Example/Button', }, @@ -83,7 +183,7 @@ describe('composeStory', () => { }); it("is not unique when composeStory is used and exportsName isn't passed", () => { const Primary = composeStory({ render: () => {} }, {}); - expect(Primary.id).toContain('unknown'); + expect(Primary.id).toContain('composedstory--unnamed-story'); }); }); }); @@ -93,7 +193,7 @@ describe('composeStories', () => { const defaultAnnotations = { render: () => '' }; it('should call composeStoryFn with stories', () => { const composeStorySpy = vi.fn((v) => v); - const module = { + const module: StoriesModule = { default: { title: 'Button', }, @@ -118,7 +218,7 @@ describe('composeStories', () => { it('should not call composeStoryFn for non-story exports', () => { const composeStorySpy = vi.fn((v) => v); - const module = { + const module: StoriesModule = { default: { title: 'Button', excludeStories: /Data/, @@ -131,7 +231,7 @@ describe('composeStories', () => { describe('non-story exports', () => { it('should filter non-story exports with excludeStories', () => { - const StoryModuleWithNonStoryExports = { + const StoryModuleWithNonStoryExports: StoriesModule = { default: { title: 'Some/Component', excludeStories: /.*Data/, @@ -149,7 +249,7 @@ describe('composeStories', () => { }); it('should filter non-story exports with includeStories', () => { - const StoryModuleWithNonStoryExports = { + const StoryModuleWithNonStoryExports: StoriesModule = { default: { title: 'Some/Component', includeStories: /.*Story/, diff --git a/code/lib/preview-api/src/modules/store/csf/portable-stories.ts b/code/lib/preview-api/src/modules/store/csf/portable-stories.ts index 91e4cdc365e1..0974f0908526 100644 --- a/code/lib/preview-api/src/modules/store/csf/portable-stories.ts +++ b/code/lib/preview-api/src/modules/store/csf/portable-stories.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import { isExportStory } from '@storybook/csf'; import type { Renderer, @@ -12,7 +13,7 @@ import type { Parameters, ComposedStoryFn, StrictArgTypes, - ComposedStoryPlayContext, + PlayFunctionContext, } from '@storybook/types'; import { HooksContext } from '../../../addons'; @@ -23,23 +24,28 @@ import { normalizeComponentAnnotations } from './normalizeComponentAnnotations'; import { getValuesFromArgTypes } from './getValuesFromArgTypes'; import { normalizeProjectAnnotations } from './normalizeProjectAnnotations'; -let GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS = composeConfigs([]); +let globalProjectAnnotations: ProjectAnnotations = {}; + +export function getPortableStoryWrapperId(storyId: string) { + return `storybook-story-${storyId}`; +} export function setProjectAnnotations( projectAnnotations: ProjectAnnotations | ProjectAnnotations[] ) { const annotations = Array.isArray(projectAnnotations) ? projectAnnotations : [projectAnnotations]; - GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS = composeConfigs(annotations); + globalProjectAnnotations = composeConfigs(annotations); } export function composeStory( storyAnnotations: LegacyStoryAnnotationsOrFn, componentAnnotations: ComponentAnnotations, - projectAnnotations: ProjectAnnotations = GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS as ProjectAnnotations, - defaultConfig: ProjectAnnotations = {}, + projectAnnotations?: ProjectAnnotations, + defaultConfig?: ProjectAnnotations, exportsName?: string ): ComposedStoryFn> { if (storyAnnotations === undefined) { + // eslint-disable-next-line local-rules/no-uncategorized-errors throw new Error('Expected a story but received undefined.'); } @@ -54,7 +60,7 @@ export function composeStory( storyName, @@ -62,10 +68,9 @@ export function composeStory({ - ...projectAnnotations, - ...defaultConfig, - }); + const normalizedProjectAnnotations = normalizeProjectAnnotations( + composeConfigs([defaultConfig ?? {}, globalProjectAnnotations, projectAnnotations ?? {}]) + ); const story = prepareStory( normalizedStory, @@ -73,11 +78,14 @@ export function composeStory = { hooks: new HooksContext(), - globals: defaultGlobals, + globals: { + ...globalsFromGlobalTypes, + ...normalizedProjectAnnotations.globals, + }, args: { ...story.initialArgs }, viewMode: 'story', loaded: {}, @@ -86,28 +94,39 @@ export function composeStory>) => + story.playFunction!({ + ...context, + ...extraContext, + // if canvasElement is not provided, we default to the root element, which comes from a decorator + // the decorator has to be implemented in the defaultAnnotations of each integrator of portable stories + canvasElement: + extraContext?.canvasElement ?? + globalThis.document?.getElementById(getPortableStoryWrapperId(context.id)), + }) + : undefined; + const composedStory: ComposedStoryFn> = Object.assign( - (extraArgs?: Partial) => { - const finalContext: StoryContext = { - ...context, - args: { ...context.initialArgs, ...extraArgs }, + function storyFn(extraArgs?: Partial) { + context.args = { + ...context.initialArgs, + ...extraArgs, }; - return story.unboundStoryFn(prepareContext(finalContext)); + return story.unboundStoryFn(prepareContext(context)); }, { + id: story.id, storyName, + load: async () => { + const loadedContext = await story.applyLoaders(context); + context.loaded = loadedContext.loaded; + }, args: story.initialArgs as Partial, parameters: story.parameters as Parameters, argTypes: story.argTypes as StrictArgTypes, - id: story.id, - play: story.playFunction - ? ((async (extraContext: ComposedStoryPlayContext) => - story.playFunction!({ - ...context, - ...extraContext, - })) as unknown as ComposedStoryPlayFn>) - : undefined, + play: playFunction as ComposedStoryPlayFn | undefined, } ); @@ -119,7 +138,6 @@ export function composeStories( globalConfig: ProjectAnnotations, composeStoryFn: ComposeStoryFn ) { - // eslint-disable-next-line @typescript-eslint/naming-convention const { default: meta, __esModule, __namedExportsOrder, ...stories } = storiesImport; const composedStories = Object.entries(stories).reduce((storiesMap, [exportsName, story]) => { if (!isExportStory(exportsName, meta)) { diff --git a/code/lib/preview/package.json b/code/lib/preview/package.json index 46e5268ce14d..cea251ed2dad 100644 --- a/code/lib/preview/package.json +++ b/code/lib/preview/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preview", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "", "keywords": [ "storybook" diff --git a/code/lib/react-dom-shim/package.json b/code/lib/react-dom-shim/package.json index b52b16c6676c..417322b679f2 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": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "", "keywords": [ "storybook" diff --git a/code/lib/router/package.json b/code/lib/router/package.json index f36dbb2ea0cf..28bc6525c2d2 100644 --- a/code/lib/router/package.json +++ b/code/lib/router/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/router", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Core Storybook Router", "keywords": [ "storybook" diff --git a/code/lib/source-loader/package.json b/code/lib/source-loader/package.json index 79599a15b4ef..634c33cf3d8c 100644 --- a/code/lib/source-loader/package.json +++ b/code/lib/source-loader/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/source-loader", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Source loader", "keywords": [ "lib", diff --git a/code/lib/telemetry/package.json b/code/lib/telemetry/package.json index 0aa31581984d..bacd55a2207c 100644 --- a/code/lib/telemetry/package.json +++ b/code/lib/telemetry/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/telemetry", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Telemetry logging for crash reports and usage statistics", "keywords": [ "storybook" diff --git a/code/lib/test/package.json b/code/lib/test/package.json index e458d9df8022..5258ec216e55 100644 --- a/code/lib/test/package.json +++ b/code/lib/test/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/test", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "", "keywords": [ "storybook" diff --git a/code/lib/theming/package.json b/code/lib/theming/package.json index 6d35da27de96..dc6f964b604e 100644 --- a/code/lib/theming/package.json +++ b/code/lib/theming/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/theming", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Core Storybook Components", "keywords": [ "storybook" diff --git a/code/lib/types/package.json b/code/lib/types/package.json index d3b9a9685708..96f6089b73dc 100644 --- a/code/lib/types/package.json +++ b/code/lib/types/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/types", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Core Storybook TS Types", "keywords": [ "storybook" diff --git a/code/lib/types/src/modules/composedStory.ts b/code/lib/types/src/modules/composedStory.ts index 5ce61bc678e8..b0a7bff6c374 100644 --- a/code/lib/types/src/modules/composedStory.ts +++ b/code/lib/types/src/modules/composedStory.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import type { Renderer, StoryId, StrictArgTypes } from '@storybook/csf'; +import type { PlayFunction, Renderer, StoryId, StrictArgTypes } from '@storybook/csf'; import type { AnnotatedStoryFn, @@ -9,7 +9,6 @@ import type { Parameters, StoryAnnotations, StoryAnnotationsOrFn, - StoryContext, } from './csf'; import type { ProjectAnnotations } from './story'; @@ -22,23 +21,6 @@ export type Store_CSFExports) // or PrimaryButton() - * PrimaryButton.play({ canvasElement: container }) - */ -export type ComposedStoryPlayContext = Partial< - StoryContext & Pick, 'canvasElement'> ->; - -export type ComposedStoryPlayFn = ( - context: ComposedStoryPlayContext -) => Promise | void; - /** * A story function with partial args, used internally by composeStory */ @@ -48,6 +30,14 @@ export type PartialArgsStoryFn = T extends (...args: infer P) => infer R + ? (...args: { [K in keyof P]?: Partial }) => R + : never; + +export type ComposedStoryPlayFn< + TRenderer extends Renderer = Renderer, + TArgs = Args, +> = MakeAllParametersOptional>>; /** * A story that got recomposed for portable stories, containing all the necessary data to be rendered in external environments */ @@ -55,9 +45,10 @@ export type ComposedStoryFn< TRenderer extends Renderer = Renderer, TArgs = Args, > = PartialArgsStoryFn & { - play: ComposedStoryPlayFn | undefined; args: TArgs; id: StoryId; + play?: ComposedStoryPlayFn; + load: () => Promise; storyName: string; parameters: Parameters; argTypes: StrictArgTypes; diff --git a/code/package.json b/code/package.json index a9cb89380c79..b73a51626082 100644 --- a/code/package.json +++ b/code/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/root", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "private": true, "description": "Storybook root", "homepage": "https://storybook.js.org/", @@ -214,7 +214,7 @@ "react-dom": "^18.2.0", "semver": "^7.3.7", "serve-static": "^1.14.1", - "svelte": "^5.0.0-next.28", + "svelte": "^5.0.0-next.65", "trash": "^7.0.0", "ts-dedent": "^2.0.0", "ts-node": "^10.9.1", diff --git a/code/presets/create-react-app/package.json b/code/presets/create-react-app/package.json index 7f5a708d578a..5275fade6100 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": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for Create React App preset", "keywords": [ "storybook" diff --git a/code/presets/html-webpack/package.json b/code/presets/html-webpack/package.json index 4ec37f3917a5..c85346ee246a 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": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for HTML: View HTML snippets in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/presets/preact-webpack/package.json b/code/presets/preact-webpack/package.json index a55d8938b8d7..0edddb4d473d 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": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for Preact: Develop Preact Component in isolation.", "keywords": [ "storybook" diff --git a/code/presets/react-webpack/package.json b/code/presets/react-webpack/package.json index 90b90e2bad23..b3f4377f9da2 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": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for React: Develop React Component in isolation with Hot Reloading", "keywords": [ "storybook" @@ -71,10 +71,13 @@ "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0", "@types/node": "^18.0.0", "@types/semver": "^7.3.4", + "find-up": "^5.0.0", "fs-extra": "^11.1.0", "magic-string": "^0.30.5", "react-docgen": "^7.0.0", + "resolve": "^1.22.8", "semver": "^7.3.7", + "tsconfig-paths": "^4.2.0", "webpack": "5" }, "devDependencies": { diff --git a/code/presets/react-webpack/src/loaders/docgen-resolver.ts b/code/presets/react-webpack/src/loaders/docgen-resolver.ts new file mode 100644 index 000000000000..71e2bde5d742 --- /dev/null +++ b/code/presets/react-webpack/src/loaders/docgen-resolver.ts @@ -0,0 +1,74 @@ +import { extname } from 'path'; +import resolve from 'resolve'; + +export class ReactDocgenResolveError extends Error { + // the magic string that react-docgen uses to check if a module is ignored + readonly code = 'MODULE_NOT_FOUND'; + + constructor(filename: string) { + super(`'${filename}' was ignored by react-docgen.`); + } +} + +/* The below code was copied from: + * https://github.com/reactjs/react-docgen/blob/df2daa8b6f0af693ecc3c4dc49f2246f60552bcb/packages/react-docgen/src/importer/makeFsImporter.ts#L14-L63 + * because it wasn't exported from the react-docgen package. + * watch out: when updating this code, also update the code in code/frameworks/react-vite/src/plugins/docgen-resolver.ts + */ + +// These extensions are sorted by priority +// resolve() will check for files in the order these extensions are sorted +export const RESOLVE_EXTENSIONS = [ + '.js', + '.cts', // These were originally not in the code, I added them + '.mts', // These were originally not in the code, I added them + '.ctsx', // These were originally not in the code, I added them + '.mtsx', // These were originally not in the code, I added them + '.ts', + '.tsx', + '.mjs', + '.cjs', + '.mts', + '.cts', + '.jsx', +]; + +export function defaultLookupModule(filename: string, basedir: string): string { + const resolveOptions = { + basedir, + extensions: RESOLVE_EXTENSIONS, + // we do not need to check core modules as we cannot import them anyway + includeCoreModules: false, + }; + + try { + return resolve.sync(filename, resolveOptions); + } catch (error) { + const ext = extname(filename); + let newFilename: string; + + // if we try to import a JavaScript file it might be that we are actually pointing to + // a TypeScript file. This can happen in ES modules as TypeScript requires to import other + // TypeScript files with .js extensions + // https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions + switch (ext) { + case '.js': + case '.mjs': + case '.cjs': + newFilename = `${filename.slice(0, -2)}ts`; + break; + + case '.jsx': + newFilename = `${filename.slice(0, -3)}tsx`; + break; + default: + throw error; + } + + return resolve.sync(newFilename, { + ...resolveOptions, + // we already know that there is an extension at this point, so no need to check other extensions + extensions: [], + }); + } +} diff --git a/code/presets/react-webpack/src/loaders/react-docgen-loader.test.ts b/code/presets/react-webpack/src/loaders/react-docgen-loader.test.ts new file mode 100644 index 000000000000..cb017a7469b7 --- /dev/null +++ b/code/presets/react-webpack/src/loaders/react-docgen-loader.test.ts @@ -0,0 +1,52 @@ +import { getReactDocgenImporter } from './react-docgen-loader'; +import { describe, it, expect, vi } from 'vitest'; + +const reactDocgenMock = vi.hoisted(() => { + return { + makeFsImporter: vi.fn().mockImplementation((fn) => fn), + }; +}); + +const reactDocgenResolverMock = vi.hoisted(() => { + return { + defaultLookupModule: vi.fn(), + }; +}); + +vi.mock('./docgen-resolver', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + defaultLookupModule: reactDocgenResolverMock.defaultLookupModule, + }; +}); + +vi.mock('react-docgen', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + makeFsImporter: reactDocgenMock.makeFsImporter, + }; +}); + +describe('getReactDocgenImporter function', () => { + it('should not map the request if a tsconfig path mapping is not available', () => { + const filename = './src/components/Button.tsx'; + const basedir = '/src'; + const imported = getReactDocgenImporter(undefined); + reactDocgenResolverMock.defaultLookupModule.mockImplementation((filen: string) => filen); + const result = (imported as any)(filename, basedir); + expect(result).toBe(filename); + }); + + it('should map the request', () => { + const mappedFile = './mapped-file.tsx'; + const matchPath = vi.fn().mockReturnValue(mappedFile); + const filename = './src/components/Button.tsx'; + const basedir = '/src'; + const imported = getReactDocgenImporter(matchPath); + reactDocgenResolverMock.defaultLookupModule.mockImplementation((filen: string) => filen); + const result = (imported as any)(filename, basedir); + expect(result).toBe(mappedFile); + }); +}); diff --git a/code/presets/react-webpack/src/loaders/react-docgen-loader.ts b/code/presets/react-webpack/src/loaders/react-docgen-loader.ts index fd22ca12bf5d..15b71f19bfd5 100644 --- a/code/presets/react-webpack/src/loaders/react-docgen-loader.ts +++ b/code/presets/react-webpack/src/loaders/react-docgen-loader.ts @@ -2,15 +2,23 @@ import { parse, builtinResolvers as docgenResolver, builtinHandlers as docgenHandlers, - builtinImporters as docgenImporters, + makeFsImporter, ERROR_CODES, utils, } from 'react-docgen'; +import * as TsconfigPaths from 'tsconfig-paths'; +import findUp from 'find-up'; import MagicString from 'magic-string'; import type { LoaderContext } from 'webpack'; import type { Handler, NodePath, babelTypes as t, Documentation } from 'react-docgen'; import { logger } from '@storybook/node-logger'; +import { + RESOLVE_EXTENSIONS, + ReactDocgenResolveError, + defaultLookupModule, +} from './docgen-resolver'; + const { getNameOrValue, isReactForwardRefCall } = utils; const actualNameHandler: Handler = function actualNameHandler(documentation, componentDefinition) { @@ -54,9 +62,11 @@ type DocObj = Documentation & { actualName: string }; const defaultHandlers = Object.values(docgenHandlers).map((handler) => handler); const defaultResolver = new docgenResolver.FindExportedDefinitionsResolver(); -const defaultImporter = docgenImporters.fsImporter; const handlers = [...defaultHandlers, actualNameHandler]; +let tsconfigPathsInitialized = false; +let matchPath: TsconfigPaths.MatchPath | undefined; + export default async function reactDocgenLoader( this: LoaderContext<{ debug: boolean }>, source: string @@ -66,12 +76,28 @@ export default async function reactDocgenLoader( const options = this.getOptions() || {}; const { debug = false } = options; + if (!tsconfigPathsInitialized) { + const tsconfigPath = await findUp('tsconfig.json', { cwd: process.cwd() }); + const tsconfig = TsconfigPaths.loadConfig(tsconfigPath); + + if (tsconfig.resultType === 'success') { + logger.info('Using tsconfig paths for react-docgen'); + matchPath = TsconfigPaths.createMatchPath(tsconfig.absoluteBaseUrl, tsconfig.paths, [ + 'browser', + 'module', + 'main', + ]); + } + + tsconfigPathsInitialized = true; + } + try { const docgenResults = parse(source, { filename: this.resourcePath, resolver: defaultResolver, handlers, - importer: defaultImporter, + importer: getReactDocgenImporter(matchPath), babelOptions: { babelrc: false, configFile: false, @@ -109,3 +135,24 @@ export default async function reactDocgenLoader( } } } + +export function getReactDocgenImporter(matchingPath: TsconfigPaths.MatchPath | undefined) { + return makeFsImporter((filename, basedir) => { + const mappedFilenameByPaths = (() => { + if (matchingPath) { + const match = matchingPath(filename); + return match || filename; + } else { + return filename; + } + })(); + + const result = defaultLookupModule(mappedFilenameByPaths, basedir); + + if (RESOLVE_EXTENSIONS.find((ext) => result.endsWith(ext))) { + return result; + } + + throw new ReactDocgenResolveError(filename); + }); +} diff --git a/code/presets/server-webpack/package.json b/code/presets/server-webpack/package.json index e4156c9429ec..0c57f088153d 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": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for Server: View HTML snippets from a server in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/presets/svelte-webpack/package.json b/code/presets/svelte-webpack/package.json index 0a60ecd47035..d80d06b70c22 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": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for Svelte: Develop Svelte Component in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -75,7 +75,7 @@ "typescript": "^5.3.2" }, "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.16", + "svelte": "^4.0.0 || ^5.0.0-next.65", "svelte-loader": "*" }, "engines": { diff --git a/code/presets/vue3-webpack/package.json b/code/presets/vue3-webpack/package.json index 2b68be009e5a..774e7247eafb 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": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook for Vue 3: Develop Vue 3 Components in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/renderers/html/package.json b/code/renderers/html/package.json index fe260db27f71..ce6c017145aa 100644 --- a/code/renderers/html/package.json +++ b/code/renderers/html/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/html", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook HTML renderer", "keywords": [ "storybook" diff --git a/code/renderers/preact/package.json b/code/renderers/preact/package.json index 4f6664fe7cc8..685eb8e50aaa 100644 --- a/code/renderers/preact/package.json +++ b/code/renderers/preact/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preact", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook Preact renderer", "keywords": [ "storybook" diff --git a/code/renderers/react/package.json b/code/renderers/react/package.json index 34f768ba1096..de60e151b77c 100644 --- a/code/renderers/react/package.json +++ b/code/renderers/react/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook React renderer", "keywords": [ "storybook" diff --git a/code/renderers/react/src/__test__/Button.stories.tsx b/code/renderers/react/src/__test__/Button.stories.tsx index 6882b957b136..277f92ddde1f 100644 --- a/code/renderers/react/src/__test__/Button.stories.tsx +++ b/code/renderers/react/src/__test__/Button.stories.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { within, userEvent } from '@storybook/testing-library'; +import { within, userEvent, fn, expect } from '@storybook/test'; import type { StoryFn as CSF2Story, StoryObj as CSF3Story, Meta } from '..'; import type { ButtonProps } from './Button'; @@ -33,14 +33,21 @@ const getCaptionForLocale = (locale: string) => { return 'μ•ˆλ…•ν•˜μ„Έμš”!'; case 'pt': return 'OlΓ‘!'; - default: + case 'en': return 'Hello!'; + default: + return undefined; } }; export const CSF2StoryWithLocale: CSF2Story = (args, { globals: { locale } }) => { const caption = getCaptionForLocale(locale); - return ; + return ( + <> +

locale: {locale}

+ + + ); }; CSF2StoryWithLocale.storyName = 'WithLocale'; @@ -84,7 +91,36 @@ export const CSF3InputFieldFilled: CSF3Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Step label', async () => { - await userEvent.type(canvas.getByTestId('input'), 'Hello world!'); + const inputEl = canvas.getByTestId('input'); + await userEvent.type(inputEl, 'Hello world!'); + await expect(inputEl).toHaveValue('Hello world!'); }); }, }; + +const mockFn = fn(); +export const LoaderStory: CSF3Story<{ mockFn: (val: string) => string }> = { + args: { + mockFn, + }, + loaders: [ + async () => { + mockFn.mockReturnValueOnce('mockFn return value'); + return { + value: 'loaded data', + }; + }, + ], + render: (args, { loaded }) => { + const data = args.mockFn('render'); + return ( +
+
{loaded.value}
+
{String(data)}
+
+ ); + }, + play: async () => { + expect(mockFn).toHaveBeenCalledWith('render'); + }, +}; diff --git a/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap b/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap index 2b92b1d68424..2779b21001ab 100644 --- a/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap +++ b/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap @@ -3,94 +3,135 @@ exports[`Renders CSF2Secondary story 1`] = `
- + +
`; -exports[`Renders CSF2StoryWithLocale story 1`] = ` +exports[`Renders CSF2StoryWithParamsAndDecorator story 1`] = `
- + +
`; -exports[`Renders CSF2StoryWithParamsAndDecorator story 1`] = ` +exports[`Renders CSF3Button story 1`] = `
- + +
`; -exports[`Renders CSF3Button story 1`] = ` +exports[`Renders CSF3ButtonWithRender story 1`] = `
- +
+

+ I am a custom render function +

+ +
+
`; -exports[`Renders CSF3ButtonWithRender story 1`] = ` +exports[`Renders CSF3InputFieldFilled story 1`] = `
-
-

- I am a custom render function -

- +
+
`; -exports[`Renders CSF3InputFieldFilled story 1`] = ` +exports[`Renders CSF3Primary story 1`] = `
- +
+ +
`; -exports[`Renders CSF3Primary story 1`] = ` +exports[`Renders LoaderStory story 1`] = `
- +
+
+ loaded data +
+
+ mockFn return value +
+
+
`; diff --git a/code/renderers/react/src/__test__/portable-stories.test.tsx b/code/renderers/react/src/__test__/portable-stories.test.tsx index afa0b70142e4..f8aae6b849f4 100644 --- a/code/renderers/react/src/__test__/portable-stories.test.tsx +++ b/code/renderers/react/src/__test__/portable-stories.test.tsx @@ -1,5 +1,5 @@ -import { vi, it, expect, afterEach, describe } from 'vitest'; import React from 'react'; +import { vi, it, expect, afterEach, describe } from 'vitest'; import { render, screen, cleanup } from '@testing-library/react'; import { addons } from '@storybook/preview-api'; import type { Meta } from '@storybook/react'; @@ -10,7 +10,7 @@ import type { Button } from './Button'; import * as stories from './Button.stories'; // example with composeStories, returns an object with all stories composed with args/decorators -const { CSF3Primary } = composeStories(stories); +const { CSF3Primary, LoaderStory } = composeStories(stories); // example with composeStory, returns a single story composed with args/decorators const Secondary = composeStory(stories.CSF2Secondary, stories.default); @@ -44,6 +44,15 @@ describe('renders', () => { const buttonElement = getByText(/foo/i); expect(buttonElement).not.toBeNull(); }); + + it('should call and compose loaders data', async () => { + await LoaderStory.load(); + const { getByTestId } = render(); + expect(getByTestId('spy-data').textContent).toEqual('mockFn return value'); + expect(getByTestId('loaded-data').textContent).toEqual('loaded data'); + // spy assertions happen in the play function and should work + await LoaderStory.play!(); + }); }); describe('projectAnnotations', () => { @@ -52,15 +61,24 @@ describe('projectAnnotations', () => { }); it('renders with default projectAnnotations', () => { + setProjectAnnotations([ + { + parameters: { injected: true }, + globalTypes: { + locale: { defaultValue: 'en' }, + }, + }, + ]); const WithEnglishText = composeStory(stories.CSF2StoryWithLocale, stories.default); const { getByText } = render(); const buttonElement = getByText('Hello!'); expect(buttonElement).not.toBeNull(); + expect(WithEnglishText.parameters?.injected).toBe(true); }); it('renders with custom projectAnnotations via composeStory params', () => { const WithPortugueseText = composeStory(stories.CSF2StoryWithLocale, stories.default, { - globalTypes: { locale: { defaultValue: 'pt' } } as any, + globals: { locale: 'pt' }, }); const { getByText } = render(); const buttonElement = getByText('OlΓ‘!'); @@ -94,7 +112,18 @@ describe('CSF3', () => { expect(screen.getByTestId('custom-render')).not.toBeNull(); }); - it('renders with play function', async () => { + it('renders with play function without canvas element', async () => { + const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default); + + render(); + + await CSF3InputFieldFilled.play!(); + + const input = screen.getByTestId('input') as HTMLInputElement; + expect(input.value).toEqual('Hello world!'); + }); + + it('renders with play function with canvas element', async () => { const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default); const { container } = render(); @@ -139,9 +168,20 @@ describe('ComposeStories types', () => { }); // Batch snapshot testing -const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName, Story]); +const testCases = Object.values(composeStories(stories)).map( + (Story) => [Story.storyName, Story] as [string, typeof Story] +); it.each(testCases)('Renders %s story', async (_storyName, Story) => { cleanup(); - const tree = await render(); - expect(tree.baseElement).toMatchSnapshot(); + + if (_storyName === 'CSF2StoryWithLocale') { + return; + } + + await Story.load(); + + const { baseElement } = await render(); + + await Story.play?.(); + expect(baseElement).toMatchSnapshot(); }); diff --git a/code/renderers/react/src/portable-stories.ts b/code/renderers/react/src/portable-stories.tsx similarity index 93% rename from code/renderers/react/src/portable-stories.ts rename to code/renderers/react/src/portable-stories.tsx index 385e0dc4c804..3493e0f3b2e5 100644 --- a/code/renderers/react/src/portable-stories.ts +++ b/code/renderers/react/src/portable-stories.tsx @@ -1,7 +1,9 @@ +import React from 'react'; import { composeStory as originalComposeStory, composeStories as originalComposeStories, setProjectAnnotations as originalSetProjectAnnotations, + getPortableStoryWrapperId, } from '@storybook/preview-api'; import type { Args, @@ -11,7 +13,7 @@ import type { StoriesWithPartialProps, } from '@storybook/types'; -import { render } from './render'; +import * as reactProjectAnnotations from './entry-preview'; import type { Meta } from './public-types'; import type { ReactRenderer } from './types'; @@ -38,7 +40,16 @@ export function setProjectAnnotations( // This will not be necessary once we have auto preset loading const defaultProjectAnnotations: ProjectAnnotations = { - render, + ...reactProjectAnnotations, + decorators: [ + function addStorybookId(StoryFn, { id }) { + return ( +
+ +
+ ); + }, + ], }; /** diff --git a/code/renderers/react/template/stories/docgen-components/imported.module.css b/code/renderers/react/template/stories/docgen-components/imported.module.css new file mode 100644 index 000000000000..a15c877ac01f --- /dev/null +++ b/code/renderers/react/template/stories/docgen-components/imported.module.css @@ -0,0 +1,3 @@ +.foo { + color: red; +} diff --git a/code/renderers/react/template/stories/docgen-components/ts-function-component/input.tsx b/code/renderers/react/template/stories/docgen-components/ts-function-component/input.tsx index 9f0c2598ff07..193c2e2c71ae 100644 --- a/code/renderers/react/template/stories/docgen-components/ts-function-component/input.tsx +++ b/code/renderers/react/template/stories/docgen-components/ts-function-component/input.tsx @@ -1,6 +1,9 @@ import React from 'react'; import { imported } from '../imported'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore (css import not supported in TS) +import styles from '../imported.module.css'; const local = 'local-value'; @@ -26,6 +29,7 @@ interface PropsWriterProps { importedReference?: string; globalReference?: any; stringGlobalName?: string; + myClass: typeof styles.foo; } /** @@ -47,6 +51,7 @@ PropsWriter.defaultProps = { importedReference: imported, globalReference: Date, stringGlobalName: 'top', + myClass: styles.foo, }; export const component = PropsWriter; diff --git a/code/renderers/server/package.json b/code/renderers/server/package.json index 8981a651a312..f4c892e2a87a 100644 --- a/code/renderers/server/package.json +++ b/code/renderers/server/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/server", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook Server renderer", "keywords": [ "storybook" diff --git a/code/renderers/svelte/package.json b/code/renderers/svelte/package.json index 70c43a4169e2..da7ad794fca2 100644 --- a/code/renderers/svelte/package.json +++ b/code/renderers/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/svelte", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook Svelte renderer", "keywords": [ "storybook" @@ -30,16 +30,15 @@ "./dist/entry-preview.mjs": "./dist/entry-preview.mjs", "./dist/entry-preview-docs.mjs": "./dist/entry-preview-docs.mjs", "./package.json": "./package.json", - "./templates/HOC.svelte": "./templates/HOC.svelte", - "./templates/PreviewRender.svelte": "./templates/PreviewRender.svelte", - "./templates/SlotDecorator.svelte": "./templates/SlotDecorator.svelte" + "./internal/PreviewRender.svelte": "./dist/components/PreviewRender.svelte", + "./internal/SlotDecorator.svelte": "./dist/components/SlotDecorator.svelte", + "./internal/createSvelte5Props": "./dist/createSvelte5Props.svelte.js" }, "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", "files": [ "dist/**/*", - "templates/**/*", "template/cli/**/*", "README.md", "*.js", @@ -62,14 +61,15 @@ "type-fest": "~2.19" }, "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.1", + "@sveltejs/vite-plugin-svelte": "^3.0.2", "expect-type": "^0.15.0", - "svelte": "^5.0.0-next.28", - "svelte-check": "^3.6.1", + "fs-extra": "^11.1.0", + "svelte": "^5.0.0-next.65", + "svelte-check": "^3.6.4", "typescript": "^5.3.2" }, "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.16" + "svelte": "^4.0.0 || ^5.0.0-next.65" }, "engines": { "node": ">=18.0.0" @@ -78,6 +78,7 @@ "access": "public" }, "bundler": { + "post": "./scripts/copy-unbundled-to-dist.ts", "entries": [ "./src/index.ts", "./src/preset.ts", diff --git a/code/renderers/svelte/scripts/copy-unbundled-to-dist.ts b/code/renderers/svelte/scripts/copy-unbundled-to-dist.ts new file mode 100644 index 000000000000..a82964679960 --- /dev/null +++ b/code/renderers/svelte/scripts/copy-unbundled-to-dist.ts @@ -0,0 +1,23 @@ +import { copy } from 'fs-extra'; +import { join } from 'path'; + +const src = join(__dirname, '..', 'src'); +const dist = join(__dirname, '..', 'dist'); + +// relative to src directory +const PATHS_TO_COPY = ['createSvelte5Props.svelte.js', 'components']; + +const run = async () => { + console.log('Copying unbundled files to dist...'); + await Promise.all( + PATHS_TO_COPY.map((pathToCopy) => + copy(join(src, pathToCopy), join(dist, pathToCopy), { overwrite: true }) + ) + ); + console.log('Done!'); +}; + +run().catch((e) => { + console.error(e); + process.exitCode = 1; +}); diff --git a/code/renderers/svelte/templates/PreviewRender.svelte b/code/renderers/svelte/src/components/PreviewRender.svelte similarity index 69% rename from code/renderers/svelte/templates/PreviewRender.svelte rename to code/renderers/svelte/src/components/PreviewRender.svelte index 581aa4d78e05..b2900a0ddb24 100644 --- a/code/renderers/svelte/templates/PreviewRender.svelte +++ b/code/renderers/svelte/src/components/PreviewRender.svelte @@ -3,7 +3,7 @@ import { dedent } from 'ts-dedent'; export let name; - export let kind; + export let title; export let storyFn; export let showError; export let storyContext; @@ -17,22 +17,22 @@ on, } = storyFn(); - let firstTime = true; - - // the first time we don't want to call storyFn two times so we just return the values - // we already have from the previous call. If storyFn changes this function will run - // again but this time firstTime will be false - function getStoryFnValue(storyFn){ - if(firstTime){ - firstTime = false; - return { - Component, - props, - on, - } - } - return storyFn(); - } + let firstTime = true; + + // the first time we don't want to call storyFn two times so we just return the values + // we already have from the previous call. If storyFn changes this function will run + // again but this time firstTime will be false + function getStoryFnValue(storyFn) { + if (firstTime) { + firstTime = false; + return { + Component, + props, + on, + }; + } + return storyFn(); + } // reactive, re-render on storyFn change $: ({ Component, props = {}, on } = getStoryFnValue(storyFn)); @@ -45,7 +45,7 @@ if (!Component) { showError({ - title: `Expecting a Svelte component from the story: "${name}" of "${kind}".`, + title: `Expecting a Svelte component from the story: "${name}" of "${title}".`, description: dedent` Did you forget to return the Svelte component configuration from the story? Use "() => ({ Component: YourComponent, props: {} })" diff --git a/code/renderers/svelte/templates/SlotDecorator.svelte b/code/renderers/svelte/src/components/SlotDecorator.svelte similarity index 100% rename from code/renderers/svelte/templates/SlotDecorator.svelte rename to code/renderers/svelte/src/components/SlotDecorator.svelte diff --git a/code/renderers/svelte/src/createSvelte5Props.svelte.js b/code/renderers/svelte/src/createSvelte5Props.svelte.js new file mode 100644 index 000000000000..bbefc19bbf43 --- /dev/null +++ b/code/renderers/svelte/src/createSvelte5Props.svelte.js @@ -0,0 +1,13 @@ +/** + * Turns an object into reactive props in Svelte 5. + * Needs to be in a separate .svelte.js file to ensure Svelte + * compiles it. + * As proposed in https://github.com/sveltejs/svelte/issues/9827#issuecomment-1845589616 + * @template TProps + * @param {TProps} data - The data to create Svelte 5 props from. + * @returns {TProps} - The created Svelte 5 props. + */ +export const createSvelte5Props = (data) => { + const props = $state(data); + return props; +}; diff --git a/code/renderers/svelte/src/decorators.ts b/code/renderers/svelte/src/decorators.ts index 07fb1753b56d..657fa91ff28e 100644 --- a/code/renderers/svelte/src/decorators.ts +++ b/code/renderers/svelte/src/decorators.ts @@ -1,12 +1,13 @@ import type { DecoratorFunction, StoryContext, LegacyStoryFn } from '@storybook/types'; import { sanitizeStoryContextUpdate } from '@storybook/preview-api'; -// ! DO NOT change this SlotDecorator import to a relative path, it will break it. -// ! A relative import will be compiled at build time, and Svelte will be unable to -// ! render the component together with the user's Svelte components -// ! importing from @storybook/svelte will make sure that it is compiled at runtime -// ! with the same bundle as the user's Svelte components -// eslint-disable-next-line import/no-extraneous-dependencies -import SlotDecorator from '@storybook/svelte/templates/SlotDecorator.svelte'; +/* +! DO NOT change this SlotDecorator import to a relative path, it will break it. +! A relative import will be compiled at build time, and Svelte will be unable to +! render the component together with the user's Svelte components +! importing from @storybook/svelte will make sure that it is compiled at runtime +! with the same bundle as the user's Svelte components +*/ +import SlotDecorator from '@storybook/svelte/internal/SlotDecorator.svelte'; import type { SvelteRenderer } from './types'; /** diff --git a/code/renderers/svelte/src/render.ts b/code/renderers/svelte/src/render.ts index 514b3128ed21..28552379bf90 100644 --- a/code/renderers/svelte/src/render.ts +++ b/code/renderers/svelte/src/render.ts @@ -1,50 +1,31 @@ import type { RenderContext, ArgsStoryFn } from '@storybook/types'; import { RESET_STORY_ARGS } from '@storybook/core-events'; -// ! DO NOT change this PreviewRender import to a relative path, it will break it. -// ! A relative import will be compiled at build time, and Svelte will be unable to -// ! render the component together with the user's Svelte components -// ! importing from @storybook/svelte will make sure that it is compiled at runtime -// ! with the same bundle as the user's Svelte components -// eslint-disable-next-line import/no-extraneous-dependencies -import PreviewRender from '@storybook/svelte/templates/PreviewRender.svelte'; +/* +! DO NOT change these PreviewRender and createSvelte5Props imports to relative paths, it will break them. +! Relative imports will be compiled at build time by tsup, but we need Svelte to compile them +! when compiling the rest of the Svelte files. +*/ +import PreviewRender from '@storybook/svelte/internal/PreviewRender.svelte'; +// @ts-expect-error Don't know why TS doesn't pick up the types export here +import { createSvelte5Props } from '@storybook/svelte/internal/createSvelte5Props'; + import { addons } from '@storybook/preview-api'; import * as svelte from 'svelte'; +import { VERSION as SVELTE_VERSION } from 'svelte/compiler'; import type { SvelteRenderer } from './types'; -const componentsByDomElement = new Map< - SvelteRenderer['canvasElement'], - any // ReturnType depends on the version of Svelte v4 or v5 ->(); - -function teardown(canvasElement: SvelteRenderer['canvasElement']) { - if (!componentsByDomElement.has(canvasElement)) { - return; - } - - componentsByDomElement.get(canvasElement)!.$destroy(); - - canvasElement.innerHTML = ''; - componentsByDomElement.delete(canvasElement); -} +const IS_SVELTE_V4 = Number(SVELTE_VERSION[0]) <= 4; -/** - * Mount the PreviewRender component to the provided canvasElement - * Either using the Svelte v4 or v5 API - */ -function createRoot(target: HTMLElement, props: any) { - if ((svelte as any).createRoot) { - // Svelte v5 - return (svelte as any).createRoot(PreviewRender, { - target, - props, - }); +export function renderToCanvas( + renderContext: RenderContext, + canvasElement: SvelteRenderer['canvasElement'] +) { + if (IS_SVELTE_V4) { + return renderToCanvasV4(renderContext, canvasElement); + } else { + return renderToCanvasV5(renderContext, canvasElement); } - // Svelte v4 - return new (PreviewRender as any)({ - target, - props, - }); } /** @@ -61,10 +42,12 @@ addons.getChannel().on(RESET_STORY_ARGS, ({ storyId }) => { storyIdsToRemountFromResetArgsEvent.add(storyId); }); -export function renderToCanvas( +const componentsByDomElementV4 = new Map(); + +function renderToCanvasV4( { storyFn, - kind, + title, name, showMain, showError, @@ -73,43 +56,123 @@ export function renderToCanvas( }: RenderContext, canvasElement: SvelteRenderer['canvasElement'] ) { - const existingComponent = componentsByDomElement.get(canvasElement); + function unmount(canvasElementToUnmount: SvelteRenderer['canvasElement']) { + if (!componentsByDomElementV4.has(canvasElementToUnmount)) { + return; + } + componentsByDomElementV4.get(canvasElementToUnmount)!.$destroy(); + componentsByDomElementV4.delete(canvasElementToUnmount); + canvasElementToUnmount.innerHTML = ''; + } + const existingComponent = componentsByDomElementV4.get(canvasElement); let remount = forceRemount; + if (storyIdsToRemountFromResetArgsEvent.has(storyContext.id)) { + remount = true; + storyIdsToRemountFromResetArgsEvent.delete(storyContext.id); + } + + if (remount) { + unmount(canvasElement); + } + if (!existingComponent || remount) { + const mountedComponent = new PreviewRender({ + target: canvasElement, + props: { + storyFn, + storyContext, + name, + title, + showError, + }, + }); + componentsByDomElementV4.set(canvasElement, mountedComponent); + } else { + existingComponent.$set({ + storyFn, + storyContext, + name, + title, + showError, + }); + } + + showMain(); + + // unmount the component when the story changes + return () => { + unmount(canvasElement); + }; +} + +const componentsByDomElementV5 = new Map< + SvelteRenderer['canvasElement'], + { mountedComponent: ReturnType<(typeof svelte)['mount']>; props: RenderContext } +>(); + +function renderToCanvasV5( + { + storyFn, + title, + name, + showMain, + showError, + storyContext, + forceRemount, + }: RenderContext, + canvasElement: SvelteRenderer['canvasElement'] +) { + function unmount(canvasElementToUnmount: SvelteRenderer['canvasElement']) { + const { mountedComponent } = componentsByDomElementV5.get(canvasElementToUnmount) ?? {}; + if (!mountedComponent) { + return; + } + svelte.unmount(mountedComponent); + componentsByDomElementV5.delete(canvasElementToUnmount); + } + + const existingComponent = componentsByDomElementV5.get(canvasElement); + + let remount = forceRemount; if (storyIdsToRemountFromResetArgsEvent.has(storyContext.id)) { remount = true; storyIdsToRemountFromResetArgsEvent.delete(storyContext.id); } if (remount) { - teardown(canvasElement); + unmount(canvasElement); } if (!existingComponent || remount) { - const createdComponent = createRoot(canvasElement, { + const props = createSvelte5Props({ storyFn, storyContext, name, - kind, + title, showError, }); - componentsByDomElement.set(canvasElement, createdComponent); + const mountedComponent = svelte.mount(PreviewRender, { + target: canvasElement, + props, + }); + componentsByDomElementV5.set(canvasElement, { mountedComponent, props }); } else { - existingComponent.$set({ + // We need to mutate the existing props for Svelte reactivity to work, we can't just re-assign them + Object.assign(existingComponent.props, { storyFn, storyContext, name, - kind, + title, showError, }); } showMain(); - // teardown the component when the story changes + // unmount the component when the story changes return () => { - teardown(canvasElement); + unmount(canvasElement); }; } diff --git a/code/renderers/svelte/template/stories/args.stories.js b/code/renderers/svelte/template/stories/args.stories.js index 7ecee85a982c..493b5519e6e2 100644 --- a/code/renderers/svelte/template/stories/args.stories.js +++ b/code/renderers/svelte/template/stories/args.stories.js @@ -14,14 +14,16 @@ export default { }; export const RemountOnResetStoryArgs = { - play: async ({ canvasElement, id }) => { + play: async ({ canvasElement, id, step }) => { const canvas = await within(canvasElement); const channel = addons.getChannel(); - // Just to ensure the story is always in a clean state from the beginning, not really part of the test - await channel.emit(RESET_STORY_ARGS, { storyId: id }); - await new Promise((resolve) => { - channel.once(STORY_RENDERED, resolve); + await step('Reset story args', async () => { + // Just to ensure the story is always in a clean state from the beginning, not really part of the test + await channel.emit(RESET_STORY_ARGS, { storyId: id }); + await new Promise((resolve) => { + channel.once(STORY_RENDERED, resolve); + }); }); const button = await canvas.getByRole('button'); await expect(button).toHaveTextContent('You clicked: 0'); @@ -29,16 +31,18 @@ export const RemountOnResetStoryArgs = { await userEvent.click(button); await expect(button).toHaveTextContent('You clicked: 1'); - await channel.emit(UPDATE_STORY_ARGS, { storyId: id, updatedArgs: { text: 'Changed' } }); - await new Promise((resolve) => { - channel.once(STORY_RENDERED, resolve); + await step("Update story args with { text: 'Changed' }", async () => { + await channel.emit(UPDATE_STORY_ARGS, { storyId: id, updatedArgs: { text: 'Changed' } }); + await new Promise((resolve) => { + channel.once(STORY_RENDERED, resolve); + }); }); await expect(button).toHaveTextContent('Changed: 1'); // expect that all state and args are reset after RESET_STORY_ARGS because Svelte needs to remount the component // most other renderers would have 'You clicked: 1' here because they don't remount the component // if this doesn't fully remount it would be 'undefined: 1' because undefined args are used as is in Svelte, and the state is kept - await channel.emit(RESET_STORY_ARGS, { storyId: id }); + await step('Reset story args', () => channel.emit(RESET_STORY_ARGS, { storyId: id })); await waitFor(async () => expect(await within(canvasElement).getByRole('button')).toHaveTextContent('You clicked: 0') ); diff --git a/code/renderers/svelte/template/stories/views/ButtonJavaScript.svelte b/code/renderers/svelte/template/stories/views/ButtonJavaScript.svelte index 23af7eaac0f0..800ee79de0f4 100644 --- a/code/renderers/svelte/template/stories/views/ButtonJavaScript.svelte +++ b/code/renderers/svelte/template/stories/views/ButtonJavaScript.svelte @@ -15,7 +15,7 @@ /** * Displays the count */ - export let count = 0; + let count = 0; /** * Button text diff --git a/code/renderers/svelte/templates/HOC.svelte b/code/renderers/svelte/templates/HOC.svelte deleted file mode 100644 index 0b6c3618701e..000000000000 --- a/code/renderers/svelte/templates/HOC.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - \ No newline at end of file diff --git a/code/renderers/vue3/package.json b/code/renderers/vue3/package.json index 1db67d97fcb5..ec3d8037453a 100644 --- a/code/renderers/vue3/package.json +++ b/code/renderers/vue3/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/vue3", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook Vue 3 renderer", "keywords": [ "storybook" diff --git a/code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts b/code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts index 239416df5c35..0d4623585de0 100644 --- a/code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts +++ b/code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts @@ -1,4 +1,4 @@ -import { userEvent, within } from '@storybook/testing-library'; +import { userEvent, within, expect, fn } from '@storybook/test'; import type { Meta, StoryFn as CSF2Story, StoryObj } from '../..'; import Button from './Button.vue'; @@ -45,8 +45,10 @@ const getCaptionForLocale = (locale: string) => { return 'μ•ˆλ…•ν•˜μ„Έμš”!'; case 'pt': return 'OlΓ‘!'; - default: + case 'en': return 'Hello!'; + default: + return undefined; } }; @@ -58,7 +60,7 @@ export const CSF2StoryWithLocale: CSF2Story = (args, { globals }) => ({ }, template: `

locale: ${globals.locale}

-
`, }); CSF2StoryWithLocale.storyName = 'WithLocale'; @@ -114,7 +116,39 @@ export const CSF3InputFieldFilled: CSF3Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Step label', async () => { - await userEvent.type(canvas.getByTestId('input'), 'Hello world!'); + const inputEl = canvas.getByTestId('input'); + await userEvent.type(inputEl, 'Hello world!'); + await expect(inputEl).toHaveValue('Hello world!'); }); }, }; + +const mockFn = fn(); +export const LoaderStory: StoryObj<{ mockFn: (val: string) => string }> = { + args: { + mockFn, + }, + loaders: [ + async () => { + mockFn.mockReturnValueOnce('mockFn return value'); + return { + value: 'loaded data', + }; + }, + ], + render: (args, { loaded }) => ({ + components: { Button }, + setup() { + return { args, data: args.mockFn('render'), loaded: loaded.value }; + }, + template: ` +
+
{{loaded}}
+
{{data}}
+
+ `, + }), + play: async () => { + expect(mockFn).toHaveBeenCalledWith('render'); + }, +}; diff --git a/code/renderers/vue3/src/__tests__/composeStories/__snapshots__/portable-stories.test.ts.snap b/code/renderers/vue3/src/__tests__/composeStories/__snapshots__/portable-stories.test.ts.snap index 75eab08758cf..b6e6feff8f61 100644 --- a/code/renderers/vue3/src/__tests__/composeStories/__snapshots__/portable-stories.test.ts.snap +++ b/code/renderers/vue3/src/__tests__/composeStories/__snapshots__/portable-stories.test.ts.snap @@ -3,56 +3,50 @@ exports[`Renders CSF2Secondary story 1`] = `
- -
- -`; - -exports[`Renders CSF2StoryWithLocale story 1`] = ` - -
-
-

- locale: undefined -

`; -exports[`Renders CSF3Button story 1`] = ` +exports[`Renders CSF2StoryWithParamsAndDecorator story 1`] = `
- +
+ +
+
`; -exports[`Renders CSF3ButtonWithRender story 1`] = ` +exports[`Renders CSF3Button story 1`] = `
-
-

- I am a custom render function -

+
+
+
+
+ +`; + exports[`Renders CSF3InputFieldFilled story 1`] = `
- +
+ +
`; @@ -77,12 +101,41 @@ exports[`Renders CSF3InputFieldFilled story 1`] = ` exports[`Renders CSF3Primary story 1`] = `
- + +
+ + +`; + +exports[`Renders LoaderStory story 1`] = ` + +
+
+
+
+ loaded data +
+
+ mockFn return value +
+
+
`; diff --git a/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts b/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts index 4c541e1c4536..36aa7c2e9c56 100644 --- a/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts +++ b/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts @@ -10,39 +10,58 @@ import type Button from './Button.vue'; import { composeStories, composeStory, setProjectAnnotations } from '../../portable-stories'; // example with composeStories, returns an object with all stories composed with args/decorators -const { CSF3Primary } = composeStories(stories); +const { CSF3Primary, LoaderStory } = composeStories(stories); // example with composeStory, returns a single story composed with args/decorators const Secondary = composeStory(stories.CSF2Secondary, stories.default); -it('renders primary button', () => { - render(CSF3Primary({ label: 'Hello world' })); - const buttonElement = screen.getByText(/Hello world/i); - expect(buttonElement).toBeInTheDocument(); -}); +describe('renders', () => { + it('renders primary button', () => { + render(CSF3Primary({ label: 'Hello world' })); + const buttonElement = screen.getByText(/Hello world/i); + expect(buttonElement).toBeInTheDocument(); + }); -it('reuses args from composed story', () => { - render(Secondary()); - const buttonElement = screen.getByRole('button'); - expect(buttonElement.textContent).toEqual(Secondary.args.label); -}); + it('reuses args from composed story', () => { + render(Secondary()); + const buttonElement = screen.getByRole('button'); + expect(buttonElement.textContent).toEqual(Secondary.args.label); + }); -it('myClickEvent handler is called', async () => { - const myClickEventSpy = vi.fn(); - render(Secondary({ onMyClickEvent: myClickEventSpy })); - const buttonElement = screen.getByRole('button'); - buttonElement.click(); - expect(myClickEventSpy).toHaveBeenCalled(); -}); + it('myClickEvent handler is called', async () => { + const myClickEventSpy = vi.fn(); + render(Secondary({ onMyClickEvent: myClickEventSpy })); + const buttonElement = screen.getByRole('button'); + buttonElement.click(); + expect(myClickEventSpy).toHaveBeenCalled(); + }); -it('reuses args from composeStories', () => { - const { getByText } = render(CSF3Primary()); - const buttonElement = getByText(/foo/i); - expect(buttonElement).toBeInTheDocument(); + it('reuses args from composeStories', () => { + const { getByText } = render(CSF3Primary()); + const buttonElement = getByText(/foo/i); + expect(buttonElement).toBeInTheDocument(); + }); + + it('should call and compose loaders data', async () => { + await LoaderStory.load(); + const { getByTestId } = render(LoaderStory()); + expect(getByTestId('spy-data').textContent).toEqual('mockFn return value'); + expect(getByTestId('loaded-data').textContent).toEqual('loaded data'); + // spy assertions happen in the play function and should work + await LoaderStory.play!(); + }); }); describe('projectAnnotations', () => { it('renders with default projectAnnotations', () => { + setProjectAnnotations([ + { + parameters: { injected: true }, + globalTypes: { + locale: { defaultValue: 'en' }, + }, + }, + ]); const WithEnglishText = composeStory(stories.CSF2StoryWithLocale, stories.default); const { getByText } = render(WithEnglishText()); const buttonElement = getByText('Hello!'); @@ -51,7 +70,7 @@ describe('projectAnnotations', () => { it('renders with custom projectAnnotations via composeStory params', () => { const WithPortugueseText = composeStory(stories.CSF2StoryWithLocale, stories.default, { - globalTypes: { locale: { defaultValue: 'pt' } } as any, + globals: { locale: 'pt' }, }); const { getByText } = render(WithPortugueseText()); const buttonElement = getByText('OlΓ‘!'); @@ -127,12 +146,14 @@ describe('ComposeStories types', () => { // Batch snapshot testing const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName, Story]); it.each(testCases)('Renders %s story', async (_storyName, Story) => { - if (typeof Story === 'string' || _storyName === 'CSF2StoryWithParamsAndDecorator') { + if (typeof Story === 'string' || _storyName === 'CSF2StoryWithLocale') { return; } + await Story.load(); + const { container, baseElement } = await render(Story()); + await Story.play?.({ canvasElement: container as HTMLElement }); await new Promise((resolve) => setTimeout(resolve, 0)); - const tree = await render(Story()); - expect(tree.baseElement).toMatchSnapshot(); + expect(baseElement).toMatchSnapshot(); }); diff --git a/code/renderers/vue3/src/portable-stories.ts b/code/renderers/vue3/src/portable-stories.ts index 4e009b25d672..aef26b39a5e7 100644 --- a/code/renderers/vue3/src/portable-stories.ts +++ b/code/renderers/vue3/src/portable-stories.ts @@ -2,6 +2,7 @@ import { composeStory as originalComposeStory, composeStories as originalComposeStories, setProjectAnnotations as originalSetProjectAnnotations, + getPortableStoryWrapperId, } from '@storybook/preview-api'; import type { Args, @@ -11,10 +12,24 @@ import type { StoriesWithPartialProps, } from '@storybook/types'; -import * as defaultProjectAnnotations from './render'; +import * as vueProjectAnnotations from './entry-preview'; import type { Meta } from './public-types'; import type { VueRenderer } from './types'; +const defaultProjectAnnotations: ProjectAnnotations = { + ...vueProjectAnnotations, + decorators: [ + function addStorybookId(story, { id }) { + return { + components: { story }, + template: `
+ +
`, + }; + }, + ], +}; + /** Function that sets the globalConfig of your Storybook. The global config is the preview module of your .storybook folder. * * It should be run a single time, so that your global config (e.g. decorators) is applied to your stories when using `composeStories` or `composeStory`. diff --git a/code/renderers/web-components/package.json b/code/renderers/web-components/package.json index 85bddd3cd0e5..fcd133473b18 100644 --- a/code/renderers/web-components/package.json +++ b/code/renderers/web-components/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/web-components", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook web-components renderer", "keywords": [ "lit", diff --git a/code/ui/blocks/package.json b/code/ui/blocks/package.json index 55d5532341db..c48cbadc893a 100644 --- a/code/ui/blocks/package.json +++ b/code/ui/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/blocks", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Storybook Doc Blocks", "keywords": [ "storybook" diff --git a/code/ui/blocks/src/components/ArgsTable/ArgRow.tsx b/code/ui/blocks/src/components/ArgsTable/ArgRow.tsx index d6e2c73ad905..8f6b6f2c5f99 100644 --- a/code/ui/blocks/src/components/ArgsTable/ArgRow.tsx +++ b/code/ui/blocks/src/components/ArgsTable/ArgRow.tsx @@ -76,12 +76,18 @@ const StyledTd = styled.td<{ expandable: boolean }>(({ theme, expandable }) => ( paddingLeft: expandable ? '40px !important' : '20px !important', })); +const toSummary = (value: any) => { + if (!value) return value; + const val = typeof value === 'string' ? value : value.name; + return { summary: val }; +}; + export const ArgRow: FC = (props) => { const [isHovered, setIsHovered] = useState(false); const { row, updateArgs, compact, expandable, initialExpandedArgs } = props; const { name, description } = row; const table = (row.table || {}) as TableAnnotation; - const type = table.type || row.type; + const type = table.type || toSummary(row.type); const defaultValue = table.defaultValue || row.defaultValue; const required = row.type?.required; const hasDescription = description != null && description !== ''; diff --git a/code/ui/components/package.json b/code/ui/components/package.json index 17d3cc5c2519..248ed68c73c5 100644 --- a/code/ui/components/package.json +++ b/code/ui/components/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/components", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Core Storybook Components", "keywords": [ "storybook" diff --git a/code/ui/manager/package.json b/code/ui/manager/package.json index 5d95ba8af64e..b13135e4eb4f 100644 --- a/code/ui/manager/package.json +++ b/code/ui/manager/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/manager", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "description": "Core Storybook UI", "keywords": [ "storybook" diff --git a/code/yarn.lock b/code/yarn.lock index 01c18ce7fef5..d155696ae2d0 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -5678,7 +5678,7 @@ __metadata: mdast-util-mdx-jsx: "npm:^3.0.0" mdast-util-mdxjs-esm: "npm:^2.0.1" prettier: "npm:^3.1.1" - recast: "npm:^0.23.1" + recast: "npm:^0.23.5" remark: "npm:^15.0.1" remark-mdx: "npm:^3.0.0" tiny-invariant: "npm:^1.3.1" @@ -5879,7 +5879,7 @@ __metadata: "@types/js-yaml": "npm:^4.0.5" fs-extra: "npm:^11.1.0" js-yaml: "npm:^4.1.0" - recast: "npm:^0.23.1" + recast: "npm:^0.23.5" ts-dedent: "npm:^2.0.0" typescript: "npm:^5.3.2" languageName: unknown @@ -6347,10 +6347,13 @@ __metadata: "@storybook/react-docgen-typescript-plugin": "npm:1.0.6--canary.9.0c3f3b7.0" "@types/node": "npm:^18.0.0" "@types/semver": "npm:^7.3.4" + find-up: "npm:^5.0.0" fs-extra: "npm:^11.1.0" magic-string: "npm:^0.30.5" react-docgen: "npm:^7.0.0" + resolve: "npm:^1.22.8" semver: "npm:^7.3.7" + tsconfig-paths: "npm:^4.2.0" typescript: "npm:^5.3.2" webpack: "npm:5" peerDependencies: @@ -6392,7 +6395,7 @@ __metadata: ts-dedent: "npm:^2.0.0" typescript: "npm:^5.3.2" peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.16 + svelte: ^4.0.0 || ^5.0.0-next.65 svelte-loader: "*" languageName: unknown linkType: soft @@ -6494,10 +6497,14 @@ __metadata: "@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.3.0" "@rollup/pluginutils": "npm:^5.0.2" "@storybook/builder-vite": "workspace:*" + "@storybook/node-logger": "workspace:*" "@storybook/react": "workspace:*" "@types/node": "npm:^18.0.0" + find-up: "npm:^5.0.0" magic-string: "npm:^0.30.0" react-docgen: "npm:^7.0.0" + resolve: "npm:^1.22.8" + tsconfig-paths: "npm:^4.2.0" typescript: "npm:^5.3.2" vite: "npm:^4.0.0" peerDependencies: @@ -6699,7 +6706,7 @@ __metadata: react-dom: "npm:^18.2.0" semver: "npm:^7.3.7" serve-static: "npm:^1.14.1" - svelte: "npm:^5.0.0-next.28" + svelte: "npm:^5.0.0-next.65" trash: "npm:^7.0.0" ts-dedent: "npm:^2.0.0" ts-node: "npm:^10.9.1" @@ -6799,7 +6806,7 @@ __metadata: "@sveltejs/vite-plugin-svelte": "npm:^3.0.1" "@types/node": "npm:^18.0.0" magic-string: "npm:^0.30.0" - svelte: "npm:^5.0.0-next.28" + svelte: "npm:^5.0.0-next.65" svelte-preprocess: "npm:^5.1.1" sveltedoc-parser: "npm:^4.2.1" ts-dedent: "npm:^2.2.0" @@ -6807,7 +6814,7 @@ __metadata: vite: "npm:^4.0.0" peerDependencies: "@sveltejs/vite-plugin-svelte": ^2.0.0 || ^3.0.0 - svelte: ^4.0.0 || ^5.0.0-next.16 + svelte: ^4.0.0 || ^5.0.0-next.65 vite: ^4.0.0 || ^5.0.0 languageName: unknown linkType: soft @@ -6824,7 +6831,7 @@ __metadata: svelte-loader: "npm:^3.1.9" typescript: "npm:^5.3.2" peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.16 + svelte: ^4.0.0 || ^5.0.0-next.65 svelte-loader: "*" languageName: unknown linkType: soft @@ -6839,16 +6846,17 @@ __metadata: "@storybook/global": "npm:^5.0.0" "@storybook/preview-api": "workspace:*" "@storybook/types": "workspace:*" - "@sveltejs/vite-plugin-svelte": "npm:^3.0.1" + "@sveltejs/vite-plugin-svelte": "npm:^3.0.2" expect-type: "npm:^0.15.0" - svelte: "npm:^5.0.0-next.28" - svelte-check: "npm:^3.6.1" + fs-extra: "npm:^11.1.0" + svelte: "npm:^5.0.0-next.65" + svelte-check: "npm:^3.6.4" sveltedoc-parser: "npm:^4.2.1" ts-dedent: "npm:^2.0.0" type-fest: "npm:~2.19" typescript: "npm:^5.3.2" peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.16 + svelte: ^4.0.0 || ^5.0.0-next.65 languageName: unknown linkType: soft @@ -6864,7 +6872,7 @@ __metadata: typescript: "npm:^5.3.2" vite: "npm:^4.0.0" peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.16 + svelte: ^4.0.0 || ^5.0.0-next.65 vite: ^4.0.0 || ^5.0.0 languageName: unknown linkType: soft @@ -7085,7 +7093,7 @@ __metadata: languageName: node linkType: hard -"@sveltejs/vite-plugin-svelte-inspector@npm:^2.0.0-next.0 || ^2.0.0": +"@sveltejs/vite-plugin-svelte-inspector@npm:^2.0.0, @sveltejs/vite-plugin-svelte-inspector@npm:^2.0.0-next.0 || ^2.0.0": version: 2.0.0 resolution: "@sveltejs/vite-plugin-svelte-inspector@npm:2.0.0" dependencies: @@ -7116,6 +7124,24 @@ __metadata: languageName: node linkType: hard +"@sveltejs/vite-plugin-svelte@npm:^3.0.2": + version: 3.0.2 + resolution: "@sveltejs/vite-plugin-svelte@npm:3.0.2" + dependencies: + "@sveltejs/vite-plugin-svelte-inspector": "npm:^2.0.0" + debug: "npm:^4.3.4" + deepmerge: "npm:^4.3.1" + kleur: "npm:^4.1.5" + magic-string: "npm:^0.30.5" + svelte-hmr: "npm:^0.15.3" + vitefu: "npm:^0.2.5" + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + vite: ^5.0.0 + checksum: 7150877f61b65a51d55916fccacb7851859b0aab9c7e4f591c98b6775a7e55f5410cc854add4c427f99978f115a92ac75f116a813b67814cc7801daac1b78439 + languageName: node + linkType: hard + "@swc/helpers@npm:0.5.2": version: 0.5.2 resolution: "@swc/helpers@npm:0.5.2" @@ -7589,6 +7615,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:^1.0.5": + version: 1.0.5 + resolution: "@types/estree@npm:1.0.5" + checksum: b3b0e334288ddb407c7b3357ca67dbee75ee22db242ca7c56fe27db4e1a31989cb8af48a84dd401deb787fe10cc6b2ab1ee82dc4783be87ededbe3d53c79c70d + languageName: node + linkType: hard + "@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^4.17.33": version: 4.17.37 resolution: "@types/express-serve-static-core@npm:4.17.37" @@ -9499,12 +9532,12 @@ __metadata: languageName: node linkType: hard -"acorn-typescript@npm:^1.4.11": - version: 1.4.12 - resolution: "acorn-typescript@npm:1.4.12" +"acorn-typescript@npm:^1.4.13": + version: 1.4.13 + resolution: "acorn-typescript@npm:1.4.13" peerDependencies: acorn: ">=8.9.0" - checksum: a3b33ed0dc321e3364da507a3decec96423736384068c88fea5ea57aeae864ea115a6c4a20b3ace71b75f4901b0657bec82d83aab30a8ad0dfc4bfc0d8337546 + checksum: f2f17cf03379d63beeb007f0feea02cebbd9af261f6b5619ea7345b177bd7a5f99752927cbf652baa3fc97962ae4561592093ab0a1c3e00ca4f354ba23c557ae languageName: node linkType: hard @@ -9540,6 +9573,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.11.3": + version: 8.11.3 + resolution: "acorn@npm:8.11.3" + bin: + acorn: bin/acorn + checksum: 3ff155f8812e4a746fee8ecff1f227d527c4c45655bb1fad6347c3cb58e46190598217551b1500f18542d2bbe5c87120cb6927f5a074a59166fbdd9468f0a299 + languageName: node + linkType: hard + "address@npm:^1.0.1": version: 1.2.2 resolution: "address@npm:1.2.2" @@ -18219,7 +18261,7 @@ __metadata: languageName: node linkType: hard -"is-reference@npm:^3.0.0, is-reference@npm:^3.0.1": +"is-reference@npm:^3.0.0, is-reference@npm:^3.0.1, is-reference@npm:^3.0.2": version: 3.0.2 resolution: "is-reference@npm:3.0.2" dependencies: @@ -24997,6 +25039,19 @@ __metadata: languageName: node linkType: hard +"recast@npm:^0.23.5": + version: 0.23.5 + resolution: "recast@npm:0.23.5" + dependencies: + ast-types: "npm:^0.16.1" + esprima: "npm:~4.0.0" + source-map: "npm:~0.6.1" + tiny-invariant: "npm:^1.3.3" + tslib: "npm:^2.0.1" + checksum: 21dc93910d12c71da77072afc3d5d4cdf97783776842efa6fd2cd7c2798d3622ace5d2f05ca5133141ef93de8a0512cbe191fe835f325bd1722f186fe449d11a + languageName: node + linkType: hard + "redent@npm:^3.0.0": version: 3.0.0 resolution: "redent@npm:3.0.0" @@ -25667,7 +25722,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:1.22.8, resolve@npm:^1.10.0, resolve@npm:^1.13.1, resolve@npm:^1.14.2, resolve@npm:^1.15.1, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4, resolve@npm:^1.4.0": +"resolve@npm:1.22.8, resolve@npm:^1.10.0, resolve@npm:^1.13.1, resolve@npm:^1.14.2, resolve@npm:^1.15.1, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4, resolve@npm:^1.22.8, resolve@npm:^1.4.0": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -25693,7 +25748,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A1.22.8#optional!builtin, resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.13.1#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.15.1#optional!builtin, resolve@patch:resolve@npm%3A^1.17.0#optional!builtin, resolve@patch:resolve@npm%3A^1.19.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin, resolve@patch:resolve@npm%3A^1.4.0#optional!builtin": +"resolve@patch:resolve@npm%3A1.22.8#optional!builtin, resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.13.1#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.15.1#optional!builtin, resolve@patch:resolve@npm%3A^1.17.0#optional!builtin, resolve@patch:resolve@npm%3A^1.19.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin, resolve@patch:resolve@npm%3A^1.4.0#optional!builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -27594,9 +27649,9 @@ __metadata: languageName: node linkType: hard -"svelte-check@npm:^3.6.1": - version: 3.6.2 - resolution: "svelte-check@npm:3.6.2" +"svelte-check@npm:^3.6.4": + version: 3.6.4 + resolution: "svelte-check@npm:3.6.4" dependencies: "@jridgewell/trace-mapping": "npm:^0.3.17" chokidar: "npm:^3.4.1" @@ -27610,7 +27665,7 @@ __metadata: svelte: ^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 bin: svelte-check: bin/svelte-check - checksum: 3f389df29268d4df9b561d0b206566e827af84923c70150b2dadfd407bcbdaccbfd561bd8b93884597de62477d62826ff1a5108854641078b692130441a49a55 + checksum: acbcc04c8c6ab7baee7ccf36ca134dcabe49fae103aa92661e7f80e01216623363fb794fec9a3f794f7003d55629373567ff485925dc33272f48cea63e7b2452 languageName: node linkType: hard @@ -27719,23 +27774,24 @@ __metadata: languageName: node linkType: hard -"svelte@npm:^5.0.0-next.28": - version: 5.0.0-next.28 - resolution: "svelte@npm:5.0.0-next.28" +"svelte@npm:^5.0.0-next.65": + version: 5.0.0-next.65 + resolution: "svelte@npm:5.0.0-next.65" dependencies: "@ampproject/remapping": "npm:^2.2.1" "@jridgewell/sourcemap-codec": "npm:^1.4.15" - acorn: "npm:^8.10.0" - acorn-typescript: "npm:^1.4.11" + "@types/estree": "npm:^1.0.5" + acorn: "npm:^8.11.3" + acorn-typescript: "npm:^1.4.13" aria-query: "npm:^5.3.0" axobject-query: "npm:^4.0.0" esm-env: "npm:^1.0.0" esrap: "npm:^1.2.1" - is-reference: "npm:^3.0.1" + is-reference: "npm:^3.0.2" locate-character: "npm:^3.0.0" - magic-string: "npm:^0.30.4" - zimmerframe: "npm:^1.1.0" - checksum: d309cd3d1a9fe16c67a626af867288b02f6e7c49311c851aeb0f36feb5ab9603ca5594338fb933dbbada41b26faea6dcef52ed6ab3e86f54626545e53059eb28 + magic-string: "npm:^0.30.5" + zimmerframe: "npm:^1.1.2" + checksum: 6a686847f887d2871eabce4888916cba6aec5bae924a76fd01f4098db1c0053d4e5d6434070d0a048eac75eaddd4fd40e3fae625a0253464f7baa6b0f147f209 languageName: node linkType: hard @@ -28028,6 +28084,13 @@ __metadata: languageName: node linkType: hard +"tiny-invariant@npm:^1.3.3": + version: 1.3.3 + resolution: "tiny-invariant@npm:1.3.3" + checksum: 65af4a07324b591a059b35269cd696aba21bef2107f29b9f5894d83cc143159a204b299553435b03874ebb5b94d019afa8b8eff241c8a4cfee95872c2e1c1c4a + languageName: node + linkType: hard + "tiny-warning@npm:^1.0.2": version: 1.0.3 resolution: "tiny-warning@npm:1.0.3" @@ -28389,7 +28452,7 @@ __metadata: languageName: node linkType: hard -"tsconfig-paths@npm:^4.0.0, tsconfig-paths@npm:^4.1.2": +"tsconfig-paths@npm:^4.0.0, tsconfig-paths@npm:^4.1.2, tsconfig-paths@npm:^4.2.0": version: 4.2.0 resolution: "tsconfig-paths@npm:4.2.0" dependencies: @@ -30847,10 +30910,10 @@ __metadata: languageName: node linkType: hard -"zimmerframe@npm:^1.1.0": - version: 1.1.0 - resolution: "zimmerframe@npm:1.1.0" - checksum: dffe3f555bb000176ed9c7577e0fb0c3eddeceb6df9bb3ff870995bac3a51b40fab5443bd3dc47ce91c1f8ecf07282742efb80771ae6a088edc0340bb217f93d +"zimmerframe@npm:^1.1.2": + version: 1.1.2 + resolution: "zimmerframe@npm:1.1.2" + checksum: 8f693609c31cbb4449db223acd61661bc93b73e615f9db6fb8c86d4ceea84ca54cbbeebcf53cf74c22a1f923b92abd18e97988a5e175c76b6ab17238e5593a9d languageName: node linkType: hard diff --git a/docs/snippets/common/my-component-play-function-with-canvas.js.mdx b/docs/snippets/common/my-component-play-function-with-canvas.js.mdx index 26ec005f70fa..a5da1d3eed5f 100644 --- a/docs/snippets/common/my-component-play-function-with-canvas.js.mdx +++ b/docs/snippets/common/my-component-play-function-with-canvas.js.mdx @@ -1,7 +1,7 @@ ```js // MyComponent.stories.js|jsx -import { getByRole, userEvent, within } from '@storybook/testing-library'; +import { userEvent, within } from '@storybook/test'; import { MyComponent } from './MyComponent'; diff --git a/docs/snippets/common/portable-stories-jest-snapshot-test.js.mdx b/docs/snippets/common/portable-stories-jest-snapshot-test.js.mdx index 8a6f022a9492..4078691b055d 100644 --- a/docs/snippets/common/portable-stories-jest-snapshot-test.js.mdx +++ b/docs/snippets/common/portable-stories-jest-snapshot-test.js.mdx @@ -25,7 +25,7 @@ const compose = (entry) => { function getAllStoryFiles() { // Place the glob you want to match your stories files const storyFiles = glob.sync( - path.join(__dirname, 'stories/**/*.(stories|story).@(js|jsx|mjs|ts|tsx)'), + path.join(__dirname, 'stories/**/*.{stories,story}.{js,jsx,mjs,ts,tsx}'), ); return storyFiles.map((filePath) => { diff --git a/docs/snippets/common/portable-stories-jest-snapshot-test.ts.mdx b/docs/snippets/common/portable-stories-jest-snapshot-test.ts.mdx index 45bcd3d4f7f1..07bb7a277c60 100644 --- a/docs/snippets/common/portable-stories-jest-snapshot-test.ts.mdx +++ b/docs/snippets/common/portable-stories-jest-snapshot-test.ts.mdx @@ -33,7 +33,7 @@ const compose = (entry: StoryFile): ReturnType> function getAllStoryFiles() { // Place the glob you want to match your stories files const storyFiles = glob.sync( - path.join(__dirname, 'stories/**/*.(stories|story).@(js|jsx|mjs|ts|tsx)'), + path.join(__dirname, 'stories/**/*.{stories,story}.{js,jsx,mjs,ts,tsx}'), ); return storyFiles.map((filePath) => { diff --git a/docs/snippets/react/login-form-with-play-function.ts-4-9.mdx b/docs/snippets/react/login-form-with-play-function.ts-4-9.mdx index d6a86876817c..84dffad550b1 100644 --- a/docs/snippets/react/login-form-with-play-function.ts-4-9.mdx +++ b/docs/snippets/react/login-form-with-play-function.ts-4-9.mdx @@ -3,9 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { within, userEvent } from '@storybook/testing-library'; - -import { expect } from '@storybook/test'; +import { userEvent, within, expect } from '@storybook/test'; import { LoginForm } from './LoginForm'; diff --git a/docs/snippets/react/login-form-with-play-function.ts.mdx b/docs/snippets/react/login-form-with-play-function.ts.mdx index f31a2a8fcbe2..de6854f0e180 100644 --- a/docs/snippets/react/login-form-with-play-function.ts.mdx +++ b/docs/snippets/react/login-form-with-play-function.ts.mdx @@ -3,9 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { within, userEvent } from '@storybook/testing-library'; - -import { expect } from '@storybook/test'; +import { userEvent, within, expect } from '@storybook/test'; import { LoginForm } from './LoginForm'; diff --git a/docs/snippets/svelte/login-form-with-play-function.js.mdx b/docs/snippets/svelte/login-form-with-play-function.js.mdx index e65f60e9a3cb..4408fc6261a0 100644 --- a/docs/snippets/svelte/login-form-with-play-function.js.mdx +++ b/docs/snippets/svelte/login-form-with-play-function.js.mdx @@ -1,8 +1,7 @@ ```js // LoginForm.stories.js -import { userEvent, within } from '@storybook/testing-library'; -import { expect } from '@storybook/test'; +import { expect, userEvent, within } from '@storybook/test'; import LoginForm from './LoginForm.svelte'; diff --git a/docs/snippets/svelte/login-form-with-play-function.ts-4-9.mdx b/docs/snippets/svelte/login-form-with-play-function.ts-4-9.mdx index 265d8fa36b9d..b975337dc799 100644 --- a/docs/snippets/svelte/login-form-with-play-function.ts-4-9.mdx +++ b/docs/snippets/svelte/login-form-with-play-function.ts-4-9.mdx @@ -2,8 +2,7 @@ // LoginForm.stories.ts import type { Meta, StoryObj } from '@storybook/svelte'; -import { userEvent, within } from '@storybook/testing-library'; -import { expect } from '@storybook/test'; +import { expect, userEvent, within } from '@storybook/test'; import LoginForm from './LoginForm.svelte'; diff --git a/docs/snippets/svelte/login-form-with-play-function.ts.mdx b/docs/snippets/svelte/login-form-with-play-function.ts.mdx index ff5891c617d8..24bcd6c1d41a 100644 --- a/docs/snippets/svelte/login-form-with-play-function.ts.mdx +++ b/docs/snippets/svelte/login-form-with-play-function.ts.mdx @@ -2,8 +2,7 @@ // LoginForm.stories.ts import type { Meta, StoryObj } from '@storybook/svelte'; -import { userEvent, within } from '@storybook/testing-library'; -import { expect } from '@storybook/test'; +import { expect, userEvent, within } from '@storybook/test'; import LoginForm from './LoginForm.svelte'; diff --git a/docs/snippets/web-components/my-component-play-function-with-canvas.js.mdx b/docs/snippets/web-components/my-component-play-function-with-canvas.js.mdx index 2ed5c0bec08d..d3812d7d1af0 100644 --- a/docs/snippets/web-components/my-component-play-function-with-canvas.js.mdx +++ b/docs/snippets/web-components/my-component-play-function-with-canvas.js.mdx @@ -1,7 +1,7 @@ ```js // MyComponent.stories.js -import { getByRole, userEvent, within } from '@storybook/testing-library'; +import { userEvent, within } from '@storybook/test'; export default { component: 'demo-my-component', diff --git a/docs/versions/next.json b/docs/versions/next.json index 7e6974b261e7..a1c6e9350e50 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"8.0.0-rc.0","info":{"plain":"Bumping 8.0.0-beta.6 to 8.0.0-rc.0. Please refer to the changelogs of previous beta releases."}} +{"version":"8.0.0-rc.1","info":{"plain":"- CLI: Fix addon compatibility check error reporting in storybook dev - [#26258](https://github.com/storybookjs/storybook/pull/26258), thanks [@yannbf](https://github.com/yannbf)!\n- Onboarding: Fix manager dist reference - [#26282](https://github.com/storybookjs/storybook/pull/26282), thanks [@shilman](https://github.com/shilman)!\n- ReactVite: Docgen ignore un-parsable files - [#26254](https://github.com/storybookjs/storybook/pull/26254), thanks [@ndelangen](https://github.com/ndelangen)!"}} diff --git a/scripts/package.json b/scripts/package.json index 34ae8e6f6a23..e2f19d61148b 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -160,7 +160,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "read-pkg-up": "^7.0.1", - "recast": "^0.23.1", + "recast": "^0.23.5", "remark": "^14.0.3", "remark-cli": "^12.0.0", "remark-lint": "^9.1.2", diff --git a/scripts/sandbox/generate.ts b/scripts/sandbox/generate.ts index 27f17f8c7ad0..410d49373d93 100755 --- a/scripts/sandbox/generate.ts +++ b/scripts/sandbox/generate.ts @@ -27,6 +27,13 @@ import { REPROS_DIRECTORY, LOCAL_REGISTRY_URL, } from '../utils/constants'; +import * as ghActions from '@actions/core'; +import dedent from 'ts-dedent'; + +const isCI = process.env.GITHUB_ACTIONS === 'true'; + +class BeforeScriptExecutionError extends Error {} +class StorybookInitError extends Error {} const sbInit = async ( cwd: string, @@ -148,76 +155,150 @@ const runGenerators = async ( const limit = pLimit(1); - await Promise.all( + const generationResults = await Promise.allSettled( generators.map(({ dirName, name, script, expected, env }) => limit(async () => { - let flags: string[] = []; - if (expected.renderer === '@storybook/html') flags = ['--type html']; - else if (expected.renderer === '@storybook/server') flags = ['--type server']; - - const time = process.hrtime(); - console.log(`🧬 Generating ${name}`); - const baseDir = join(REPROS_DIRECTORY, dirName); const beforeDir = join(baseDir, BEFORE_DIR_NAME); - await emptyDir(baseDir); - - // We do the creation inside a temp dir to avoid yarn container problems - const createBaseDir = directory(); - if (!script.includes('pnp')) { - await setupYarn({ cwd: createBaseDir }); - } - - const createBeforeDir = join(createBaseDir, BEFORE_DIR_NAME); - - // Some tools refuse to run inside an existing directory and replace the contents, - // where as others are very picky about what directories can be called. So we need to - // handle different modes of operation. - if (script.includes('{{beforeDir}}')) { - const scriptWithBeforeDir = script.replaceAll('{{beforeDir}}', BEFORE_DIR_NAME); - await runCommand( - scriptWithBeforeDir, - { - cwd: createBaseDir, - timeout: SCRIPT_TIMEOUT, - }, - debug + try { + let flags: string[] = []; + if (expected.renderer === '@storybook/html') flags = ['--type html']; + else if (expected.renderer === '@storybook/server') flags = ['--type server']; + + const time = process.hrtime(); + console.log(`🧬 Generating ${name} (${{ dirName }})`); + await emptyDir(baseDir); + + // We do the creation inside a temp dir to avoid yarn container problems + const createBaseDir = directory(); + if (!script.includes('pnp')) { + await setupYarn({ cwd: createBaseDir }); + } + + const createBeforeDir = join(createBaseDir, BEFORE_DIR_NAME); + + // Some tools refuse to run inside an existing directory and replace the contents, + // where as others are very picky about what directories can be called. So we need to + // handle different modes of operation. + try { + if (script.includes('{{beforeDir}}')) { + const scriptWithBeforeDir = script.replaceAll('{{beforeDir}}', BEFORE_DIR_NAME); + await runCommand( + scriptWithBeforeDir, + { + cwd: createBaseDir, + timeout: SCRIPT_TIMEOUT, + }, + debug + ); + } else { + await ensureDir(createBeforeDir); + await runCommand(script, { cwd: createBeforeDir, timeout: SCRIPT_TIMEOUT }, debug); + } + } catch (error) { + const message = `❌ Failed to execute before-script for template: ${name} (${dirName})`; + if (isCI) { + ghActions.error(dedent`${message} + ${(error as any).stack}`); + } else { + console.error(message); + console.error(error); + } + throw new BeforeScriptExecutionError(message, { cause: error }); + } + + await localizeYarnConfigFiles(createBaseDir, createBeforeDir); + + // Now move the created before dir into it's final location and add storybook + await move(createBeforeDir, beforeDir); + + // Make sure there are no git projects in the folder + await remove(join(beforeDir, '.git')); + + try { + await addStorybook({ baseDir, localRegistry, flags, debug, env }); + } catch (error) { + const message = `❌ Failed to initialize Storybook in template: ${name} (${dirName})`; + if (isCI) { + ghActions.error(dedent`${message} + ${(error as any).stack}`); + } else { + console.error(message); + console.error(error); + } + throw new StorybookInitError(message, { + cause: error, + }); + } + await addDocumentation(baseDir, { name, dirName }); + + console.log( + `βœ… Generated ${name} (${dirName}) in ./${relative( + process.cwd(), + baseDir + )} successfully in ${prettyTime(process.hrtime(time))}` ); - } else { - await ensureDir(createBeforeDir); - await runCommand(script, { cwd: createBeforeDir, timeout: SCRIPT_TIMEOUT }, debug); + } catch (error) { + throw error; + } finally { + // Remove node_modules to save space and avoid GH actions failing + // They're not uploaded to the git sandboxes repo anyway + if (process.env.CLEANUP_SANDBOX_NODE_MODULES) { + console.log(`πŸ—‘οΈ Removing ${join(beforeDir, 'node_modules')}`); + await remove(join(beforeDir, 'node_modules')); + console.log(`πŸ—‘οΈ Removing ${join(baseDir, AFTER_DIR_NAME, 'node_modules')}`); + await remove(join(baseDir, AFTER_DIR_NAME, 'node_modules')); + } } + }) + ) + ); - await localizeYarnConfigFiles(createBaseDir, createBeforeDir); - - // Now move the created before dir into it's final location and add storybook - await move(createBeforeDir, beforeDir); + const hasGenerationErrors = generationResults.some((result) => result.status === 'rejected'); - // Make sure there are no git projects in the folder - await remove(join(beforeDir, '.git')); + if (!isCI) { + if (hasGenerationErrors) { + throw new Error(`Some sandboxes failed to generate`); + } + return; + } - await addStorybook({ baseDir, localRegistry, flags, debug, env }); + ghActions.summary.addHeading('Sandbox generation summary'); - await addDocumentation(baseDir, { name, dirName }); + if (!hasGenerationErrors) { + await ghActions.summary.addRaw('βœ… Success!').write(); + return; + } - // Remove node_modules to save space and avoid GH actions failing - // They're not uploaded to the git sandboxes repo anyway - if (process.env.CLEANUP_SANDBOX_NODE_MODULES) { - console.log(`πŸ—‘οΈ Removing ${join(beforeDir, 'node_modules')}`); - await remove(join(beforeDir, 'node_modules')); - console.log(`πŸ—‘οΈ Removing ${join(baseDir, AFTER_DIR_NAME, 'node_modules')}`); - await remove(join(baseDir, AFTER_DIR_NAME, 'node_modules')); + await ghActions.summary + .addRaw('Some sandboxes failed, see the job log for detailed errors') + .addTable([ + [ + { data: 'Name', header: true }, + { data: 'Key', header: true }, + { data: 'Result', header: true }, + ], + ...generationResults.map((result, index) => { + const { name, dirName } = generators[index]; + const row = [name, `\`${dirName}\``]; + if (result.status === 'fulfilled') { + row.push('🟒 Pass'); + return row; } + const generationError = (result as PromiseRejectedResult).reason as Error; + if (generationError instanceof BeforeScriptExecutionError) { + row.push('πŸ”΄ Failed to execute before script'); + } else if (generationError instanceof StorybookInitError) { + row.push('πŸ”΄ Failed to initialize Storybook'); + } else { + row.push('πŸ”΄ Failed with unknown error'); + } + return row; + }), + ]) + .write(); - console.log( - `βœ… Created ${dirName} in ./${relative( - process.cwd(), - baseDir - )} successfully in ${prettyTime(process.hrtime(time))}` - ); - }) - ) - ); + throw new Error(`Some sandboxes failed to generate`); }; export const options = createOptions({ @@ -278,7 +359,7 @@ if (esMain(import.meta.url)) { .action((optionValues) => { generate(optionValues) .catch((e) => { - console.trace(e); + console.error(e); process.exit(1); }) .then(() => { diff --git a/scripts/sandbox/publish.ts b/scripts/sandbox/publish.ts index e4307690efdd..334e8a9177eb 100755 --- a/scripts/sandbox/publish.ts +++ b/scripts/sandbox/publish.ts @@ -1,14 +1,15 @@ import program from 'commander'; -import { join } from 'path'; +import { dirname, join, relative } from 'path'; import { existsSync } from 'fs'; import * as tempy from 'tempy'; -import { copy, emptyDir, readdir, remove, stat, writeFile } from 'fs-extra'; +import { copy, emptyDir, remove, writeFile } from 'fs-extra'; import { execaCommand } from 'execa'; import { getTemplatesData, renderTemplate } from './utils/template'; // eslint-disable-next-line import/no-cycle import { commitAllToGit } from './utils/git'; import { REPROS_DIRECTORY } from '../utils/constants'; +import { glob } from 'glob'; export const logger = console; @@ -31,15 +32,21 @@ const publish = async (options: PublishOptions & { tmpFolder: string }) => { // otherwise old files will stick around and result inconsistent states logger.log(`πŸ—‘ Delete existing template dirs from clone`); - const files = await Promise.all( - ( - await readdir(REPROS_DIRECTORY) - ).map(async (f) => ({ path: f, stats: await stat(join(REPROS_DIRECTORY, f)) })) - ); + + // empty all existing directories for sandboxes that have a successful after-storybook directory await Promise.all( - files - .filter(({ stats, path }) => stats.isDirectory && !path.startsWith('.')) - .map(async ({ path }) => emptyDir(join(tmpFolder, path))) + // find all successfully generated after-storybook/README.md files + // eg. /home/repros/react-vite/default-ts/after-storybook/README.md + // README.md being the last file generated, thus representing a successful generation + (await glob(join(REPROS_DIRECTORY, '**', 'after-storybook/README.md'))).map((readmePath) => { + // get the after-storybook path relative to the source 'repros' directory + // eg. ./react-vite/default-ts/after-storybook + const pathRelativeToSource = relative(REPROS_DIRECTORY, dirname(readmePath)); + // get the actual path to the corresponding sandbox directory in the clone + // eg. /home/sandboxes-clone/react-vite/default-ts + const sandboxDirectoryToEmpty = join(tmpFolder, pathRelativeToSource, '..'); + return emptyDir(sandboxDirectoryToEmpty); + }) ); logger.log(`🚚 Moving template files into the repository`); diff --git a/scripts/yarn.lock b/scripts/yarn.lock index 85371e960f38..8e10967d94ab 100644 --- a/scripts/yarn.lock +++ b/scripts/yarn.lock @@ -2807,7 +2807,7 @@ __metadata: react: "npm:^18.2.0" react-dom: "npm:^18.2.0" read-pkg-up: "npm:^7.0.1" - recast: "npm:^0.23.1" + recast: "npm:^0.23.5" remark: "npm:^14.0.3" remark-cli: "npm:^12.0.0" remark-lint: "npm:^9.1.2" @@ -4497,19 +4497,6 @@ __metadata: languageName: node linkType: hard -"assert@npm:^2.0.0": - version: 2.1.0 - resolution: "assert@npm:2.1.0" - dependencies: - call-bind: "npm:^1.0.2" - is-nan: "npm:^1.3.2" - object-is: "npm:^1.1.5" - object.assign: "npm:^4.1.4" - util: "npm:^0.12.5" - checksum: 7271a5da883c256a1fa690677bf1dd9d6aa882139f2bed1cd15da4f9e7459683e1da8e32a203d6cc6767e5e0f730c77a9532a87b896b4b0af0dd535f668775f0 - languageName: node - linkType: hard - "assertion-error@npm:^1.1.0": version: 1.1.0 resolution: "assertion-error@npm:1.1.0" @@ -8953,16 +8940,6 @@ __metadata: languageName: node linkType: hard -"is-nan@npm:^1.3.2": - version: 1.3.2 - resolution: "is-nan@npm:1.3.2" - dependencies: - call-bind: "npm:^1.0.0" - define-properties: "npm:^1.1.3" - checksum: 8bfb286f85763f9c2e28ea32e9127702fe980ffd15fa5d63ade3be7786559e6e21355d3625dd364c769c033c5aedf0a2ed3d4025d336abf1b9241e3d9eddc5b0 - languageName: node - linkType: hard - "is-natural-number@npm:^4.0.1": version: 4.0.1 resolution: "is-natural-number@npm:4.0.1" @@ -12762,16 +12739,16 @@ __metadata: languageName: node linkType: hard -"recast@npm:^0.23.1": - version: 0.23.4 - resolution: "recast@npm:0.23.4" +"recast@npm:^0.23.5": + version: 0.23.5 + resolution: "recast@npm:0.23.5" dependencies: - assert: "npm:^2.0.0" ast-types: "npm:^0.16.1" esprima: "npm:~4.0.0" source-map: "npm:~0.6.1" + tiny-invariant: "npm:^1.3.3" tslib: "npm:^2.0.1" - checksum: d719633be8029e28f23b8191d4a525c5dbdac721792ab3cb5e9dfcf1694fb93f3c147b186916195a9c7fa0711f1e4990ba457cdcee02faed3899d4a80da1bd1f + checksum: 21dc93910d12c71da77072afc3d5d4cdf97783776842efa6fd2cd7c2798d3622ace5d2f05ca5133141ef93de8a0512cbe191fe835f325bd1722f186fe449d11a languageName: node linkType: hard @@ -14593,6 +14570,13 @@ __metadata: languageName: node linkType: hard +"tiny-invariant@npm:^1.3.3": + version: 1.3.3 + resolution: "tiny-invariant@npm:1.3.3" + checksum: 65af4a07324b591a059b35269cd696aba21bef2107f29b9f5894d83cc143159a204b299553435b03874ebb5b94d019afa8b8eff241c8a4cfee95872c2e1c1c4a + languageName: node + linkType: hard + "tinybench@npm:^2.5.1": version: 2.5.1 resolution: "tinybench@npm:2.5.1" @@ -15447,7 +15431,7 @@ __metadata: languageName: node linkType: hard -"util@npm:^0.12.4, util@npm:^0.12.5": +"util@npm:^0.12.4": version: 0.12.5 resolution: "util@npm:0.12.5" dependencies: