diff --git a/.circleci/config.yml b/.circleci/config.yml index e124f267f1ba..f67b712e04df 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -836,19 +836,19 @@ workflows: requires: - unit-tests - create-sandboxes: - parallelism: 13 + parallelism: 14 requires: - build - build-sandboxes: - parallelism: 13 + parallelism: 14 requires: - create-sandboxes - chromatic-sandboxes: - parallelism: 10 + parallelism: 11 requires: - build-sandboxes - e2e-production: - parallelism: 8 + parallelism: 9 requires: - build-sandboxes - e2e-dev: @@ -856,7 +856,7 @@ workflows: requires: - create-sandboxes - test-runner-production: - parallelism: 8 + parallelism: 9 requires: - build-sandboxes - vitest-integration: @@ -912,19 +912,19 @@ workflows: requires: - unit-tests - create-sandboxes: - parallelism: 19 + parallelism: 20 requires: - build - build-sandboxes: - parallelism: 19 + parallelism: 20 requires: - create-sandboxes - chromatic-sandboxes: - parallelism: 16 + parallelism: 17 requires: - build-sandboxes - e2e-production: - parallelism: 14 + parallelism: 15 requires: - build-sandboxes - e2e-dev: @@ -932,7 +932,7 @@ workflows: requires: - create-sandboxes - test-runner-production: - parallelism: 14 + parallelism: 15 requires: - build-sandboxes - vitest-integration: @@ -986,22 +986,22 @@ workflows: requires: - build - create-sandboxes: - parallelism: 36 + parallelism: 37 requires: - build # - smoke-test-sandboxes: # disabled for now # requires: # - create-sandboxes - build-sandboxes: - parallelism: 36 + parallelism: 37 requires: - create-sandboxes - chromatic-sandboxes: - parallelism: 33 + parallelism: 34 requires: - build-sandboxes - e2e-production: - parallelism: 31 + parallelism: 32 requires: - build-sandboxes - e2e-dev: @@ -1009,7 +1009,7 @@ workflows: requires: - create-sandboxes - test-runner-production: - parallelism: 31 + parallelism: 32 requires: - build-sandboxes - vitest-integration: diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index d50dc7efdf27..c0e166bea164 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -17,6 +17,8 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 2 - name: Set node version uses: actions/setup-node@v4 with: diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index bfd6341e7850..2a2eb7b7453d 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,72 @@ +## 8.5.0-beta.5 + +- Addon Test: Only reset story count on file change when watch mode is enabled - [#30121](https://github.com/storybookjs/storybook/pull/30121), thanks @ghengeveld! +- Build: Revert Downgrade to esbuild 0.24.0 - [#30120](https://github.com/storybookjs/storybook/pull/30120), thanks @yannbf! +- Core: Fix `ERR_PACKAGE_PATH_NOT_EXPORTED` in `@storybook/node-logger` - [#30093](https://github.com/storybookjs/storybook/pull/30093), thanks @JReinhold! +- React: Use Act wrapper in Storybook for component rendering - [#30037](https://github.com/storybookjs/storybook/pull/30037), thanks @valentinpalkovic! +- Vite: Add extra entries to `optimizeDeps` - [#30117](https://github.com/storybookjs/storybook/pull/30117), thanks @ndelangen! + +## 8.5.0-beta.4 + +- Addon Themes: Deprecate useThemeParameters - [#30111](https://github.com/storybookjs/storybook/pull/30111), thanks @yannbf! +- Build: Downgrade to esbuild 0.24.0 - [#30116](https://github.com/storybookjs/storybook/pull/30116), thanks @yannbf! +- CLI: Re-Add Nuxt support - [#28607](https://github.com/storybookjs/storybook/pull/28607), thanks @valentinpalkovic! +- Core: Prevent infinite rerendering caused by comparison by reference - [#30081](https://github.com/storybookjs/storybook/pull/30081), thanks @ghengeveld! + +## 8.5.0-beta.3 + +- Addon A11y: Fix skipped status handling in Testing Module - [#30077](https://github.com/storybookjs/storybook/pull/30077), thanks @valentinpalkovic! +- Core: Float context menu button on top of story titles in sidebar - [#30080](https://github.com/storybookjs/storybook/pull/30080), thanks @ghengeveld! +- Onboarding: Replace `react-confetti` with `@neoconfetti/react` - [#30098](https://github.com/storybookjs/storybook/pull/30098), thanks @ndelangen! + +## 8.5.0-beta.2 + +- Addon Test: Clear coverage data when starting or watching - [#30072](https://github.com/storybookjs/storybook/pull/30072), thanks @ghengeveld! +- Addon Test: Improve error message on missing coverage package - [#30088](https://github.com/storybookjs/storybook/pull/30088), thanks @JReinhold! +- UI: Fix test provider event handling on startup - [#30083](https://github.com/storybookjs/storybook/pull/30083), thanks @ghengeveld! +- UI: Keep failing stories in the sidebar, disregarding filters - [#30086](https://github.com/storybookjs/storybook/pull/30086), thanks @JReinhold! + +## 8.5.0-beta.1 + +- Addon A11y: Add conditional rendering for a11y violation number in Testing Module - [#30073](https://github.com/storybookjs/storybook/pull/30073), thanks @valentinpalkovic! +- Addon A11y: Remove warnings API - [#30049](https://github.com/storybookjs/storybook/pull/30049), thanks @kasperpeulen! +- Addon A11y: Show errors of axe properly - [#30050](https://github.com/storybookjs/storybook/pull/30050), thanks @kasperpeulen! +- Addon Test: Fix printing null% for coverage - [#30061](https://github.com/storybookjs/storybook/pull/30061), thanks @ghengeveld! +- Telemetry: Add metadata distinguishing "apps" from "design systems" - [#30070](https://github.com/storybookjs/storybook/pull/30070), thanks @tmeasday! + +## 8.5.0-beta.0 + +- Automigration: Improve setup file transformation and version range handling for a11y migration - [#30060](https://github.com/storybookjs/storybook/pull/30060), thanks @valentinpalkovic! +- Next.js: Support v15.1.1 - [#30068](https://github.com/storybookjs/storybook/pull/30068), thanks @valentinpalkovic! + +## 8.5.0-alpha.22 + +- Addon Docs: Dynamically import rehype - [#29544](https://github.com/storybookjs/storybook/pull/29544), thanks @valentinpalkovic! +- Addon Test: Fix duplicate `test.include` patterns - [#30029](https://github.com/storybookjs/storybook/pull/30029), thanks @JReinhold! +- Addon Test: Fix environment variable for Vitest Storybook integration - [#30054](https://github.com/storybookjs/storybook/pull/30054), thanks @valentinpalkovic! +- Addon Test: Use local storybook binary instead - [#30021](https://github.com/storybookjs/storybook/pull/30021), thanks @kasperpeulen! +- Addon Test: Wait for 2 seconds before showing result mismatch warning - [#30002](https://github.com/storybookjs/storybook/pull/30002), thanks @ghengeveld! +- Angular: Support statsJson in angular schemas - [#29233](https://github.com/storybookjs/storybook/pull/29233), thanks @yannbf! +- Core: Fix `scrollIntoView` behavior and reimplement testing module time rendering - [#30044](https://github.com/storybookjs/storybook/pull/30044), thanks @ghengeveld! +- Docs: Add code snippet to addons panel - [#29253](https://github.com/storybookjs/storybook/pull/29253), thanks @larsrickert! +- Next.js: Fix webpack fsCache not working - [#29654](https://github.com/storybookjs/storybook/pull/29654), thanks @sentience! +- Nextjs-Vite: Add TS docgen support - [#29824](https://github.com/storybookjs/storybook/pull/29824), thanks @yannbf! +- Nextjs-Vite: Fix docgen types in main config - [#30042](https://github.com/storybookjs/storybook/pull/30042), thanks @yannbf! +- React: Fix RSC compatibility with addon-themes and hooks - [#26243](https://github.com/storybookjs/storybook/pull/26243), thanks @shilman! +- UI: Fix controls and parameters on tag-filtered stories - [#30038](https://github.com/storybookjs/storybook/pull/30038), thanks @shilman! + +## 8.5.0-alpha.21 + +- Addon A11y: Add typesVersions support for TypeScript definitions in a11y package - [#30005](https://github.com/storybookjs/storybook/pull/30005), thanks @valentinpalkovic! +- Addon A11y: Refactor environment variable handling for Vitest integration - [#30022](https://github.com/storybookjs/storybook/pull/30022), thanks @valentinpalkovic! +- Addon A11y: Run the a11y automigration on postInstall - [#30004](https://github.com/storybookjs/storybook/pull/30004), thanks @kasperpeulen! +- Addon A11y: Update accessibility status handling in TestProviderRender - [#30027](https://github.com/storybookjs/storybook/pull/30027), thanks @valentinpalkovic! +- Addon Onboarding: Prebundle react-confetti - [#29996](https://github.com/storybookjs/storybook/pull/29996), thanks @yannbf! +- Addon Test: Correctly stop Storybook when Vitest closes - [#30012](https://github.com/storybookjs/storybook/pull/30012), thanks @JReinhold! +- Addon Test: Show sub test provider toggle state in main testing module - [#30019](https://github.com/storybookjs/storybook/pull/30019), thanks @ghengeveld! +- Addon Test: Wrap sub-paths exported with `require.resolve` - [#30026](https://github.com/storybookjs/storybook/pull/30026), thanks @ndelangen! +- Core: Fix bundling of React - [#30003](https://github.com/storybookjs/storybook/pull/30003), thanks @yannbf! + ## 8.5.0-alpha.20 - Addon Test: Add `@vitest/coverage-v8` during postinstall if no coverage reporter is installed - [#29993](https://github.com/storybookjs/storybook/pull/29993), thanks @ghengeveld! diff --git a/MIGRATION.md b/MIGRATION.md index fd3ce9e10be5..7ab60984a274 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,6 +1,8 @@

Migration

- [From version 8.4.x to 8.5.x](#from-version-84x-to-85x) + - [Introducing features.developmentModeForBuild](#introducing-featuresdevelopmentmodeforbuild) + - [Added source code panel to docs](#added-source-code-panel-to-docs) - [Addon-a11y: Component test integration](#addon-a11y-component-test-integration) - [Addon-a11y: Deprecated `parameters.a11y.manual`](#addon-a11y-deprecated-parametersa11ymanual) - [Indexing behavior of @storybook/experimental-addon-test is changed](#indexing-behavior-of-storybookexperimental-addon-test-is-changed) @@ -109,17 +111,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) @@ -145,7 +147,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) @@ -159,7 +161,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) @@ -210,7 +212,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) @@ -425,14 +427,47 @@ ## From version 8.4.x to 8.5.x +### Introducing features.developmentModeForBuild + +As part of our ongoing efforts to improve the testability and debuggability of Storybook, we are introducing a new feature flag: `developmentModeForBuild`. This feature flag allows you to set `process.env.NODE_ENV` to `development` in built Storybooks, enabling development-related optimizations that are typically disabled in production builds. + +In development mode, React and other libraries often include additional checks and warnings that help catch potential issues early. These checks are usually stripped out in production builds to optimize performance. However, when running tests or debugging issues in a built Storybook, having these additional checks can be incredibly valuable. One such feature is React's `act`, which ensures that all updates related to a test are processed and applied before making assertions. `act` is crucial for reliable and predictable test results, but it only works correctly when `NODE_ENV` is set to `development`. + +```js +// main.js +export default { + features: { + developmentModeForBuild: true, + }, +}; +``` + +### Added source code panel to docs + +Starting in 8.5, Storybook Docs (`@storybook/addon-docs`) automatically adds a new addon panel to stories that displays a source snippet beneath each story. This works similarly to the existing [source snippet doc block](https://storybook.js.org/docs/writing-docs/doc-blocks#source), but in the story view. It is intended to replace the [Storysource addon](https://storybook.js.org/addons/@storybook/addon-storysource). + +If you wish to disable this panel globally, add the following line to your `.storybook/preview.js` project configuration. You can also selectively disable/enable at the story level. + +```js +export default { + parameters: { + docs: { + codePanel: false, + }, + }, +}; +``` + ### Addon-a11y: Component test integration -In Storybook 8.4, we introduced a new addon called [addon test](https://storybook.js.org/docs/writing-tests/test-addon). Powered by Vitest under the hood, this addon lets you watch, run, and debug your component tests directly in Storybook. +In Storybook 8.4, we introduced the [Test addon](https://storybook.js.org/docs/writing-tests/test-addon) (`@storybook/experimental-addon-test`). Powered by Vitest under the hood, this addon lets you watch, run, and debug your component tests directly in Storybook. + +In Storybook 8.5, we revamped the [Accessibility addon](https://storybook.js.org/docs/writing-tests/accessibility-testing) (`@storybook/addon-a11y`) to integrate it with the component tests feature. This means you can now extend your component tests to include accessibility tests. -In Storybook 8.5, we revamped the Accessibility addon (`@storybook/addon-a11y`) to integrate it with the component tests feature. This means you can now extend your component tests to include accessibility tests. If you upgrade to Storybook 8.5 via `npx storybook@latest upgrade`, the Accessibility addon will be automatically configured to work with the component tests. However, if you're upgrading manually and you have the [addon test](https://storybook.js.org/docs/writing-tests/test-addon) installed, adjust your configuration as follows: +If you upgrade to Storybook 8.5 via `npx storybook@latest upgrade`, the Accessibility addon will be automatically configured to work with the component tests. However, if you're upgrading manually and you have the Test addon installed, adjust your configuration as follows: ```diff -// .storybook/vitest.config.ts +// .storybook/vitest.setup.ts ... +import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview'; diff --git a/code/.storybook/main.ts b/code/.storybook/main.ts index ab8af9af8f4a..9870741de29e 100644 --- a/code/.storybook/main.ts +++ b/code/.storybook/main.ts @@ -4,6 +4,7 @@ import type { StorybookConfig } from '../frameworks/react-vite'; const componentsPath = join(__dirname, '../core/src/components'); const managerApiPath = join(__dirname, '../core/src/manager-api'); +const imageContextPath = join(__dirname, '..//frameworks/nextjs/src/image-context.ts'); const config: StorybookConfig = { stories: [ @@ -132,6 +133,7 @@ const config: StorybookConfig = { features: { viewportStoryGlobals: true, backgroundsStoryGlobals: true, + developmentModeForBuild: true, }, viteFinal: async (viteConfig, { configType }) => { const { mergeConfig } = await import('vite'); @@ -145,6 +147,7 @@ const config: StorybookConfig = { 'storybook/internal/components': componentsPath, '@storybook/manager-api': managerApiPath, 'storybook/internal/manager-api': managerApiPath, + 'sb-original/image-context': imageContextPath, } : {}), }, diff --git a/code/.storybook/preview.tsx b/code/.storybook/preview.tsx index 017419318b0a..23f95a0c5d5e 100644 --- a/code/.storybook/preview.tsx +++ b/code/.storybook/preview.tsx @@ -358,9 +358,6 @@ export const parameters = { opacity: 0.4, }, }, - a11y: { - warnings: ['minor', 'moderate', 'serious', 'critical'], - }, }; -export const tags = ['test', 'vitest']; +export const tags = ['test', 'vitest', '!a11ytest']; diff --git a/code/addons/a11y/package.json b/code/addons/a11y/package.json index 3d686e99ed60..b6a82ae028fd 100644 --- a/code/addons/a11y/package.json +++ b/code/addons/a11y/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-a11y", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Test component compliance with web accessibility standards", "keywords": [ "a11y", @@ -38,7 +38,8 @@ }, "./manager": "./dist/manager.js", "./register": "./dist/manager.js", - "./package.json": "./package.json" + "./package.json": "./package.json", + "./postinstall": "./dist/postinstall.js" }, "main": "dist/index.js", "module": "dist/index.mjs", @@ -74,6 +75,7 @@ "@storybook/global": "^5.0.0", "@storybook/icons": "^1.2.12", "@testing-library/react": "^14.0.0", + "execa": "^9.5.2", "picocolors": "^1.1.0", "pretty-format": "^29.7.0", "react": "^18.2.0", @@ -97,6 +99,9 @@ ], "previewEntries": [ "./src/preview.tsx" + ], + "nodeEntries": [ + "./src/postinstall.ts" ] }, "gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae16", diff --git a/code/addons/a11y/src/components/A11YPanel.tsx b/code/addons/a11y/src/components/A11YPanel.tsx index dc048d97b941..61ea9a663af4 100644 --- a/code/addons/a11y/src/components/A11YPanel.tsx +++ b/code/addons/a11y/src/components/A11YPanel.tsx @@ -133,7 +133,11 @@ export const A11YPanel: React.FC = () => { <> The accessibility scan encountered an error.
- {typeof error === 'string' ? error : JSON.stringify(error)} + {typeof error === 'string' + ? error + : error instanceof Error + ? error.toString() + : JSON.stringify(error)} )} diff --git a/code/addons/a11y/src/params.ts b/code/addons/a11y/src/params.ts index dd4357687340..e66a0813a42c 100644 --- a/code/addons/a11y/src/params.ts +++ b/code/addons/a11y/src/params.ts @@ -6,8 +6,6 @@ export interface Setup { options: RunOptions; } -type Impact = NonNullable; - export interface A11yParameters { element?: ElementContext; config?: Spec; @@ -15,5 +13,4 @@ export interface A11yParameters { /** @deprecated Use globals.a11y.manual instead */ manual?: boolean; disable?: boolean; - warnings?: Impact[]; } diff --git a/code/addons/a11y/src/postinstall.ts b/code/addons/a11y/src/postinstall.ts new file mode 100644 index 000000000000..9a64796f5e1b --- /dev/null +++ b/code/addons/a11y/src/postinstall.ts @@ -0,0 +1,15 @@ +import type { PostinstallOptions } from '@storybook/cli/src/add'; + +// eslint-disable-next-line depend/ban-dependencies +import { execa } from 'execa'; + +const $ = execa({ + preferLocal: true, + stdio: 'inherit', + // we stream the stderr to the console + reject: false, +}); + +export default async function postinstall(options: PostinstallOptions) { + await $`storybook automigrate addonA11yAddonTest ${options.yes ? '--yes' : ''}`; +} diff --git a/code/addons/a11y/src/preview.test.tsx b/code/addons/a11y/src/preview.test.tsx index 334f7f924aeb..d09ba89462ed 100644 --- a/code/addons/a11y/src/preview.test.tsx +++ b/code/addons/a11y/src/preview.test.tsx @@ -156,60 +156,6 @@ describe('afterEach', () => { }); }); - it('should report warning status when there are only warnings', async () => { - const context = createContext({ - parameters: { - a11y: { - warnings: ['minor'], - }, - }, - }); - const result = { - violations: [ - { impact: 'minor', nodes: [] }, - { impact: 'critical', nodes: [] }, - ], - }; - mockedRun.mockResolvedValue(result as any); - - await expect(async () => experimental_afterEach(context)).rejects.toThrow(); - - expect(mockedRun).toHaveBeenCalledWith(context.parameters.a11y); - expect(context.reporting.addReport).toHaveBeenCalledWith({ - type: 'a11y', - version: 1, - result, - status: 'failed', - }); - }); - - it('should report error status when there are warnings and errors', async () => { - const context = createContext({ - parameters: { - a11y: { - warnings: ['minor'], - }, - }, - }); - const result = { - violations: [ - { impact: 'minor', nodes: [] }, - { impact: 'critical', nodes: [] }, - ], - }; - mockedRun.mockResolvedValue(result as any); - - await expect(async () => experimental_afterEach(context)).rejects.toThrow(); - - expect(mockedRun).toHaveBeenCalledWith(context.parameters.a11y); - expect(context.reporting.addReport).toHaveBeenCalledWith({ - type: 'a11y', - version: 1, - result, - status: 'failed', - }); - }); - it('should run accessibility checks if "a11ytest" flag is not available and is not running in Vitest', async () => { const context = createContext({ tags: [], diff --git a/code/addons/a11y/src/preview.tsx b/code/addons/a11y/src/preview.tsx index f7d2f9aa43ff..e496894cb113 100644 --- a/code/addons/a11y/src/preview.tsx +++ b/code/addons/a11y/src/preview.tsx @@ -21,7 +21,6 @@ export const experimental_afterEach: AfterEach = async ({ }) => { const a11yParameter: A11yParameters | undefined = parameters.a11y; const a11yGlobals = globals.a11y; - const warnings = a11yParameter?.warnings ?? []; const shouldRunEnvironmentIndependent = a11yParameter?.manual !== true && @@ -38,15 +37,11 @@ export const experimental_afterEach: AfterEach = async ({ if (result) { const hasViolations = (result?.violations.length ?? 0) > 0; - const hasErrors = result?.violations.some( - (violation) => !warnings.includes(violation.impact!) - ); - reporting.addReport({ type: 'a11y', version: 1, result: result, - status: hasErrors ? 'failed' : hasViolations ? 'warning' : 'passed', + status: hasViolations ? 'failed' : 'passed', }); /** @@ -58,7 +53,7 @@ export const experimental_afterEach: AfterEach = async ({ * implement proper try catch handling. */ if (getIsVitestStandaloneRun()) { - if (hasErrors) { + if (hasViolations) { // @ts-expect-error - todo - fix type extension of expect from @storybook/test expect(result).toHaveNoViolations(); } diff --git a/code/addons/a11y/src/utils.ts b/code/addons/a11y/src/utils.ts index 0864a2e3b2f7..f0612d5dfc73 100644 --- a/code/addons/a11y/src/utils.ts +++ b/code/addons/a11y/src/utils.ts @@ -1,25 +1,17 @@ export function getIsVitestStandaloneRun() { try { - return process.env.VITEST_STORYBOOK === 'false'; - } catch { - try { - // @ts-expect-error Suppress TypeScript warning about wrong setting. Doesn't matter, because we don't use tsc for bundling. - return import.meta.env.VITEST_STORYBOOK === 'false'; - } catch (e) { - return false; - } + // @ts-expect-error Suppress TypeScript warning about wrong setting. Doesn't matter, because we don't use tsc for bundling. + return import.meta.env.VITEST_STORYBOOK === 'false'; + } catch (e) { + return false; } } export function getIsVitestRunning() { try { - return process?.env.MODE === 'test'; - } catch { - try { - // @ts-expect-error Suppress TypeScript warning about wrong setting. Doesn't matter, because we don't use tsc for bundling. - return import.meta.env.MODE === 'test'; - } catch (e) { - return false; - } + // @ts-expect-error Suppress TypeScript warning about wrong setting. Doesn't matter, because we don't use tsc for bundling. + return import.meta.env.MODE === 'test'; + } catch (e) { + return false; } } diff --git a/code/addons/actions/package.json b/code/addons/actions/package.json index 45fc3815ec5f..f5032604dcf3 100644 --- a/code/addons/actions/package.json +++ b/code/addons/actions/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-actions", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 ef24a496f8b3..aecb2b2a990d 100644 --- a/code/addons/backgrounds/package.json +++ b/code/addons/backgrounds/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-backgrounds", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 0162f7ed1c0c..adf5675c056a 100644 --- a/code/addons/controls/package.json +++ b/code/addons/controls/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-controls", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 2cb49f00af6e..0b416f5bf770 100644 --- a/code/addons/docs/package.json +++ b/code/addons/docs/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-docs", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Document component usage and properties in Markdown", "keywords": [ "addon", @@ -71,7 +71,12 @@ "./angular": "./angular/index.js", "./angular/index.js": "./angular/index.js", "./web-components/index.js": "./web-components/index.js", - "./package.json": "./package.json" + "./package.json": "./package.json", + "./manager": { + "types": "./dist/manager.d.ts", + "import": "./dist/manager.mjs", + "require": "./dist/manager.js" + } }, "main": "dist/index.js", "module": "dist/index.mjs", @@ -129,7 +134,11 @@ "./src/preview.ts", "./src/blocks.ts", "./src/shims/mdx-react-shim.ts", - "./src/mdx-loader.ts" + "./src/mdx-loader.ts", + "./src/manager.tsx" + ], + "managerEntries": [ + "./src/manager.tsx" ] }, "gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae16", diff --git a/code/addons/docs/src/manager.tsx b/code/addons/docs/src/manager.tsx new file mode 100644 index 000000000000..0b84cd6e36f8 --- /dev/null +++ b/code/addons/docs/src/manager.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { AddonPanel, type SyntaxHighlighterFormatTypes } from 'storybook/internal/components'; +import { ADDON_ID, PANEL_ID, PARAM_KEY, SNIPPET_RENDERED } from 'storybook/internal/docs-tools'; +import { addons, types, useAddonState, useChannel } from 'storybook/internal/manager-api'; + +import { Source } from '@storybook/blocks'; + +addons.register(ADDON_ID, (api) => { + addons.add(PANEL_ID, { + title: 'Code', + type: types.PANEL, + paramKey: PARAM_KEY, + /** + * This code panel can be disabled by the user by adding this parameter: + * + * @example + * + * ```ts + * parameters: { + * docs: { + * codePanel: false, + * }, + * }, + * ``` + */ + disabled: (parameters) => { + return ( + !!parameters && + typeof parameters[PARAM_KEY] === 'object' && + parameters[PARAM_KEY].codePanel === false + ); + }, + match: ({ viewMode }) => viewMode === 'story', + render: ({ active }) => { + const [codeSnippet, setSourceCode] = useAddonState<{ + source: string; + format: SyntaxHighlighterFormatTypes; + }>(ADDON_ID, { + source: '', + format: 'html', + }); + + useChannel({ + [SNIPPET_RENDERED]: ({ source, format }) => { + setSourceCode({ source, format }); + }, + }); + + return ( + + + + ); + }, + }); +}); diff --git a/code/addons/docs/src/plugins/mdx-plugin.ts b/code/addons/docs/src/plugins/mdx-plugin.ts index 40d8e88c3f3a..36fe3b691e97 100644 --- a/code/addons/docs/src/plugins/mdx-plugin.ts +++ b/code/addons/docs/src/plugins/mdx-plugin.ts @@ -3,8 +3,6 @@ import { dirname, join } from 'node:path'; import type { Options } from 'storybook/internal/types'; import { createFilter } from '@rollup/pluginutils'; -import rehypeExternalLinks from 'rehype-external-links'; -import rehypeSlug from 'rehype-slug'; import type { Plugin } from 'vite'; import type { CompileOptions } from '../compiler'; @@ -24,6 +22,9 @@ export async function mdxPlugin(options: Options): Promise { const presetOptions = await presets.apply>('options', {}); const mdxPluginOptions = presetOptions?.mdxPluginOptions as CompileOptions; + const rehypeSlug = (await import('rehype-slug')).default; + const rehypeExternalLinks = (await import('rehype-external-links')).default; + return { name: 'storybook:mdx-plugin', enforce: 'pre', diff --git a/code/addons/docs/src/preset.ts b/code/addons/docs/src/preset.ts index e418b5e2e8aa..9cff707e8472 100644 --- a/code/addons/docs/src/preset.ts +++ b/code/addons/docs/src/preset.ts @@ -5,9 +5,6 @@ import type { DocsOptions, Options, PresetProperty } from 'storybook/internal/ty import type { CsfPluginOptions } from '@storybook/csf-plugin'; -import rehypeExternalLinks from 'rehype-external-links'; -import rehypeSlug from 'rehype-slug'; - import type { CompileOptions } from './compiler'; /** @@ -42,6 +39,9 @@ async function webpack( const { csfPluginOptions = {}, mdxPluginOptions = {} } = options; + const rehypeSlug = (await import('rehype-slug')).default; + const rehypeExternalLinks = (await import('rehype-external-links')).default; + const mdxLoaderOptions: CompileOptions = await options.presets.apply('mdxLoaderOptions', { ...mdxPluginOptions, mdxCompileOptions: { @@ -175,6 +175,9 @@ export const viteFinal = async (config: any, options: Options) => { const { plugins = [] } = config; const { mdxPlugin } = await import('./plugins/mdx-plugin'); + const rehypeSlug = (await import('rehype-slug')).default; + const rehypeExternalLinks = (await import('rehype-external-links')).default; + // Use the resolvedReact preset to alias react and react-dom to either the users version or the version shipped with addon-docs const { react, reactDom, mdx } = await getResolvedReact(options); diff --git a/code/addons/docs/template/stories/sourcePanel/index.stories.tsx b/code/addons/docs/template/stories/sourcePanel/index.stories.tsx new file mode 100644 index 000000000000..9958096cb815 --- /dev/null +++ b/code/addons/docs/template/stories/sourcePanel/index.stories.tsx @@ -0,0 +1,23 @@ +export default { + component: globalThis.Components.Button, + tags: ['autodocs'], + parameters: { + chromatic: { disable: true }, + docs: { + codePanel: false, + }, + }, +}; + +export const One = { args: { label: 'One' } }; + +export const Two = { args: { label: 'Two' } }; + +export const WithSource = { + args: { label: 'Three' }, + parameters: { + docs: { + codePanel: true, + }, + }, +}; diff --git a/code/addons/essentials/package.json b/code/addons/essentials/package.json index bb428b318872..0d8baa2a6099 100644 --- a/code/addons/essentials/package.json +++ b/code/addons/essentials/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-essentials", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Curated addons to bring out the best of Storybook", "keywords": [ "addon", @@ -40,6 +40,7 @@ }, "./backgrounds/manager": "./dist/backgrounds/manager.js", "./controls/manager": "./dist/controls/manager.js", + "./docs/manager": "./dist/docs/manager.js", "./docs/preview": { "types": "./dist/docs/preview.d.ts", "import": "./dist/docs/preview.mjs", @@ -114,10 +115,14 @@ "./src/docs/preset.ts", "./src/docs/mdx-react-shim.ts" ], + "entries": [ + "./src/docs/manager.ts" + ], "managerEntries": [ "./src/actions/manager.ts", "./src/backgrounds/manager.ts", "./src/controls/manager.ts", + "./src/docs/manager.ts", "./src/measure/manager.ts", "./src/outline/manager.ts", "./src/toolbars/manager.ts", diff --git a/code/addons/essentials/src/docs/manager.ts b/code/addons/essentials/src/docs/manager.ts new file mode 100644 index 000000000000..6101f7d79261 --- /dev/null +++ b/code/addons/essentials/src/docs/manager.ts @@ -0,0 +1,2 @@ +// @ts-expect-error (no types needed for this) +export * from '@storybook/addon-docs/manager'; diff --git a/code/addons/essentials/src/index.ts b/code/addons/essentials/src/index.ts index 5809420bc1b8..a72554227ba2 100644 --- a/code/addons/essentials/src/index.ts +++ b/code/addons/essentials/src/index.ts @@ -88,9 +88,9 @@ export function addons(options: PresetOptions) { // NOTE: The order of these addons is important. return [ - 'docs', 'controls', 'actions', + 'docs', 'backgrounds', 'viewport', 'toolbars', diff --git a/code/addons/gfm/package.json b/code/addons/gfm/package.json index 95bd0aa7e641..cd39a40b45b1 100644 --- a/code/addons/gfm/package.json +++ b/code/addons/gfm/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-mdx-gfm", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "GitHub Flavored Markdown in Storybook", "keywords": [ "addon", diff --git a/code/addons/highlight/package.json b/code/addons/highlight/package.json index 00c3280e990a..c096a643d8c7 100644 --- a/code/addons/highlight/package.json +++ b/code/addons/highlight/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-highlight", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 3891ce1ce740..e11acb7b7951 100644 --- a/code/addons/interactions/package.json +++ b/code/addons/interactions/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-interactions", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 158008a17bae..49eda0998378 100644 --- a/code/addons/jest/package.json +++ b/code/addons/jest/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-jest", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 9acb23a278cc..553f715bce4e 100644 --- a/code/addons/links/package.json +++ b/code/addons/links/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-links", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Link stories together to build demos and prototypes with your UI components", "keywords": [ "storybook-addons", diff --git a/code/addons/measure/package.json b/code/addons/measure/package.json index 12e162262346..9833652a6fad 100644 --- a/code/addons/measure/package.json +++ b/code/addons/measure/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-measure", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 6992d234f2e6..43723a24d6e5 100644 --- a/code/addons/onboarding/package.json +++ b/code/addons/onboarding/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-onboarding", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook Addon Onboarding - Introduces a new onboarding experience", "keywords": [ "storybook-addons", @@ -44,10 +44,8 @@ "check": "jiti ../../../scripts/prepare/check.ts", "prep": "jiti ../../../scripts/prepare/addon-bundle.ts" }, - "dependencies": { - "react-confetti": "^6.1.0" - }, "devDependencies": { + "@neoconfetti/react": "^1.0.0", "@radix-ui/react-dialog": "^1.0.5", "@storybook/icons": "^1.2.12", "@storybook/react": "workspace:*", diff --git a/code/addons/onboarding/src/Onboarding.tsx b/code/addons/onboarding/src/Onboarding.tsx index b861d918204a..54e264f67254 100644 --- a/code/addons/onboarding/src/Onboarding.tsx +++ b/code/addons/onboarding/src/Onboarding.tsx @@ -268,17 +268,7 @@ export default function Onboarding({ api }: { api: API }) { return ( - {showConfetti && ( - { - confetti?.reset(); - setShowConfetti(false); - }} - /> - )} + {showConfetti && } {step === '1:Intro' ? ( setStep('2:Controls')} /> ) : ( diff --git a/code/addons/onboarding/src/components/Confetti/Confetti.stories.tsx b/code/addons/onboarding/src/components/Confetti/Confetti.stories.tsx index b55fdf783b34..3540aadc2a85 100644 --- a/code/addons/onboarding/src/components/Confetti/Confetti.stories.tsx +++ b/code/addons/onboarding/src/components/Confetti/Confetti.stories.tsx @@ -8,11 +8,19 @@ const meta: Meta = { component: Confetti, parameters: { chromatic: { disableSnapshot: true }, + layout: 'fullscreen', }, decorators: [ (StoryFn) => ( -
- +
+ Falling confetti! 🎉
), @@ -23,41 +31,4 @@ export default meta; type Story = StoryObj; -export const Default: Story = { - args: { - recycle: true, - numberOfPieces: 200, - top: undefined, - left: undefined, - width: undefined, - height: undefined, - friction: 0.99, - wind: 0, - gravity: 0.1, - initialVelocityX: 4, - initialVelocityY: 10, - tweenDuration: 5000, - }, -}; - -export const OneTimeConfetti: Story = { - args: { - ...Default.args, - numberOfPieces: 800, - recycle: false, - tweenDuration: 20000, - onConfettiComplete: (confetti) => { - confetti?.reset(); - }, - }, -}; - -export const Positioned: Story = { - args: { - ...Default.args, - top: 100, - left: 300, - width: 300, - height: 250, - }, -}; +export const Default: Story = {}; diff --git a/code/addons/onboarding/src/components/Confetti/Confetti.tsx b/code/addons/onboarding/src/components/Confetti/Confetti.tsx index b9b816d3e051..cebc40454909 100644 --- a/code/addons/onboarding/src/components/Confetti/Confetti.tsx +++ b/code/addons/onboarding/src/components/Confetti/Confetti.tsx @@ -1,131 +1,34 @@ -import React, { useEffect } from 'react'; -import { useState } from 'react'; -import { createPortal } from 'react-dom'; +import React, { type ComponentProps } from 'react'; import { styled } from 'storybook/internal/theming'; -import ReactConfetti from 'react-confetti'; +import { Confetti as ReactConfetti } from '@neoconfetti/react'; -interface ConfettiProps extends Omit, 'drawShape'> { - top?: number; - left?: number; - width?: number; - height?: number; - numberOfPieces?: number; - recycle?: boolean; - colors?: string[]; -} +const Wrapper = styled.div({ + zIndex: 9999, + position: 'fixed', + top: 0, + left: '50%', + width: '50%', + height: '100%', +}); -const Wrapper = styled.div<{ - width: number; - height: number; - top: number; - left: number; -}>(({ width, height, left, top }) => ({ - width: `${width}px`, - height: `${height}px`, - left: `${left}px`, - top: `${top}px`, - position: 'relative', - overflow: 'hidden', -})); - -export function Confetti({ - top = 0, - left = 0, - width = window.innerWidth, - height = window.innerHeight, +export const Confetti = React.memo(function Confetti({ + timeToFade = 5000, colors = ['#CA90FF', '#FC521F', '#66BF3C', '#FF4785', '#FFAE00', '#1EA7FD'], ...confettiProps -}: ConfettiProps): React.ReactPortal { - const [confettiContainer] = useState(() => { - const container = document.createElement('div'); - container.setAttribute('id', 'confetti-container'); - container.setAttribute( - 'style', - 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;' - ); - - return container; - }); - - useEffect(() => { - document.body.appendChild(confettiContainer); - - return () => { - document.body.removeChild(confettiContainer); - }; - }, []); - - return createPortal( - - - , - confettiContainer +}: ComponentProps & { timeToFade?: number }) { + return ( + + + ); -} - -enum ParticleShape { - Circle = 1, - Square = 2, - TShape = 3, - LShape = 4, - Triangle = 5, - QuarterCircle = 6, -} - -function getRandomInt(min: number, max: number) { - return Math.floor(Math.random() * (max - min)) + min; -} - -function draw(this: any, context: CanvasRenderingContext2D) { - this.shape = this.shape || getRandomInt(1, 6); - - switch (this.shape) { - case ParticleShape.Square: { - const cornerRadius = 2; - const width = this.w / 2; - const height = this.h / 2; - - context.moveTo(-width + cornerRadius, -height); - context.lineTo(width - cornerRadius, -height); - context.arcTo(width, -height, width, -height + cornerRadius, cornerRadius); - context.lineTo(width, height - cornerRadius); - context.arcTo(width, height, width - cornerRadius, height, cornerRadius); - context.lineTo(-width + cornerRadius, height); - context.arcTo(-width, height, -width, height - cornerRadius, cornerRadius); - context.lineTo(-width, -height + cornerRadius); - context.arcTo(-width, -height, -width + cornerRadius, -height, cornerRadius); - - break; - } - case ParticleShape.TShape: { - context.rect(-4, -4, 8, 16); - context.rect(-12, -4, 24, 8); - break; - } - case ParticleShape.LShape: { - context.rect(-4, -4, 8, 16); - context.rect(-4, -4, 24, 8); - break; - } - case ParticleShape.Circle: { - context.arc(0, 0, this.radius, 0, 2 * Math.PI); - break; - } - case ParticleShape.Triangle: { - context.moveTo(16, 4); - context.lineTo(4, 24); - context.lineTo(24, 24); - break; - } - case ParticleShape.QuarterCircle: { - context.arc(4, -4, 4, -Math.PI / 2, 0); - context.lineTo(4, 0); - break; - } - } - - context.closePath(); - context.fill(); -} +}); diff --git a/code/addons/outline/package.json b/code/addons/outline/package.json index b28d642cffa9..949927f567ee 100644 --- a/code/addons/outline/package.json +++ b/code/addons/outline/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-outline", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 f5e0067ff5fd..80bf2c7bba1a 100644 --- a/code/addons/storysource/package.json +++ b/code/addons/storysource/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-storysource", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "View a story’s source code to see how it works and paste into your app", "keywords": [ "addon", diff --git a/code/addons/test/package.json b/code/addons/test/package.json index 35a4077926e1..22add74762d1 100644 --- a/code/addons/test/package.json +++ b/code/addons/test/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/experimental-addon-test", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Integrate Vitest with Storybook", "keywords": [ "storybook-addons", diff --git a/code/addons/test/src/components/ContextMenuItem.tsx b/code/addons/test/src/components/ContextMenuItem.tsx index 52c3b98061f9..229a396b5388 100644 --- a/code/addons/test/src/components/ContextMenuItem.tsx +++ b/code/addons/test/src/components/ContextMenuItem.tsx @@ -67,7 +67,7 @@ export const ContextMenuItem: FC<{ padding="small" disabled={state.crashed || isDisabled} > - + } /> diff --git a/code/addons/test/src/components/Description.tsx b/code/addons/test/src/components/Description.tsx index a94b775b5c95..58a80dbfdccc 100644 --- a/code/addons/test/src/components/Description.tsx +++ b/code/addons/test/src/components/Description.tsx @@ -12,7 +12,7 @@ export const Wrapper = styled.div(({ theme }) => ({ whiteSpace: 'nowrap', textOverflow: 'ellipsis', fontSize: theme.typography.size.s1, - color: theme.barTextColor, + color: theme.textMutedColor, })); const PositiveText = styled.span(({ theme }) => ({ @@ -60,10 +60,10 @@ export function Description({ state, ...props }: DescriptionProps) { ); } else if (state.progress?.finishedAt) { description = ( - + <> + Ran {state.progress.numTotalTests} {state.progress.numTotalTests === 1 ? 'test' : 'tests'}{' '} + + ); } else if (state.watching) { description = 'Watching for file changes'; diff --git a/code/addons/test/src/components/Interaction.tsx b/code/addons/test/src/components/Interaction.tsx index 4ceef384d02a..75797c65c5a3 100644 --- a/code/addons/test/src/components/Interaction.tsx +++ b/code/addons/test/src/components/Interaction.tsx @@ -23,7 +23,7 @@ const MethodCallWrapper = styled.div(() => ({ const RowContainer = styled('div', { shouldForwardProp: (prop) => !['call', 'pausedAt'].includes(prop.toString()), -})<{ call: Call; pausedAt: Call['id'] }>( +})<{ call: Call; pausedAt: Call['id'] | undefined }>( ({ theme, call }) => ({ position: 'relative', display: 'flex', @@ -117,6 +117,9 @@ const RowMessage = styled('div')(({ theme }) => ({ export const Exception = ({ exception }: { exception: Call['exception'] }) => { const filter = useAnsiToHtmlFilter(); + if (!exception) { + return null; + } if (isJestError(exception)) { return ; } @@ -187,7 +190,7 @@ export const Interaction = ({ - {childCallIds?.length > 0 && ( + {(childCallIds?.length ?? 0) > 0 && ( } diff --git a/code/addons/test/src/components/InteractionsPanel.stories.tsx b/code/addons/test/src/components/InteractionsPanel.stories.tsx index 24eefed2028c..f8cabbe24d53 100644 --- a/code/addons/test/src/components/InteractionsPanel.stories.tsx +++ b/code/addons/test/src/components/InteractionsPanel.stories.tsx @@ -58,7 +58,6 @@ const meta = { endRef: null, // prop for the AddonPanel used as wrapper of Panel active: true, - storyId: 'story-id', }, } as Meta; diff --git a/code/addons/test/src/components/InteractionsPanel.tsx b/code/addons/test/src/components/InteractionsPanel.tsx index 164e28b782fb..896350b926c7 100644 --- a/code/addons/test/src/components/InteractionsPanel.tsx +++ b/code/addons/test/src/components/InteractionsPanel.tsx @@ -44,8 +44,6 @@ interface InteractionsPanelProps { onScrollToEnd?: () => void; hasResultMismatch?: boolean; browserTestStatus?: CallStates; - storyId: StoryId; - testRunId: string; } const Container = styled.div(({ theme }) => ({ @@ -105,20 +103,12 @@ export const InteractionsPanel: React.FC = React.memo( endRef, hasResultMismatch, browserTestStatus, - storyId, - testRunId, }) { const filter = useAnsiToHtmlFilter(); return ( - {hasResultMismatch && ( - - )} + {hasResultMismatch && } {(interactions.length > 0 || hasException) && ( { if (line.startsWith('expect(')) { const received = getParams(line, 7); - const remainderIndex = received && 7 + received.length; + const remainderIndex = received ? 7 + received.length : 0; const matcher = received && line.slice(remainderIndex).match(/\.(to|last|nth)[A-Z]\w+\(/); if (matcher) { - const expectedIndex = remainderIndex + matcher.index + matcher[0].length; + const expectedIndex = remainderIndex + (matcher.index ?? 0) + matcher[0].length; const expected = getParams(line, expectedIndex); if (expected) { return [ diff --git a/code/addons/test/src/components/MethodCall.tsx b/code/addons/test/src/components/MethodCall.tsx index 59b907d13daa..34d1e6bb6f58 100644 --- a/code/addons/test/src/components/MethodCall.tsx +++ b/code/addons/test/src/components/MethodCall.tsx @@ -139,7 +139,7 @@ export const Node = ({ case Object.prototype.hasOwnProperty.call(value, '__class__'): return ; case Object.prototype.hasOwnProperty.call(value, '__callId__'): - return ; + return ; /* eslint-enable no-underscore-dangle */ case Object.prototype.toString.call(value) === '[object Object]': @@ -418,7 +418,7 @@ export const MethodCall = ({ callsById, }: { call?: Call; - callsById: Map; + callsById?: Map; }) => { // Call might be undefined during initial render, can be safely ignored. if (!call) { @@ -434,7 +434,7 @@ export const MethodCall = ({ const callId = (elem as CallRef).__callId__; return [ callId ? ( - + ) : ( {elem as any} ), diff --git a/code/addons/test/src/components/Panel.tsx b/code/addons/test/src/components/Panel.tsx index 584872bad8c3..d6ea74843151 100644 --- a/code/addons/test/src/components/Panel.tsx +++ b/code/addons/test/src/components/Panel.tsx @@ -19,17 +19,9 @@ import { global } from '@storybook/global'; import { type Call, CallStates, EVENTS, type LogItem } from '@storybook/instrumenter'; import type { API_StatusValue } from '@storybook/types'; -import { ADDON_ID, TEST_PROVIDER_ID } from '../constants'; +import { ADDON_ID, STORYBOOK_ADDON_TEST_CHANNEL, TEST_PROVIDER_ID } from '../constants'; import { InteractionsPanel } from './InteractionsPanel'; -interface Interaction extends Call { - status: Call['status']; - childCallIds: Call['id'][]; - isHidden: boolean; - isCollapsed: boolean; - toggleCollapsed: () => void; -} - const INITIAL_CONTROL_STATES = { start: false, back: false, @@ -60,7 +52,7 @@ export const getInteractions = ({ const childCallMap = new Map(); return log - .map(({ callId, ancestors, status }) => { + .map(({ callId, ancestors, status }) => { let isHidden = false; ancestors.forEach((ancestor) => { if (collapsed.has(ancestor)) { @@ -68,11 +60,12 @@ export const getInteractions = ({ } childCallMap.set(ancestor, (childCallMap.get(ancestor) || []).concat(callId)); }); - return { ...calls.get(callId), status, isHidden }; + return { ...calls.get(callId)!, status, isHidden }; }) - .map((call) => { + .map((call) => { const status = call.status === CallStates.ERROR && + call.ancestors && callsById.get(call.ancestors.slice(-1)[0])?.status === CallStates.ACTIVE ? CallStates.ACTIVE : call.status; @@ -114,6 +107,7 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId // local state const [scrollTarget, setScrollTarget] = useState(undefined); const [collapsed, setCollapsed] = useState>(new Set()); + const [hasResultMismatch, setResultMismatch] = useState(false); const { controlStates = INITIAL_CONTROL_STATES, @@ -130,7 +124,7 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId const calls = useRef>>(new Map()); const setCall = ({ status, ...call }: Call) => calls.current.set(call.id, call); - const endRef = useRef(); + const endRef = useRef(); useEffect(() => { let observer: IntersectionObserver; if (global.IntersectionObserver) { @@ -150,6 +144,7 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId { [EVENTS.CALL]: setCall, [EVENTS.SYNC]: (payload) => { + // @ts-expect-error TODO set((s) => { const list = getInteractions({ log: payload.logItems, @@ -213,6 +208,7 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId ); useEffect(() => { + // @ts-expect-error TODO set((s) => { const list = getInteractions({ log: log.current, @@ -226,7 +222,7 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId interactionsCount: list.filter(({ method }) => method !== 'step').length, }; }); - }, [collapsed]); + }, [set, collapsed]); const controls = useMemo( () => ({ @@ -239,7 +235,7 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId emit(FORCE_REMOUNT, { storyId }); }, }), - [storyId] + [emit, storyId] ); const storyFilePath = useParameter('fileName', ''); @@ -249,25 +245,52 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId const hasException = !!caughtException || !!unhandledErrors || + // @ts-expect-error TODO interactions.some((v) => v.status === CallStates.ERROR); const storyStatus = storyStatuses[storyId]?.[TEST_PROVIDER_ID]; + const storyTestStatus = storyStatus?.status; - const browserTestStatus = React.useMemo(() => { + const browserTestStatus = useMemo(() => { if (!isPlaying && (interactions.length > 0 || hasException)) { return hasException ? CallStates.ERROR : CallStates.DONE; } - return isPlaying ? CallStates.ACTIVE : null; + return isPlaying ? CallStates.ACTIVE : undefined; }, [isPlaying, interactions, hasException]); - const hasResultMismatch = React.useMemo(() => { - return ( - browserTestStatus !== null && - browserTestStatus !== CallStates.ACTIVE && - storyStatus?.status !== undefined && - statusMap[browserTestStatus] !== storyStatus.status - ); - }, [browserTestStatus, storyStatus]); + const { testRunId } = storyStatus?.data || {}; + + useEffect(() => { + const isMismatch = + browserTestStatus && + storyTestStatus && + storyTestStatus !== 'pending' && + storyTestStatus !== statusMap[browserTestStatus]; + + if (isMismatch) { + const timeout = setTimeout( + () => + setResultMismatch((currentValue) => { + if (!currentValue) { + emit(STORYBOOK_ADDON_TEST_CHANNEL, { + type: 'test-discrepancy', + payload: { + browserStatus: browserTestStatus === CallStates.DONE ? 'PASS' : 'FAIL', + cliStatus: browserTestStatus === CallStates.DONE ? 'FAIL' : 'PASS', + storyId, + testRunId, + }, + }); + } + return true; + }), + 2000 + ); + return () => clearTimeout(timeout); + } else { + setResultMismatch(false); + } + }, [emit, browserTestStatus, storyTestStatus, storyId, testRunId]); if (isErrored) { return ; @@ -288,10 +311,9 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId unhandledErrors={unhandledErrors} isPlaying={isPlaying} pausedAt={pausedAt} + // @ts-expect-error TODO endRef={endRef} onScrollToEnd={scrollTarget && scrollToTarget} - storyId={storyId} - testRunId={storyStatus?.data?.testRunId} /> ); diff --git a/code/addons/test/src/components/RelativeTime.stories.tsx b/code/addons/test/src/components/RelativeTime.stories.tsx new file mode 100644 index 000000000000..4d3c6af0f6d8 --- /dev/null +++ b/code/addons/test/src/components/RelativeTime.stories.tsx @@ -0,0 +1,47 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { RelativeTime } from './RelativeTime'; + +const meta = { + component: RelativeTime, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const JustNow: Story = { + args: { + timestamp: Date.now() - 1000 * 10, + }, +}; + +export const AMinuteAgo: Story = { + args: { + timestamp: Date.now() - 1000 * 60, + }, +}; + +export const MinutesAgo: Story = { + args: { + timestamp: Date.now() - 1000 * 60 * 2, + }, +}; + +export const HoursAgo: Story = { + args: { + timestamp: Date.now() - 1000 * 60 * 60 * 3, + }, +}; + +export const Yesterday: Story = { + args: { + timestamp: Date.now() - 1000 * 60 * 60 * 24, + }, +}; + +export const DaysAgo: Story = { + args: { + timestamp: Date.now() - 1000 * 60 * 60 * 24 * 3, + }, +}; diff --git a/code/addons/test/src/components/RelativeTime.tsx b/code/addons/test/src/components/RelativeTime.tsx index fa9e7cf6d549..4d4cf3c48693 100644 --- a/code/addons/test/src/components/RelativeTime.tsx +++ b/code/addons/test/src/components/RelativeTime.tsx @@ -1,41 +1,35 @@ import { useEffect, useState } from 'react'; -export function getRelativeTimeString(date: Date): string { - const delta = Math.round((date.getTime() - Date.now()) / 1000); - const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity]; - const units: Intl.RelativeTimeFormatUnit[] = [ - 'second', - 'minute', - 'hour', - 'day', - 'week', - 'month', - 'year', - ]; - - const unitIndex = cutoffs.findIndex((cutoff) => cutoff > Math.abs(delta)); - const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1; - const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }); - return rtf.format(Math.floor(delta / divisor), units[unitIndex]); -} - -export const RelativeTime = ({ timestamp, testCount }: { timestamp: Date; testCount: number }) => { - const [relativeTimeString, setRelativeTimeString] = useState(null); +export const RelativeTime = ({ timestamp }: { timestamp?: number }) => { + const [timeAgo, setTimeAgo] = useState(null); useEffect(() => { if (timestamp) { - setRelativeTimeString(getRelativeTimeString(timestamp).replace(/^now$/, 'just now')); - - const interval = setInterval(() => { - setRelativeTimeString(getRelativeTimeString(timestamp).replace(/^now$/, 'just now')); - }, 10000); - + setTimeAgo(Date.now() - timestamp); + const interval = setInterval(() => setTimeAgo(Date.now() - timestamp), 10000); return () => clearInterval(interval); } }, [timestamp]); - return ( - relativeTimeString && - `Ran ${testCount} ${testCount === 1 ? 'test' : 'tests'} ${relativeTimeString}` - ); + if (timeAgo === null) { + return null; + } + + const seconds = Math.round(timeAgo / 1000); + if (seconds < 60) { + return `just now`; + } + + const minutes = Math.floor(seconds / 60); + if (minutes < 60) { + return minutes === 1 ? `a minute ago` : `${minutes} minutes ago`; + } + + const hours = Math.floor(minutes / 60); + if (hours < 24) { + return hours === 1 ? `an hour ago` : `${hours} hours ago`; + } + + const days = Math.floor(hours / 24); + return days === 1 ? `yesterday` : `${days} days ago`; }; diff --git a/code/addons/test/src/components/StatusBadge.tsx b/code/addons/test/src/components/StatusBadge.tsx index d730b8ef985c..a906b501a939 100644 --- a/code/addons/test/src/components/StatusBadge.tsx +++ b/code/addons/test/src/components/StatusBadge.tsx @@ -14,7 +14,7 @@ const StyledBadge = styled.div(({ theme, status }) => { [CallStates.ERROR]: theme.color.negative, [CallStates.ACTIVE]: theme.color.warning, [CallStates.WAITING]: theme.color.warning, - }[status]; + }[status!]; return { padding: '4px 6px 4px 8px;', borderRadius: '4px', @@ -36,7 +36,7 @@ export const StatusBadge: React.FC = ({ status }) => { [CallStates.ERROR]: 'Fail', [CallStates.ACTIVE]: 'Runs', [CallStates.WAITING]: 'Runs', - }[status]; + }[status!]; return ( {badgeText} diff --git a/code/addons/test/src/components/Subnav.tsx b/code/addons/test/src/components/Subnav.tsx index bf9d8436cee0..88fcbd5c4522 100644 --- a/code/addons/test/src/components/Subnav.tsx +++ b/code/addons/test/src/components/Subnav.tsx @@ -109,7 +109,7 @@ const RerunButton = styled(StyledIconButton)< >(({ theme, animating, disabled }) => ({ opacity: disabled ? 0.5 : 1, svg: { - animation: animating && `${theme.animation.rotate360} 200ms ease-out`, + animation: animating ? `${theme.animation.rotate360} 200ms ease-out` : undefined, }, })); diff --git a/code/addons/test/src/components/TestDiscrepancyMessage.stories.tsx b/code/addons/test/src/components/TestDiscrepancyMessage.stories.tsx index 840c08cdf3d3..81553cdc5a32 100644 --- a/code/addons/test/src/components/TestDiscrepancyMessage.stories.tsx +++ b/code/addons/test/src/components/TestDiscrepancyMessage.stories.tsx @@ -23,9 +23,6 @@ export default { parameters: { layout: 'fullscreen', }, - args: { - storyId: 'story-id', - }, decorators: [ (storyFn) => ( {storyFn()} diff --git a/code/addons/test/src/components/TestDiscrepancyMessage.tsx b/code/addons/test/src/components/TestDiscrepancyMessage.tsx index bbdf74e36a65..2ff2e97c9f77 100644 --- a/code/addons/test/src/components/TestDiscrepancyMessage.tsx +++ b/code/addons/test/src/components/TestDiscrepancyMessage.tsx @@ -1,13 +1,12 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { Link } from 'storybook/internal/components'; import { useStorybookApi } from 'storybook/internal/manager-api'; import { styled } from 'storybook/internal/theming'; -import type { StoryId } from 'storybook/internal/types'; import { CallStates } from '@storybook/instrumenter'; -import { DOCUMENTATION_DISCREPANCY_LINK, STORYBOOK_ADDON_TEST_CHANNEL } from '../constants'; +import { DOCUMENTATION_DISCREPANCY_LINK } from '../constants'; const Wrapper = styled.div(({ theme: { color, typography, background } }) => ({ textAlign: 'start', @@ -32,40 +31,24 @@ const Wrapper = styled.div(({ theme: { color, typography, background } }) => ({ })); interface TestDiscrepancyMessageProps { - browserTestStatus: CallStates; - storyId: StoryId; - testRunId: string; + browserTestStatus?: CallStates; } -export const TestDiscrepancyMessage = ({ - browserTestStatus, - storyId, - testRunId, -}: TestDiscrepancyMessageProps) => { + +export const TestDiscrepancyMessage = ({ browserTestStatus }: TestDiscrepancyMessageProps) => { const api = useStorybookApi(); const docsUrl = api.getDocsUrl({ subpath: DOCUMENTATION_DISCREPANCY_LINK, versioned: true, renderer: true, }); - const message = `This component test passed in ${browserTestStatus === CallStates.DONE ? 'this browser' : 'CLI'}, but the tests failed in ${browserTestStatus === CallStates.ERROR ? 'this browser' : 'CLI'}.`; - - useEffect( - () => - api.emit(STORYBOOK_ADDON_TEST_CHANNEL, { - type: 'test-discrepancy', - payload: { - browserStatus: browserTestStatus === CallStates.DONE ? 'PASS' : 'FAIL', - cliStatus: browserTestStatus === CallStates.DONE ? 'FAIL' : 'PASS', - storyId, - testRunId, - }, - }), - [api, browserTestStatus, storyId, testRunId] - ); + const [passed, failed] = + browserTestStatus === CallStates.ERROR + ? ['the CLI', 'this browser'] + : ['this browser', 'the CLI']; return ( - {message}{' '} + This component test passed in {passed}, but the tests failed in {failed}.{' '} Learn what could cause this diff --git a/code/addons/test/src/components/TestProviderRender.stories.tsx b/code/addons/test/src/components/TestProviderRender.stories.tsx index dead913949cf..5111c1507a49 100644 --- a/code/addons/test/src/components/TestProviderRender.stories.tsx +++ b/code/addons/test/src/components/TestProviderRender.stories.tsx @@ -43,7 +43,7 @@ const baseState: TestProviderState = { cancellable: true, cancelling: false, crashed: false, - error: null, + error: undefined, failed: false, running: false, watching: false, @@ -52,6 +52,10 @@ const baseState: TestProviderState = { coverage: false, }, details: { + config: { + a11y: false, + coverage: false, + }, testResults: [ { endTime: 0, @@ -141,6 +145,10 @@ export const WithCoverageNegative: Story = { ...config, ...baseState, details: { + config: { + a11y: false, + coverage: true, + }, testResults: [], coverageSummary: { percentage: 20, @@ -162,6 +170,10 @@ export const WithCoverageWarning: Story = { ...baseState, details: { testResults: [], + config: { + a11y: false, + coverage: true, + }, coverageSummary: { percentage: 50, status: 'warning', @@ -182,6 +194,10 @@ export const WithCoveragePositive: Story = { ...baseState, details: { testResults: [], + config: { + a11y: false, + coverage: true, + }, coverageSummary: { percentage: 80, status: 'positive', @@ -206,6 +222,10 @@ export const Editing: Story = { }, details: { testResults: [], + config: { + a11y: false, + coverage: false, + }, }, }, }, @@ -229,6 +249,10 @@ export const EditingAndWatching: Story = { }, details: { testResults: [], + config: { + a11y: true, + coverage: true, // should be automatically disabled in the UI + }, }, }, }, diff --git a/code/addons/test/src/components/TestProviderRender.tsx b/code/addons/test/src/components/TestProviderRender.tsx index 017ecc2cf27f..4b1f4c35bc4d 100644 --- a/code/addons/test/src/components/TestProviderRender.tsx +++ b/code/addons/test/src/components/TestProviderRender.tsx @@ -76,6 +76,16 @@ const StopIcon = styled(StopAltIcon)({ width: 10, }); +const ItemTitle = styled.span<{ enabled?: boolean }>( + ({ enabled, theme }) => + !enabled && { + color: theme.textMutedColor, + '&:after': { + content: '" (disabled)"', + }, + } +); + const statusOrder: TestStatus[] = ['failed', 'warning', 'pending', 'passed', 'skipped']; const statusMap: Record['status']> = { failed: 'negative', @@ -104,6 +114,8 @@ export const TestProviderRender: FC< state.config || { a11y: false, coverage: false } ); + const isStoryEntry = entryId?.includes('--') ?? false; + const a11yResults = useMemo(() => { if (!isA11yAddon) { return []; @@ -118,16 +130,22 @@ export const TestProviderRender: FC< }, [isA11yAddon, state.details?.testResults, entryId]); const a11yStatus = useMemo<'positive' | 'warning' | 'negative' | 'unknown'>(() => { + if (state.running) { + return 'unknown'; + } + if (!isA11yAddon || config.a11y === false) { return 'unknown'; } - if (!a11yResults) { + const definedA11yResults = a11yResults?.filter(Boolean) ?? []; + + if (!definedA11yResults || definedA11yResults.length === 0) { return 'unknown'; } - const failed = a11yResults.some((result) => result?.status === 'failed'); - const warning = a11yResults.some((result) => result?.status === 'warning'); + const failed = definedA11yResults.some((result) => result?.status === 'failed'); + const warning = definedA11yResults.some((result) => result?.status === 'warning'); if (failed) { return 'negative'; @@ -136,13 +154,26 @@ export const TestProviderRender: FC< } return 'positive'; - }, [a11yResults, isA11yAddon, config.a11y]); + }, [state.running, isA11yAddon, config.a11y, a11yResults]); - const a11yNotPassedAmount = a11yResults?.filter( - (result) => result?.status === 'failed' || result?.status === 'warning' - ).length; + const a11yNotPassedAmount = state.config?.a11y + ? a11yResults?.filter((result) => result?.status === 'failed' || result?.status === 'warning') + .length + : undefined; + + const a11ySkippedAmount = + state.running || !state?.details.config?.a11y || !state.config?.a11y + ? null + : a11yResults?.filter((result) => !result).length; + + const a11ySkippedLabel = a11ySkippedAmount + ? a11ySkippedAmount === 1 && isStoryEntry + ? '(skipped)' + : `(${a11ySkippedAmount} skipped)` + : ''; + + const storyId = isStoryEntry ? entryId : undefined; - const storyId = entryId?.includes('--') ? entryId : undefined; const results = (state.details?.testResults || []) .flatMap((test) => { if (!entryId) { @@ -154,7 +185,11 @@ export const TestProviderRender: FC< }) .sort((a, b) => statusOrder.indexOf(a.status) - statusOrder.indexOf(b.status)); - const status = (state.failed ? 'failed' : results[0]?.status) || 'unknown'; + const status = state.running + ? 'unknown' + : state.failed + ? 'failed' + : (results[0]?.status ?? 'unknown'); const openPanel = (id: string, panelId: string) => { api.selectStory(id); @@ -233,7 +268,7 @@ export const TestProviderRender: FC< /> Coverage} icon={} right={ Accessibility} icon={} right={ r.status === 'failed' || r.status === 'warning' ); - openPanel(firstNotPassed.storyId, PANEL_ID); + if (firstNotPassed) { + openPanel(firstNotPassed.storyId, PANEL_ID); + } } - : null + : undefined } icon={ state.crashed ? ( @@ -285,10 +322,11 @@ export const TestProviderRender: FC< /> {coverageSummary ? ( Coverage} href={'/coverage/index.html'} // @ts-expect-error ListItem doesn't include all anchor attributes in types, but it is an achor element target="_blank" + aria-label="Open coverage report" icon={ } - right={`${coverageSummary.percentage}%`} + right={ + coverageSummary.percentage ? ( + + {coverageSummary.percentage} % + + ) : null + } /> ) : ( Coverage} icon={} /> )} {isA11yAddon && ( Accessibility {a11ySkippedLabel}} onClick={ (a11yStatus === 'negative' || a11yStatus === 'warning') && a11yResults.length ? () => { @@ -317,12 +361,14 @@ export const TestProviderRender: FC< (report) => report.status === 'failed' || report.status === 'warning' ) ); - openPanel(firstNotPassed.storyId, A11y_ADDON_PANEL_ID); + if (firstNotPassed) { + openPanel(firstNotPassed.storyId, A11y_ADDON_PANEL_ID); + } } - : null + : undefined } icon={} - right={a11yNotPassedAmount || null} + right={isStoryEntry ? null : a11yNotPassedAmount || null} /> )} diff --git a/code/addons/test/src/constants.ts b/code/addons/test/src/constants.ts index 0453930e3758..58463326dcd7 100644 --- a/code/addons/test/src/constants.ts +++ b/code/addons/test/src/constants.ts @@ -12,6 +12,13 @@ export const DOCUMENTATION_FATAL_ERROR_LINK = `${DOCUMENTATION_LINK}#what-happen export const COVERAGE_DIRECTORY = 'coverage'; +export const SUPPORTED_FRAMEWORKS = [ + '@storybook/nextjs', + '@storybook/experimental-nextjs-vite', + '@storybook/sveltekit', +]; + +export const SUPPORTED_RENDERERS = ['@storybook/react', '@storybook/svelte', '@storybook/vue3']; export interface Config { coverage: boolean; a11y: boolean; @@ -19,6 +26,7 @@ export interface Config { export type Details = { testResults: TestResult[]; + config: Config; coverageSummary?: { status: 'positive' | 'warning' | 'negative' | 'unknown'; percentage: number; diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 4db0c5defcbd..7264e5fe49cc 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -38,6 +38,7 @@ addons.register(ADDON_ID, (api) => { runnable: true, watchable: true, name: 'Component tests', + // @ts-expect-error: TODO: Fix types render: (state) => { const [isModalOpen, setModalOpen] = useState(false); return ( @@ -55,6 +56,7 @@ addons.register(ADDON_ID, (api) => { ); }, + // @ts-expect-error: TODO: Fix types sidebarContextMenu: ({ context, state }) => { if (context.type === 'docs') { return null; @@ -72,67 +74,81 @@ addons.register(ADDON_ID, (api) => { ); }, + // @ts-expect-error: TODO: Fix types stateUpdater: (state, update) => { - if (!update.details?.testResults) { - return; + const updated = { + ...state, + ...update, + details: { ...state.details, ...update.details }, + }; + + if ((!state.running && update.running) || (!state.watching && update.watching)) { + // Clear coverage data when starting test run or enabling watch mode + delete updated.details.coverageSummary; } - (async () => { - await api.experimental_updateStatus( - TEST_PROVIDER_ID, - Object.fromEntries( - update.details.testResults.flatMap((testResult) => - testResult.results - .filter(({ storyId }) => storyId) - .map(({ storyId, status, testRunId, ...rest }) => [ - storyId, - { - title: 'Component tests', - status: statusMap[status], - description: - 'failureMessages' in rest && rest.failureMessages - ? rest.failureMessages.join('\n') - : '', - data: { testRunId }, - onClick: openTestsPanel, - sidebarContextMenu: false, - } satisfies API_StatusObject, - ]) + if (update.details?.testResults) { + (async () => { + await api.experimental_updateStatus( + TEST_PROVIDER_ID, + Object.fromEntries( + // @ts-expect-error: TODO: Fix types + update.details.testResults.flatMap((testResult) => + testResult.results + .filter(({ storyId }) => storyId) + .map(({ storyId, status, testRunId, ...rest }) => [ + storyId, + { + title: 'Component tests', + status: statusMap[status], + description: + 'failureMessages' in rest && rest.failureMessages + ? rest.failureMessages.join('\n') + : '', + data: { testRunId }, + onClick: openTestsPanel, + sidebarContextMenu: false, + } satisfies API_StatusObject, + ]) + ) ) - ) - ); + ); - await api.experimental_updateStatus( - 'storybook/addon-a11y/test-provider', - Object.fromEntries( - update.details.testResults.flatMap((testResult) => - testResult.results - .filter(({ storyId }) => storyId) - .map(({ storyId, testRunId, reports }) => { - const a11yReport = reports.find((r: any) => r.type === 'a11y'); - return [ - storyId, - a11yReport - ? ({ - title: 'Accessibility tests', - description: '', - status: statusMap[a11yReport.status], - data: { testRunId }, - onClick: () => { - api.setSelectedPanel('storybook/a11y/panel'); - api.togglePanel(true); - }, - sidebarContextMenu: false, - } satisfies API_StatusObject) - : null, - ]; - }) + await api.experimental_updateStatus( + 'storybook/addon-a11y/test-provider', + Object.fromEntries( + // @ts-expect-error: TODO: Fix types + update.details.testResults.flatMap((testResult) => + testResult.results + .filter(({ storyId }) => storyId) + .map(({ storyId, testRunId, reports }) => { + const a11yReport = reports.find((r: any) => r.type === 'a11y'); + return [ + storyId, + a11yReport + ? ({ + title: 'Accessibility tests', + description: '', + status: statusMap[a11yReport.status], + data: { testRunId }, + onClick: () => { + api.setSelectedPanel('storybook/a11y/panel'); + api.togglePanel(true); + }, + sidebarContextMenu: false, + } satisfies API_StatusObject) + : null, + ]; + }) + ) ) - ) - ); - })(); + ); + })(); + } + + return updated; }, - } as Addon_TestProviderType); + } satisfies Omit, 'id'>); } const filter = ({ state }: Combo) => { @@ -147,7 +163,7 @@ addons.register(ADDON_ID, (api) => { match: ({ viewMode }) => viewMode === 'story', render: ({ active }) => { return ( - + {({ storyId }) => } ); diff --git a/code/addons/test/src/node/boot-test-runner.ts b/code/addons/test/src/node/boot-test-runner.ts index 3f0329807e98..f1fe3ddc94f3 100644 --- a/code/addons/test/src/node/boot-test-runner.ts +++ b/code/addons/test/src/node/boot-test-runner.ts @@ -24,7 +24,7 @@ const MAX_START_TIME = 30000; const vitestModulePath = join(__dirname, 'node', 'vitest.mjs'); // Events that were triggered before Vitest was ready are queued up and resent once it's ready -const eventQueue: { type: string; args: any[] }[] = []; +const eventQueue: { type: string; args?: any[] }[] = []; let child: null | ChildProcess; let ready = false; @@ -87,7 +87,7 @@ const bootTestRunner = async (channel: Channel) => { if (result.type === 'ready') { // Resend events that triggered (during) the boot sequence, now that Vitest is ready while (eventQueue.length) { - const { type, args } = eventQueue.shift(); + const { type, args } = eventQueue.shift()!; child?.send({ type, args, from: 'server' }); } diff --git a/code/addons/test/src/node/coverage-reporter.ts b/code/addons/test/src/node/coverage-reporter.ts index 452643cd9d60..cc1dd548000f 100644 --- a/code/addons/test/src/node/coverage-reporter.ts +++ b/code/addons/test/src/node/coverage-reporter.ts @@ -8,7 +8,7 @@ import type { TestManager } from './test-manager'; export type StorybookCoverageReporterOptions = { testManager: TestManager; - coverageOptions: ResolvedCoverageOptions<'v8'>; + coverageOptions: ResolvedCoverageOptions<'v8'> | undefined; }; export default class StorybookCoverageReporter extends ReportBase implements Partial { @@ -32,7 +32,7 @@ export default class StorybookCoverageReporter extends ReportBase implements Par // Fallback to Vitest's default watermarks https://vitest.dev/config/#coverage-watermarks const [lowWatermark = 50, highWatermark = 80] = - this.#coverageOptions.watermarks?.statements ?? []; + this.#coverageOptions?.watermarks?.statements ?? []; const coverageDetails: Details['coverageSummary'] = { percentage, diff --git a/code/addons/test/src/node/reporter.ts b/code/addons/test/src/node/reporter.ts index 43191cbc2fdc..1f71e1bf9670 100644 --- a/code/addons/test/src/node/reporter.ts +++ b/code/addons/test/src/node/reporter.ts @@ -173,6 +173,7 @@ export class StorybookReporter implements Reporter { } as TestingModuleProgressReportProgress, details: { testResults, + config: this.testManager.config, }, }; } @@ -219,7 +220,7 @@ export class StorybookReporter implements Reporter { (t) => t.status === 'failed' && t.results.length === 0 ); - const reducedTestSuiteFailures = new Set(); + const reducedTestSuiteFailures = new Set(); testSuiteFailures.forEach((t) => { reducedTestSuiteFailures.add(t.message); @@ -239,7 +240,7 @@ export class StorybookReporter implements Reporter { message: Array.from(reducedTestSuiteFailures).reduce( (acc, curr) => `${acc}\n${curr}`, '' - ), + )!, } : { name: `${unhandledErrors.length} unhandled error${unhandledErrors?.length > 1 ? 's' : ''}`, diff --git a/code/addons/test/src/node/test-manager.test.ts b/code/addons/test/src/node/test-manager.test.ts index db77ab2f3e2e..985f74c97595 100644 --- a/code/addons/test/src/node/test-manager.test.ts +++ b/code/addons/test/src/node/test-manager.test.ts @@ -22,10 +22,11 @@ const vitest = vi.hoisted(() => ({ configOverride: { actualTestNamePattern: undefined, get testNamePattern() { - return this.actualTestNamePattern; + return this.actualTestNamePattern!; }, set testNamePattern(value: string) { setTestNamePattern(value); + // @ts-expect-error Ignore for testing this.actualTestNamePattern = value; }, }, @@ -105,11 +106,11 @@ describe('TestManager', () => { it('should handle watch mode request', async () => { const testManager = await TestManager.start(mockChannel, options); - expect(testManager.watchMode).toBe(false); + expect(testManager.config.watchMode).toBe(false); expect(createVitest).toHaveBeenCalledTimes(1); await testManager.handleWatchModeRequest({ providerId: TEST_PROVIDER_ID, watchMode: true }); - expect(testManager.watchMode).toBe(true); + expect(testManager.config.watchMode).toBe(true); expect(createVitest).toHaveBeenCalledTimes(1); // shouldn't restart vitest }); @@ -149,7 +150,7 @@ describe('TestManager', () => { it('should handle coverage toggling', async () => { const testManager = await TestManager.start(mockChannel, options); - expect(testManager.coverage).toBe(false); + expect(testManager.config.coverage).toBe(false); expect(createVitest).toHaveBeenCalledTimes(1); createVitest.mockClear(); @@ -157,7 +158,7 @@ describe('TestManager', () => { providerId: TEST_PROVIDER_ID, config: { coverage: true, a11y: false }, }); - expect(testManager.coverage).toBe(true); + expect(testManager.config.coverage).toBe(true); expect(createVitest).toHaveBeenCalledTimes(1); createVitest.mockClear(); @@ -165,21 +166,21 @@ describe('TestManager', () => { providerId: TEST_PROVIDER_ID, config: { coverage: false, a11y: false }, }); - expect(testManager.coverage).toBe(false); + expect(testManager.config.coverage).toBe(false); expect(createVitest).toHaveBeenCalledTimes(1); }); it('should temporarily disable coverage on focused tests', async () => { vitest.globTestSpecs.mockImplementation(() => tests); const testManager = await TestManager.start(mockChannel, options); - expect(testManager.coverage).toBe(false); + expect(testManager.config.coverage).toBe(false); expect(createVitest).toHaveBeenCalledTimes(1); await testManager.handleConfigChange({ providerId: TEST_PROVIDER_ID, config: { coverage: true, a11y: false }, }); - expect(testManager.coverage).toBe(true); + expect(testManager.config.coverage).toBe(true); expect(createVitest).toHaveBeenCalledTimes(2); await testManager.handleRunRequest({ diff --git a/code/addons/test/src/node/test-manager.ts b/code/addons/test/src/node/test-manager.ts index 4770f2b5a174..1a19e587eeea 100644 --- a/code/addons/test/src/node/test-manager.ts +++ b/code/addons/test/src/node/test-manager.ts @@ -18,9 +18,11 @@ import { VitestManager } from './vitest-manager'; export class TestManager { vitestManager: VitestManager; - watchMode = false; - - coverage = false; + config = { + watchMode: false, + coverage: false, + a11y: false, + }; constructor( private channel: Channel, @@ -44,23 +46,22 @@ export class TestManager { return; } + const previousConfig = this.config; + + this.config = { + ...this.config, + ...payload.config, + } satisfies Config; + process.env.VITEST_STORYBOOK_CONFIG = JSON.stringify(payload.config); - if (this.coverage !== payload.config.coverage) { - this.coverage = payload.config.coverage; + if (previousConfig.coverage !== payload.config.coverage) { try { await this.vitestManager.restartVitest({ - coverage: this.coverage, + coverage: this.config.coverage, }); } catch (e) { - const isV8 = e.message?.includes('@vitest/coverage-v8'); - const isIstanbul = e.message?.includes('@vitest/coverage-istanbul'); - - if (e.message?.includes('Error: Failed to load url') && (isIstanbul || isV8)) { - const coveragePackage = isIstanbul ? 'coverage-istanbul' : 'coverage-v8'; - e.message = `Please install the @vitest/${coveragePackage} package to run with coverage`; - } - this.reportFatalError('Failed to change coverage mode', e); + this.reportFatalError('Failed to change coverage configuration', e); } } } @@ -69,7 +70,7 @@ export class TestManager { if (payload.providerId !== TEST_PROVIDER_ID) { return; } - this.watchMode = payload.watchMode; + this.config.watchMode = payload.watchMode; if (payload.config) { this.handleConfigChange({ @@ -78,14 +79,14 @@ export class TestManager { }); } - if (this.coverage) { + if (this.config.coverage) { try { if (payload.watchMode) { // if watch mode is toggled on and coverage is already enabled, restart vitest without coverage to automatically disable it await this.vitestManager.restartVitest({ coverage: false }); } else { // if watch mode is toggled off and coverage is already enabled, restart vitest with coverage to automatically re-enable it - await this.vitestManager.restartVitest({ coverage: this.coverage }); + await this.vitestManager.restartVitest({ coverage: this.config.coverage }); } } catch (e) { this.reportFatalError('Failed to change watch mode while coverage was enabled', e); @@ -111,7 +112,7 @@ export class TestManager { as a coverage report for a subset of stories is not useful. */ const temporarilyDisableCoverage = - this.coverage && !this.watchMode && (payload.storyIds ?? []).length > 0; + this.config.coverage && !this.config.watchMode && (payload.storyIds ?? []).length > 0; if (temporarilyDisableCoverage) { await this.vitestManager.restartVitest({ coverage: false, @@ -124,7 +125,7 @@ export class TestManager { if (temporarilyDisableCoverage) { // Re-enable coverage if it was temporarily disabled because of a subset of stories was run - await this.vitestManager.restartVitest({ coverage: this.coverage }); + await this.vitestManager.restartVitest({ coverage: this.config.coverage }); } } catch (e) { this.reportFatalError('Failed to run tests', e); diff --git a/code/addons/test/src/node/vitest-manager.ts b/code/addons/test/src/node/vitest-manager.ts index 7267f3dcea38..4145acf18a3f 100644 --- a/code/addons/test/src/node/vitest-manager.ts +++ b/code/addons/test/src/node/vitest-manager.ts @@ -14,7 +14,7 @@ import type { TestingModuleRunRequestPayload } from 'storybook/internal/core-eve import type { DocsIndexEntry, StoryIndex, StoryIndexEntry } from '@storybook/types'; -import path, { normalize } from 'pathe'; +import path, { dirname, join, normalize } from 'pathe'; import slash from 'slash'; import { COVERAGE_DIRECTORY, type Config } from '../constants'; @@ -29,6 +29,11 @@ type TagsFilter = { skip: string[]; }; +const packageDir = dirname(require.resolve('@storybook/experimental-addon-test/package.json')); + +// We have to tell Vitest that it runs as part of Storybook +process.env.VITEST_STORYBOOK = 'true'; + export class VitestManager { vitest: Vitest | null = null; @@ -44,10 +49,10 @@ export class VitestManager { const { createVitest } = await import('vitest/node'); const storybookCoverageReporter: [string, StorybookCoverageReporterOptions] = [ - '@storybook/experimental-addon-test/internal/coverage-reporter', + join(packageDir, 'dist/node/coverage-reporter.js'), { testManager: this.testManager, - coverageOptions: this.vitest?.config?.coverage as ResolvedCoverageOptions<'v8'>, + coverageOptions: this.vitest?.config?.coverage as ResolvedCoverageOptions<'v8'> | undefined, }, ]; const coverageOptions = ( @@ -63,31 +68,17 @@ export class VitestManager { : { enabled: false } ) as CoverageOptions; - this.vitest = await createVitest( - 'test', - { - watch: true, - passWithNoTests: false, - // TODO: - // Do we want to enable Vite's default reporter? - // The output in the terminal might be too spamy and it might be better to - // find a way to just show errors and warnings for example - // Otherwise it might be hard for the user to discover Storybook related logs - reporters: ['default', new StorybookReporter(this.testManager)], - coverage: coverageOptions, - }, - { - define: { - // polyfilling process.env.VITEST_STORYBOOK to 'true' in the browser - 'process.env.VITEST_STORYBOOK': 'true', - }, - } - ); - - this.vitest.configOverride.env = { - // We signal to the test runner that we are running it via Storybook - VITEST_STORYBOOK: 'true', - }; + this.vitest = await createVitest('test', { + watch: true, + passWithNoTests: false, + // TODO: + // Do we want to enable Vite's default reporter? + // The output in the terminal might be too spamy and it might be better to + // find a way to just show errors and warnings for example + // Otherwise it might be hard for the user to discover Storybook related logs + reporters: ['default', new StorybookReporter(this.testManager)], + coverage: coverageOptions, + }); if (this.vitest) { this.vitest.onCancel(() => { @@ -97,16 +88,22 @@ export class VitestManager { try { await this.vitest.init(); - } catch (e) { + } catch (e: any) { + let message = 'Failed to initialize Vitest'; const isV8 = e.message?.includes('@vitest/coverage-v8'); const isIstanbul = e.message?.includes('@vitest/coverage-istanbul'); - if (e.message?.includes('Error: Failed to load url') && (isIstanbul || isV8)) { + if ( + (e.message?.includes('Failed to load url') && (isIstanbul || isV8)) || + // Vitest will sometimes not throw the correct missing-package-detection error, so we have to check for this as well + (e instanceof TypeError && + e?.message === "Cannot read properties of undefined (reading 'name')") + ) { const coveragePackage = isIstanbul ? 'coverage-istanbul' : 'coverage-v8'; - e.message = `Please install the @vitest/${coveragePackage} package to run with coverage`; + message += `\n\nPlease install the @vitest/${coveragePackage} package to collect coverage\n`; } - - this.testManager.reportFatalError('Failed to init Vitest', e); + this.testManager.reportFatalError(message, e); + return; } await this.setupWatchers(); @@ -151,7 +148,7 @@ export class VitestManager { ])) as StoryIndex; const storyIds = requestStoryIds || Object.keys(index.entries); return storyIds.map((id) => index.entries[id]).filter((story) => story.type === 'story'); - } catch (e) { + } catch (e: any) { log('Failed to fetch story index: ' + e.message); return []; } @@ -202,7 +199,7 @@ export class VitestManager { this.filterStories(story, spec.moduleId, { include, exclude, skip }) ); if (matches.length) { - if (!this.testManager.watchMode) { + if (!this.testManager.config.watchMode) { // Clear the file cache if watch mode is not enabled this.updateLastChanged(spec.moduleId); } @@ -319,20 +316,21 @@ export class VitestManager { const id = slash(file); this.vitest?.logger.clearHighlightCache(id); this.updateLastChanged(id); - this.storyCountForCurrentRun = 0; // when watch mode is disabled, don't trigger any tests (below) // but still invalidate the cache for the changed file, which is handled above - if (!this.testManager.watchMode) { + if (!this.testManager.config.watchMode) { return; } + + this.storyCountForCurrentRun = 0; await this.runAffectedTests(file); } async registerVitestConfigListener() { this.vitest?.server?.watcher.on('change', async (file) => { file = normalize(file); - const isConfig = file === this.vitest.server.config.configFile; + const isConfig = file === this.vitest?.server.config.configFile; if (isConfig) { log('Restarting Vitest due to config change'); await this.closeVitest(); diff --git a/code/addons/test/src/postinstall.ts b/code/addons/test/src/postinstall.ts index 55f97eb6b0da..b3c3dba95943 100644 --- a/code/addons/test/src/postinstall.ts +++ b/code/addons/test/src/postinstall.ts @@ -16,7 +16,7 @@ import { readConfig, writeConfig } from 'storybook/internal/csf-tools'; import { colors, logger } from 'storybook/internal/node-logger'; // eslint-disable-next-line depend/ban-dependencies -import { execa } from 'execa'; +import { $ } from 'execa'; import { findUp } from 'find-up'; import { dirname, extname, join, relative, resolve } from 'pathe'; import picocolors from 'picocolors'; @@ -25,6 +25,7 @@ import { coerce, satisfies } from 'semver'; import { dedent } from 'ts-dedent'; import { type PostinstallOptions } from '../../../lib/cli-storybook/src/add'; +import { SUPPORTED_FRAMEWORKS, SUPPORTED_RENDERERS } from './constants'; import { printError, printInfo, printSuccess, step } from './postinstall-logger'; import { getAddonNames } from './utils'; @@ -55,8 +56,7 @@ export default async function postInstall(options: PostinstallOptions) { const allDeps = await packageManager.getAllDependencies(); // only install these dependencies if they are not already installed const dependencies = ['vitest', '@vitest/browser', 'playwright'].filter((p) => !allDeps[p]); - const vitestVersionSpecifier = - allDeps.vitest || (await packageManager.getInstalledVersion('vitest')); + const vitestVersionSpecifier = await packageManager.getInstalledVersion('vitest'); const coercedVitestVersion = vitestVersionSpecifier ? coerce(vitestVersionSpecifier) : null; // if Vitest is installed, we use the same version to keep consistency across Vitest packages const vitestVersionToInstall = vitestVersionSpecifier ?? 'latest'; @@ -106,18 +106,11 @@ export default async function postInstall(options: PostinstallOptions) { } } - const annotationsImport = [ - '@storybook/nextjs', - '@storybook/experimental-nextjs-vite', - '@storybook/sveltekit', - ].includes(info.frameworkPackageName) + const annotationsImport = SUPPORTED_FRAMEWORKS.includes(info.frameworkPackageName) ? info.frameworkPackageName === '@storybook/nextjs' ? '@storybook/experimental-nextjs-vite' : info.frameworkPackageName - : info.rendererPackageName && - ['@storybook/react', '@storybook/svelte', '@storybook/vue3'].includes( - info.rendererPackageName - ) + : info.rendererPackageName && SUPPORTED_RENDERERS.includes(info.rendererPackageName) ? info.rendererPackageName : null; @@ -177,14 +170,14 @@ export default async function postInstall(options: PostinstallOptions) { reasons.push( dedent` Please check the documentation for more information about its requirements and installation: - ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/vitest-plugin`)} + ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/test-addon`)} ` ); } else { reasons.push( dedent` Fear not, however, you can follow the manual installation process instead at: - ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/vitest-plugin#manual`)} + ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/test-addon#manual-setup`)} ` ); } @@ -227,22 +220,9 @@ export default async function postInstall(options: PostinstallOptions) { } if (shouldUninstall) { - await execa( - packageManager.getRemoteRunCommand(), - [ - 'storybook', - 'remove', - addonInteractionsName, - '--package-manager', - options.packageManager, - '--config-dir', - options.configDir, - ], - { - shell: true, - stdio: 'inherit', - } - ); + await $({ + stdio: 'inherit', + })`storybook remove ${addonInteractionsName} --package-manager ${options.packageManager} --config-dir ${options.configDir}`; } } @@ -321,7 +301,7 @@ export default async function postInstall(options: PostinstallOptions) { ${colors.gray(vitestSetupFile)} Please refer to the documentation to complete the setup manually: - ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/vitest-plugin#manual`)} + ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/test-addon#manual-setup`)} ` ); logger.line(1); @@ -381,7 +361,7 @@ export default async function postInstall(options: PostinstallOptions) { your existing workspace file automatically, you must do it yourself. This was the last step. Please refer to the documentation to complete the setup manually: - ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/vitest-plugin#manual`)} + ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/test-addon#manual-setup`)} ` ); logger.line(1); @@ -397,13 +377,13 @@ export default async function postInstall(options: PostinstallOptions) { '🚨 Oh no!', dedent` You seem to have an existing test configuration in your Vite config file: - ${colors.gray(vitestWorkspaceFile || '')} + ${colors.gray(viteConfigFile || '')} I was able to configure most of the addon but could not safely extend your existing workspace file automatically, you must do it yourself. This was the last step. Please refer to the documentation to complete the setup manually: - ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/vitest-plugin#manual`)} + ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/test-addon#manual-setup`)} ` ); logger.line(1); @@ -429,14 +409,14 @@ export default async function postInstall(options: PostinstallOptions) { import { defineWorkspace } from 'vitest/config'; import { storybookTest } from '@storybook/experimental-addon-test/vitest-plugin';${vitestInfo.frameworkPluginImport} - // More info at: https://storybook.js.org/docs/writing-tests/vitest-plugin + // More info at: https://storybook.js.org/docs/writing-tests/test-addon export default defineWorkspace([ '${relative(dirname(browserWorkspaceFile), rootConfig)}', { extends: '${viteConfigFile ? relative(dirname(browserWorkspaceFile), viteConfigFile) : ''}', plugins: [ // The plugin will run tests for the stories defined in your Storybook config - // See options at: https://storybook.js.org/docs/writing-tests/vitest-plugin#storybooktest + // See options at: https://storybook.js.org/docs/writing-tests/test-addon#storybooktest storybookTest({ configDir: '${options.configDir}' }),${vitestInfo.frameworkPluginDocs + vitestInfo.frameworkPluginCall} ], test: { @@ -469,11 +449,11 @@ export default async function postInstall(options: PostinstallOptions) { import { defineConfig } from 'vitest/config'; import { storybookTest } from '@storybook/experimental-addon-test/vitest-plugin';${vitestInfo.frameworkPluginImport} - // More info at: https://storybook.js.org/docs/writing-tests/vitest-plugin + // More info at: https://storybook.js.org/docs/writing-tests/test-addon export default defineConfig({ plugins: [ // The plugin will run tests for the stories defined in your Storybook config - // See options at: https://storybook.js.org/docs/writing-tests/vitest-plugin#storybooktest + // See options at: https://storybook.js.org/docs/writing-tests/test-addon#storybooktest storybookTest({ configDir: '${options.configDir}' }),${vitestInfo.frameworkPluginDocs + vitestInfo.frameworkPluginCall} ], test: { @@ -503,7 +483,7 @@ export default async function postInstall(options: PostinstallOptions) { • When using the Vitest extension in your editor, all of your stories will be shown as tests! Check the documentation for more information about its features and options at: - ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/vitest-plugin`)} + ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/test-addon`)} ` ); logger.line(1); diff --git a/code/addons/test/src/vitest-plugin/global-setup.ts b/code/addons/test/src/vitest-plugin/global-setup.ts index ca287c105e2b..8526c48e245e 100644 --- a/code/addons/test/src/vitest-plugin/global-setup.ts +++ b/code/addons/test/src/vitest-plugin/global-setup.ts @@ -74,13 +74,15 @@ export const teardown = async () => { logger.verbose('Stopping Storybook process'); await new Promise((resolve, reject) => { // Storybook starts multiple child processes, so we need to kill the whole tree - treeKill(storybookProcess.pid, 'SIGTERM', (error) => { - if (error) { - logger.error('Failed to stop Storybook process:'); - reject(error); - return; - } - resolve(); - }); + if (storybookProcess?.pid) { + treeKill(storybookProcess.pid, 'SIGTERM', (error) => { + if (error) { + logger.error('Failed to stop Storybook process:'); + reject(error); + return; + } + resolve(); + }); + } }); }; diff --git a/code/addons/test/src/vitest-plugin/index.ts b/code/addons/test/src/vitest-plugin/index.ts index eadadb4440e5..180075f9f891 100644 --- a/code/addons/test/src/vitest-plugin/index.ts +++ b/code/addons/test/src/vitest-plugin/index.ts @@ -1,4 +1,6 @@ /* eslint-disable no-underscore-dangle */ +import { dirname } from 'node:path'; + import type { Plugin } from 'vitest/config'; import { mergeConfig } from 'vitest/config'; import type { ViteUserConfig } from 'vitest/config'; @@ -23,7 +25,6 @@ import sirv from 'sirv'; import { convertPathToPattern } from 'tinyglobby'; import { dedent } from 'ts-dedent'; -import { TestManager } from '../node/test-manager'; import type { InternalOptions, UserOptions } from './types'; const WORKING_DIR = process.cwd(); @@ -63,6 +64,8 @@ const getStoryGlobsAndFiles = async ( }; }; +const packageDir = dirname(require.resolve('@storybook/experimental-addon-test/package.json')); + export const storybookTest = async (options?: UserOptions): Promise => { const finalOptions = { ...defaultOptions, @@ -124,7 +127,7 @@ export const storybookTest = async (options?: UserOptions): Promise => { .replace('', `${headHtmlSnippet ?? ''}`) .replace('', `${bodyHtmlSnippet ?? ''}`); }, - async config(inputConfig_DoNotMutate) { + async config(inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED) { // ! We're not mutating the input config, instead we're returning a new partial config // ! see https://vite.dev/guide/api-plugin.html#config try { @@ -143,18 +146,23 @@ export const storybookTest = async (options?: UserOptions): Promise => { // plugin.name?.startsWith('vitest:browser') // ) + // We signal the test runner that we are not running it via Storybook + // We are overriding the environment variable to 'true' if vitest runs via @storybook/addon-test's backend + const vitestStorybook = process.env.VITEST_STORYBOOK ?? 'false'; + const baseConfig: Omit = { test: { setupFiles: [ - '@storybook/experimental-addon-test/internal/setup-file', + join(packageDir, 'dist/vitest-plugin/setup-file.mjs'), // if the existing setupFiles is a string, we have to include it otherwise we're overwriting it - typeof inputConfig_DoNotMutate.test?.setupFiles === 'string' && - inputConfig_DoNotMutate.test?.setupFiles, - ].filter(Boolean), + typeof inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test + ?.setupFiles === 'string' && + inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test?.setupFiles, + ].filter(Boolean) as string[], ...(finalOptions.storybookScript ? { - globalSetup: ['@storybook/experimental-addon-test/internal/global-setup'], + globalSetup: [join(packageDir, 'dist/vitest-plugin/global-setup.mjs')], } : {}), @@ -162,9 +170,8 @@ export const storybookTest = async (options?: UserOptions): Promise => { ...storybookEnv, // To be accessed by the setup file __STORYBOOK_URL__: finalOptions.storybookUrl, - // We signal the test runner that we are not running it via Storybook - // We are overriding the environment variable to 'true' if vitest runs via @storybook/addon-test's backend - VITEST_STORYBOOK: 'false', + + VITEST_STORYBOOK: vitestStorybook, __VITEST_INCLUDE_TAGS__: finalOptions.tags.include.join(','), __VITEST_EXCLUDE_TAGS__: finalOptions.tags.exclude.join(','), __VITEST_SKIP_TAGS__: finalOptions.tags.skip.join(','), @@ -175,7 +182,8 @@ export const storybookTest = async (options?: UserOptions): Promise => { .map((path) => convertPathToPattern(path)), // if the existing deps.inline is true, we keep it as-is, because it will inline everything - ...(inputConfig_DoNotMutate.test?.server?.deps?.inline !== true + ...(inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test?.server?.deps + ?.inline !== true ? { server: { deps: { @@ -185,8 +193,9 @@ export const storybookTest = async (options?: UserOptions): Promise => { } : {}), + // @ts-expect-error: TODO browser: { - ...inputConfig_DoNotMutate.test?.browser, + ...inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test?.browser, commands: { getInitialGlobals: () => { const envConfig = JSON.parse(process.env.VITEST_STORYBOOK_CONFIG ?? '{}'); @@ -203,8 +212,9 @@ export const storybookTest = async (options?: UserOptions): Promise => { }, }, // if there is a test.browser config AND test.browser.screenshotFailures is not explicitly set, we set it to false - ...(inputConfig_DoNotMutate.test?.browser && - inputConfig_DoNotMutate.test.browser.screenshotFailures === undefined + ...(inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test?.browser && + inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test.browser + .screenshotFailures === undefined ? { screenshotFailures: false, } @@ -213,7 +223,11 @@ export const storybookTest = async (options?: UserOptions): Promise => { }, envPrefix: Array.from( - new Set([...(inputConfig_DoNotMutate.envPrefix || []), 'STORYBOOK_', 'VITE_']) + new Set([ + ...(inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.envPrefix || []), + 'STORYBOOK_', + 'VITE_', + ]) ), resolve: { @@ -239,8 +253,6 @@ export const storybookTest = async (options?: UserOptions): Promise => { }, define: { - // polyfilling process.env.VITEST_STORYBOOK to 'false' in the browser - 'process.env.VITEST_STORYBOOK': JSON.stringify('false'), ...(frameworkName?.includes('vue3') ? { __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false' } : {}), @@ -254,8 +266,14 @@ export const storybookTest = async (options?: UserOptions): Promise => { ); // alert the user of problems - if (inputConfig_DoNotMutate.test.include?.length > 0) { - console.warn( + if ( + (inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test?.include?.length ?? + 0) > 0 + ) { + // remove the user's existing include, because we're replacing it with our own heuristic based on main.ts#stories + // @ts-expect-error: Ignore + inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test.include = []; + console.log( picocolors.yellow(dedent` Warning: Starting in Storybook 8.5.0-alpha.18, the "test.include" option in Vitest is discouraged in favor of just using the "stories" field in your Storybook configuration. @@ -270,19 +288,21 @@ export const storybookTest = async (options?: UserOptions): Promise => { return config; }, async configureServer(server) { - for (const staticDir of staticDirs) { - try { - const { staticPath, targetEndpoint } = mapStaticDir(staticDir, directories.configDir); - server.middlewares.use( - targetEndpoint, - sirv(staticPath, { - dev: true, - etag: true, - extensions: [], - }) - ); - } catch (e) { - console.warn(e); + if (staticDirs) { + for (const staticDir of staticDirs) { + try { + const { staticPath, targetEndpoint } = mapStaticDir(staticDir, directories.configDir); + server.middlewares.use( + targetEndpoint, + sirv(staticPath, { + dev: true, + etag: true, + extensions: [], + }) + ); + } catch (e) { + console.warn(e); + } } } }, diff --git a/code/addons/test/src/vitest-plugin/viewports.ts b/code/addons/test/src/vitest-plugin/viewports.ts index a8bcc90bc408..905ee44fc937 100644 --- a/code/addons/test/src/vitest-plugin/viewports.ts +++ b/code/addons/test/src/vitest-plugin/viewports.ts @@ -85,7 +85,7 @@ export const setViewport = async (parameters: Parameters = {}, globals: Globals let viewportWidth = DEFAULT_VIEWPORT_DIMENSIONS.width; let viewportHeight = DEFAULT_VIEWPORT_DIMENSIONS.height; - if (defaultViewport in viewports) { + if (defaultViewport && defaultViewport in viewports) { const styles = viewports[defaultViewport].styles as ViewportStyles; if (styles?.width && styles?.height) { const { width, height } = styles; diff --git a/code/addons/test/tsconfig.json b/code/addons/test/tsconfig.json index 060b5d432fc7..e8a15eafa0bd 100644 --- a/code/addons/test/tsconfig.json +++ b/code/addons/test/tsconfig.json @@ -5,7 +5,7 @@ "module": "Preserve", "moduleResolution": "Bundler", "types": ["vitest"], - "strict": false + "strict": true }, "include": ["src/**/*", "./typings.d.ts"] } diff --git a/code/addons/themes/docs/api.md b/code/addons/themes/docs/api.md index a45818ab724e..d9c2d2285fc1 100644 --- a/code/addons/themes/docs/api.md +++ b/code/addons/themes/docs/api.md @@ -109,6 +109,9 @@ export const myCustomDecorator = ### `useThemeParameters` +(⛔️ **Deprecated**) +_Do not use this hook anymore. Access the theme directly via the context instead e.g. `context.parameters.themes`_ + Returns the theme parameters for this addon. ```js @@ -152,14 +155,14 @@ Let's use Vuetify as an example. Vuetify uses it's own global state to know whic import { DecoratorHelpers } from '@storybook/addon-themes'; import { useTheme } from 'vuetify'; -const { initializeThemeState, pluckThemeFromContext, useThemeParameters } = DecoratorHelpers; +const { initializeThemeState, pluckThemeFromContext } = DecoratorHelpers; export const withVuetifyTheme = ({ themes, defaultTheme }) => { initializeThemeState(Object.keys(themes), defaultTheme); return (story, context) => { const selectedTheme = pluckThemeFromContext(context); - const { themeOverride } = useThemeParameters(); + const { themeOverride } = context.parameters.themes ?? {}; const selected = themeOverride || selectedTheme || defaultTheme; diff --git a/code/addons/themes/package.json b/code/addons/themes/package.json index 3acb32417fbd..acfd4e156a91 100644 --- a/code/addons/themes/package.json +++ b/code/addons/themes/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-themes", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Switch between multiple themes for you components in Storybook", "keywords": [ "css", diff --git a/code/addons/themes/src/decorators/class-name.decorator.tsx b/code/addons/themes/src/decorators/class-name.decorator.tsx index 4bc56202f038..42b403a5b382 100644 --- a/code/addons/themes/src/decorators/class-name.decorator.tsx +++ b/code/addons/themes/src/decorators/class-name.decorator.tsx @@ -1,7 +1,8 @@ import { useEffect } from 'storybook/internal/preview-api'; import type { DecoratorFunction, Renderer } from 'storybook/internal/types'; -import { initializeThemeState, pluckThemeFromContext, useThemeParameters } from './helpers'; +import { PARAM_KEY } from '../constants'; +import { initializeThemeState, pluckThemeFromContext } from './helpers'; export interface ClassNameStrategyConfiguration { themes: Record; @@ -22,7 +23,7 @@ export const withThemeByClassName = ({ initializeThemeState(Object.keys(themes), defaultTheme); return (storyFn, context) => { - const { themeOverride } = useThemeParameters(); + const { themeOverride } = context.parameters[PARAM_KEY] ?? {}; const selected = pluckThemeFromContext(context); useEffect(() => { diff --git a/code/addons/themes/src/decorators/data-attribute.decorator.tsx b/code/addons/themes/src/decorators/data-attribute.decorator.tsx index 6d4b1cfe26f9..55e4f258bfd2 100644 --- a/code/addons/themes/src/decorators/data-attribute.decorator.tsx +++ b/code/addons/themes/src/decorators/data-attribute.decorator.tsx @@ -1,7 +1,8 @@ import { useEffect } from 'storybook/internal/preview-api'; import type { DecoratorFunction, Renderer } from 'storybook/internal/types'; -import { initializeThemeState, pluckThemeFromContext, useThemeParameters } from './helpers'; +import { PARAM_KEY } from '../constants'; +import { initializeThemeState, pluckThemeFromContext } from './helpers'; export interface DataAttributeStrategyConfiguration { themes: Record; @@ -22,7 +23,7 @@ export const withThemeByDataAttribute = ({ }: DataAttributeStrategyConfiguration): DecoratorFunction => { initializeThemeState(Object.keys(themes), defaultTheme); return (storyFn, context) => { - const { themeOverride } = useThemeParameters(); + const { themeOverride } = context.parameters[PARAM_KEY] ?? {}; const selected = pluckThemeFromContext(context); useEffect(() => { diff --git a/code/addons/themes/src/decorators/helpers.ts b/code/addons/themes/src/decorators/helpers.ts index 237b0943568c..97c70dd1f0a4 100644 --- a/code/addons/themes/src/decorators/helpers.ts +++ b/code/addons/themes/src/decorators/helpers.ts @@ -1,6 +1,9 @@ +import { deprecate } from 'storybook/internal/client-logger'; import { addons, useParameter } from 'storybook/internal/preview-api'; import type { StoryContext } from 'storybook/internal/types'; +import dedent from 'ts-dedent'; + import type { ThemeParameters } from '../constants'; import { DEFAULT_THEME_PARAMETERS, GLOBAL_KEY, PARAM_KEY, THEMING_EVENTS } from '../constants'; @@ -12,8 +15,18 @@ export function pluckThemeFromContext({ globals }: StoryContext): string { return globals[GLOBAL_KEY] || ''; } -export function useThemeParameters(): ThemeParameters { - return useParameter(PARAM_KEY, DEFAULT_THEME_PARAMETERS) as ThemeParameters; +export function useThemeParameters(context?: StoryContext): ThemeParameters { + deprecate( + dedent`The useThemeParameters function is deprecated. Please access parameters via the context directly instead e.g. + - const { themeOverride } = context.parameters.themes ?? {}; + ` + ); + + if (!context) { + return useParameter(PARAM_KEY, DEFAULT_THEME_PARAMETERS) as ThemeParameters; + } + + return context.parameters[PARAM_KEY] ?? DEFAULT_THEME_PARAMETERS; } export function initializeThemeState(themeNames: string[], defaultTheme: string) { diff --git a/code/addons/themes/src/decorators/provider.decorator.tsx b/code/addons/themes/src/decorators/provider.decorator.tsx index 8985a9773fad..41964e7afcfc 100644 --- a/code/addons/themes/src/decorators/provider.decorator.tsx +++ b/code/addons/themes/src/decorators/provider.decorator.tsx @@ -4,7 +4,8 @@ import React from 'react'; import { useMemo } from 'storybook/internal/preview-api'; import type { DecoratorFunction, Renderer } from 'storybook/internal/types'; -import { initializeThemeState, pluckThemeFromContext, useThemeParameters } from './helpers'; +import { PARAM_KEY } from '../constants'; +import { initializeThemeState, pluckThemeFromContext } from './helpers'; type Theme = Record; type ThemeMap = Record; @@ -32,7 +33,8 @@ export const withThemeFromJSXProvider = ({ // eslint-disable-next-line react/display-name return (storyFn, context) => { - const { themeOverride } = useThemeParameters(); + // eslint-disable-next-line react/destructuring-assignment + const { themeOverride } = context.parameters[PARAM_KEY] ?? {}; const selected = pluckThemeFromContext(context); const theme = useMemo(() => { diff --git a/code/addons/toolbars/package.json b/code/addons/toolbars/package.json index f4917fae862c..2799a7a8d7d7 100644 --- a/code/addons/toolbars/package.json +++ b/code/addons/toolbars/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-toolbars", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 2cad1d10517d..a41b266ffaf0 100644 --- a/code/addons/viewport/package.json +++ b/code/addons/viewport/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-viewport", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Build responsive components by adjusting Storybook’s viewport size and orientation", "keywords": [ "addon", diff --git a/code/builders/builder-vite/package.json b/code/builders/builder-vite/package.json index 12c37e443cc2..5fd9910d7fab 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "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-vite/src/build.ts b/code/builders/builder-vite/src/build.ts index fa7d1ee4f76e..9782081c0465 100644 --- a/code/builders/builder-vite/src/build.ts +++ b/code/builders/builder-vite/src/build.ts @@ -35,9 +35,22 @@ export async function build(options: Options) { } : {}), }, - }).build; + } as InlineConfig).build; - const finalConfig = await presets.apply('viteFinal', config, options); + const finalConfig = (await presets.apply('viteFinal', config, options)) as InlineConfig; + + if (options.features?.developmentModeForBuild) { + finalConfig.plugins?.push({ + name: 'storybook:define-env', + config: () => { + return { + define: { + 'process.env.NODE_ENV': JSON.stringify('development'), + }, + }; + }, + }); + } const turbosnapPluginName = 'rollup-plugin-turbosnap'; const hasTurbosnapPlugin = diff --git a/code/builders/builder-vite/src/index.ts b/code/builders/builder-vite/src/index.ts index 785db459cec4..7051cc116363 100644 --- a/code/builders/builder-vite/src/index.ts +++ b/code/builders/builder-vite/src/index.ts @@ -4,8 +4,6 @@ import { readFile } from 'node:fs/promises'; import { NoStatsForViteDevError } from 'storybook/internal/server-errors'; import type { Middleware, Options } from 'storybook/internal/types'; -import sirv from 'sirv'; -import { corePath } from 'storybook/core-path'; import type { ViteDevServer } from 'vite'; import { build as viteBuild } from './build'; diff --git a/code/builders/builder-vite/src/optimizeDeps.ts b/code/builders/builder-vite/src/optimizeDeps.ts index cbade7a268a6..74dd0090be42 100644 --- a/code/builders/builder-vite/src/optimizeDeps.ts +++ b/code/builders/builder-vite/src/optimizeDeps.ts @@ -13,16 +13,58 @@ const INCLUDE_CANDIDATES = [ '@emotion/core', '@emotion/is-prop-valid', '@emotion/styled', + '@storybook/addon-a11y/preview', + '@storybook/addon-backgrounds/preview', + '@storybook/addon-designs/blocks', + '@storybook/addon-docs/preview', + '@storybook/addon-essentials/actions/preview', + '@storybook/addon-essentials/actions/preview', + '@storybook/addon-essentials/backgrounds/preview', + '@storybook/addon-essentials/docs/preview', + '@storybook/addon-essentials/highlight/preview', + '@storybook/addon-essentials/measure/preview', + '@storybook/addon-essentials/outline/preview', + '@storybook/addon-essentials/viewport/preview', + '@storybook/addon-highlight/preview', + '@storybook/addon-links/preview', + '@storybook/addon-measure/preview', + '@storybook/addon-outline/preview', + '@storybook/addon-themes', + '@storybook/addon-themes/preview', + '@storybook/addon-viewport', + '@storybook/addon-viewport/preview', + '@storybook/blocks', + '@storybook/components', + '@storybook/experimental-addon-test/preview', + '@storybook/experimental-nextjs-vite/dist/preview.mjs', + '@storybook/html', + '@storybook/html/dist/entry-preview-docs.mjs', + '@storybook/html/dist/entry-preview.mjs', + '@storybook/preact', + '@storybook/preact/dist/entry-preview-docs.mjs', + '@storybook/preact/dist/entry-preview.mjs', '@storybook/react > acorn-jsx', '@storybook/react', + '@storybook/react/dist/entry-preview-docs.mjs', + '@storybook/react/dist/entry-preview-rsc.mjs', + '@storybook/react/dist/entry-preview.mjs', '@storybook/svelte', + '@storybook/svelte/dist/entry-preview-docs.mjs', + '@storybook/svelte/dist/entry-preview.mjs', + '@storybook/theming', '@storybook/vue3', + '@storybook/vue3/dist/entry-preview-docs.mjs', + '@storybook/vue3/dist/entry-preview.mjs', + '@storybook/web-components', + '@storybook/web-components/dist/entry-preview-docs.mjs', + '@storybook/web-components/dist/entry-preview.mjs', 'acorn-jsx', 'acorn-walk', 'acorn', 'airbnb-js-shims', 'ansi-to-html', 'axe-core', + 'chromatic/isChromatic', 'color-convert', 'deep-object-diff', 'doctrine', @@ -73,6 +115,8 @@ const INCLUDE_CANDIDATES = [ 'lodash/upperFirst.js', 'lodash/upperFirst', 'memoizerific', + 'mockdate', + 'msw-storybook-addon', 'overlayscrollbars', 'polished', 'prettier/parser-babel', @@ -100,8 +144,11 @@ const INCLUDE_CANDIDATES = [ 'refractor/lang/typescript.js', 'refractor/lang/yaml.js', 'regenerator-runtime/runtime.js', + 'sb-original/default-loader', + 'sb-original/image-context', 'slash', 'store2', + 'storybook/internal/preview/runtime', 'synchronous-promise', 'telejson', 'ts-dedent', diff --git a/code/builders/builder-webpack5/package.json b/code/builders/builder-webpack5/package.json index 31dc74add9ca..3e5c45e92f98 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" diff --git a/code/builders/builder-webpack5/src/index.ts b/code/builders/builder-webpack5/src/index.ts index a8af6e699ad4..71be9982973a 100644 --- a/code/builders/builder-webpack5/src/index.ts +++ b/code/builders/builder-webpack5/src/index.ts @@ -16,13 +16,15 @@ import prettyTime from 'pretty-hrtime'; import sirv from 'sirv'; import { corePath } from 'storybook/core-path'; import type { Configuration, Stats, StatsOptions } from 'webpack'; -import webpack, { ProgressPlugin } from 'webpack'; +import webpackDep, { DefinePlugin, ProgressPlugin } from 'webpack'; import webpackDevMiddleware from 'webpack-dev-middleware'; import webpackHotMiddleware from 'webpack-hot-middleware'; export * from './types'; export * from './preview/virtual-module-mapping'; +export const WebpackDefinePlugin = DefinePlugin; + export const printDuration = (startTime: [number, number]) => prettyTime(process.hrtime(startTime)) .replace(' ms', ' milliseconds') @@ -51,8 +53,8 @@ export const executor = { get: async (options: Options) => { const version = ((await options.presets.apply('webpackVersion')) || '5') as string; const webpackInstance = - (await options.presets.apply<{ default: typeof webpack }>('webpackInstance'))?.default || - webpack; + (await options.presets.apply<{ default: typeof webpackDep }>('webpackInstance'))?.default || + webpackDep; checkWebpackVersion({ version }, '5', 'builder-webpack5'); return webpackInstance; }, diff --git a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts index 763f2bf15646..aa9e34c73afe 100644 --- a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts +++ b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts @@ -195,7 +195,9 @@ export default async ( }), new DefinePlugin({ ...stringifyProcessEnvs(envs), - NODE_ENV: JSON.stringify(process.env.NODE_ENV), + NODE_ENV: JSON.stringify( + features?.developmentModeForBuild && isProd ? 'development' : process.env.NODE_ENV + ), }), new ProvidePlugin({ process: require.resolve('process/browser.js') }), isProd ? null : new HotModuleReplacementPlugin(), diff --git a/code/core/assets/server/addon.tsconfig.json b/code/core/assets/server/addon.tsconfig.json index 9161d5ff8254..38452bcdfa20 100644 --- a/code/core/assets/server/addon.tsconfig.json +++ b/code/core/assets/server/addon.tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "jsx": "react-jsx", + "jsx": "react", "jsxImportSource": "react" } } diff --git a/code/core/package.json b/code/core/package.json index ac5c8991b31f..8771b8f76c1e 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" diff --git a/code/core/scripts/helpers/sourcefiles.ts b/code/core/scripts/helpers/sourcefiles.ts index 2f437961a91e..e7a5394cb9b7 100644 --- a/code/core/scripts/helpers/sourcefiles.ts +++ b/code/core/scripts/helpers/sourcefiles.ts @@ -2,6 +2,7 @@ import { readdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { GlobalRegistrator } from '@happy-dom/global-registrator'; +import { isNotNil } from 'es-toolkit'; import { dedent, esbuild, getWorkspace, prettier } from '../../../../scripts/prepare/tools'; import { temporaryFile } from '../../src/common/utils/cli'; @@ -26,7 +27,7 @@ export const generateSourceFiles = async () => { async function generateVersionsFile(prettierConfig: prettier.Options | null): Promise { const location = join(__dirname, '..', '..', 'src', 'common', 'versions.ts'); - const workspace = await getWorkspace(); + const workspace = (await getWorkspace()).filter(isNotNil); const versions = JSON.stringify( workspace @@ -55,7 +56,7 @@ async function generateVersionsFile(prettierConfig: prettier.Options | null): Pr } async function generateFrameworksFile(prettierConfig: prettier.Options | null): Promise { - const thirdPartyFrameworks = ['qwik', 'solid', 'react-rsbuild', 'vue3-rsbuild']; + const thirdPartyFrameworks = ['qwik', 'solid', 'nuxt', 'react-rsbuild', 'vue3-rsbuild']; const location = join(__dirname, '..', '..', 'src', 'types', 'modules', 'frameworks.ts'); const frameworksDirectory = join(__dirname, '..', '..', '..', 'frameworks'); diff --git a/code/core/src/__mocks__/page.ts b/code/core/src/__mocks__/page.ts new file mode 100644 index 000000000000..fb87bbd306b8 --- /dev/null +++ b/code/core/src/__mocks__/page.ts @@ -0,0 +1 @@ +// empty file only matched on path diff --git a/code/core/src/__mocks__/path/to/Screens/index.jsx b/code/core/src/__mocks__/path/to/Screens/index.jsx new file mode 100644 index 000000000000..fb87bbd306b8 --- /dev/null +++ b/code/core/src/__mocks__/path/to/Screens/index.jsx @@ -0,0 +1 @@ +// empty file only matched on path diff --git a/code/core/src/cli/detect.test.ts b/code/core/src/cli/detect.test.ts index ea7f8139fda3..409cf92effa7 100644 --- a/code/core/src/cli/detect.test.ts +++ b/code/core/src/cli/detect.test.ts @@ -43,6 +43,28 @@ const MOCK_FRAMEWORK_FILES: { }, }, }, + { + name: ProjectType.NUXT, + files: { + 'package.json': { + dependencies: { + nuxt: '^3.11.2', + }, + }, + }, + }, + { + name: ProjectType.NUXT, + files: { + 'package.json': { + dependencies: { + // Nuxt projects may have Vue 3 as an explicit dependency + nuxt: '^3.11.2', + vue: '^3.0.0', + }, + }, + }, + }, { name: ProjectType.VUE3, files: { @@ -435,16 +457,6 @@ describe('Detect', () => { expect(result).toBe(ProjectType.UNDETECTED); }); - // TODO(blaine): Remove once Nuxt3 is supported - it(`UNSUPPORTED for Nuxt framework above version 3.0.0`, () => { - const result = detectFrameworkPreset({ - dependencies: { - nuxt: '3.0.0', - }, - }); - expect(result).toBe(ProjectType.UNSUPPORTED); - }); - // TODO: The mocking in this test causes tests after it to fail it('REACT_SCRIPTS for custom react scripts config', () => { const forkedReactScriptsConfig = { diff --git a/code/core/src/cli/detect.ts b/code/core/src/cli/detect.ts index 5b50abee4301..a771f6476f8c 100644 --- a/code/core/src/cli/detect.ts +++ b/code/core/src/cli/detect.ts @@ -123,7 +123,11 @@ export async function detectBuilder(packageManager: JsPackageManager, projectTyp } // REWORK - if (webpackConfig || (dependencies.webpack && dependencies.vite !== undefined)) { + if ( + webpackConfig || + ((dependencies.webpack || dependencies['@nuxt/webpack-builder']) && + dependencies.vite !== undefined) + ) { commandLog('Detected webpack project. Setting builder to webpack')(); return CoreBuilder.Webpack5; } @@ -138,6 +142,8 @@ export async function detectBuilder(packageManager: JsPackageManager, projectTyp case ProjectType.NEXTJS: case ProjectType.EMBER: return CoreBuilder.Webpack5; + case ProjectType.NUXT: + return CoreBuilder.Vite; default: const { builder } = await prompts( { @@ -207,6 +213,13 @@ export async function detectLanguage(packageManager: JsPackageManager) { } else if (semver.lt(typescriptVersion, '3.8.0')) { logger.warn('Detected TypeScript < 3.8, populating with JavaScript examples'); } + } else { + // No direct dependency on TypeScript, but could be a transitive dependency + // This is eg the case for Nuxt projects, which support a recent version of TypeScript + // Check for tsconfig.json (https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) + if (existsSync('tsconfig.json')) { + language = SupportedLanguage.TYPESCRIPT_4_9; + } } return language; diff --git a/code/core/src/cli/dirs.ts b/code/core/src/cli/dirs.ts index 11ca9cb67441..6e6867e38d57 100644 --- a/code/core/src/cli/dirs.ts +++ b/code/core/src/cli/dirs.ts @@ -40,7 +40,7 @@ export async function getRendererDir( ) { const externalFramework = externalFrameworks.find((framework) => framework.name === renderer); const frameworkPackageName = - externalFramework?.renderer || externalFramework?.packageName || `@storybook/${renderer}`; + externalFramework?.packageName || externalFramework?.renderer || `@storybook/${renderer}`; const packageJsonPath = join(frameworkPackageName, 'package.json'); diff --git a/code/core/src/cli/helpers.ts b/code/core/src/cli/helpers.ts index 27e91f001ac1..d5ebb9b65a2d 100644 --- a/code/core/src/cli/helpers.ts +++ b/code/core/src/cli/helpers.ts @@ -153,6 +153,7 @@ export const frameworkToDefaultBuilder: Record< 'html-vite': CoreBuilder.Vite, 'html-webpack5': CoreBuilder.Webpack5, nextjs: CoreBuilder.Webpack5, + nuxt: CoreBuilder.Vite, 'experimental-nextjs-vite': CoreBuilder.Vite, 'preact-vite': CoreBuilder.Vite, 'preact-webpack5': CoreBuilder.Webpack5, diff --git a/code/core/src/cli/project_types.ts b/code/core/src/cli/project_types.ts index 25148d2bc089..d9ca64e25345 100644 --- a/code/core/src/cli/project_types.ts +++ b/code/core/src/cli/project_types.ts @@ -1,5 +1,5 @@ import type { - SupportedRenderers as CoreSupportedFrameworks, + SupportedRenderers as CoreSupportedRenderers, SupportedFrameworks, } from '@storybook/core/types'; @@ -24,10 +24,16 @@ export type ExternalFramework = { export const externalFrameworks: ExternalFramework[] = [ { name: 'qwik', packageName: 'storybook-framework-qwik' }, { name: 'solid', frameworks: ['storybook-solidjs-vite'], renderer: 'storybook-solidjs' }, + { + name: 'nuxt', + packageName: '@storybook-vue/nuxt', + frameworks: ['@storybook-vue/nuxt'], + renderer: '@storybook/vue3', + }, ]; -/** @deprecated Please use `SupportedFrameworks` from `@storybook/types` instead */ -export type SupportedRenderers = CoreSupportedFrameworks; +/** @deprecated Please use `SupportedRenderers` from `@storybook/types` instead */ +export type SupportedRenderers = CoreSupportedRenderers; export const SUPPORTED_RENDERERS: SupportedRenderers[] = [ 'react', @@ -52,6 +58,7 @@ export enum ProjectType { WEBPACK_REACT = 'WEBPACK_REACT', NEXTJS = 'NEXTJS', VUE3 = 'VUE3', + NUXT = 'NUXT', ANGULAR = 'ANGULAR', EMBER = 'EMBER', WEB_COMPONENTS = 'WEB_COMPONENTS', @@ -121,6 +128,13 @@ export type TemplateConfiguration = { * specific. */ export const supportedTemplates: TemplateConfiguration[] = [ + { + preset: ProjectType.NUXT, + dependencies: ['nuxt'], + matcherFunction: ({ dependencies }) => { + return dependencies?.every(Boolean) ?? true; + }, + }, { preset: ProjectType.VUE3, dependencies: { @@ -242,10 +256,7 @@ export const supportedTemplates: TemplateConfiguration[] = [ // users an "Unsupported framework" message export const unsupportedTemplate: TemplateConfiguration = { preset: ProjectType.UNSUPPORTED, - dependencies: { - // TODO(blaine): Remove when we support Nuxt 3 - nuxt: (versionRange) => eqMajor(versionRange, 3), - }, + dependencies: {}, matcherFunction: ({ dependencies }) => { return dependencies?.some(Boolean) ?? false; }, diff --git a/code/core/src/common/utils/framework-to-renderer.ts b/code/core/src/common/utils/framework-to-renderer.ts index 7ae4c3b057a5..72cb4c1e2484 100644 --- a/code/core/src/common/utils/framework-to-renderer.ts +++ b/code/core/src/common/utils/framework-to-renderer.ts @@ -24,6 +24,7 @@ export const frameworkToRenderer: Record< sveltekit: 'svelte', 'vue3-vite': 'vue3', 'vue3-webpack5': 'vue3', + nuxt: 'vue3', 'web-components-vite': 'web-components', 'web-components-webpack5': 'web-components', 'react-rsbuild': 'react', diff --git a/code/core/src/common/versions.ts b/code/core/src/common/versions.ts index 766faaec02cf..1b87cbdf7440 100644 --- a/code/core/src/common/versions.ts +++ b/code/core/src/common/versions.ts @@ -1,88 +1,88 @@ // auto generated file, do not edit export default { - '@storybook/addon-a11y': '8.5.0-alpha.20', - '@storybook/addon-actions': '8.5.0-alpha.20', - '@storybook/addon-backgrounds': '8.5.0-alpha.20', - '@storybook/addon-controls': '8.5.0-alpha.20', - '@storybook/addon-docs': '8.5.0-alpha.20', - '@storybook/addon-essentials': '8.5.0-alpha.20', - '@storybook/addon-mdx-gfm': '8.5.0-alpha.20', - '@storybook/addon-highlight': '8.5.0-alpha.20', - '@storybook/addon-interactions': '8.5.0-alpha.20', - '@storybook/addon-jest': '8.5.0-alpha.20', - '@storybook/addon-links': '8.5.0-alpha.20', - '@storybook/addon-measure': '8.5.0-alpha.20', - '@storybook/addon-onboarding': '8.5.0-alpha.20', - '@storybook/addon-outline': '8.5.0-alpha.20', - '@storybook/addon-storysource': '8.5.0-alpha.20', - '@storybook/experimental-addon-test': '8.5.0-alpha.20', - '@storybook/addon-themes': '8.5.0-alpha.20', - '@storybook/addon-toolbars': '8.5.0-alpha.20', - '@storybook/addon-viewport': '8.5.0-alpha.20', - '@storybook/builder-vite': '8.5.0-alpha.20', - '@storybook/builder-webpack5': '8.5.0-alpha.20', - '@storybook/core': '8.5.0-alpha.20', - '@storybook/builder-manager': '8.5.0-alpha.20', - '@storybook/channels': '8.5.0-alpha.20', - '@storybook/client-logger': '8.5.0-alpha.20', - '@storybook/components': '8.5.0-alpha.20', - '@storybook/core-common': '8.5.0-alpha.20', - '@storybook/core-events': '8.5.0-alpha.20', - '@storybook/core-server': '8.5.0-alpha.20', - '@storybook/csf-tools': '8.5.0-alpha.20', - '@storybook/docs-tools': '8.5.0-alpha.20', - '@storybook/manager': '8.5.0-alpha.20', - '@storybook/manager-api': '8.5.0-alpha.20', - '@storybook/node-logger': '8.5.0-alpha.20', - '@storybook/preview': '8.5.0-alpha.20', - '@storybook/preview-api': '8.5.0-alpha.20', - '@storybook/router': '8.5.0-alpha.20', - '@storybook/telemetry': '8.5.0-alpha.20', - '@storybook/theming': '8.5.0-alpha.20', - '@storybook/types': '8.5.0-alpha.20', - '@storybook/angular': '8.5.0-alpha.20', - '@storybook/ember': '8.5.0-alpha.20', - '@storybook/experimental-nextjs-vite': '8.5.0-alpha.20', - '@storybook/html-vite': '8.5.0-alpha.20', - '@storybook/html-webpack5': '8.5.0-alpha.20', - '@storybook/nextjs': '8.5.0-alpha.20', - '@storybook/preact-vite': '8.5.0-alpha.20', - '@storybook/preact-webpack5': '8.5.0-alpha.20', - '@storybook/react-native-web-vite': '8.5.0-alpha.20', - '@storybook/react-vite': '8.5.0-alpha.20', - '@storybook/react-webpack5': '8.5.0-alpha.20', - '@storybook/server-webpack5': '8.5.0-alpha.20', - '@storybook/svelte-vite': '8.5.0-alpha.20', - '@storybook/svelte-webpack5': '8.5.0-alpha.20', - '@storybook/sveltekit': '8.5.0-alpha.20', - '@storybook/vue3-vite': '8.5.0-alpha.20', - '@storybook/vue3-webpack5': '8.5.0-alpha.20', - '@storybook/web-components-vite': '8.5.0-alpha.20', - '@storybook/web-components-webpack5': '8.5.0-alpha.20', - '@storybook/blocks': '8.5.0-alpha.20', - storybook: '8.5.0-alpha.20', - sb: '8.5.0-alpha.20', - '@storybook/cli': '8.5.0-alpha.20', - '@storybook/codemod': '8.5.0-alpha.20', - '@storybook/core-webpack': '8.5.0-alpha.20', - 'create-storybook': '8.5.0-alpha.20', - '@storybook/csf-plugin': '8.5.0-alpha.20', - '@storybook/instrumenter': '8.5.0-alpha.20', - '@storybook/react-dom-shim': '8.5.0-alpha.20', - '@storybook/source-loader': '8.5.0-alpha.20', - '@storybook/test': '8.5.0-alpha.20', - '@storybook/preset-create-react-app': '8.5.0-alpha.20', - '@storybook/preset-html-webpack': '8.5.0-alpha.20', - '@storybook/preset-preact-webpack': '8.5.0-alpha.20', - '@storybook/preset-react-webpack': '8.5.0-alpha.20', - '@storybook/preset-server-webpack': '8.5.0-alpha.20', - '@storybook/preset-svelte-webpack': '8.5.0-alpha.20', - '@storybook/preset-vue3-webpack': '8.5.0-alpha.20', - '@storybook/html': '8.5.0-alpha.20', - '@storybook/preact': '8.5.0-alpha.20', - '@storybook/react': '8.5.0-alpha.20', - '@storybook/server': '8.5.0-alpha.20', - '@storybook/svelte': '8.5.0-alpha.20', - '@storybook/vue3': '8.5.0-alpha.20', - '@storybook/web-components': '8.5.0-alpha.20', + '@storybook/addon-a11y': '8.5.0-beta.5', + '@storybook/addon-actions': '8.5.0-beta.5', + '@storybook/addon-backgrounds': '8.5.0-beta.5', + '@storybook/addon-controls': '8.5.0-beta.5', + '@storybook/addon-docs': '8.5.0-beta.5', + '@storybook/addon-essentials': '8.5.0-beta.5', + '@storybook/addon-mdx-gfm': '8.5.0-beta.5', + '@storybook/addon-highlight': '8.5.0-beta.5', + '@storybook/addon-interactions': '8.5.0-beta.5', + '@storybook/addon-jest': '8.5.0-beta.5', + '@storybook/addon-links': '8.5.0-beta.5', + '@storybook/addon-measure': '8.5.0-beta.5', + '@storybook/addon-onboarding': '8.5.0-beta.5', + '@storybook/addon-outline': '8.5.0-beta.5', + '@storybook/addon-storysource': '8.5.0-beta.5', + '@storybook/experimental-addon-test': '8.5.0-beta.5', + '@storybook/addon-themes': '8.5.0-beta.5', + '@storybook/addon-toolbars': '8.5.0-beta.5', + '@storybook/addon-viewport': '8.5.0-beta.5', + '@storybook/builder-vite': '8.5.0-beta.5', + '@storybook/builder-webpack5': '8.5.0-beta.5', + '@storybook/core': '8.5.0-beta.5', + '@storybook/builder-manager': '8.5.0-beta.5', + '@storybook/channels': '8.5.0-beta.5', + '@storybook/client-logger': '8.5.0-beta.5', + '@storybook/components': '8.5.0-beta.5', + '@storybook/core-common': '8.5.0-beta.5', + '@storybook/core-events': '8.5.0-beta.5', + '@storybook/core-server': '8.5.0-beta.5', + '@storybook/csf-tools': '8.5.0-beta.5', + '@storybook/docs-tools': '8.5.0-beta.5', + '@storybook/manager': '8.5.0-beta.5', + '@storybook/manager-api': '8.5.0-beta.5', + '@storybook/node-logger': '8.5.0-beta.5', + '@storybook/preview': '8.5.0-beta.5', + '@storybook/preview-api': '8.5.0-beta.5', + '@storybook/router': '8.5.0-beta.5', + '@storybook/telemetry': '8.5.0-beta.5', + '@storybook/theming': '8.5.0-beta.5', + '@storybook/types': '8.5.0-beta.5', + '@storybook/angular': '8.5.0-beta.5', + '@storybook/ember': '8.5.0-beta.5', + '@storybook/experimental-nextjs-vite': '8.5.0-beta.5', + '@storybook/html-vite': '8.5.0-beta.5', + '@storybook/html-webpack5': '8.5.0-beta.5', + '@storybook/nextjs': '8.5.0-beta.5', + '@storybook/preact-vite': '8.5.0-beta.5', + '@storybook/preact-webpack5': '8.5.0-beta.5', + '@storybook/react-native-web-vite': '8.5.0-beta.5', + '@storybook/react-vite': '8.5.0-beta.5', + '@storybook/react-webpack5': '8.5.0-beta.5', + '@storybook/server-webpack5': '8.5.0-beta.5', + '@storybook/svelte-vite': '8.5.0-beta.5', + '@storybook/svelte-webpack5': '8.5.0-beta.5', + '@storybook/sveltekit': '8.5.0-beta.5', + '@storybook/vue3-vite': '8.5.0-beta.5', + '@storybook/vue3-webpack5': '8.5.0-beta.5', + '@storybook/web-components-vite': '8.5.0-beta.5', + '@storybook/web-components-webpack5': '8.5.0-beta.5', + '@storybook/blocks': '8.5.0-beta.5', + storybook: '8.5.0-beta.5', + sb: '8.5.0-beta.5', + '@storybook/cli': '8.5.0-beta.5', + '@storybook/codemod': '8.5.0-beta.5', + '@storybook/core-webpack': '8.5.0-beta.5', + 'create-storybook': '8.5.0-beta.5', + '@storybook/csf-plugin': '8.5.0-beta.5', + '@storybook/instrumenter': '8.5.0-beta.5', + '@storybook/react-dom-shim': '8.5.0-beta.5', + '@storybook/source-loader': '8.5.0-beta.5', + '@storybook/test': '8.5.0-beta.5', + '@storybook/preset-create-react-app': '8.5.0-beta.5', + '@storybook/preset-html-webpack': '8.5.0-beta.5', + '@storybook/preset-preact-webpack': '8.5.0-beta.5', + '@storybook/preset-react-webpack': '8.5.0-beta.5', + '@storybook/preset-server-webpack': '8.5.0-beta.5', + '@storybook/preset-svelte-webpack': '8.5.0-beta.5', + '@storybook/preset-vue3-webpack': '8.5.0-beta.5', + '@storybook/html': '8.5.0-beta.5', + '@storybook/preact': '8.5.0-beta.5', + '@storybook/react': '8.5.0-beta.5', + '@storybook/server': '8.5.0-beta.5', + '@storybook/svelte': '8.5.0-beta.5', + '@storybook/vue3': '8.5.0-beta.5', + '@storybook/web-components': '8.5.0-beta.5', }; diff --git a/code/core/src/components/components/Loader/Loader.tsx b/code/core/src/components/components/Loader/Loader.tsx index 26d01ad3f05d..dace42a9b351 100644 --- a/code/core/src/components/components/Loader/Loader.tsx +++ b/code/core/src/components/components/Loader/Loader.tsx @@ -63,7 +63,7 @@ const ProgressBar = styled.div(({ theme }) => ({ const ProgressMessage = styled.div(({ theme }) => ({ minHeight: '2em', fontSize: `${theme.typography.size.s1}px`, - color: theme.barTextColor, + color: theme.textMutedColor, })); const ErrorIcon = styled(LightningOffIcon)(({ theme }) => ({ diff --git a/code/core/src/components/components/tooltip/ListItem.tsx b/code/core/src/components/components/tooltip/ListItem.tsx index 2a93287af503..10e513cc1b6b 100644 --- a/code/core/src/components/components/tooltip/ListItem.tsx +++ b/code/core/src/components/components/tooltip/ListItem.tsx @@ -40,7 +40,7 @@ const Title = styled(({ active, loading, disabled, ...rest }: TitleProps) => disabled ? { - color: transparentize(0.7, theme.color.defaultText), + color: theme.textMutedColor, } : {} ); diff --git a/code/core/src/manager-api/lib/stories.test.ts b/code/core/src/manager-api/lib/stories.test.ts index 76aaadd7f99d..3f0cbe63b5be 100644 --- a/code/core/src/manager-api/lib/stories.test.ts +++ b/code/core/src/manager-api/lib/stories.test.ts @@ -2,8 +2,10 @@ import { describe, expect, it } from 'vitest'; import type { API_PreparedStoryIndex, StoryIndexV2, StoryIndexV3 } from '@storybook/core/types'; +import type { State } from '../root'; import { mockEntries } from '../tests/mockStoriesEntries'; import { + transformStoryIndexToStoriesHash, transformStoryIndexV2toV3, transformStoryIndexV3toV4, transformStoryIndexV4toV5, @@ -216,3 +218,60 @@ describe('transformStoryIndexV4toV5', () => { `); }); }); + +describe('transformStoryIndexToStoriesHash', () => { + it('does not apply filters to failing stories', () => { + // Arrange - set up an index with two stories, one of which has a failing status + const indexV5: API_PreparedStoryIndex = { + v: 5, + entries: { + '1': { + id: '1', + type: 'story', + title: 'Story 1', + name: 'Story 1', + importPath: './path/to/story-1.ts', + parameters: {}, + tags: [], + }, + '2': { + id: '2', + type: 'story', + title: 'Story 2', + name: 'Story 2', + importPath: './path/to/story-2.ts', + parameters: {}, + tags: [], + }, + }, + }; + + const filters: State['filters'] = { + someFilter: () => false, + }; + + const status: State['status'] = { + '1': { someStatus: { status: 'error', title: 'broken', description: 'very bad' } }, + '2': { someStatus: { status: 'success', title: 'perfect', description: 'nice' } }, + }; + + const options = { + provider: { + getConfig: () => ({ sidebar: {} }), + } as any, + docsOptions: { docsMode: false }, + filters, + status, + }; + + // Act - transform the index to hashes + const result = transformStoryIndexToStoriesHash(indexV5, options); + + // Assert - the failing story is still present in the result, even though the filters remove all stories + expect(Object.keys(result)).toHaveLength(2); + expect(result['story-1']).toBeTruthy(); + expect(result['1']).toBeTruthy(); + expect(result['story-2']).toBeUndefined(); + expect(result['2']).toBeUndefined(); + }); +}); diff --git a/code/core/src/manager-api/lib/stories.ts b/code/core/src/manager-api/lib/stories.ts index 59b59f070a10..b38d30000531 100644 --- a/code/core/src/manager-api/lib/stories.ts +++ b/code/core/src/manager-api/lib/stories.ts @@ -192,11 +192,17 @@ export const transformStoryIndexToStoriesHash = ( const entryValues = Object.values(index.entries).filter((entry: any) => { let result = true; + // All stories with a failing status should always show up, regardless of the applied filters + const storyStatus = status[entry.id]; + if (Object.values(storyStatus ?? {}).some(({ status: s }) => s === 'error')) { + return result; + } + Object.values(filters).forEach((filter: any) => { if (result === false) { return; } - result = filter({ ...entry, status: status[entry.id] }); + result = filter({ ...entry, status: storyStatus }); }); return result; @@ -342,6 +348,7 @@ export const transformStoryIndexToStoriesHash = ( .reduce(addItem, orphanHash); }; +/** Now we need to patch in the existing prepared stories */ export const addPreparedStories = (newHash: API_IndexHash, oldHash?: API_IndexHash) => { if (!oldHash) { return newHash; diff --git a/code/core/src/manager-api/modules/layout.ts b/code/core/src/manager-api/modules/layout.ts index 468d51af1b8a..6687c36b1137 100644 --- a/code/core/src/manager-api/modules/layout.ts +++ b/code/core/src/manager-api/modules/layout.ts @@ -100,7 +100,7 @@ export const defaultLayoutState: SubState = { panelPosition: 'bottom', showTabs: true, }, - selectedPanel: undefined, + selectedPanel: 'chromaui/addon-visual-tests/panel', theme: create(), }; diff --git a/code/core/src/manager-api/modules/refs.ts b/code/core/src/manager-api/modules/refs.ts index 166c04786adc..cb9b8916ce29 100644 --- a/code/core/src/manager-api/modules/refs.ts +++ b/code/core/src/manager-api/modules/refs.ts @@ -179,7 +179,15 @@ export const init: ModuleFn = ( }, changeRefVersion: async (id, url) => { const { versions, title } = api.getRefs()[id]; - const ref: API_SetRefData = { id, url, versions, title, index: {}, expanded: true }; + const ref: API_SetRefData = { + id, + url, + versions, + title, + index: {}, + filteredIndex: {}, + expanded: true, + }; await api.setRef(id, { ...ref, type: 'unknown' }, false); await api.checkRef(ref); @@ -292,6 +300,7 @@ export const init: ModuleFn = ( // eslint-disable-next-line @typescript-eslint/naming-convention let internal_index: StoryIndex | undefined; let index: API_IndexHash | undefined; + let filteredIndex: API_IndexHash | undefined; const { filters } = store.getState(); const { storyMapper = defaultStoryMapper } = provider.getConfig(); const ref = api.getRefs()[id]; @@ -304,19 +313,28 @@ export const init: ModuleFn = ( : storyIndex; // @ts-expect-error (could be undefined) - index = transformStoryIndexToStoriesHash(storyIndex, { + filteredIndex = transformStoryIndexToStoriesHash(storyIndex, { provider, docsOptions, filters, status: {}, }); + // @ts-expect-error (could be undefined) + index = transformStoryIndexToStoriesHash(storyIndex, { + provider, + docsOptions, + filters: {}, + status: {}, + }); } if (index) { index = addRefIds(index, ref); } - - await api.updateRef(id, { ...ref, ...rest, index, internal_index }); + if (filteredIndex) { + filteredIndex = addRefIds(filteredIndex, ref); + } + await api.updateRef(id, { ...ref, ...rest, index, filteredIndex, internal_index }); }, updateRef: async (id, data) => { diff --git a/code/core/src/manager-api/modules/stories.ts b/code/core/src/manager-api/modules/stories.ts index 3c5aac769c6a..fa295bc20567 100644 --- a/code/core/src/manager-api/modules/stories.ts +++ b/code/core/src/manager-api/modules/stories.ts @@ -568,41 +568,61 @@ export const init: ModuleFn = ({ // The story index we receive on fetchStoryIndex is not, but all the prepared fields are optional // so we can cast one to the other easily enough setIndex: async (input) => { - const { index: oldHash, status, filters } = store.getState(); - const newHash = transformStoryIndexToStoriesHash(input, { + const { filteredIndex: oldFilteredHash, index: oldHash, status, filters } = store.getState(); + const newFilteredHash = transformStoryIndexToStoriesHash(input, { provider, docsOptions, status, filters, }); + const newHash = transformStoryIndexToStoriesHash(input, { + provider, + docsOptions, + status, + filters: {}, + }); - // Now we need to patch in the existing prepared stories - const output = addPreparedStories(newHash, oldHash); - - await store.setState({ internal_index: input, index: output, indexError: undefined }); + await store.setState({ + internal_index: input, + filteredIndex: addPreparedStories(newFilteredHash, oldFilteredHash), + index: addPreparedStories(newHash, oldHash), + indexError: undefined, + }); }, + // FIXME: is there a bug where filtered stories get added back in on updateStory??? updateStory: async ( storyId: StoryId, update: StoryUpdate, ref?: API_ComposedRef ): Promise => { if (!ref) { - const { index } = store.getState(); - if (!index) { - return; + const { index, filteredIndex } = store.getState(); + if (index) { + index[storyId] = { + ...index[storyId], + ...update, + } as API_StoryEntry; + } + if (filteredIndex) { + filteredIndex[storyId] = { + ...filteredIndex[storyId], + ...update, + } as API_StoryEntry; + } + if (index || filteredIndex) { + await store.setState({ index, filteredIndex }); } + } else { + const { id: refId, index, filteredIndex }: any = ref; index[storyId] = { ...index[storyId], ...update, } as API_StoryEntry; - await store.setState({ index }); - } else { - const { id: refId, index }: any = ref; - index[storyId] = { - ...index[storyId], + filteredIndex[storyId] = { + ...filteredIndex[storyId], ...update, } as API_StoryEntry; - await fullAPI.updateRef(refId, { index }); + await fullAPI.updateRef(refId, { index, filteredIndex }); } }, updateDocs: async ( @@ -611,22 +631,33 @@ export const init: ModuleFn = ({ ref?: API_ComposedRef ): Promise => { if (!ref) { - const { index } = store.getState(); - if (!index) { - return; + const { index, filteredIndex } = store.getState(); + if (index) { + index[docsId] = { + ...index[docsId], + ...update, + } as API_DocsEntry; } + if (filteredIndex) { + filteredIndex[docsId] = { + ...filteredIndex[docsId], + ...update, + } as API_DocsEntry; + } + if (index || filteredIndex) { + await store.setState({ index, filteredIndex }); + } + } else { + const { id: refId, index, filteredIndex }: any = ref; index[docsId] = { ...index[docsId], ...update, } as API_DocsEntry; - await store.setState({ index }); - } else { - const { id: refId, index }: any = ref; - index[docsId] = { - ...index[docsId], + filteredIndex[docsId] = { + ...filteredIndex[docsId], ...update, } as API_DocsEntry; - await fullAPI.updateRef(refId, { index }); + await fullAPI.updateRef(refId, { index, filteredIndex }); } }, setPreviewInitialized: async (ref) => { diff --git a/code/core/src/manager-api/root.tsx b/code/core/src/manager-api/root.tsx index c2784329ab8f..0889a560c8a0 100644 --- a/code/core/src/manager-api/root.tsx +++ b/code/core/src/manager-api/root.tsx @@ -41,7 +41,7 @@ import { STORY_CHANGED, } from '@storybook/core/core-events'; -import { mergeWith } from 'es-toolkit'; +import { isEqual } from 'es-toolkit'; import { createContext } from './context'; import getInitialState from './initial-state'; @@ -219,16 +219,9 @@ class ManagerProvider extends Component { } shouldComponentUpdate(nextProps: ManagerProviderProps, nextState: State): boolean { - const prevState = this.state; const prevProps = this.props; - - if (prevState !== nextState) { - return true; - } - if (prevProps.path !== nextProps.path) { - return true; - } - return false; + const prevState = this.state; + return prevProps.path !== nextProps.path || !isEqual(prevState, nextState); } initModules = () => { diff --git a/code/core/src/manager-api/tests/refs.test.ts b/code/core/src/manager-api/tests/refs.test.ts index c8d65baafc57..b9d39ee6a88b 100644 --- a/code/core/src/manager-api/tests/refs.test.ts +++ b/code/core/src/manager-api/tests/refs.test.ts @@ -291,6 +291,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": undefined, "id": "fake", "index": undefined, "indexError": { @@ -360,6 +361,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": undefined, "id": "fake", "index": undefined, "indexError": { @@ -504,6 +506,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -522,6 +525,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -601,6 +605,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -682,6 +687,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -763,6 +769,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": undefined, "id": "fake", "index": undefined, "internal_index": undefined, @@ -905,6 +912,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": undefined, "id": "fake", "index": undefined, "internal_index": undefined, @@ -987,6 +995,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -1068,6 +1077,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -1227,18 +1237,20 @@ describe('Refs API', () => { }, }; + const transformOptions = { + provider: provider as any, + docsOptions: {}, + filters: {}, + status: {}, + }; const initialState: Partial = { refs: { fake: { id: 'fake', url: 'https://example.com', previewInitialized: true, - index: transformStoryIndexToStoriesHash(index, { - provider: provider as any, - docsOptions: {}, - filters: {}, - status: {}, - }), + index: transformStoryIndexToStoriesHash(index, transformOptions), + filteredIndex: transformStoryIndexToStoriesHash(index, transformOptions), internal_index: index, }, }, @@ -1261,10 +1273,10 @@ describe('Refs API', () => { await api.setRef('fake', { storyIndex: index }); - await expect(api.getRefs().fake.index).toEqual( + await expect(api.getRefs().fake.filteredIndex).toEqual( expect.objectContaining({ 'a--1': expect.anything() }) ); - await expect(api.getRefs().fake.index).not.toEqual( + await expect(api.getRefs().fake.filteredIndex).not.toEqual( expect.objectContaining({ 'a--2': expect.anything() }) ); }); diff --git a/code/core/src/manager-api/tests/stories.test.ts b/code/core/src/manager-api/tests/stories.test.ts index b652c87cd7a1..6f480061d9ec 100644 --- a/code/core/src/manager-api/tests/stories.test.ts +++ b/code/core/src/manager-api/tests/stories.test.ts @@ -765,10 +765,15 @@ describe('stories API', () => { source: '', sourceLocation: '', type: '', - ref: { id: 'refId', index: { 'a--1': { args: { a: 'b' } } } } as any, + ref: { + id: 'refId', + index: { 'a--1': { args: { a: 'b' } } }, + filteredIndex: { 'a--1': { args: { a: 'b' } } }, + } as any, }); provider.channel.emit(STORY_ARGS_UPDATED, { storyId: 'a--1', args: { foo: 'bar' } }); expect(fullAPI.updateRef).toHaveBeenCalledWith('refId', { + filteredIndex: { 'a--1': { args: { foo: 'bar' } } }, index: { 'a--1': { args: { foo: 'bar' } } }, }); }); @@ -1539,6 +1544,7 @@ describe('stories API', () => { }) ); }); + it('updates state', async () => { const moduleArgs = createMockModuleArgs({}); const { api } = initStories(moduleArgs as unknown as ModuleArgs); @@ -1565,9 +1571,9 @@ describe('stories API', () => { await api.setIndex({ v: 5, entries: navigationEntries }); await api.experimental_setFilter('myCustomFilter', (item: any) => item.id.startsWith('a')); - const { index } = store.getState(); + const { filteredIndex } = store.getState(); - expect(index).toMatchInlineSnapshot(` + expect(filteredIndex).toMatchInlineSnapshot(` { "a": { "children": [ @@ -1624,7 +1630,7 @@ describe('stories API', () => { ); // empty, because there are no stories with status - expect(store.getState().index).toMatchInlineSnapshot('{}'); + expect(store.getState().filteredIndex).toMatchInlineSnapshot('{}'); // setting status should update the index await api.experimental_updateStatus('a-addon-id', { @@ -1636,7 +1642,7 @@ describe('stories API', () => { 'a--2': { status: 'success', title: 'a addon title', description: '' }, }); - expect(store.getState().index).toMatchInlineSnapshot(` + expect(store.getState().filteredIndex).toMatchInlineSnapshot(` { "a": { "children": [ @@ -1676,9 +1682,9 @@ describe('stories API', () => { await api.setIndex({ v: 5, entries: navigationEntries }); - const { index } = store.getState(); + const { filteredIndex } = store.getState(); - expect(index).toMatchInlineSnapshot(` + expect(filteredIndex).toMatchInlineSnapshot(` { "a": { "children": [ diff --git a/code/core/src/manager-api/version.ts b/code/core/src/manager-api/version.ts index 0e6208fa11f8..a769bd799920 100644 --- a/code/core/src/manager-api/version.ts +++ b/code/core/src/manager-api/version.ts @@ -1 +1 @@ -export const version = '8.5.0-alpha.20'; +export const version = '8.5.0-beta.5'; diff --git a/code/core/src/manager/components/panel/Panel.tsx b/code/core/src/manager/components/panel/Panel.tsx index 701cd94925e3..dc80c20949d2 100644 --- a/code/core/src/manager/components/panel/Panel.tsx +++ b/code/core/src/manager/components/panel/Panel.tsx @@ -60,7 +60,7 @@ export const AddonPanel = React.memo<{ return ( ({ const isActive = rp.path.includes(`tab=${tab.id}`); return ( { rp.applyQueryParams({ tab: tabIdToApply }); @@ -146,7 +146,7 @@ export const ToolbarComp = React.memo(function ToolbarComp({ {tabs.map((tab, index) => { return ( { api.applyQueryParams({ tab: tab.id === 'canvas' ? undefined : tab.id }); diff --git a/code/core/src/manager/components/sidebar/ContextMenu.tsx b/code/core/src/manager/components/sidebar/ContextMenu.tsx index a2d98eab6a85..ac4aeb671d55 100644 --- a/code/core/src/manager/components/sidebar/ContextMenu.tsx +++ b/code/core/src/manager/components/sidebar/ContextMenu.tsx @@ -2,6 +2,7 @@ import type { ComponentProps, FC, SyntheticEvent } from 'react'; import React, { useMemo, useState } from 'react'; import { TooltipLinkList, WithTooltip } from '@storybook/core/components'; +import { styled } from '@storybook/core/theming'; import { type API_HashEntry, Addon_TypesEnum } from '@storybook/core/types'; import { EllipsisIcon } from '@storybook/icons'; @@ -18,6 +19,16 @@ const empty = { node: null, }; +const PositionedWithTooltip = styled(WithTooltip)({ + position: 'absolute', + right: 0, +}); + +const FloatingStatusButton = styled(StatusButton)({ + background: 'var(--tree-node-background-hover)', + boxShadow: '0 0 5px 5px var(--tree-node-background-hover)', +}); + export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) => { const [hoverCount, setHoverCount] = useState(0); const [isOpen, setIsOpen] = useState(false); @@ -63,7 +74,7 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) return { onMouseEnter: handlers.onMouseEnter, node: isRendered ? ( - } > - + - - + + ) : null, }; }, [context, handlers, isOpen, isRendered, links]); diff --git a/code/core/src/manager/components/sidebar/Explorer.stories.tsx b/code/core/src/manager/components/sidebar/Explorer.stories.tsx index 908ead2be9f3..34adad0a32a0 100644 --- a/code/core/src/manager/components/sidebar/Explorer.stories.tsx +++ b/code/core/src/manager/components/sidebar/Explorer.stories.tsx @@ -34,7 +34,7 @@ const simple: Record = { url: 'iframe.html', previewInitialized: true, // @ts-expect-error (invalid input) - index: mockDataset.withRoot, + filteredIndex: mockDataset.withRoot, }, }; @@ -47,7 +47,7 @@ const withRefs: Record = { previewInitialized: true, type: 'auto-inject', // @ts-expect-error (invalid input) - index: mockDataset.noRoot, + filteredIndex: mockDataset.noRoot, }, injected: { id: 'injected', @@ -56,7 +56,7 @@ const withRefs: Record = { previewInitialized: false, type: 'auto-inject', // @ts-expect-error (invalid input) - index: mockDataset.noRoot, + filteredIndex: mockDataset.noRoot, }, unknown: { id: 'unknown', @@ -65,7 +65,7 @@ const withRefs: Record = { previewInitialized: true, type: 'unknown', // @ts-expect-error (invalid input) - index: mockDataset.noRoot, + filteredIndex: mockDataset.noRoot, }, lazy: { id: 'lazy', @@ -74,7 +74,7 @@ const withRefs: Record = { previewInitialized: false, type: 'lazy', // @ts-expect-error (invalid input) - index: mockDataset.withRoot, + filteredIndex: mockDataset.withRoot, }, }; diff --git a/code/core/src/manager/components/sidebar/LegacyRender.tsx b/code/core/src/manager/components/sidebar/LegacyRender.tsx index 82bc83bd1b34..f8afa4317f7f 100644 --- a/code/core/src/manager/components/sidebar/LegacyRender.tsx +++ b/code/core/src/manager/components/sidebar/LegacyRender.tsx @@ -32,7 +32,7 @@ const TitleWrapper = styled.div<{ crashed?: boolean }>(({ crashed, theme }) => ( const DescriptionWrapper = styled.div(({ theme }) => ({ fontSize: theme.typography.size.s1, - color: theme.barTextColor, + color: theme.textMutedColor, })); const Progress = styled(ProgressSpinner)({ diff --git a/code/core/src/manager/components/sidebar/Refs.stories.tsx b/code/core/src/manager/components/sidebar/Refs.stories.tsx index a042970beadc..3b6e37efce50 100644 --- a/code/core/src/manager/components/sidebar/Refs.stories.tsx +++ b/code/core/src/manager/components/sidebar/Refs.stories.tsx @@ -37,11 +37,11 @@ export default { }; const { menu } = standardHeaderData; -const index = mockDataset.withRoot; +const filteredIndex = mockDataset.withRoot; const storyId = '1-12-121'; -export const simpleData = { menu, index, storyId }; -export const loadingData = { menu, index: {} }; +export const simpleData = { menu, filteredIndex, storyId }; +export const loadingData = { menu, filteredIndex: {} }; // @ts-expect-error (non strict) const indexError: Error = (() => { @@ -60,14 +60,14 @@ const refs: Record = { previewInitialized: false, type: 'lazy', // @ts-expect-error (invalid input) - index, + filteredIndex, }, empty: { id: 'empty', title: 'It is empty because no stories were loaded', url: 'https://example.com', type: 'lazy', - index: {}, + filteredIndex: {}, previewInitialized: false, }, startInjected_unknown: { @@ -77,7 +77,7 @@ const refs: Record = { type: 'unknown', previewInitialized: false, // @ts-expect-error (invalid input) - index, + filteredIndex, }, startInjected_loading: { id: 'startInjected_loading', @@ -86,7 +86,7 @@ const refs: Record = { type: 'auto-inject', previewInitialized: false, // @ts-expect-error (invalid input) - index, + filteredIndex, }, startInjected_ready: { id: 'startInjected_ready', @@ -95,7 +95,7 @@ const refs: Record = { type: 'auto-inject', previewInitialized: true, // @ts-expect-error (invalid input) - index, + filteredIndex, }, versions: { id: 'versions', @@ -103,7 +103,7 @@ const refs: Record = { url: 'https://example.com', type: 'lazy', // @ts-expect-error (invalid input) - index, + filteredIndex, versions: { '1.0.0': 'https://example.com/v1', '2.0.0': 'https://example.com' }, previewInitialized: true, }, @@ -113,7 +113,7 @@ const refs: Record = { url: 'https://example.com', type: 'lazy', // @ts-expect-error (invalid input) - index, + filteredIndex, versions: { '1.0.0': 'https://example.com/v1', '2.0.0': 'https://example.com/v2' }, previewInitialized: true, }, @@ -138,7 +138,7 @@ const refs: Record = { title: 'This storybook has a very very long name for some reason', url: 'https://example.com', // @ts-expect-error (invalid input) - index, + filteredIndex, type: 'lazy', versions: { '111.111.888-new': 'https://example.com/new', @@ -154,7 +154,7 @@ const refs: Record = { previewInitialized: false, type: 'lazy', // @ts-expect-error (invalid input) - index, + filteredIndex, }, }; diff --git a/code/core/src/manager/components/sidebar/Refs.tsx b/code/core/src/manager/components/sidebar/Refs.tsx index 7412a627e889..b4d2a79ca351 100644 --- a/code/core/src/manager/components/sidebar/Refs.tsx +++ b/code/core/src/manager/components/sidebar/Refs.tsx @@ -81,7 +81,7 @@ export const Ref: FC = React. const { docsOptions } = useStorybookState(); const api = useStorybookApi(); const { - index, + filteredIndex: index, id: refId, title = refId, isLoading: isLoadingMain, diff --git a/code/core/src/manager/components/sidebar/SearchResults.tsx b/code/core/src/manager/components/sidebar/SearchResults.tsx index 7573befe34a1..b7689b7d64d0 100644 --- a/code/core/src/manager/components/sidebar/SearchResults.tsx +++ b/code/core/src/manager/components/sidebar/SearchResults.tsx @@ -70,7 +70,7 @@ const NoResults = styled.div(({ theme }) => ({ lineHeight: `18px`, color: theme.color.defaultText, small: { - color: theme.barTextColor, + color: theme.textMutedColor, fontSize: `${theme.typography.size.s1}px`, }, })); diff --git a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx index 53f22957f17d..0e2df01f6aed 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx @@ -111,7 +111,7 @@ const refs: Record = { title: 'This is a ref', url: 'https://example.com', type: 'lazy', - index, + filteredIndex: index, previewInitialized: true, }, }; @@ -123,7 +123,7 @@ const refsError = { optimized: { ...refs.optimized, // @ts-expect-error (non strict) - index: undefined as IndexHash, + filteredIndex: undefined as IndexHash, indexError, }, }; @@ -132,7 +132,7 @@ const refsEmpty = { optimized: { ...refs.optimized, // type: 'auto-inject', - index: {} as IndexHash, + filteredIndex: {} as IndexHash, }, }; diff --git a/code/core/src/manager/components/sidebar/Sidebar.tsx b/code/core/src/manager/components/sidebar/Sidebar.tsx index d4784cc8c562..dfec7fba7826 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.tsx @@ -93,6 +93,7 @@ const useCombination = ( () => ({ [DEFAULT_REF_ID]: { index, + filteredIndex: index, indexError, previewInitialized, status, diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx index 37a872810a6a..38efdae4f682 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.tsx @@ -1,7 +1,7 @@ -import React, { Fragment, useEffect, useRef, useState } from 'react'; +import React, { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { styled } from '@storybook/core/theming'; -import { type API_FilterFunction, type API_StatusValue } from '@storybook/core/types'; +import { type API_FilterFunction } from '@storybook/core/types'; import { TESTING_MODULE_CRASH_REPORT, @@ -119,7 +119,8 @@ export const SidebarBottomBase = ({ api.experimental_setFilter('sidebar-bottom-filter', filter); }, [api, hasWarnings, hasErrors, warningsActive, errorsActive]); - useEffect(() => { + // Register listeners before the first render + useLayoutEffect(() => { const onCrashReport = ({ providerId, ...details }: TestingModuleCrashReportPayload) => { api.updateTestProviderState(providerId, { error: { name: 'Crashed!', message: details.error.message }, diff --git a/code/core/src/manager/components/sidebar/StatusButton.tsx b/code/core/src/manager/components/sidebar/StatusButton.tsx index 9d1b49998df7..8002cc87ba33 100644 --- a/code/core/src/manager/components/sidebar/StatusButton.tsx +++ b/code/core/src/manager/components/sidebar/StatusButton.tsx @@ -3,7 +3,7 @@ import { styled } from '@storybook/core/theming'; import type { API_StatusValue } from '@storybook/types'; import type { Theme } from '@emotion/react'; -import { transparentize } from 'polished'; +import { darken, lighten, transparentize } from 'polished'; const withStatusColor = ({ theme, status }: { theme: Theme; status: API_StatusValue }) => { const defaultColor = @@ -43,6 +43,19 @@ export const StatusButton = styled(IconButton)<{ '&:hover': { color: theme.color.secondary, + background: + theme.base === 'dark' + ? darken(0.3, theme.color.secondary) + : lighten(0.4, theme.color.secondary), + }, + + '[data-selected="true"] &': { + background: theme.color.secondary, + boxShadow: `0 0 5px 5px ${theme.color.secondary}`, + + '&:hover': { + background: lighten(0.1, theme.color.secondary), + }, }, '&:focus': { diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index d08ab8ffc4d7..cbfc1e330be7 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -24,9 +24,10 @@ import type { StoryEntry, } from '@storybook/core/manager-api'; -import { transparentize } from 'polished'; +import { darken, lighten } from 'polished'; import type { Link } from '../../../components/components/tooltip/TooltipLinkList'; +import { MEDIA_DESKTOP_BREAKPOINT } from '../../constants'; import { getGroupStatus, getHighestStatus, statusMapping } from '../../utils/status'; import { createId, @@ -66,7 +67,7 @@ const CollapseButton = styled.button(({ theme }) => ({ '&:hover, &:focus': { outline: 'none', - background: transparentize(0.93, theme.color.secondary), + background: 'var(--tree-node-background-hover)', }, })); @@ -79,9 +80,19 @@ export const LeafNodeStyleWrapper = styled.div(({ theme }) => ({ background: 'transparent', minHeight: 28, borderRadius: 4, + overflow: 'hidden', + '--tree-node-background-hover': theme.background.content, + + [MEDIA_DESKTOP_BREAKPOINT]: { + '--tree-node-background-hover': theme.background.app, + }, '&:hover, &:focus': { - background: transparentize(0.93, theme.color.secondary), + '--tree-node-background-hover': + theme.base === 'dark' + ? darken(0.35, theme.color.secondary) + : lighten(0.45, theme.color.secondary), + background: 'var(--tree-node-background-hover)', outline: 'none', }, @@ -94,11 +105,11 @@ export const LeafNodeStyleWrapper = styled.div(({ theme }) => ({ }, '& [data-displayed="on"] + *': { - display: 'none', + visibility: 'hidden', }, '&:hover [data-displayed="off"] + *': { - display: 'none', + visibility: 'hidden', }, '&[data-selected="true"]': { @@ -107,7 +118,8 @@ export const LeafNodeStyleWrapper = styled.div(({ theme }) => ({ fontWeight: theme.typography.weight.bold, '&&:hover, &&:focus': { - background: theme.color.secondary, + '--tree-node-background-hover': theme.color.secondary, + background: 'var(--tree-node-background-hover)', }, svg: { color: theme.color.lightest }, }, diff --git a/code/core/src/manager/components/sidebar/useHighlighted.ts b/code/core/src/manager/components/sidebar/useHighlighted.ts index d0f06fc87aa8..37701c7b1eec 100644 --- a/code/core/src/manager/components/sidebar/useHighlighted.ts +++ b/code/core/src/manager/components/sidebar/useHighlighted.ts @@ -22,6 +22,25 @@ export interface HighlightedProps { const fromSelection = (selection: Selection): Highlight => selection ? { itemId: selection.storyId, refId: selection.refId } : null; +const scrollToSelector = ( + selector: string, + options: { + containerRef?: RefObject; + center?: boolean; + attempts?: number; + delay?: number; + } = {}, + _attempt = 1 +) => { + const { containerRef, center = false, attempts = 3, delay = 500 } = options; + const element = (containerRef ? containerRef.current : document)?.querySelector(selector); + if (element) { + scrollIntoView(element, center); + } else if (_attempt <= attempts) { + setTimeout(scrollToSelector, delay, selector, options, _attempt + 1); + } +}; + export const useHighlighted = ({ containerRef, isLoading, @@ -65,14 +84,10 @@ export const useHighlighted = ({ const highlight = fromSelection(selected); updateHighlighted(highlight); if (highlight) { - const { itemId, refId } = highlight; - setTimeout(() => { - scrollIntoView( - // @ts-expect-error (non strict) - containerRef.current?.querySelector(`[data-item-id="${itemId}"][data-ref-id="${refId}"]`), - true // make sure it's clearly visible by centering it - ); - }, 0); + scrollToSelector(`[data-item-id="${highlight.itemId}"][data-ref-id="${highlight.refId}"]`, { + containerRef, + center: true, + }); } }, [containerRef, selected, updateHighlighted]); diff --git a/code/core/src/manager/container/Panel.tsx b/code/core/src/manager/container/Panel.tsx index c81e489d8f68..f8cc2877cef0 100644 --- a/code/core/src/manager/container/Panel.tsx +++ b/code/core/src/manager/container/Panel.tsx @@ -32,6 +32,12 @@ const getPanels = (api: API) => { if (paramKey && parameters && parameters[paramKey] && parameters[paramKey].disable) { return; } + if ( + panel.disabled === true || + (typeof panel.disabled === 'function' && panel.disabled(parameters)) + ) { + return; + } filteredPanels[id] = panel; }); diff --git a/code/core/src/manager/container/Sidebar.tsx b/code/core/src/manager/container/Sidebar.tsx index bc05d1713b59..723d9989ac10 100755 --- a/code/core/src/manager/container/Sidebar.tsx +++ b/code/core/src/manager/container/Sidebar.tsx @@ -27,7 +27,7 @@ const Sidebar = React.memo(function Sideber({ onMenuClick }: SidebarProps) { // is actually the stories hash. We should fix this up and make it consistent. // eslint-disable-next-line @typescript-eslint/naming-convention internal_index, - index, + filteredIndex: index, status, indexError, previewInitialized, diff --git a/code/core/src/manager/utils/tree.ts b/code/core/src/manager/utils/tree.ts index 3002eb97a77f..dfe6dfb4cf24 100644 --- a/code/core/src/manager/utils/tree.ts +++ b/code/core/src/manager/utils/tree.ts @@ -85,10 +85,14 @@ export const scrollIntoView = (element: Element, center = false) => { return; } const { top, bottom } = element.getBoundingClientRect(); - const isInView = - top >= 0 && bottom <= (globalWindow.innerHeight || document.documentElement.clientHeight); - - if (!isInView) { + if (!top || !bottom) { + return; + } + const bottomOffset = + document?.querySelector('#sidebar-bottom-wrapper')?.getBoundingClientRect().top || + globalWindow.innerHeight || + document.documentElement.clientHeight; + if (bottom > bottomOffset) { element.scrollIntoView({ block: center ? 'center' : 'nearest' }); } }; diff --git a/code/core/src/telemetry/exec-command-count-lines.test.ts b/code/core/src/telemetry/exec-command-count-lines.test.ts new file mode 100644 index 000000000000..eacfe9f72952 --- /dev/null +++ b/code/core/src/telemetry/exec-command-count-lines.test.ts @@ -0,0 +1,71 @@ +import type { Transform } from 'node:stream'; +import { PassThrough } from 'node:stream'; + +import { beforeEach, describe, expect, it, vitest } from 'vitest'; + +// eslint-disable-next-line depend/ban-dependencies +import { execaCommand as rawExecaCommand } from 'execa'; + +import { execCommandCountLines } from './exec-command-count-lines'; + +vitest.mock('execa'); + +const execaCommand = vitest.mocked(rawExecaCommand); +beforeEach(() => { + execaCommand.mockReset(); +}); + +type ExecaStreamer = typeof Promise & { + stdout: Transform; + kill: () => void; +}; + +function createExecaStreamer() { + let resolver: () => void; + const promiseLike: ExecaStreamer = new Promise((aResolver, aRejecter) => { + resolver = aResolver; + }) as any; + + promiseLike.stdout = new PassThrough(); + // @ts-expect-error technically it is invalid to use resolver "before" it is assigned (but not really) + promiseLike.kill = resolver; + return promiseLike; +} + +describe('execCommandCountLines', () => { + it('counts lines, many', async () => { + const streamer = createExecaStreamer(); + execaCommand.mockReturnValue(streamer as any); + + const promise = execCommandCountLines('some command'); + + streamer.stdout.write('First line\n'); + streamer.stdout.write('Second line\n'); + streamer.kill(); + + expect(await promise).toEqual(2); + }); + + it('counts lines, one', async () => { + const streamer = createExecaStreamer(); + execaCommand.mockReturnValue(streamer as any); + + const promise = execCommandCountLines('some command'); + + streamer.stdout.write('First line\n'); + streamer.kill(); + + expect(await promise).toEqual(1); + }); + + it('counts lines, none', async () => { + const streamer = createExecaStreamer(); + execaCommand.mockReturnValue(streamer as any); + + const promise = execCommandCountLines('some command'); + + streamer.kill(); + + expect(await promise).toEqual(0); + }); +}); diff --git a/code/core/src/telemetry/exec-command-count-lines.ts b/code/core/src/telemetry/exec-command-count-lines.ts new file mode 100644 index 000000000000..fdc4547ce464 --- /dev/null +++ b/code/core/src/telemetry/exec-command-count-lines.ts @@ -0,0 +1,35 @@ +import { createInterface } from 'node:readline'; + +// eslint-disable-next-line depend/ban-dependencies +import { execaCommand } from 'execa'; + +/** + * Execute a command in the local terminal and count the lines in the result + * + * @param command The command to execute. + * @param options Execa options + * @returns The number of lines the command returned + */ +export async function execCommandCountLines( + command: string, + options?: Parameters[1] +) { + const process = execaCommand(command, { shell: true, buffer: false, ...options }); + if (!process.stdout) { + // eslint-disable-next-line local-rules/no-uncategorized-errors + throw new Error('Unexpected missing stdout'); + } + + let lineCount = 0; + const rl = createInterface(process.stdout); + rl.on('line', () => { + lineCount += 1; + }); + + // If the process errors, this will throw + await process; + + rl.close(); + + return lineCount; +} diff --git a/code/core/src/telemetry/get-application-file-count.test.ts b/code/core/src/telemetry/get-application-file-count.test.ts new file mode 100644 index 000000000000..7fc570689147 --- /dev/null +++ b/code/core/src/telemetry/get-application-file-count.test.ts @@ -0,0 +1,14 @@ +import { join } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { getApplicationFilesCountUncached } from './get-application-file-count'; + +const mocksDir = join(__dirname, '..', '__mocks__'); + +describe('getApplicationFilesCount', () => { + it('should find files with correct names', async () => { + const files = await getApplicationFilesCountUncached(mocksDir); + expect(files).toMatchInlineSnapshot(`2`); + }); +}); diff --git a/code/core/src/telemetry/get-application-file-count.ts b/code/core/src/telemetry/get-application-file-count.ts new file mode 100644 index 000000000000..4f4807ddff00 --- /dev/null +++ b/code/core/src/telemetry/get-application-file-count.ts @@ -0,0 +1,32 @@ +import { sep } from 'node:path'; + +import { execCommandCountLines } from './exec-command-count-lines'; +import { runTelemetryOperation } from './run-telemetry-operation'; + +// We are looking for files with the word "page" or "screen" somewhere in them with these exts +const nameMatches = ['page', 'screen']; +const extensions = ['js', 'jsx', 'ts', 'tsx']; + +export const getApplicationFilesCountUncached = async (basePath: string) => { + const bothCasesNameMatches = nameMatches.flatMap((match) => [ + match, + [match[0].toUpperCase(), ...match.slice(1)].join(''), + ]); + + const globs = bothCasesNameMatches.flatMap((match) => + extensions.map((extension) => `"${basePath}${sep}*${match}*.${extension}"`) + ); + + try { + const command = `git ls-files -- ${globs.join(' ')}`; + return await execCommandCountLines(command); + } catch { + return undefined; + } +}; + +export const getApplicationFileCount = async (path: string) => { + return runTelemetryOperation('applicationFiles', async () => + getApplicationFilesCountUncached(path) + ); +}; diff --git a/code/core/src/telemetry/get-has-router-package.test.ts b/code/core/src/telemetry/get-has-router-package.test.ts new file mode 100644 index 000000000000..8504a5bc4d84 --- /dev/null +++ b/code/core/src/telemetry/get-has-router-package.test.ts @@ -0,0 +1,29 @@ +import { expect, it } from 'vitest'; + +import { getHasRouterPackage } from './get-has-router-package'; + +it('returns true if there is a routing package in package.json', () => { + expect( + getHasRouterPackage({ + dependencies: { + react: '^18', + 'react-dom': '^18', + 'react-router': '^6', + }, + }) + ).toBe(true); +}); + +it('returns false if there is a routing package in package.json dependencies', () => { + expect( + getHasRouterPackage({ + dependencies: { + react: '^18', + 'react-dom': '^18', + }, + devDependencies: { + 'react-router': '^6', + }, + }) + ).toBe(false); +}); diff --git a/code/core/src/telemetry/get-has-router-package.ts b/code/core/src/telemetry/get-has-router-package.ts new file mode 100644 index 000000000000..5873c3832d25 --- /dev/null +++ b/code/core/src/telemetry/get-has-router-package.ts @@ -0,0 +1,37 @@ +import type { PackageJson } from '../types'; + +const routerPackages = new Set([ + 'react-router', + 'react-router-dom', + 'remix', + '@tanstack/react-router', + 'expo-router', + '@reach/router', + 'react-easy-router', + '@remix-run/router', + 'wouter', + 'wouter-preact', + 'preact-router', + 'vue-router', + 'unplugin-vue-router', + '@angular/router', + '@solidjs/router', + + // metaframeworks that imply routing + 'next', + 'react-scripts', + 'gatsby', + 'nuxt', + '@sveltejs/kit', +]); + +/** + * @param packageJson The package JSON of the project + * @returns Boolean Does this project use a routing package? + */ +export function getHasRouterPackage(packageJson: PackageJson) { + // NOTE: we just check real dependencies; if it is in dev dependencies, it may just be an example + return Object.keys(packageJson?.dependencies ?? {}).some((depName) => + routerPackages.has(depName) + ); +} diff --git a/code/core/src/telemetry/get-portable-stories-usage.ts b/code/core/src/telemetry/get-portable-stories-usage.ts index cd9da7f4f589..0831b484ab69 100644 --- a/code/core/src/telemetry/get-portable-stories-usage.ts +++ b/code/core/src/telemetry/get-portable-stories-usage.ts @@ -1,37 +1,18 @@ -// eslint-disable-next-line depend/ban-dependencies -import { execaCommand } from 'execa'; - -import { createFileSystemCache, resolvePathInStorybookCache } from '../common'; - -const cache = createFileSystemCache({ - basePath: resolvePathInStorybookCache('portable-stories'), - ns: 'storybook', - ttl: 24 * 60 * 60 * 1000, // 24h -}); +import { execCommandCountLines } from './exec-command-count-lines'; +import { runTelemetryOperation } from './run-telemetry-operation'; export const getPortableStoriesFileCountUncached = async (path?: string) => { - const command = `git grep -l composeStor` + (path ? ` -- ${path}` : ''); - const { stdout } = await execaCommand(command, { - cwd: process.cwd(), - shell: true, - }); - - return stdout.split('\n').filter(Boolean).length; + try { + const command = `git grep -l composeStor` + (path ? ` -- ${path}` : ''); + return await execCommandCountLines(command); + } catch (err: any) { + // exit code 1 if no matches are found + return err.exitCode === 1 ? 0 : undefined; + } }; -const CACHE_KEY = 'portableStories'; export const getPortableStoriesFileCount = async (path?: string) => { - let cached = await cache.get(CACHE_KEY); - if (!cached) { - try { - const count = await getPortableStoriesFileCountUncached(); - cached = { count }; - await cache.set(CACHE_KEY, cached); - } catch (err: any) { - // exit code 1 if no matches are found - const count = err.exitCode === 1 ? 0 : null; - cached = { count }; - } - } - return cached.count; + return runTelemetryOperation('portableStories', async () => + getPortableStoriesFileCountUncached(path) + ); }; diff --git a/code/core/src/telemetry/run-telemetry-operation.ts b/code/core/src/telemetry/run-telemetry-operation.ts new file mode 100644 index 000000000000..29d2aee6c721 --- /dev/null +++ b/code/core/src/telemetry/run-telemetry-operation.ts @@ -0,0 +1,25 @@ +import { createFileSystemCache, resolvePathInStorybookCache } from '../common'; + +const cache = createFileSystemCache({ + basePath: resolvePathInStorybookCache('telemetry'), + ns: 'storybook', + ttl: 24 * 60 * 60 * 1000, // 24h +}); + +/** + * Run an (expensive) operation, caching the result in a FS cache for 24 hours. + * + * NOTE: if the operation returns `undefined` the value will not be cached. Use this to indicate + * that the operation failed. + */ +export const runTelemetryOperation = async (cacheKey: string, operation: () => Promise) => { + let cached = await cache.get(cacheKey); + if (cached === undefined) { + cached = await operation(); + // Undefined indicates an error, setting isn't really valuable. + if (cached !== undefined) { + await cache.set(cacheKey, cached); + } + } + return cached; +}; diff --git a/code/core/src/telemetry/storybook-metadata.test.ts b/code/core/src/telemetry/storybook-metadata.test.ts index 06bf355c0688..8d73ff7b437d 100644 --- a/code/core/src/telemetry/storybook-metadata.test.ts +++ b/code/core/src/telemetry/storybook-metadata.test.ts @@ -12,6 +12,8 @@ const packageJsonMock: PackageJson = { version: 'x.x.x', }; +const packageJsonPath = process.cwd(); + const mainJsMock: StorybookConfig = { stories: [], }; @@ -126,6 +128,7 @@ describe('storybook-metadata', () => { it('should parse pnp paths for known frameworks', async () => { const unixResult = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: { @@ -144,6 +147,7 @@ describe('storybook-metadata', () => { const windowsResult = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: { @@ -164,6 +168,7 @@ describe('storybook-metadata', () => { it('should parse pnp paths for unknown frameworks', async () => { const unixResult = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: { @@ -178,6 +183,7 @@ describe('storybook-metadata', () => { const windowsResult = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: { @@ -198,6 +204,7 @@ describe('storybook-metadata', () => { const unixResult = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: { @@ -215,6 +222,7 @@ describe('storybook-metadata', () => { cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue('C:\\Users\\foo\\my-project'); const windowsResult = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: { @@ -232,6 +240,7 @@ describe('storybook-metadata', () => { it('should return frameworkOptions from mainjs', async () => { const reactResult = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: { @@ -250,6 +259,7 @@ describe('storybook-metadata', () => { const angularResult = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: { @@ -279,6 +289,7 @@ describe('storybook-metadata', () => { 'storybook-addon-deprecated': 'x.x.z', }, } as PackageJson, + packageJsonPath, mainConfig: { ...mainJsMock, addons: [ @@ -319,6 +330,7 @@ describe('storybook-metadata', () => { const result = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, features, @@ -332,6 +344,7 @@ describe('storybook-metadata', () => { expect( await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: '@storybook/react-vite', @@ -347,6 +360,7 @@ describe('storybook-metadata', () => { it('should return the number of refs', async () => { const res = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, refs: { @@ -361,6 +375,7 @@ describe('storybook-metadata', () => { it('only reports addon options for addon-essentials', async () => { const res = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, addons: [ @@ -395,6 +410,7 @@ describe('storybook-metadata', () => { [metaFramework]: 'x.x.x', }, } as PackageJson, + packageJsonPath, mainConfig: mainJsMock, }); expect(res.metaFramework).toEqual({ diff --git a/code/core/src/telemetry/storybook-metadata.ts b/code/core/src/telemetry/storybook-metadata.ts index 75804b813fc2..5258eef0ffb7 100644 --- a/code/core/src/telemetry/storybook-metadata.ts +++ b/code/core/src/telemetry/storybook-metadata.ts @@ -1,3 +1,5 @@ +import { dirname } from 'node:path'; + import { getProjectRoot, getStorybookConfiguration, @@ -9,10 +11,12 @@ import type { PackageJson, StorybookConfig } from '@storybook/core/types'; import { readConfig } from '@storybook/core/csf-tools'; import { detect, getNpmVersion } from 'detect-package-manager'; -import { findPackage } from 'fd-package-json'; +import { findPackage, findPackagePath } from 'fd-package-json'; +import { getApplicationFileCount } from './get-application-file-count'; import { getChromaticVersionSpecifier } from './get-chromatic-version'; import { getFrameworkInfo } from './get-framework-info'; +import { getHasRouterPackage } from './get-has-router-package'; import { getMonorepoType } from './get-monorepo-type'; import { getPortableStoriesFileCount } from './get-portable-stories-usage'; import { getActualPackageVersion, getActualPackageVersions } from './package-json'; @@ -41,9 +45,11 @@ export const sanitizeAddonName = (name: string) => { // Analyze a combination of information from main.js and package.json // to provide telemetry over a Storybook project export const computeStorybookMetadata = async ({ + packageJsonPath, packageJson, mainConfig, }: { + packageJsonPath: string; packageJson: PackageJson; mainConfig: StorybookConfig & Record; }): Promise => { @@ -100,6 +106,8 @@ export const computeStorybookMetadata = async ({ ) ); + metadata.hasRouterPackage = getHasRouterPackage(packageJson); + const monorepoType = getMonorepoType(); if (monorepoType) { metadata.monorepo = monorepoType; @@ -209,11 +217,13 @@ export const computeStorybookMetadata = async ({ const storybookVersion = storybookPackages[storybookInfo.frameworkPackage]?.version; const portableStoriesFileCount = await getPortableStoriesFileCount(); + const applicationFileCount = await getApplicationFileCount(dirname(packageJsonPath)); return { ...metadata, ...frameworkInfo, portableStoriesFileCount, + applicationFileCount, storybookVersion, storybookVersionSpecifier: storybookInfo.version, language, @@ -223,13 +233,29 @@ export const computeStorybookMetadata = async ({ }; }; +async function getPackageJsonDetails() { + const packageJsonPath = await findPackagePath(process.cwd()); + if (packageJsonPath) { + return { + packageJsonPath, + packageJson: (await findPackage(packageJsonPath)) || {}, + }; + } + + // If we don't find a `package.json`, we assume it "would have" been in the current working directory + return { + packageJsonPath: process.cwd(), + packageJson: {}, + }; +} + let cachedMetadata: StorybookMetadata; export const getStorybookMetadata = async (_configDir?: string) => { if (cachedMetadata) { return cachedMetadata; } - const packageJson = (await findPackage(process.cwd())) || {}; + const { packageJson, packageJsonPath } = await getPackageJsonDetails(); // TODO: improve the way configDir is extracted, as a "storybook" script might not be present // Scenarios: // 1. user changed it to something else e.g. "storybook:dev" @@ -243,6 +269,6 @@ export const getStorybookMetadata = async (_configDir?: string) => { ) as string)) ?? '.storybook'; const mainConfig = await loadMainConfig({ configDir }); - cachedMetadata = await computeStorybookMetadata({ mainConfig, packageJson }); + cachedMetadata = await computeStorybookMetadata({ mainConfig, packageJson, packageJsonPath }); return cachedMetadata; }; diff --git a/code/core/src/telemetry/types.ts b/code/core/src/telemetry/types.ts index 757f5afc197e..e43373e5e61b 100644 --- a/code/core/src/telemetry/types.ts +++ b/code/core/src/telemetry/types.ts @@ -59,6 +59,7 @@ export type StorybookMetadata = { version: string; }; testPackages?: Record; + hasRouterPackage?: boolean; hasStorybookEslint?: boolean; hasStaticDirs?: boolean; hasCustomWebpack?: boolean; @@ -69,6 +70,7 @@ export type StorybookMetadata = { usesGlobals?: boolean; }; portableStoriesFileCount?: number; + applicationFileCount?: number; }; export interface Payload { diff --git a/code/core/src/types/modules/addons.ts b/code/core/src/types/modules/addons.ts index 47f5aec6412b..b2c7b4ac2f3c 100644 --- a/code/core/src/types/modules/addons.ts +++ b/code/core/src/types/modules/addons.ts @@ -5,7 +5,7 @@ import type { TestProviderConfig, TestingModuleProgressReportProgress } from '.. import type { RenderData as RouterData } from '../../router/types'; import type { ThemeVars } from '../../theming/types'; import type { API_SidebarOptions } from './api'; -import type { API_HashEntry, API_StatusState, API_StatusUpdate } from './api-stories'; +import type { API_HashEntry, API_StoryEntry } from './api-stories'; import type { Args, ArgsStoryFn as ArgsStoryFnForFramework, @@ -392,7 +392,7 @@ export interface Addon_BaseType { /** @unstable */ paramKey?: string; /** @unstable */ - disabled?: boolean; + disabled?: boolean | ((parameters: API_StoryEntry['parameters']) => boolean); /** @unstable */ hidden?: boolean; } diff --git a/code/core/src/types/modules/api.ts b/code/core/src/types/modules/api.ts index 30edbe36ad29..f5e2d6f32100 100644 --- a/code/core/src/types/modules/api.ts +++ b/code/core/src/types/modules/api.ts @@ -155,6 +155,7 @@ export type API_StoryMapper = (ref: API_ComposedRef, story: SetStoriesStory) => export interface API_LoadedRefData { index?: API_IndexHash; + filteredIndex?: API_IndexHash; indexError?: Error; previewInitialized: boolean; } @@ -180,6 +181,7 @@ export type API_ComposedRefUpdate = Partial< | 'type' | 'expanded' | 'index' + | 'filteredIndex' | 'versions' | 'loginUrl' | 'version' diff --git a/code/core/src/types/modules/core-common.ts b/code/core/src/types/modules/core-common.ts index 3254723fc4ee..2b4624a0564b 100644 --- a/code/core/src/types/modules/core-common.ts +++ b/code/core/src/types/modules/core-common.ts @@ -380,6 +380,8 @@ export interface StorybookConfigRaw { viewportStoryGlobals?: boolean; /** Use globals & globalTypes for configuring the backgrounds addon */ backgroundsStoryGlobals?: boolean; + /** Set NODE_ENV to development in built Storybooks for better testability and debuggability */ + developmentModeForBuild?: boolean; }; build?: TestBuildConfig; diff --git a/code/core/src/types/modules/frameworks.ts b/code/core/src/types/modules/frameworks.ts index e3e1b6383a7f..8e0ec1a7eea0 100644 --- a/code/core/src/types/modules/frameworks.ts +++ b/code/core/src/types/modules/frameworks.ts @@ -21,5 +21,6 @@ export type SupportedFrameworks = | 'web-components-webpack5' | 'qwik' | 'solid' + | 'nuxt' | 'react-rsbuild' | 'vue3-rsbuild'; diff --git a/code/core/src/types/modules/renderers.ts b/code/core/src/types/modules/renderers.ts index 4fcf0be99d87..e6fd0f650bf3 100644 --- a/code/core/src/types/modules/renderers.ts +++ b/code/core/src/types/modules/renderers.ts @@ -11,4 +11,5 @@ export type SupportedRenderers = | 'html' | 'web-components' | 'server' - | 'solid'; + | 'solid' + | 'nuxt'; diff --git a/code/core/template/stories/rendering.stories.ts b/code/core/template/stories/rendering.stories.ts index b0d0a7b06a1d..77e9432ef512 100644 --- a/code/core/template/stories/rendering.stories.ts +++ b/code/core/template/stories/rendering.stories.ts @@ -42,6 +42,38 @@ export const ForceRemount = { tags: ['!test', '!vitest'], }; +let loadedLabel = 'Initial'; + +/** + * This story demonstrates what happens when rendering (loaders) have side effects, and can possibly + * interleave with each other Triggering multiple force remounts quickly should only result in a + * single remount in the end and the label should be 'Loaded. Click Me' at the end. If loaders are + * interleaving it would result in a label of 'Error: Interleaved loaders. Click Me' Similarly, + * changing args rapidly should only cause one rerender at a time, producing the same result. + */ +export const SlowLoader = { + parameters: { + chromatic: { disable: true }, + }, + loaders: [ + async () => { + loadedLabel = 'Loading...'; + await new Promise((resolve) => setTimeout(resolve, 1000)); + loadedLabel = loadedLabel === 'Loading...' ? 'Loaded.' : 'Error: Interleaved loaders.'; + return { label: loadedLabel }; + }, + ], + decorators: [ + (storyFn: any, context: any) => + storyFn({ + args: { + ...context.args, + label: `${context.loaded.label} ${context.args.label}`, + }, + }), + ], +}; + export const ChangeArgs = { play: async ({ canvasElement, id }: PlayFunctionContext) => { const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; @@ -74,35 +106,3 @@ export const ChangeArgs = { await expect(button).toHaveFocus(); }, }; - -let loadedLabel = 'Initial'; - -/** - * This story demonstrates what happens when rendering (loaders) have side effects, and can possibly - * interleave with each other Triggering multiple force remounts quickly should only result in a - * single remount in the end and the label should be 'Loaded. Click Me' at the end. If loaders are - * interleaving it would result in a label of 'Error: Interleaved loaders. Click Me' Similarly, - * changing args rapidly should only cause one rerender at a time, producing the same result. - */ -export const SlowLoader = { - parameters: { - chromatic: { disable: true }, - }, - loaders: [ - async () => { - loadedLabel = 'Loading...'; - await new Promise((resolve) => setTimeout(resolve, 1000)); - loadedLabel = loadedLabel === 'Loading...' ? 'Loaded.' : 'Error: Interleaved loaders.'; - return { label: loadedLabel }; - }, - ], - decorators: [ - (storyFn: any, context: any) => - storyFn({ - args: { - ...context.args, - label: `${context.loaded.label} ${context.args.label}`, - }, - }), - ], -}; diff --git a/code/deprecated/builder-manager/package.json b/code/deprecated/builder-manager/package.json index 62cda75bafd3..e5118a8c957a 100644 --- a/code/deprecated/builder-manager/package.json +++ b/code/deprecated/builder-manager/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/builder-manager", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook manager builder", "keywords": [ "storybook" diff --git a/code/deprecated/channels/package.json b/code/deprecated/channels/package.json index f05e0f4b7459..4ce43a5a47eb 100644 --- a/code/deprecated/channels/package.json +++ b/code/deprecated/channels/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/channels", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "", "keywords": [ "storybook" diff --git a/code/deprecated/client-logger/package.json b/code/deprecated/client-logger/package.json index d58ca7e64716..e92abdd99cf7 100644 --- a/code/deprecated/client-logger/package.json +++ b/code/deprecated/client-logger/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/client-logger", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "", "keywords": [ "storybook" diff --git a/code/deprecated/components/package.json b/code/deprecated/components/package.json index 94f77c383899..505500302c1b 100644 --- a/code/deprecated/components/package.json +++ b/code/deprecated/components/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/components", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Core Storybook Components", "keywords": [ "storybook" diff --git a/code/deprecated/core-common/package.json b/code/deprecated/core-common/package.json index 66994e0d8bdb..c9b99bcbd507 100644 --- a/code/deprecated/core-common/package.json +++ b/code/deprecated/core-common/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core-common", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" diff --git a/code/deprecated/core-events/package.json b/code/deprecated/core-events/package.json index 3bb7a8fb597f..ca0f2220c5f9 100644 --- a/code/deprecated/core-events/package.json +++ b/code/deprecated/core-events/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core-events", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Event names used in storybook core", "keywords": [ "storybook" diff --git a/code/deprecated/core-server/package.json b/code/deprecated/core-server/package.json index e2a34f4e8af6..f2583597b814 100644 --- a/code/deprecated/core-server/package.json +++ b/code/deprecated/core-server/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core-server", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" diff --git a/code/deprecated/csf-tools/package.json b/code/deprecated/csf-tools/package.json index 5c83ffcbb119..d7e81cfebe07 100644 --- a/code/deprecated/csf-tools/package.json +++ b/code/deprecated/csf-tools/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/csf-tools", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Parse and manipulate CSF and Storybook config files", "keywords": [ "storybook" diff --git a/code/deprecated/docs-tools/package.json b/code/deprecated/docs-tools/package.json index 1c98b0f0d3ec..e41e131d1380 100644 --- a/code/deprecated/docs-tools/package.json +++ b/code/deprecated/docs-tools/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/docs-tools", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Shared utility functions for frameworks to implement docs", "keywords": [ "storybook" diff --git a/code/deprecated/manager-api/package.json b/code/deprecated/manager-api/package.json index ba3117ca0233..b474990df916 100644 --- a/code/deprecated/manager-api/package.json +++ b/code/deprecated/manager-api/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/manager-api", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Core Storybook Manager API & Context", "keywords": [ "storybook" diff --git a/code/deprecated/manager/package.json b/code/deprecated/manager/package.json index bb7eeb7188b8..361f79026195 100644 --- a/code/deprecated/manager/package.json +++ b/code/deprecated/manager/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/manager", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Core Storybook UI", "keywords": [ "storybook" diff --git a/code/deprecated/node-logger/package.json b/code/deprecated/node-logger/package.json index b1700e6e65ca..cb464d5a7bcd 100644 --- a/code/deprecated/node-logger/package.json +++ b/code/deprecated/node-logger/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/node-logger", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "", "keywords": [ "storybook" @@ -23,7 +23,7 @@ "exports": { ".": { "types": "./shim.d.ts", - "module": "./shim.mjs", + "import": "./shim.mjs", "require": "./shim.js" }, "./package.json": "./package.json" diff --git a/code/deprecated/preview-api/package.json b/code/deprecated/preview-api/package.json index 6c23642f7859..c6279096606f 100644 --- a/code/deprecated/preview-api/package.json +++ b/code/deprecated/preview-api/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preview-api", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "", "keywords": [ "storybook" diff --git a/code/deprecated/preview/package.json b/code/deprecated/preview/package.json index 37550e11099f..9a8e640f2f51 100644 --- a/code/deprecated/preview/package.json +++ b/code/deprecated/preview/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preview", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "", "keywords": [ "storybook" diff --git a/code/deprecated/router/package.json b/code/deprecated/router/package.json index 501562f1e6ea..e73f2154fa53 100644 --- a/code/deprecated/router/package.json +++ b/code/deprecated/router/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/router", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Core Storybook Router", "keywords": [ "storybook" diff --git a/code/deprecated/telemetry/package.json b/code/deprecated/telemetry/package.json index 6fe8cd1b0ac4..cf98e902a38a 100644 --- a/code/deprecated/telemetry/package.json +++ b/code/deprecated/telemetry/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/telemetry", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Telemetry logging for crash reports and usage statistics", "keywords": [ "storybook" diff --git a/code/deprecated/theming/package.json b/code/deprecated/theming/package.json index e91486a6bebc..dfad0d3ac422 100644 --- a/code/deprecated/theming/package.json +++ b/code/deprecated/theming/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/theming", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Core Storybook Components", "keywords": [ "storybook" diff --git a/code/deprecated/types/package.json b/code/deprecated/types/package.json index 214340eddaa9..a10b2cf6770a 100644 --- a/code/deprecated/types/package.json +++ b/code/deprecated/types/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/types", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Core Storybook TS Types", "keywords": [ "storybook" diff --git a/code/e2e-tests/addon-docs.spec.ts b/code/e2e-tests/addon-docs.spec.ts index 54c046a8aec7..6d0542bf4b8d 100644 --- a/code/e2e-tests/addon-docs.spec.ts +++ b/code/e2e-tests/addon-docs.spec.ts @@ -191,7 +191,7 @@ test.describe('addon-docs', () => { test('should resolve react to the correct version', async ({ page }) => { test.skip( - templateName?.includes('nextjs'), + templateName?.includes('nextjs') || templateName?.includes('nuxt'), 'TODO: remove this once sandboxes are synced (SOON!!)' ); // Arrange - Navigate to MDX docs diff --git a/code/e2e-tests/addon-onboarding.spec.ts b/code/e2e-tests/addon-onboarding.spec.ts new file mode 100644 index 000000000000..85181d8abf2c --- /dev/null +++ b/code/e2e-tests/addon-onboarding.spec.ts @@ -0,0 +1,53 @@ +import { expect, test } from '@playwright/test'; +import process from 'process'; + +import { SbPage } from './util'; + +const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:8001'; +const templateName = process.env.STORYBOOK_TEMPLATE_NAME || ''; +const type = process.env.STORYBOOK_TYPE || 'dev'; + +const supportsOnboarding = + templateName.includes('react') || + templateName.includes('vue3') || + templateName.includes('angular') || + templateName.includes('next'); + +test.describe('addon-onboarding', () => { + test.skip(type === 'build', `Skipping addon tests for production Storybooks`); + test.skip( + !supportsOnboarding, + `Skipping ${templateName}, which does not have addon-onboarding set up.` + ); + test('the onboarding flow', async ({ page }) => { + await page.goto(`${storybookUrl}/?path=/onboarding`); + const sbPage = new SbPage(page, expect); + await sbPage.waitUntilLoaded(); + + await expect(page.getByRole('heading', { name: 'Meet your new frontend' })).toBeVisible(); + await page.locator('#storybook-addon-onboarding').getByRole('button').click(); + + await expect(page.getByText('Interactive story playground')).toBeVisible(); + await page.getByLabel('Next').click(); + + await expect(page.getByText('Save your changes as a new')).toBeVisible(); + await page.getByLabel('Next').click(); + + await expect(page.getByRole('heading', { name: 'Create new story' })).toBeVisible(); + await page.getByPlaceholder('Story export name').click(); + + // this is needed because the e2e test will generate a new file in the system + // which we don't know of its location (it runs in different sandboxes) + // so we just create a random id to make it easier to run tests + const id = Math.random().toString(36).substring(7); + await page.getByPlaceholder('Story export name').fill('Test-' + id); + await page.getByRole('button', { name: 'Create' }).click(); + + await expect(page.getByText('You just added your first')).toBeVisible(); + await page.getByLabel('Last').click(); + + await expect( + sbPage.previewIframe().getByRole('heading', { name: 'Configure your project' }) + ).toBeVisible(); + }); +}); diff --git a/code/e2e-tests/composition.spec.ts b/code/e2e-tests/composition.spec.ts index 15bc6c4c4ed9..ec5a96e5898d 100644 --- a/code/e2e-tests/composition.spec.ts +++ b/code/e2e-tests/composition.spec.ts @@ -27,8 +27,12 @@ test.describe('composition', () => { await page.locator('[id="storybook\\@7\\.6\\.18_components-badge"]').click(); await expect( - page.locator('[id="storybook\\@7\\.6\\.18_components-badge--default"]') - ).toBeVisible(); + page + .locator('iframe[title="storybook-ref-storybook\\@7\\.6\\.18"]') + .contentFrame() + .locator('#storybook-root') + .getByText('Default') + ).toBeVisible({ timeout: 15000 }); // Expect composed stories `to be available in the search await page.getByPlaceholder('Find components').fill('Button'); diff --git a/code/e2e-tests/preview-api.spec.ts b/code/e2e-tests/preview-api.spec.ts index cfabaf89674c..31b566027fd9 100644 --- a/code/e2e-tests/preview-api.spec.ts +++ b/code/e2e-tests/preview-api.spec.ts @@ -65,6 +65,7 @@ test.describe('preview-api', () => { const root = sbPage.previewRoot(); + await sbPage.viewAddonPanel('Controls'); const labelControl = sbPage.page.locator('#control-label'); await expect(root.getByText('Loaded. Click me')).toBeVisible(); diff --git a/code/e2e-tests/util.ts b/code/e2e-tests/util.ts index 057dc91f715d..d9faad22939a 100644 --- a/code/e2e-tests/util.ts +++ b/code/e2e-tests/util.ts @@ -1,3 +1,4 @@ +/* eslint-disable local-rules/no-uncategorized-errors */ import { toId } from '@storybook/csf'; import type { Expect, Page } from '@playwright/test'; @@ -61,6 +62,7 @@ export class SbPage { await this.expect(selected).toHaveAttribute('data-selected', 'true'); await this.previewRoot(); + await this.waitUntilLoaded(); } async navigateToUnattachedDocs(title: string, name = 'docs') { @@ -80,7 +82,25 @@ export class SbPage { const selected = storyLink; await this.expect(selected).toHaveAttribute('data-selected', 'true'); - await this.previewRoot(); + await this.waitForStoryLoaded(); + } + + async waitForStoryLoaded() { + try { + const root = this.previewRoot(); + // Wait until there is at least one child (a story element) in the preview iframe + await root.locator(':scope > *').first().waitFor({ + state: 'attached', + timeout: 10000, + }); + } catch (error: any) { + if (error.name === 'TimeoutError') { + throw new Error( + 'The Storybook iframe did not have children within the specified timeout. Did the story load correctly?' + ); + } + throw error; + } } async waitUntilLoaded() { @@ -112,6 +132,8 @@ export class SbPage { const storyLoadingPage = root.locator('.sb-preparing-story'); await docsLoadingPage.waitFor({ state: 'hidden' }); await storyLoadingPage.waitFor({ state: 'hidden' }); + + await this.waitForStoryLoaded(); } previewIframe() { @@ -143,6 +165,30 @@ export class SbPage { getCanvasBodyElement() { return this.previewIframe().locator('body'); } + + // utility to try and decrease flake + async retryTimes( + fn: () => Promise, + options?: { + retries?: number; + delay?: number; + } + ): Promise { + let attempts = 0; + const { retries = 3, delay = 0 } = options || {}; + while (attempts < retries) { + try { + await fn(); + return; + } catch (error) { + attempts++; + if (attempts === retries) { + throw error; + } + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + } } const templateName: keyof typeof allTemplates = process.env.STORYBOOK_TEMPLATE_NAME || ('' as any); diff --git a/code/frameworks/angular/package.json b/code/frameworks/angular/package.json index 06a9b4834586..2105d16d28bb 100644 --- a/code/frameworks/angular/package.json +++ b/code/frameworks/angular/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/angular", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Angular: Develop Angular components in isolation with hot reloading.", "keywords": [ "storybook", diff --git a/code/frameworks/angular/src/builders/build-storybook/schema.json b/code/frameworks/angular/src/builders/build-storybook/schema.json index 6d976bf5dd0d..77e455cdb1be 100644 --- a/code/frameworks/angular/src/builders/build-storybook/schema.json +++ b/code/frameworks/angular/src/builders/build-storybook/schema.json @@ -77,6 +77,11 @@ "description": "Write Webpack Stats JSON to disk", "default": false }, + "statsJson": { + "type": ["boolean", "string"], + "description": "Write stats JSON to disk", + "default": false + }, "previewUrl": { "type": "string", "description": "Disables the default storybook preview and lets you use your own" diff --git a/code/frameworks/angular/src/builders/start-storybook/schema.json b/code/frameworks/angular/src/builders/start-storybook/schema.json index 729c3ea5b724..64d53bd5481e 100644 --- a/code/frameworks/angular/src/builders/start-storybook/schema.json +++ b/code/frameworks/angular/src/builders/start-storybook/schema.json @@ -139,6 +139,11 @@ "description": "Write Webpack Stats JSON to disk", "default": false }, + "statsJson": { + "type": ["boolean", "string"], + "description": "Write stats JSON to disk", + "default": false + }, "previewUrl": { "type": "string", "description": "Disables the default storybook preview and lets you use your own" diff --git a/code/frameworks/ember/package.json b/code/frameworks/ember/package.json index 18ff9618d203..bd1ae51f689e 100644 --- a/code/frameworks/ember/package.json +++ b/code/frameworks/ember/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/ember", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "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/experimental-nextjs-vite/package.json b/code/frameworks/experimental-nextjs-vite/package.json index 2fe79c99238a..da9fce69dedd 100644 --- a/code/frameworks/experimental-nextjs-vite/package.json +++ b/code/frameworks/experimental-nextjs-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/experimental-nextjs-vite", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Next.js and Vite", "keywords": [ "storybook", @@ -97,6 +97,7 @@ "dependencies": { "@storybook/builder-vite": "workspace:*", "@storybook/react": "workspace:*", + "@storybook/react-vite": "workspace:*", "@storybook/test": "workspace:*", "styled-jsx": "5.1.6", "vite-plugin-storybook-nextjs": "^1.1.0" diff --git a/code/frameworks/experimental-nextjs-vite/src/preset.ts b/code/frameworks/experimental-nextjs-vite/src/preset.ts index 0a725be35804..633f62a5dceb 100644 --- a/code/frameworks/experimental-nextjs-vite/src/preset.ts +++ b/code/frameworks/experimental-nextjs-vite/src/preset.ts @@ -4,6 +4,7 @@ import path from 'node:path'; import type { PresetProperty } from 'storybook/internal/types'; import type { StorybookConfigVite } from '@storybook/builder-vite'; +import { viteFinal as reactViteFinal } from '@storybook/react-vite/preset'; import { dirname, join } from 'path'; import vitePluginStorybookNextjs from 'vite-plugin-storybook-nextjs'; @@ -34,11 +35,13 @@ export const previewAnnotations: PresetProperty<'previewAnnotations'> = (entry = }; export const viteFinal: StorybookConfigVite['viteFinal'] = async (config, options) => { - config.plugins = config.plugins || []; + const reactConfig = await reactViteFinal(config, options); + const { plugins = [] } = reactConfig; + const { nextConfigPath } = await options.presets.apply('frameworkOptions'); const nextDir = nextConfigPath ? path.dirname(nextConfigPath) : undefined; - config.plugins.push(vitePluginStorybookNextjs({ dir: nextDir })); + plugins.push(vitePluginStorybookNextjs({ dir: nextDir })); - return config; + return reactConfig; }; diff --git a/code/frameworks/experimental-nextjs-vite/src/routing/app-router-provider.tsx b/code/frameworks/experimental-nextjs-vite/src/routing/app-router-provider.tsx index 68e01381948c..eec03995d266 100644 --- a/code/frameworks/experimental-nextjs-vite/src/routing/app-router-provider.tsx +++ b/code/frameworks/experimental-nextjs-vite/src/routing/app-router-provider.tsx @@ -44,8 +44,6 @@ function getSelectedParams(currentTree: FlightRouterState, params: Params = {}): continue; } - // Ensure catchAll and optional catchall are turned into an array - // Ensure catchAll and optional catchall are turned into an array const isCatchAll = isDynamicParameter && (segment[2] === 'c' || segment[2] === 'oc'); @@ -82,6 +80,16 @@ export const AppRouterProvider: React.FC @@ -106,10 +114,18 @@ export const AppRouterProvider: React.FC = v15.1.1 + parentTree: tree, + parentCacheNode: newLazyCacheNode, + // END url: pathname, + loading: null, }} > {children} diff --git a/code/frameworks/experimental-nextjs-vite/src/types.ts b/code/frameworks/experimental-nextjs-vite/src/types.ts index 8de91a4430d9..0221787dccb6 100644 --- a/code/frameworks/experimental-nextjs-vite/src/types.ts +++ b/code/frameworks/experimental-nextjs-vite/src/types.ts @@ -1,9 +1,7 @@ -import type { - CompatibleString, - StorybookConfig as StorybookConfigBase, -} from 'storybook/internal/types'; +import type { CompatibleString } from 'storybook/internal/types'; -import type { BuilderOptions, StorybookConfigVite } from '@storybook/builder-vite'; +import type { BuilderOptions } from '@storybook/builder-vite'; +import type { StorybookConfig as StorybookConfigReactVite } from '@storybook/react-vite'; type FrameworkName = CompatibleString<'@storybook/experimental-nextjs-vite'>; type BuilderName = CompatibleString<'@storybook/builder-vite'>; @@ -21,7 +19,7 @@ type StorybookConfigFramework = { name: FrameworkName; options: FrameworkOptions; }; - core?: StorybookConfigBase['core'] & { + core?: StorybookConfigReactVite['core'] & { builder?: | BuilderName | { @@ -32,9 +30,5 @@ type StorybookConfigFramework = { }; /** The interface for Storybook configuration in `main.ts` files. */ -export type StorybookConfig = Omit< - StorybookConfigBase, - keyof StorybookConfigVite | keyof StorybookConfigFramework -> & - StorybookConfigVite & - StorybookConfigFramework & {}; +export type StorybookConfig = Omit & + StorybookConfigFramework; diff --git a/code/frameworks/html-vite/package.json b/code/frameworks/html-vite/package.json index 02a11a32b867..08fb3adfa574 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 356198becd23..82f8536ca243 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 88ccba6855a9..964a6ff14419 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/nextjs", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Next.js", "keywords": [ "storybook", diff --git a/code/frameworks/nextjs/src/config/webpack.ts b/code/frameworks/nextjs/src/config/webpack.ts index 76edac25c81c..4b10922c9df9 100644 --- a/code/frameworks/nextjs/src/config/webpack.ts +++ b/code/frameworks/nextjs/src/config/webpack.ts @@ -1,8 +1,7 @@ import type { NextConfig } from 'next'; import type { Configuration as WebpackConfig } from 'webpack'; -import { DefinePlugin } from 'webpack'; -import { addScopedAlias, resolveNextConfig, setAlias } from '../utils'; +import { addScopedAlias, resolveNextConfig } from '../utils'; const tryResolve = (path: string) => { try { @@ -48,12 +47,15 @@ export const configureConfig = async ({ addScopedAlias(baseConfig, 'react-dom/server', 'next/dist/compiled/react-dom/server'); } - setupRuntimeConfig(baseConfig, nextConfig); + await setupRuntimeConfig(baseConfig, nextConfig); return nextConfig; }; -const setupRuntimeConfig = (baseConfig: WebpackConfig, nextConfig: NextConfig): void => { +const setupRuntimeConfig = async ( + baseConfig: WebpackConfig, + nextConfig: NextConfig +): Promise => { const definePluginConfig: Record = { // this mimics what nextjs does client side // https://github.com/vercel/next.js/blob/57702cb2a9a9dba4b552e0007c16449cf36cfb44/packages/next/client/index.tsx#L101 @@ -67,5 +69,7 @@ const setupRuntimeConfig = (baseConfig: WebpackConfig, nextConfig: NextConfig): definePluginConfig['process.env.__NEXT_NEW_LINK_BEHAVIOR'] = newNextLinkBehavior; - baseConfig.plugins?.push(new DefinePlugin(definePluginConfig)); + // Load DefinePlugin with a dynamic import to ensure that Next.js can first + // replace webpack with its own internal instance, and we get that here. + baseConfig.plugins?.push(new (await import('webpack')).default.DefinePlugin(definePluginConfig)); }; diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index 3463910175e1..6b90ae543a22 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -10,31 +10,29 @@ import type { ConfigItem, PluginItem, TransformOptions } from '@babel/core'; import { loadPartialConfig } from '@babel/core'; import semver from 'semver'; -import { configureAliases } from './aliases/webpack'; -import { configureBabelLoader } from './babel/loader'; import nextBabelPreset from './babel/preset'; -import { configureCompatibilityAliases } from './compatibility/compatibility-map'; import { configureConfig } from './config/webpack'; -import { configureCss } from './css/webpack'; -import { configureNextExportMocks } from './export-mocks/webpack'; -import { configureFastRefresh } from './fastRefresh/webpack'; import TransformFontImports from './font/babel'; -import { configureNextFont } from './font/webpack/configureNextFont'; -import { configureImages } from './images/webpack'; -import { configureImports } from './imports/webpack'; -import { configureNodePolyfills } from './nodePolyfills/webpack'; -import { configureRSC } from './rsc/webpack'; -import { configureStyledJsx } from './styledJsx/webpack'; -import { configureSWCLoader } from './swc/loader'; import type { FrameworkOptions, StorybookConfig } from './types'; -import { configureRuntimeNextjsVersionResolution, getNextjsVersion } from './utils'; export const addons: PresetProperty<'addons'> = [ dirname(require.resolve(join('@storybook/preset-react-webpack', 'package.json'))), ]; export const core: PresetProperty<'core'> = async (config, options) => { - const framework = await options.presets.apply('framework'); + const framework = await options.presets.apply('framework'); + + // Load the Next.js configuration before we need it in webpackFinal (below). + // This gives Next.js an opportunity to override some of webpack's internals + // (see next/dist/server/config-utils.js) before @storybook/builder-webpack5 + // starts to use it. Without this, webpack's file system cache (fsCache: true) + // does not work. + await configureConfig({ + // Pass in a dummy webpack config object for now, since we don't want to + // modify the real one yet. We pass in the real one in webpackFinal. + baseConfig: {}, + nextConfigPath: typeof framework === 'string' ? undefined : framework.options.nextConfigPath, + }); return { ...config, @@ -144,6 +142,22 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig, nextConfigPath, }); + // Use dynamic imports to ensure these modules that use webpack load after + // Next.js has been configured (above), and has replaced webpack with its precompiled + // version. + const { configureNextFont } = await import('./font/webpack/configureNextFont'); + const { configureRuntimeNextjsVersionResolution, getNextjsVersion } = await import('./utils'); + const { configureImports } = await import('./imports/webpack'); + const { configureCss } = await import('./css/webpack'); + const { configureImages } = await import('./images/webpack'); + const { configureStyledJsx } = await import('./styledJsx/webpack'); + const { configureNodePolyfills } = await import('./nodePolyfills/webpack'); + const { configureAliases } = await import('./aliases/webpack'); + const { configureFastRefresh } = await import('./fastRefresh/webpack'); + const { configureRSC } = await import('./rsc/webpack'); + const { configureSWCLoader } = await import('./swc/loader'); + const { configureBabelLoader } = await import('./babel/loader'); + const babelRCPath = join(getProjectRoot(), '.babelrc'); const babelConfigPath = join(getProjectRoot(), 'babel.config.js'); const hasBabelConfig = existsSync(babelRCPath) || existsSync(babelConfigPath); diff --git a/code/frameworks/nextjs/src/routing/app-router-provider.tsx b/code/frameworks/nextjs/src/routing/app-router-provider.tsx index 36e980b1a5fe..3f06a6668648 100644 --- a/code/frameworks/nextjs/src/routing/app-router-provider.tsx +++ b/code/frameworks/nextjs/src/routing/app-router-provider.tsx @@ -80,6 +80,16 @@ export const AppRouterProvider: React.FC @@ -104,8 +114,16 @@ export const AppRouterProvider: React.FC = v15.1.1 + parentTree: tree, + parentCacheNode: newLazyCacheNode, + // END url: pathname, loading: null, }} diff --git a/code/frameworks/preact-vite/package.json b/code/frameworks/preact-vite/package.json index fb80969ea2a6..ec95f3fcf6fc 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 158b8645be84..50c51eee007c 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Preact: Develop Preact Component in isolation.", "keywords": [ "storybook" diff --git a/code/frameworks/react-native-web-vite/package.json b/code/frameworks/react-native-web-vite/package.json index 4b6c1143687e..b7392768f46b 100644 --- a/code/frameworks/react-native-web-vite/package.json +++ b/code/frameworks/react-native-web-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react-native-web-vite", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Develop react-native components an isolated web environment with hot reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/react-native-web-vite/src/preset.ts b/code/frameworks/react-native-web-vite/src/preset.ts index 3ccd4240bf65..44e847171dda 100644 --- a/code/frameworks/react-native-web-vite/src/preset.ts +++ b/code/frameworks/react-native-web-vite/src/preset.ts @@ -1,4 +1,3 @@ -// @ts-expect-error FIXME import { viteFinal as reactViteFinal } from '@storybook/react-vite/preset'; import { esbuildFlowPlugin, flowPlugin } from '@bunchtogether/vite-plugin-flow'; diff --git a/code/frameworks/react-vite/package.json b/code/frameworks/react-vite/package.json index 178ff614e547..2db52d151651 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for React and Vite: Develop React components in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -35,6 +35,16 @@ "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/index.d.ts" + ], + "preset": [ + "dist/preset.d.ts" + ] + } + }, "files": [ "dist/**/*", "README.md", diff --git a/code/frameworks/react-vite/src/preset.ts b/code/frameworks/react-vite/src/preset.ts index cef1a270f33b..a01721dadacc 100644 --- a/code/frameworks/react-vite/src/preset.ts +++ b/code/frameworks/react-vite/src/preset.ts @@ -12,7 +12,7 @@ export const core: PresetProperty<'core'> = { renderer: getAbsolutePath('@storybook/react'), }; -export const viteFinal: StorybookConfig['viteFinal'] = async (config, { presets }) => { +export const viteFinal: NonNullable = async (config, { presets }) => { const { plugins = [] } = config; // Add docgen plugin diff --git a/code/frameworks/react-webpack5/package.json b/code/frameworks/react-webpack5/package.json index ff54c5fbe72c..868da3831029 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for React: Develop React Component in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/react-webpack5/src/preset.ts b/code/frameworks/react-webpack5/src/preset.ts index 9e233459c10b..08f870a23054 100644 --- a/code/frameworks/react-webpack5/src/preset.ts +++ b/code/frameworks/react-webpack5/src/preset.ts @@ -2,6 +2,8 @@ import { dirname, join } from 'node:path'; import type { PresetProperty } from 'storybook/internal/types'; +import { WebpackDefinePlugin } from '@storybook/builder-webpack5'; + import type { StorybookConfig } from './types'; const getAbsolutePath = (input: I): I => @@ -24,12 +26,23 @@ export const core: PresetProperty<'core'> = async (config, options) => { }; }; -export const webpack: StorybookConfig['webpack'] = async (config) => { +export const webpack: StorybookConfig['webpack'] = async (config, options) => { config.resolve = config.resolve || {}; config.resolve.alias = { ...config.resolve?.alias, '@storybook/react': getAbsolutePath('@storybook/react'), }; + + if (options.features?.developmentModeForBuild) { + config.plugins = [ + // @ts-expect-error Ignore this error, because in the `webpack` preset the user actually hasn't defined a config yet. + ...config.plugins, + new WebpackDefinePlugin({ + NODE_ENV: JSON.stringify('development'), + }), + ]; + } + return config; }; diff --git a/code/frameworks/server-webpack5/package.json b/code/frameworks/server-webpack5/package.json index 728774d9db91..24e17e444527 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 168c9fdfd5c3..b17aea752397 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Svelte and Vite: Develop Svelte components in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/svelte-webpack5/package.json b/code/frameworks/svelte-webpack5/package.json index ed6fbe94b336..12bc1d7d84eb 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Svelte: Develop Svelte Component in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/sveltekit/package.json b/code/frameworks/sveltekit/package.json index fba6bdb225f6..6e90749fedf3 100644 --- a/code/frameworks/sveltekit/package.json +++ b/code/frameworks/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/sveltekit", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for SvelteKit", "keywords": [ "storybook", diff --git a/code/frameworks/vue3-vite/package.json b/code/frameworks/vue3-vite/package.json index 2f3ae021c76a..c863dc5bbfe9 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 3100bb0b6289..bcb49a1fb6e2 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 836c140ca82c..1000ee226865 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 4d52e1e849e9..1922e8bb0c28 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for web-components: View web components snippets in isolation with Hot Reloading.", "keywords": [ "lit", diff --git a/code/lib/blocks/package.json b/code/lib/blocks/package.json index c38aa59f379c..d815acc1a453 100644 --- a/code/lib/blocks/package.json +++ b/code/lib/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/blocks", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook Doc Blocks", "keywords": [ "storybook" diff --git a/code/lib/cli-sb/package.json b/code/lib/cli-sb/package.json index 9fb9cbcc6121..5210403c97b9 100644 --- a/code/lib/cli-sb/package.json +++ b/code/lib/cli-sb/package.json @@ -1,6 +1,6 @@ { "name": "sb", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook CLI", "keywords": [ "storybook" diff --git a/code/lib/cli-storybook/package.json b/code/lib/cli-storybook/package.json index fd8411206ce2..f08e2354adad 100644 --- a/code/lib/cli-storybook/package.json +++ b/code/lib/cli-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/cli", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook CLI", "keywords": [ "storybook" diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.test.ts index 71110733d896..87da579183cb 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.test.ts @@ -7,7 +7,13 @@ import dedent from 'ts-dedent'; import { getAddonNames } from '../helpers/mainConfigFile'; import { addonA11yAddonTest, transformSetupFile } from './addon-a11y-addon-test'; -vi.mock('../helpers/mainConfigFile'); +vi.mock('../helpers/mainConfigFile', async (importOriginal) => { + const mod = (await importOriginal()) as any; + return { + ...mod, + getAddonNames: vi.fn(), + }; +}); // mock fs.existsSync vi.mock('fs', async (importOriginal) => { @@ -46,6 +52,20 @@ describe('addonA11yAddonTest', () => { expect(result).toBeNull(); }); + it('should return null if provided framework is not supported', async () => { + vi.mocked(getAddonNames).mockReturnValue([ + '@storybook/addon-a11y', + '@storybook/experimental-addon-test', + ]); + const result = await addonA11yAddonTest.check({ + mainConfig: { + framework: '@storybook/angular', + }, + configDir: '', + } as any); + expect(result).toBeNull(); + }); + it('should return setupFile and transformedSetupCode if vitest.setup file exists', async () => { vi.mocked(getAddonNames).mockReturnValue([ '@storybook/addon-a11y', @@ -54,7 +74,12 @@ describe('addonA11yAddonTest', () => { vi.mocked(existsSync).mockReturnValue(true); vi.mocked(readFileSync).mockReturnValue('const annotations = setProjectAnnotations([]);'); - const result = await addonA11yAddonTest.check({ mainConfig, configDir } as any); + const result = await addonA11yAddonTest.check({ + mainConfig: { + framework: '@storybook/react-vite', + }, + configDir, + } as any); expect(result).toEqual({ setupFile: path.join(configDir, 'vitest.setup.js'), transformedSetupCode: expect.any(String), @@ -71,7 +96,12 @@ describe('addonA11yAddonTest', () => { throw new Error('Test error'); }); - const result = await addonA11yAddonTest.check({ mainConfig, configDir } as any); + const result = await addonA11yAddonTest.check({ + mainConfig: { + framework: '@storybook/sveltekit', + }, + configDir, + } as any); expect(result).toEqual({ setupFile: path.join(configDir, 'vitest.setup.js'), transformedSetupCode: null, @@ -141,7 +171,8 @@ describe('addonA11yAddonTest', () => { `; vi.mocked(readFileSync).mockReturnValue(source); - const transformedCode = transformSetupFile(setupFile); + const s = readFileSync(setupFile, 'utf8'); + const transformedCode = transformSetupFile(s); expect(transformedCode).toMatchInlineSnapshot(` "import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview"; import { beforeAll } from 'vitest'; @@ -169,7 +200,35 @@ describe('addonA11yAddonTest', () => { `; vi.mocked(readFileSync).mockReturnValue(source); - const transformedCode = transformSetupFile(setupFile); + const s = readFileSync(setupFile, 'utf8'); + const transformedCode = transformSetupFile(s); + expect(transformedCode).toMatchInlineSnapshot(` + "import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview"; + import { beforeAll } from 'vitest'; + import { setProjectAnnotations } from 'storybook'; + import * as projectAnnotations from './preview'; + + const project = setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]); + + beforeAll(project.beforeAll);" + `); + }); + + it('should transform setup file correctly - project annotation is not an array', () => { + const setupFile = '/path/to/vitest.setup.ts'; + const source = dedent` + import { beforeAll } from 'vitest'; + import { setProjectAnnotations } from 'storybook'; + import * as projectAnnotations from './preview'; + + const project = setProjectAnnotations(projectAnnotations); + + beforeAll(project.beforeAll); + `; + vi.mocked(readFileSync).mockReturnValue(source); + + const s = readFileSync(setupFile, 'utf8'); + const transformedCode = transformSetupFile(s); expect(transformedCode).toMatchInlineSnapshot(` "import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview"; import { beforeAll } from 'vitest'; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.ts index ae848cf57689..a8cd25cdf371 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.ts @@ -1,10 +1,17 @@ +import { rendererPackages } from 'storybook/internal/common'; + import { existsSync, readFileSync, writeFileSync } from 'fs'; import * as jscodeshift from 'jscodeshift'; import path from 'path'; import picocolors from 'picocolors'; import { dedent } from 'ts-dedent'; -import { getAddonNames } from '../helpers/mainConfigFile'; +// Relative path import to avoid dependency to @storybook/test +import { + SUPPORTED_FRAMEWORKS, + SUPPORTED_RENDERERS, +} from '../../../../../addons/test/src/constants'; +import { getAddonNames, getFrameworkPackageName, getRendererName } from '../helpers/mainConfigFile'; import type { Fix } from '../types'; export const vitestFileExtensions = ['.js', '.ts', '.cts', '.mts', '.cjs', '.mjs'] as const; @@ -22,8 +29,7 @@ interface AddonA11yAddonTestOptions { */ export const addonA11yAddonTest: Fix = { id: 'addonA11yAddonTest', - // TODO: Change to the correct version after testing - versionRange: ['<8.5.0', '*'], + versionRange: ['<8.5.0', '>=8.5.0'], promptType(result) { if (result.setupFile === null) { @@ -36,11 +42,23 @@ export const addonA11yAddonTest: Fix = { async check({ mainConfig, configDir }) { const addons = getAddonNames(mainConfig); + const frameworkPackageName = getFrameworkPackageName(mainConfig); + const rendererPackageName = getRendererName(mainConfig); + const hasA11yAddon = !!addons.find((addon) => addon.includes('@storybook/addon-a11y')); const hasTestAddon = !!addons.find((addon) => addon.includes('@storybook/experimental-addon-test') ); + if ( + !SUPPORTED_FRAMEWORKS.find((framework) => frameworkPackageName?.includes(framework)) && + !SUPPORTED_RENDERERS.find((renderer) => + rendererPackageName?.includes(rendererPackages[renderer]) + ) + ) { + return null; + } + if (!hasA11yAddon || !hasTestAddon || !configDir) { return null; } @@ -53,7 +71,11 @@ export const addonA11yAddonTest: Fix = { try { if (vitestSetupFile) { - const transformedSetupCode = transformSetupFile(vitestSetupFile); + const source = readFileSync(vitestSetupFile, 'utf8'); + if (source.includes('@storybook/addon-a11y')) { + return null; + } + const transformedSetupCode = transformSetupFile(source); return { setupFile: vitestSetupFile, transformedSetupCode, @@ -124,8 +146,7 @@ export const addonA11yAddonTest: Fix = { }, }; -export function transformSetupFile(setupFile: string) { - const source = readFileSync(setupFile, 'utf8'); +export function transformSetupFile(source: string) { const j = jscodeshift.withParser('ts'); const root = j(source); @@ -148,9 +169,14 @@ export function transformSetupFile(setupFile: string) { throw new Error('Could not find setProjectAnnotations call in vitest.setup file'); } - // Add a11yAddonAnnotations to the annotations array - setProjectAnnotationsCall.find(j.ArrayExpression).forEach((p) => { - p.value.elements.unshift(j.identifier('a11yAddonAnnotations')); + // Add a11yAddonAnnotations to the annotations array or create a new array if argument is a string + setProjectAnnotationsCall.forEach((p) => { + if (p.value.arguments.length === 1 && p.value.arguments[0].type === 'ArrayExpression') { + p.value.arguments[0].elements.unshift(j.identifier('a11yAddonAnnotations')); + } else if (p.value.arguments.length === 1 && p.value.arguments[0].type === 'Identifier') { + const arg = p.value.arguments[0]; + p.value.arguments[0] = j.arrayExpression([j.identifier('a11yAddonAnnotations'), arg]); + } }); // Add the import declaration at the top diff --git a/code/lib/cli-storybook/src/sandbox-templates.ts b/code/lib/cli-storybook/src/sandbox-templates.ts index f905d2841803..0324635b1ffe 100644 --- a/code/lib/cli-storybook/src/sandbox-templates.ts +++ b/code/lib/cli-storybook/src/sandbox-templates.ts @@ -150,7 +150,10 @@ const baseTemplates = { }, modifications: { mainConfig: { - features: { experimentalRSC: true }, + features: { + experimentalRSC: true, + developmentModeForBuild: true, + }, }, extraDependencies: ['server-only', 'prop-types'], }, @@ -167,7 +170,10 @@ const baseTemplates = { }, modifications: { mainConfig: { - features: { experimentalRSC: true }, + features: { + experimentalRSC: true, + developmentModeForBuild: true, + }, }, extraDependencies: ['server-only', 'prop-types'], }, @@ -184,7 +190,10 @@ const baseTemplates = { }, modifications: { mainConfig: { - features: { experimentalRSC: true }, + features: { + experimentalRSC: true, + developmentModeForBuild: true, + }, }, extraDependencies: ['server-only', 'prop-types'], }, @@ -200,10 +209,13 @@ const baseTemplates = { builder: '@storybook/builder-webpack5', }, modifications: { - extraDependencies: ['server-only', 'prop-types'], mainConfig: { - features: { experimentalRSC: true }, + features: { + experimentalRSC: true, + developmentModeForBuild: true, + }, }, + extraDependencies: ['server-only', 'prop-types'], }, skipTasks: ['e2e-tests-dev', 'bench', 'vitest-integration'], }, @@ -219,7 +231,10 @@ const baseTemplates = { modifications: { mainConfig: { framework: '@storybook/experimental-nextjs-vite', - features: { experimentalRSC: true }, + features: { + experimentalRSC: true, + developmentModeForBuild: true, + }, }, extraDependencies: [ 'server-only', @@ -242,7 +257,10 @@ const baseTemplates = { modifications: { mainConfig: { framework: '@storybook/experimental-nextjs-vite', - features: { experimentalRSC: true }, + features: { + experimentalRSC: true, + developmentModeForBuild: true, + }, }, extraDependencies: [ 'server-only', @@ -263,6 +281,11 @@ const baseTemplates = { }, modifications: { extraDependencies: ['prop-types'], + mainConfig: { + features: { + developmentModeForBuild: true, + }, + }, }, skipTasks: ['e2e-tests-dev', 'bench'], }, @@ -276,6 +299,11 @@ const baseTemplates = { }, modifications: { extraDependencies: ['prop-types'], + mainConfig: { + features: { + developmentModeForBuild: true, + }, + }, }, skipTasks: ['bench'], }, @@ -302,6 +330,11 @@ const baseTemplates = { }, modifications: { extraDependencies: ['prop-types'], + mainConfig: { + features: { + developmentModeForBuild: true, + }, + }, }, skipTasks: ['e2e-tests-dev', 'bench'], }, @@ -400,6 +433,16 @@ const baseTemplates = { }, skipTasks: ['e2e-tests-dev', 'bench'], }, + 'nuxt-vite/default-ts': { + name: 'Nuxt v3 (Vite | TypeScript)', + script: 'npx nuxi init --packageManager yarn --gitInit false {{beforeDir}}', + expected: { + framework: '@storybook-vue/nuxt', + renderer: '@storybook/vue3', + builder: '@storybook/builder-vite', + }, + skipTasks: ['e2e-tests-dev', 'bench', 'vitest-integration'], + }, 'html-webpack/default': { name: 'HTML Latest (Webpack | JavaScript)', script: 'yarn create webpack5-html {{beforeDir}}', @@ -809,6 +852,7 @@ export const normal: TemplateKey[] = [ 'react-vite/default-ts', 'angular-cli/default-ts', 'vue3-vite/default-ts', + 'nuxt-vite/default-ts', 'lit-vite/default-ts', 'svelte-vite/default-ts', 'svelte-kit/skeleton-ts', diff --git a/code/lib/cli/package.json b/code/lib/cli/package.json index 8e7150357548..cdc83ea4bbb2 100644 --- a/code/lib/cli/package.json +++ b/code/lib/cli/package.json @@ -1,6 +1,6 @@ { "name": "storybook", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook's CLI - install, dev, build, upgrade, and more", "keywords": [ "cli", @@ -311,7 +311,8 @@ ], "scripts": { "check": "jiti ../../../scripts/prepare/check.ts", - "prep": "jiti ../../../scripts/prepare/bundle.ts" + "prep": "jiti ../../../scripts/prepare/bundle.ts", + "sb": "node ./bin/index.js" }, "dependencies": { "@storybook/core": "workspace:*" diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json index 4826ca8fc29f..324c7bc26320 100644 --- a/code/lib/codemod/package.json +++ b/code/lib/codemod/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/codemod", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "A collection of codemod scripts written with JSCodeshift", "keywords": [ "storybook" diff --git a/code/lib/core-webpack/package.json b/code/lib/core-webpack/package.json index af5e0cb57a93..798463e83457 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" diff --git a/code/lib/create-storybook/package.json b/code/lib/create-storybook/package.json index 9a05e6de9adb..05255add8f42 100644 --- a/code/lib/create-storybook/package.json +++ b/code/lib/create-storybook/package.json @@ -1,6 +1,6 @@ { "name": "create-storybook", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Initialize Storybook into your project", "homepage": "https://github.com/storybookjs/storybook/tree/next/code/lib/create-storybook", "bugs": { diff --git a/code/lib/create-storybook/src/generators/NUXT/index.ts b/code/lib/create-storybook/src/generators/NUXT/index.ts new file mode 100644 index 000000000000..a7d7be640eb7 --- /dev/null +++ b/code/lib/create-storybook/src/generators/NUXT/index.ts @@ -0,0 +1,31 @@ +import { baseGenerator } from '../baseGenerator'; +import type { Generator } from '../types'; + +const generator: Generator = async (packageManager, npmOptions, options) => { + await baseGenerator( + packageManager, + npmOptions, + options, + 'vue3', + { + extraPackages: async () => { + return ['@nuxtjs/storybook']; + }, + installFrameworkPackages: false, + componentsDestinationPath: './components', + extraMain: { + stories: ['../components/**/*.mdx', '../components/**/*.stories.@(js|jsx|ts|tsx|mdx)'], + }, + }, + 'nuxt' + ); + // Add nuxtjs/storybook to nuxt.config.js + await packageManager.runPackageCommand('nuxi', [ + 'module', + 'add', + '@nuxtjs/storybook', + '--skipInstall', + ]); +}; + +export default generator; diff --git a/code/lib/create-storybook/src/generators/baseGenerator.ts b/code/lib/create-storybook/src/generators/baseGenerator.ts index 84daecba1b46..1b0c917edd58 100644 --- a/code/lib/create-storybook/src/generators/baseGenerator.ts +++ b/code/lib/create-storybook/src/generators/baseGenerator.ts @@ -1,14 +1,14 @@ import { mkdir } from 'node:fs/promises'; import { dirname, join } from 'node:path'; -import type { NpmOptions } from 'storybook/internal/cli'; -import type { Builder, SupportedRenderers } from 'storybook/internal/cli'; +import type { Builder, NpmOptions } from 'storybook/internal/cli'; import { SupportedLanguage, externalFrameworks } from 'storybook/internal/cli'; import { copyTemplateFiles } from 'storybook/internal/cli'; import { configureEslintPlugin, extractEslintInfo } from 'storybook/internal/cli'; import { detectBuilder } from 'storybook/internal/cli'; import type { JsPackageManager } from 'storybook/internal/common'; import { getPackageDetails, versions as packageVersions } from 'storybook/internal/common'; +import type { SupportedRenderers } from 'storybook/internal/types'; import type { SupportedFrameworks } from 'storybook/internal/types'; // eslint-disable-next-line depend/ban-dependencies @@ -27,6 +27,7 @@ const defaultOptions: FrameworkOptions = { staticDir: undefined, addScripts: true, addMainFile: true, + addPreviewFile: true, addComponents: true, webpackCompiler: () => undefined, extraMain: undefined, @@ -34,6 +35,7 @@ const defaultOptions: FrameworkOptions = { extensions: undefined, componentsDestinationPath: undefined, storybookConfigFolder: '.storybook', + installFrameworkPackages: true, }; const getBuilderDetails = (builder: string) => { @@ -82,13 +84,10 @@ const getFrameworkPackage = (framework: string | undefined, renderer: string, bu ); } - if (externalFramework.frameworks !== undefined) { - return externalFramework.frameworks.find((item) => - item.match(new RegExp(`-${storybookBuilder}`)) - ); - } - - return externalFramework.packageName; + return ( + externalFramework.frameworks?.find((item) => item.match(new RegExp(`-${storybookBuilder}`))) ?? + externalFramework.packageName + ); }; const getRendererPackage = (framework: string | undefined, renderer: string) => { @@ -117,6 +116,7 @@ const getFrameworkDetails = ( framework?: string; renderer?: string; rendererId: SupportedRenderers; + frameworkPackage?: string; } => { const frameworkPackage = getFrameworkPackage(framework, renderer, builder); invariant(frameworkPackage, 'Missing framework package.'); @@ -144,6 +144,7 @@ const getFrameworkDetails = ( return { packages: [rendererPackage, frameworkPackage], framework: frameworkPackagePath, + frameworkPackage, rendererId: renderer, type: 'framework', }; @@ -169,8 +170,18 @@ const stripVersions = (addons: string[]) => addons.map((addon) => getPackageDeta const hasInteractiveStories = (rendererId: SupportedRenderers) => ['react', 'angular', 'preact', 'svelte', 'vue3', 'html', 'solid', 'qwik'].includes(rendererId); -const hasFrameworkTemplates = (framework?: SupportedFrameworks) => - framework ? ['angular', 'nextjs', 'react-native-web-vite'].includes(framework) : false; +const hasFrameworkTemplates = (framework?: SupportedFrameworks) => { + if (!framework) { + return false; + } + // Nuxt has framework templates, but for sandboxes we create them from the Vue3 renderer + // As the Nuxt framework templates are not compatible with the stories we need for CI. + // See: https://github.com/storybookjs/storybook/pull/28607#issuecomment-2467903327 + if (framework === 'nuxt') { + return process.env.IN_STORYBOOK_SANDBOX !== 'true'; + } + return ['angular', 'nextjs', 'react-native-web-vite'].includes(framework); +}; export async function baseGenerator( packageManager: JsPackageManager, @@ -193,6 +204,7 @@ export async function baseGenerator( rendererId, framework: frameworkInclude, builder: builderInclude, + frameworkPackage, } = getFrameworkDetails( renderer, builder, @@ -208,12 +220,14 @@ export async function baseGenerator( staticDir, addScripts, addMainFile, + addPreviewFile, addComponents, extraMain, extensions, storybookConfigFolder, componentsDestinationPath, webpackCompiler, + installFrameworkPackages, } = { ...defaultOptions, ...options, @@ -278,7 +292,7 @@ export async function baseGenerator( const allPackages = [ 'storybook', getExternalFramework(rendererId) ? undefined : `@storybook/${rendererId}`, - ...frameworkPackages, + ...(installFrameworkPackages ? frameworkPackages : []), ...addonPackages, ...(extraPackagesToInstall || []), ].filter(Boolean); @@ -320,9 +334,9 @@ export async function baseGenerator( addDependenciesSpinner.succeed(); } - // Passing `recursive: true` ensures that the method doesn't throw when - // the directory already exists. - await mkdir(`./${storybookConfigFolder}`, { recursive: true }); + if (addMainFile || addPreviewFile) { + await mkdir(`./${storybookConfigFolder}`, { recursive: true }); + } if (addMainFile) { const prefixes = shouldApplyRequireWrapperOnPackageNames @@ -351,6 +365,7 @@ export async function baseGenerator( name: frameworkInclude, options: options.framework || {}, }, + frameworkPackage, prefixes, storybookConfigFolder, addons: shouldApplyRequireWrapperOnPackageNames @@ -370,12 +385,14 @@ export async function baseGenerator( }); } - await configurePreview({ - frameworkPreviewParts, - storybookConfigFolder: storybookConfigFolder as string, - language, - rendererId, - }); + if (addPreviewFile) { + await configurePreview({ + frameworkPreviewParts, + storybookConfigFolder: storybookConfigFolder as string, + language, + rendererId, + }); + } if (addScripts) { await packageManager.addStorybookCommandInScripts({ diff --git a/code/lib/create-storybook/src/generators/configure.test.ts b/code/lib/create-storybook/src/generators/configure.test.ts index 853e1102a055..e593810dd971 100644 --- a/code/lib/create-storybook/src/generators/configure.test.ts +++ b/code/lib/create-storybook/src/generators/configure.test.ts @@ -26,6 +26,7 @@ describe('configureMain', () => { framework: { name: '@storybook/react-vite', }, + frameworkPackage: '@storybook/react-vite', }); const { calls } = vi.mocked(fsp.writeFile).mock; @@ -55,6 +56,7 @@ describe('configureMain', () => { framework: { name: '@storybook/react-vite', }, + frameworkPackage: '@storybook/react-vite', }); const { calls } = vi.mocked(fsp.writeFile).mock; @@ -89,6 +91,7 @@ describe('configureMain', () => { framework: { name: "%%path.dirname(require.resolve(path.join('@storybook/react-webpack5', 'package.json')))%%", }, + frameworkPackage: '@storybook/react-webpack5', }); const { calls } = vi.mocked(fsp.writeFile).mock; diff --git a/code/lib/create-storybook/src/generators/configure.ts b/code/lib/create-storybook/src/generators/configure.ts index c7002c58c045..401509dc2399 100644 --- a/code/lib/create-storybook/src/generators/configure.ts +++ b/code/lib/create-storybook/src/generators/configure.ts @@ -13,6 +13,7 @@ interface ConfigureMainOptions { storybookConfigFolder: string; language: SupportedLanguage; prefixes: string[]; + frameworkPackage: string; /** * Extra values for main.js * @@ -61,6 +62,7 @@ export async function configureMain({ extensions = ['js', 'jsx', 'mjs', 'ts', 'tsx'], storybookConfigFolder, language, + frameworkPackage, prefixes = [], ...custom }: ConfigureMainOptions) { @@ -78,8 +80,6 @@ export async function configureMain({ let mainConfigTemplate = dedent`<><>const config<> = <>; export default config;`; - const frameworkPackage = sanitizeFramework(custom.framework?.name); - if (!frameworkPackage) { mainConfigTemplate = mainConfigTemplate.replace('<>', '').replace('<>', ''); logger.warn('Could not find framework package name'); diff --git a/code/lib/create-storybook/src/generators/types.ts b/code/lib/create-storybook/src/generators/types.ts index e9192a47c3c9..3c07d97e27cc 100644 --- a/code/lib/create-storybook/src/generators/types.ts +++ b/code/lib/create-storybook/src/generators/types.ts @@ -23,6 +23,7 @@ export interface FrameworkOptions { staticDir?: string; addScripts?: boolean; addMainFile?: boolean; + addPreviewFile?: boolean; addComponents?: boolean; webpackCompiler?: ({ builder }: { builder: Builder }) => 'babel' | 'swc' | undefined; extraMain?: any; @@ -30,6 +31,7 @@ export interface FrameworkOptions { framework?: Record; storybookConfigFolder?: string; componentsDestinationPath?: string; + installFrameworkPackages?: boolean; } export type Generator = ( diff --git a/code/lib/create-storybook/src/initiate.ts b/code/lib/create-storybook/src/initiate.ts index 6c1c6559d262..970cef3d137b 100644 --- a/code/lib/create-storybook/src/initiate.ts +++ b/code/lib/create-storybook/src/initiate.ts @@ -27,6 +27,7 @@ import angularGenerator from './generators/ANGULAR'; import emberGenerator from './generators/EMBER'; import htmlGenerator from './generators/HTML'; import nextjsGenerator from './generators/NEXTJS'; +import nuxtGenerator from './generators/NUXT'; import preactGenerator from './generators/PREACT'; import qwikGenerator from './generators/QWIK'; import reactGenerator from './generators/REACT'; @@ -117,6 +118,11 @@ const installStorybook = async ( commandLog('Adding Storybook support to your "Vue 3" app') ); + case ProjectType.NUXT: + return nuxtGenerator(packageManager, npmOptions, generatorOptions).then( + commandLog('Adding Storybook support to your "Nuxt" app') + ); + case ProjectType.ANGULAR: commandLog('Adding Storybook support to your "Angular" app'); return angularGenerator(packageManager, npmOptions, generatorOptions, options); diff --git a/code/lib/csf-plugin/package.json b/code/lib/csf-plugin/package.json index 363ee09250f7..3b57203d481f 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Enrich CSF files via static analysis", "keywords": [ "storybook" diff --git a/code/lib/instrumenter/package.json b/code/lib/instrumenter/package.json index 559bcbad40c4..84b42100656e 100644 --- a/code/lib/instrumenter/package.json +++ b/code/lib/instrumenter/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/instrumenter", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "", "keywords": [ "storybook" diff --git a/code/lib/react-dom-shim/package.json b/code/lib/react-dom-shim/package.json index a25cbdb8c8ed..13eacc85b7ed 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "", "keywords": [ "storybook" diff --git a/code/lib/source-loader/package.json b/code/lib/source-loader/package.json index 2de7c8411c93..409cdf2a3a04 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Source loader", "keywords": [ "lib", diff --git a/code/lib/test/package.json b/code/lib/test/package.json index 792d88e03dd3..da8c9beb9229 100644 --- a/code/lib/test/package.json +++ b/code/lib/test/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/test", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "", "keywords": [ "storybook" diff --git a/code/nx.json b/code/nx.json index 688fc38b5dd6..eaaabd089ea4 100644 --- a/code/nx.json +++ b/code/nx.json @@ -3,6 +3,7 @@ "extends": "nx/presets/npm.json", "nxCloudAccessToken": "NGVmYTkxMmItYzY3OS00MjkxLTk1ZDktZDFmYTFmNmVlNGY4fHJlYWQ=", "defaultBase": "next", + "useLegacyCache": true, "parallel": 8, "plugins": [ { diff --git a/code/package.json b/code/package.json index 35bc57478fb8..b8531c49d34d 100644 --- a/code/package.json +++ b/code/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/root", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "private": true, "description": "Storybook root", "homepage": "https://storybook.js.org/", @@ -90,11 +90,11 @@ "type-fest": "~2.19" }, "dependencies": { - "@chromatic-com/storybook": "^3.2.0", + "@chromatic-com/storybook": "^3.2.2", "@happy-dom/global-registrator": "^14.12.0", - "@nx/eslint": "20.1.4", - "@nx/vite": "20.1.4", - "@nx/workspace": "20.1.4", + "@nx/eslint": "20.2.2", + "@nx/vite": "20.2.2", + "@nx/workspace": "20.2.2", "@playwright/test": "1.48.1", "@storybook/addon-a11y": "workspace:*", "@storybook/addon-actions": "workspace:*", @@ -202,7 +202,7 @@ "husky": "^4.3.7", "lint-staged": "^13.2.2", "mock-require": "^3.0.3", - "nx": "20.1.4", + "nx": "20.2.2", "prettier": "^3.1.1", "prettier-plugin-brace-style": "^0.6.2", "prettier-plugin-css-order": "^2.1.2", diff --git a/code/presets/create-react-app/package.json b/code/presets/create-react-app/package.json index 6e6a149e7606..034243e84eb8 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 9ca2cb19ba36..b0d642817560 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 6442f22d7c2f..b9afcadcc54a 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 ecdae18620ff..d6dbc6b2988b 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for React: Develop React Component in isolation with Hot Reloading", "keywords": [ "storybook" diff --git a/code/presets/server-webpack/package.json b/code/presets/server-webpack/package.json index 275e323e0c90..11dd58d5e508 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 668564406d78..31132600ffc7 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Svelte: Develop Svelte Component in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/presets/vue3-webpack/package.json b/code/presets/vue3-webpack/package.json index a2b686257fae..997b4807d360 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.5.0-alpha.20", + "version": "8.5.0-beta.5", "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 c127ef64950c..78f5d26b0802 100644 --- a/code/renderers/html/package.json +++ b/code/renderers/html/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/html", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook HTML renderer", "keywords": [ "storybook" diff --git a/code/renderers/preact/package.json b/code/renderers/preact/package.json index 3005d5e0e57e..a740e9671db2 100644 --- a/code/renderers/preact/package.json +++ b/code/renderers/preact/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preact", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook Preact renderer", "keywords": [ "storybook" diff --git a/code/renderers/react/package.json b/code/renderers/react/package.json index 6e2c8d94047e..3df61fc8f8cf 100644 --- a/code/renderers/react/package.json +++ b/code/renderers/react/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook React renderer", "keywords": [ "storybook" diff --git a/code/renderers/react/src/act-compat.ts b/code/renderers/react/src/act-compat.ts index 36e56712e02b..7d64e0f7c3be 100644 --- a/code/renderers/react/src/act-compat.ts +++ b/code/renderers/react/src/act-compat.ts @@ -40,15 +40,15 @@ function withGlobalActEnvironment(actImplementation: (callback: () => void) => P return result; }); if (callbackNeedsToBeAwaited) { - const thenable: Promise = actResult; + const thenable = actResult; return { then: (resolve: (param: any) => void, reject: (param: any) => void) => { thenable.then( - (returnValue) => { + (returnValue: any) => { setReactActEnvironment(previousActEnvironment); resolve(returnValue); }, - (error) => { + (error: any) => { setReactActEnvironment(previousActEnvironment); reject(error); } @@ -68,4 +68,7 @@ function withGlobalActEnvironment(actImplementation: (callback: () => void) => P }; } -export const act = withGlobalActEnvironment(reactAct); +export const act = + process.env.NODE_ENV === 'production' + ? (cb: (...args: any[]) => any) => cb() + : withGlobalActEnvironment(reactAct); diff --git a/code/renderers/react/src/entry-preview.tsx b/code/renderers/react/src/entry-preview.tsx index 08d625b5729d..b2e6c1de5b18 100644 --- a/code/renderers/react/src/entry-preview.tsx +++ b/code/renderers/react/src/entry-preview.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import semver from 'semver'; +import { act, getReactActEnvironment, setReactActEnvironment } from './act-compat'; import type { Decorator } from './public-types'; export const parameters = { renderer: 'react' }; @@ -10,9 +11,9 @@ export { renderToCanvas } from './renderToCanvas'; export { mount } from './mount'; export const decorators: Decorator[] = [ - (Story, context) => { + (story, context) => { if (!context.parameters?.react?.rsc) { - return ; + return story(); } const major = semver.major(React.version); @@ -21,10 +22,73 @@ export const decorators: Decorator[] = [ throw new Error('React Server Components require React >= 18.3'); } - return ( - - - - ); + return {story()}; }, ]; + +export const beforeAll = async () => { + try { + // copied from + // https://github.com/testing-library/react-testing-library/blob/3dcd8a9649e25054c0e650d95fca2317b7008576/src/pure.js + const { configure } = await import('@storybook/test'); + + configure({ + unstable_advanceTimersWrapper: (cb) => { + return act(cb); + }, + // For more context about why we need disable act warnings in waitFor: + // https://github.com/reactwg/react-18/discussions/102 + asyncWrapper: async (cb) => { + const previousActEnvironment = getReactActEnvironment(); + setReactActEnvironment(false); + try { + const result = await cb(); + // Drain microtask queue. + // Otherwise we'll restore the previous act() environment, before we resolve the `waitFor` call. + // The caller would have no chance to wrap the in-flight Promises in `act()` + await new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 0); + + if (jestFakeTimersAreEnabled()) { + // @ts-expect-error global jest + jest.advanceTimersByTime(0); + } + }); + + return result; + } finally { + setReactActEnvironment(previousActEnvironment); + } + }, + eventWrapper: (cb) => { + let result; + act(() => { + result = cb(); + return result; + }); + return result; + }, + }); + } catch (e) { + // no-op + // @storybook/test might not be available + } +}; + +/** The function is used to configure jest's fake timers in environments where React's act is enabled */ +function jestFakeTimersAreEnabled() { + // @ts-expect-error global jest + if (typeof jest !== 'undefined' && jest !== null) { + return ( + // legacy timers + + // eslint-disable-next-line no-underscore-dangle + (setTimeout as any)._isMockFunction === true || // modern timers + Object.prototype.hasOwnProperty.call(setTimeout, 'clock') + ); + } + + return false; +} diff --git a/code/renderers/react/src/portable-stories.tsx b/code/renderers/react/src/portable-stories.tsx index 7b906c9f4bde..ca29c8c7de72 100644 --- a/code/renderers/react/src/portable-stories.tsx +++ b/code/renderers/react/src/portable-stories.tsx @@ -17,7 +17,6 @@ import type { StoryAnnotationsOrFn, } from 'storybook/internal/types'; -import { act, getReactActEnvironment, setReactActEnvironment } from './act-compat'; import * as reactProjectAnnotations from './entry-preview'; import type { Meta } from './public-types'; import type { ReactRenderer } from './types'; @@ -55,67 +54,14 @@ export function setProjectAnnotations( // This will not be necessary once we have auto preset loading export const INTERNAL_DEFAULT_PROJECT_ANNOTATIONS: ProjectAnnotations = { ...reactProjectAnnotations, - beforeAll: async function reactBeforeAll() { - try { - // copied from - // https://github.com/testing-library/react-testing-library/blob/3dcd8a9649e25054c0e650d95fca2317b7008576/src/pure.js - const { configure } = await import('@storybook/test'); - - configure({ - unstable_advanceTimersWrapper: (cb) => { - return act(cb); - }, - // For more context about why we need disable act warnings in waitFor: - // https://github.com/reactwg/react-18/discussions/102 - asyncWrapper: async (cb) => { - const previousActEnvironment = getReactActEnvironment(); - setReactActEnvironment(false); - try { - const result = await cb(); - // Drain microtask queue. - // Otherwise we'll restore the previous act() environment, before we resolve the `waitFor` call. - // The caller would have no chance to wrap the in-flight Promises in `act()` - await new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, 0); - - if (jestFakeTimersAreEnabled()) { - // @ts-expect-error global jest - jest.advanceTimersByTime(0); - } - }); - - return result; - } finally { - setReactActEnvironment(previousActEnvironment); - } - }, - eventWrapper: (cb) => { - let result; - act(() => { - result = cb(); - }); - return result; - }, - }); - } catch (e) { - // no-op - // @storybook/test might not be available - } - }, renderToCanvas: async (renderContext, canvasElement) => { if (renderContext.storyContext.testingLibraryRender == null) { - let unmount: () => void; - - await act(async () => { - unmount = await reactProjectAnnotations.renderToCanvas(renderContext, canvasElement); - }); + // eslint-disable-next-line no-underscore-dangle + renderContext.storyContext.parameters.__isPortableStory = true; + const unmount = await reactProjectAnnotations.renderToCanvas(renderContext, canvasElement); return async () => { - await act(() => { - unmount(); - }); + await unmount(); }; } const { @@ -209,19 +155,3 @@ export function composeStories; } - -/** The function is used to configure jest's fake timers in environments where React's act is enabled */ -function jestFakeTimersAreEnabled() { - // @ts-expect-error global jest - if (typeof jest !== 'undefined' && jest !== null) { - return ( - // legacy timers - - // eslint-disable-next-line no-underscore-dangle - (setTimeout as any)._isMockFunction === true || // modern timers - Object.prototype.hasOwnProperty.call(setTimeout, 'clock') - ); - } - - return false; -} diff --git a/code/renderers/react/src/renderToCanvas.tsx b/code/renderers/react/src/renderToCanvas.tsx index 3ae6136f9582..4ae1acbb7fe9 100644 --- a/code/renderers/react/src/renderToCanvas.tsx +++ b/code/renderers/react/src/renderToCanvas.tsx @@ -5,7 +5,7 @@ import type { RenderContext } from 'storybook/internal/types'; import { global } from '@storybook/global'; -import { getReactActEnvironment } from './act-compat'; +import { act } from './act-compat'; import type { ReactRenderer, StoryContext } from './types'; const { FRAMEWORK_OPTIONS } = global; @@ -58,9 +58,10 @@ export async function renderToCanvas( const { renderElement, unmountElement } = await import('@storybook/react-dom-shim'); const Story = unboundStoryFn as FC>; - const isActEnabled = getReactActEnvironment(); + // eslint-disable-next-line no-underscore-dangle + const isPortableStory = storyContext.parameters.__isPortableStory; - const content = isActEnabled ? ( + const content = isPortableStory ? ( ) : ( @@ -80,7 +81,13 @@ export async function renderToCanvas( unmountElement(canvasElement); } - await renderElement(element, canvasElement, storyContext?.parameters?.react?.rootOptions); + await act(async () => { + await renderElement(element, canvasElement, storyContext?.parameters?.react?.rootOptions); + }); - return () => unmountElement(canvasElement); + return async () => { + await act(() => { + unmountElement(canvasElement); + }); + }; } diff --git a/code/renderers/react/template/cli/js/Button.jsx b/code/renderers/react/template/cli/js/Button.jsx index 5b36a6347d07..dabe38e0e82a 100644 --- a/code/renderers/react/template/cli/js/Button.jsx +++ b/code/renderers/react/template/cli/js/Button.jsx @@ -5,7 +5,13 @@ import PropTypes from 'prop-types'; import './button.css'; /** Primary UI component for user interaction */ -export const Button = ({ primary, backgroundColor, size, label, ...props }) => { +export const Button = ({ + primary = false, + backgroundColor = null, + size = 'medium', + label, + ...props +}) => { const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; return ( -export default { - title: 'Addons/Test', +const meta = { + title: 'Addons/Group/Test', component: Component, } as Meta; +export default meta; + +type Story = StoryObj; + const { pass } = instrument({ pass: async () => {}, }, { intercept: true }) -export const ExpectedFailure = { +export const ExpectedFailure: Story = { args: { forceFailure: false, }, @@ -28,34 +32,61 @@ export const ExpectedFailure = { throw new Error('Expected failure'); } } -} satisfies StoryAnnotations; +}; -export const ExpectedSuccess = { +export const ExpectedSuccess: Story = { play: async () => { await pass(); } -} satisfies StoryAnnotations; +}; -export const LongRunning = { +export const LongRunning: Story = { loaders: [async () => new Promise((resolve) => setTimeout(resolve, 800))], -} satisfies StoryAnnotations; +}; // Tests will pass in browser, but fail in CLI -export const MismatchFailure = { +export const MismatchFailure: Story = { play: async () => { await pass(); if(!globalThis.__vitest_browser__) { throw new Error('Expected failure'); } } -} satisfies StoryAnnotations; +}; // Tests will fail in browser, but pass in CLI -export const MismatchSuccess = { +export const MismatchSuccess: Story = { play: async () => { await pass(); if(globalThis.__vitest_browser__) { throw new Error('Unexpected success'); } + }, + tags: ['fail-on-purpose'], +}; + +export const PreviewHeadTest: Story = { + play: async () => { + const styles = window.getComputedStyle(document.body); + // set in preview-head.html + expect(styles.backgroundColor).toBe('rgb(250, 250, 210)'); + // set in main.js#previewHead + expect(styles.borderColor).toBe('rgb(255, 0, 0)'); } -} satisfies StoryAnnotations; \ No newline at end of file +}; + +export const StaticDirTest: Story = { + play: async () => { + const path = '/test-static-dirs/static.js'; + const { staticFunction } = await import(/* @vite-ignore */path); + expect(staticFunction()).toBe(true); + } +} + +export const ViteFinalTest: Story = { + play: async () => { + // @ts-expect-error TS doesn't know about the alias + const { aliasedFunction } = await import('test-alias'); + expect(aliasedFunction()).toBe(true); + } +} diff --git a/test-storybooks/portable-stories-kitchen-sink/react/stories/Button.playwright.tsx b/test-storybooks/portable-stories-kitchen-sink/react/stories/Button.playwright.tsx index db4590c82fab..ee201e587159 100644 --- a/test-storybooks/portable-stories-kitchen-sink/react/stories/Button.playwright.tsx +++ b/test-storybooks/portable-stories-kitchen-sink/react/stories/Button.playwright.tsx @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ + import { createTest } from '@storybook/react/experimental-playwright'; import { test as base, expect } from '@playwright/experimental-ct-react'; import stories, { SingleComposedStory, WithSpanishGlobal } from './Button.stories.playwright'; diff --git a/test-storybooks/portable-stories-kitchen-sink/react/stories/Button.stories.tsx b/test-storybooks/portable-stories-kitchen-sink/react/stories/Button.stories.tsx index 9d2cf8230964..2c976b43cc43 100644 --- a/test-storybooks/portable-stories-kitchen-sink/react/stories/Button.stories.tsx +++ b/test-storybooks/portable-stories-kitchen-sink/react/stories/Button.stories.tsx @@ -67,6 +67,8 @@ export const CSF3Primary: CSF3Story = { size: 'large', primary: true, }, + // Accessibility is failing for the Button + tags: ['fail-on-purpose'], }; export const CSF3Button: CSF3Story = { diff --git a/test-storybooks/portable-stories-kitchen-sink/react/stories/OtherComponent.stories.tsx b/test-storybooks/portable-stories-kitchen-sink/react/stories/OtherComponent.stories.tsx new file mode 100644 index 000000000000..dd410fe9c748 --- /dev/null +++ b/test-storybooks/portable-stories-kitchen-sink/react/stories/OtherComponent.stories.tsx @@ -0,0 +1,22 @@ +import { Meta, type StoryObj } from '@storybook/react' + +const Component = () => + +const meta = { + title: 'Addons/Group/Other', + component: Component, +} as Meta; + +export default meta; + +type Story = StoryObj; + +export const Passes: Story = { +}; + +export const Fails: Story = { + play: async () => { + throw new Error('Expected failure'); + }, + tags: ['fail-on-purpose'], +}; diff --git a/test-storybooks/portable-stories-kitchen-sink/react/vitest.workspace.ts b/test-storybooks/portable-stories-kitchen-sink/react/vitest.workspace.ts index f1657f3acddd..effd954bd780 100644 --- a/test-storybooks/portable-stories-kitchen-sink/react/vitest.workspace.ts +++ b/test-storybooks/portable-stories-kitchen-sink/react/vitest.workspace.ts @@ -5,14 +5,15 @@ export default defineWorkspace([ { extends: "vite.config.ts", plugins: [ - storybookTest(), + storybookTest(process.env.SKIP_FAIL_ON_PURPOSE ? { + tags: { + exclude: ["fail-on-purpose"], + } + } : undefined), ], test: { name: "storybook", pool: "threads", - include: [ - "stories/AddonTest.stories.?(c|m)[jt]s?(x)", - ], deps: { optimizer: { web: { @@ -30,4 +31,4 @@ export default defineWorkspace([ environment: "happy-dom", }, }, -]); \ No newline at end of file +]); diff --git a/test-storybooks/portable-stories-kitchen-sink/react/yarn.lock b/test-storybooks/portable-stories-kitchen-sink/react/yarn.lock index e3484dc01721..b8242119352f 100644 --- a/test-storybooks/portable-stories-kitchen-sink/react/yarn.lock +++ b/test-storybooks/portable-stories-kitchen-sink/react/yarn.lock @@ -12,7 +12,7 @@ __metadata: languageName: node linkType: hard -"@ampproject/remapping@npm:^2.2.0": +"@ampproject/remapping@npm:^2.2.0, @ampproject/remapping@npm:^2.3.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" dependencies: @@ -135,6 +135,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-string-parser@npm:7.25.9" + checksum: 10/c28656c52bd48e8c1d9f3e8e68ecafd09d949c57755b0d353739eb4eae7ba4f7e67e92e4036f1cd43378cc1397a2c943ed7bcaf5949b04ab48607def0258b775 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.25.7": version: 7.25.7 resolution: "@babel/helper-validator-identifier@npm:7.25.7" @@ -142,6 +149,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-validator-identifier@npm:7.25.9" + checksum: 10/3f9b649be0c2fd457fa1957b694b4e69532a668866b8a0d81eabfa34ba16dbf3107b39e0e7144c55c3c652bf773ec816af8df4a61273a2bb4eb3145ca9cf478e + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.25.7": version: 7.25.7 resolution: "@babel/helper-validator-option@npm:7.25.7" @@ -182,6 +196,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.25.4": + version: 7.26.2 + resolution: "@babel/parser@npm:7.26.2" + dependencies: + "@babel/types": "npm:^7.26.0" + bin: + parser: ./bin/babel-parser.js + checksum: 10/8baee43752a3678ad9f9e360ec845065eeee806f1fdc8e0f348a8a0e13eef0959dabed4a197c978896c493ea205c804d0a1187cc52e4a1ba017c7935bab4983d + languageName: node + linkType: hard + "@babel/plugin-syntax-async-generators@npm:^7.8.4": version: 7.8.4 resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" @@ -437,6 +462,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.25.4, @babel/types@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/types@npm:7.26.0" + dependencies: + "@babel/helper-string-parser": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + checksum: 10/40780741ecec886ed9edae234b5eb4976968cc70d72b4e5a40d55f83ff2cc457de20f9b0f4fe9d858350e43dab0ea496e7ef62e2b2f08df699481a76df02cd6e + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -444,12 +479,12 @@ __metadata: languageName: node linkType: hard -"@bundled-es-modules/cookie@npm:^2.0.0": - version: 2.0.0 - resolution: "@bundled-es-modules/cookie@npm:2.0.0" +"@bundled-es-modules/cookie@npm:^2.0.1": + version: 2.0.1 + resolution: "@bundled-es-modules/cookie@npm:2.0.1" dependencies: - cookie: "npm:^0.5.0" - checksum: 10/c8ef02aa5d3f6c786cfa407e1c93b4af29c600eb09990973f47a7a49e4771c1bec37c8f8e567638bb9cbc41f4e38d065ff1d8eaf9bf91f0c3613a6d60bc82c8c + cookie: "npm:^0.7.2" + checksum: 10/0038a5e82c41bfcd722afedabeb6961a5f15747b3681d7f4b61e35eb1e33130039e10ee9250dc9c9e4d3915ce1aeee717c0fb92225111574f0a030411abc0987 languageName: node linkType: hard @@ -911,58 +946,48 @@ __metadata: languageName: node linkType: hard -"@inquirer/confirm@npm:^3.0.0": - version: 3.2.0 - resolution: "@inquirer/confirm@npm:3.2.0" +"@inquirer/confirm@npm:^5.0.0": + version: 5.0.2 + resolution: "@inquirer/confirm@npm:5.0.2" dependencies: - "@inquirer/core": "npm:^9.1.0" - "@inquirer/type": "npm:^1.5.3" - checksum: 10/6b032a26c64075dc14769558720b17f09bc6784a223bbf2c85ec42e491be6ce4c4b83518433c47e05d7e8836ba680ab1b2f6b9c553410d4326582308a1fd2259 + "@inquirer/core": "npm:^10.1.0" + "@inquirer/type": "npm:^3.0.1" + peerDependencies: + "@types/node": ">=18" + checksum: 10/4e775b80b689adeb0b2852ed79b368ef23a82fe3d5f580a562f4af7cdf002a19e0ec1b3b95acc6d49427a72c0fcb5b6548e0cdcafe2f0d3f3d6a923e04aabd0c languageName: node linkType: hard -"@inquirer/core@npm:^9.1.0": - version: 9.2.1 - resolution: "@inquirer/core@npm:9.2.1" +"@inquirer/core@npm:^10.1.0": + version: 10.1.0 + resolution: "@inquirer/core@npm:10.1.0" dependencies: - "@inquirer/figures": "npm:^1.0.6" - "@inquirer/type": "npm:^2.0.0" - "@types/mute-stream": "npm:^0.0.4" - "@types/node": "npm:^22.5.5" - "@types/wrap-ansi": "npm:^3.0.0" + "@inquirer/figures": "npm:^1.0.8" + "@inquirer/type": "npm:^3.0.1" ansi-escapes: "npm:^4.3.2" cli-width: "npm:^4.1.0" - mute-stream: "npm:^1.0.0" + mute-stream: "npm:^2.0.0" signal-exit: "npm:^4.1.0" strip-ansi: "npm:^6.0.1" wrap-ansi: "npm:^6.2.0" yoctocolors-cjs: "npm:^2.1.2" - checksum: 10/bf35e46e70add8ffa9e9d4ae6b528ac660484afca082bca31af95ce8a145a2f8c8d0d07cc7a8627771452e68ade9849c9c9c450a004133ed10ac2d6730900452 + checksum: 10/5d097d0484c1b758f788b792d29395199bdc84af3e8cd4d9273e31de2c5202839b6edf299056956044ba7fb097c4cee7b5c0288e094a380c045082b044f9946e languageName: node linkType: hard -"@inquirer/figures@npm:^1.0.6": - version: 1.0.7 - resolution: "@inquirer/figures@npm:1.0.7" - checksum: 10/ce896860de9d822a7c2a212667bcfd0f04cf2ce86d9a2411cc9c077bb59cd61732cb5f72ac66e88d52912466eec433f005bf8a25efa658f41e1a32f3977080bd - languageName: node - linkType: hard - -"@inquirer/type@npm:^1.5.3": - version: 1.5.5 - resolution: "@inquirer/type@npm:1.5.5" - dependencies: - mute-stream: "npm:^1.0.0" - checksum: 10/bd3f3d7510785af4ad599e042e99e4be6380f52f79f3db140fe6fed0a605acf27b1a0a20fb5cc688eaf7b8aa0c36dacb1d89c7bba4586f38cbf58ba9f159e7b5 +"@inquirer/figures@npm:^1.0.8": + version: 1.0.8 + resolution: "@inquirer/figures@npm:1.0.8" + checksum: 10/0e5e4fbb15e799e818c598fcc3558ef076daf78662149711b046723fd6316381e95f7d5573d6ef0062095ad22c6ac98833033f0948df5c722932107a567fd9c3 languageName: node linkType: hard -"@inquirer/type@npm:^2.0.0": - version: 2.0.0 - resolution: "@inquirer/type@npm:2.0.0" - dependencies: - mute-stream: "npm:^1.0.0" - checksum: 10/e85f359866c28cce06272d2d51cc17788a5c9de9fda7f181c27775dd26821de0dacbc947b521cfe2009cd2965ec54696799035ef3a25a9a5794e47d8e8bdf794 +"@inquirer/type@npm:^3.0.1": + version: 3.0.1 + resolution: "@inquirer/type@npm:3.0.1" + peerDependencies: + "@types/node": ">=18" + checksum: 10/af412f1e7541d43554b02199ae71a2039a1bff5dc51ceefd87de9ece55b199682733b28810fb4b6cb3ed4a159af4cc4a26d4bb29c58dd127e7d9dbda0797d8e7 languageName: node linkType: hard @@ -1287,7 +1312,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": version: 0.3.25 resolution: "@jridgewell/trace-mapping@npm:0.3.25" dependencies: @@ -1309,9 +1334,9 @@ __metadata: languageName: node linkType: hard -"@mswjs/interceptors@npm:^0.35.8": - version: 0.35.9 - resolution: "@mswjs/interceptors@npm:0.35.9" +"@mswjs/interceptors@npm:^0.37.0": + version: 0.37.3 + resolution: "@mswjs/interceptors@npm:0.37.3" dependencies: "@open-draft/deferred-promise": "npm:^2.2.0" "@open-draft/logger": "npm:^0.3.0" @@ -1319,7 +1344,7 @@ __metadata: is-node-process: "npm:^1.2.0" outvariant: "npm:^1.4.3" strict-event-emitter: "npm:^0.5.1" - checksum: 10/9eaf8d7876c9a38c2c9a1259873f8ad27ab41c68a49f7e14a55cd9f596458d9232adb85a5084b044d4eead3be1e7ef5bf54ed6d774d16b02d96caf1e7faa2ab3 + checksum: 10/3d3e2e073feead8702c18dc97e5201785865292b32bd882c4d80461adc3380483b33517c55d7c6c1e53723f5e2ecf50cca0412e6ecd2eb771f4eaabfa2138932 languageName: node linkType: hard @@ -1780,11 +1805,11 @@ __metadata: linkType: soft "@storybook/components@file:../../../code/deprecated/components::locator=portable-stories-react%40workspace%3A.": - version: 8.5.0-alpha.18 - resolution: "@storybook/components@file:../../../code/deprecated/components#../../../code/deprecated/components::hash=aad2fe&locator=portable-stories-react%40workspace%3A." + version: 8.5.0-alpha.20 + resolution: "@storybook/components@file:../../../code/deprecated/components#../../../code/deprecated/components::hash=40954d&locator=portable-stories-react%40workspace%3A." peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/44d4a8a54fd32d94b03dc9f29e2d68ca3c37156b56055b94493f5f626786a02349c195859c48b4f8fa25e209ce12f385305d601135f7e63bdc4541fa0645cae5 + checksum: 10/ff9107e9c84c5d522a0067f77e217a9963c4bdde27032fdd38efae906f67df7b65488969a9cee89715d49fa89213fe04a72af825ac118e72e835f4be648409ff languageName: node linkType: hard @@ -1840,15 +1865,14 @@ __metadata: linkType: hard "@storybook/experimental-addon-test@file:../../../code/addons/test::locator=portable-stories-react%40workspace%3A.": - version: 8.5.0-alpha.18 - resolution: "@storybook/experimental-addon-test@file:../../../code/addons/test#../../../code/addons/test::hash=21369f&locator=portable-stories-react%40workspace%3A." + version: 8.5.0-alpha.20 + resolution: "@storybook/experimental-addon-test@file:../../../code/addons/test#../../../code/addons/test::hash=1aa098&locator=portable-stories-react%40workspace%3A." dependencies: "@storybook/csf": "npm:0.1.12" "@storybook/global": "npm:^5.0.0" "@storybook/icons": "npm:^1.2.12" "@storybook/instrumenter": "workspace:*" "@storybook/test": "workspace:*" - "@storybook/theming": "workspace:*" polished: "npm:^4.2.2" prompts: "npm:^2.4.0" ts-dedent: "npm:^2.2.0" @@ -1864,7 +1888,7 @@ __metadata: optional: true vitest: optional: true - checksum: 10/2081814e214dc1dd31144870a6a4ea7637c9c241ab02044488be57e19402c206c0037d449197f77bb4262147703f6d0b27f09c9f6cc2ee358c97fd7d1cdfa908 + checksum: 10/42188bb3814cae7c04df03c75994f8df971cc2ce71bda9e536211b96468bbdf2b8babc78526fdec1b2ddbd4d703118474f23c31f2bddffb6fa8947dc35ef0f19 languageName: node linkType: hard @@ -1897,20 +1921,20 @@ __metadata: linkType: soft "@storybook/manager-api@file:../../../code/deprecated/manager-api::locator=portable-stories-react%40workspace%3A.": - version: 8.5.0-alpha.18 - resolution: "@storybook/manager-api@file:../../../code/deprecated/manager-api#../../../code/deprecated/manager-api::hash=c1892e&locator=portable-stories-react%40workspace%3A." + version: 8.5.0-alpha.20 + resolution: "@storybook/manager-api@file:../../../code/deprecated/manager-api#../../../code/deprecated/manager-api::hash=92b213&locator=portable-stories-react%40workspace%3A." peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/405745f48728bfa4d8340fe2403bca0d60f803ed346c12d20a63ab9472ba1d8d1021aa12ee0fd36a6b41aec42d8c2a657acffeb9c8663aa32edcda490882e7f5 + checksum: 10/c63e624ab2d90f2992ecb2a650d9e54ace0f7f699d425bc68f616f870651a2b5f3c6c7d66be84aaab9200a9f01686376a7532f905f0dbd4dddb5542afe855584 languageName: node linkType: hard "@storybook/preview-api@file:../../../code/deprecated/preview-api::locator=portable-stories-react%40workspace%3A.": - version: 8.5.0-alpha.18 - resolution: "@storybook/preview-api@file:../../../code/deprecated/preview-api#../../../code/deprecated/preview-api::hash=0085a8&locator=portable-stories-react%40workspace%3A." + version: 8.5.0-alpha.20 + resolution: "@storybook/preview-api@file:../../../code/deprecated/preview-api#../../../code/deprecated/preview-api::hash=b70449&locator=portable-stories-react%40workspace%3A." peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/29c5f7134b4300bac03c42f57c3138e24315516b23515893cfb4a144786327fb6e0a90d5470d951eb22e055466769fc9ad4577658afc7da43b60870c0d06e767 + checksum: 10/3251de34aca72eb6c82b7aa8fde6e877e0974294d97eb60f63819b890b399cd7703a4dfde151180cf81918b36b3523bdbafe0731d232bda7d77c9f3b8fc5b75b languageName: node linkType: hard @@ -1991,11 +2015,11 @@ __metadata: linkType: soft "@storybook/theming@file:../../../code/deprecated/theming::locator=portable-stories-react%40workspace%3A.": - version: 8.5.0-alpha.18 - resolution: "@storybook/theming@file:../../../code/deprecated/theming#../../../code/deprecated/theming::hash=dd5360&locator=portable-stories-react%40workspace%3A." + version: 8.5.0-alpha.20 + resolution: "@storybook/theming@file:../../../code/deprecated/theming#../../../code/deprecated/theming::hash=db0fbd&locator=portable-stories-react%40workspace%3A." peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/8f8ecbd709ff4a8e0bafa19642497aad0d6ee40f2e613793a4248cb3825765783dd40eb57d180cb6cc4ad1f8acbd4e4a1a1b3448245b89e28581e25289c88c86 + checksum: 10/de77ed744ce172b1007cf94b4b5ff1cf9cf8492d4ddb9ac1ca61d76b18ed73f76e911c1518c1b66ba37c8d2d4e0f63f2eb3bddc64cd0beb7d64f952a45f9b8b7 languageName: node linkType: hard @@ -2356,16 +2380,7 @@ __metadata: languageName: node linkType: hard -"@types/mute-stream@npm:^0.0.4": - version: 0.0.4 - resolution: "@types/mute-stream@npm:0.0.4" - dependencies: - "@types/node": "npm:*" - checksum: 10/af8d83ad7b68ea05d9357985daf81b6c9b73af4feacb2f5c2693c7fd3e13e5135ef1bd083ce8d5bdc8e97acd28563b61bb32dec4e4508a8067fcd31b8a098632 - languageName: node - linkType: hard - -"@types/node@npm:*, @types/node@npm:^22.5.5": +"@types/node@npm:*": version: 22.7.7 resolution: "@types/node@npm:22.7.7" dependencies: @@ -2456,13 +2471,6 @@ __metadata: languageName: node linkType: hard -"@types/wrap-ansi@npm:^3.0.0": - version: 3.0.0 - resolution: "@types/wrap-ansi@npm:3.0.0" - checksum: 10/8aa644946ca4e859668c36b8e2bcf2ac4bdee59dac760414730ea57be8a93ae9166ebd40a088f2ab714843aaea2a2a67f0e6e6ec11cfc9c8701b2466ca1c4089 - languageName: node - linkType: hard - "@types/yargs-parser@npm:*": version: 21.0.3 resolution: "@types/yargs-parser@npm:21.0.3" @@ -2697,21 +2705,21 @@ __metadata: linkType: hard "@vitest/browser@npm:^2.1.3": - version: 2.1.3 - resolution: "@vitest/browser@npm:2.1.3" + version: 2.1.8 + resolution: "@vitest/browser@npm:2.1.8" dependencies: "@testing-library/dom": "npm:^10.4.0" "@testing-library/user-event": "npm:^14.5.2" - "@vitest/mocker": "npm:2.1.3" - "@vitest/utils": "npm:2.1.3" - magic-string: "npm:^0.30.11" - msw: "npm:^2.3.5" - sirv: "npm:^2.0.4" + "@vitest/mocker": "npm:2.1.8" + "@vitest/utils": "npm:2.1.8" + magic-string: "npm:^0.30.12" + msw: "npm:^2.6.4" + sirv: "npm:^3.0.0" tinyrainbow: "npm:^1.2.0" ws: "npm:^8.18.0" peerDependencies: playwright: "*" - vitest: 2.1.3 + vitest: 2.1.8 webdriverio: "*" peerDependenciesMeta: playwright: @@ -2720,7 +2728,33 @@ __metadata: optional: true webdriverio: optional: true - checksum: 10/e639496fa529140fb9e7dce97890c5b75fffbfb41881bee5ef25b194832d3cadcb77490d9b54777bfa968b993f6878649fe4961d6ef312ca1222b9a2fc8d4f12 + checksum: 10/6063e02222440347bbc23b2c54e259078aa83a29869337b9ffd642be5a4321ac3ddf3c0bbe4eac5237eb0bb8b9fa17d21d2c31299376de407716e3c7dd3b704c + languageName: node + linkType: hard + +"@vitest/coverage-v8@npm:^2.1.3": + version: 2.1.8 + resolution: "@vitest/coverage-v8@npm:2.1.8" + dependencies: + "@ampproject/remapping": "npm:^2.3.0" + "@bcoe/v8-coverage": "npm:^0.2.3" + debug: "npm:^4.3.7" + istanbul-lib-coverage: "npm:^3.2.2" + istanbul-lib-report: "npm:^3.0.1" + istanbul-lib-source-maps: "npm:^5.0.6" + istanbul-reports: "npm:^3.1.7" + magic-string: "npm:^0.30.12" + magicast: "npm:^0.3.5" + std-env: "npm:^3.8.0" + test-exclude: "npm:^7.0.1" + tinyrainbow: "npm:^1.2.0" + peerDependencies: + "@vitest/browser": 2.1.8 + vitest: 2.1.8 + peerDependenciesMeta: + "@vitest/browser": + optional: true + checksum: 10/2e1e7fe2a20c1eec738f6d84d890bed4aa5138094943dd1229962c2c42428a1a517c8a4ad4fb52637d7494f044440e061e9bc5982a83df95223db185d5a28f4d languageName: node linkType: hard @@ -2736,35 +2770,34 @@ __metadata: languageName: node linkType: hard -"@vitest/expect@npm:2.1.3": - version: 2.1.3 - resolution: "@vitest/expect@npm:2.1.3" +"@vitest/expect@npm:2.1.8": + version: 2.1.8 + resolution: "@vitest/expect@npm:2.1.8" dependencies: - "@vitest/spy": "npm:2.1.3" - "@vitest/utils": "npm:2.1.3" - chai: "npm:^5.1.1" + "@vitest/spy": "npm:2.1.8" + "@vitest/utils": "npm:2.1.8" + chai: "npm:^5.1.2" tinyrainbow: "npm:^1.2.0" - checksum: 10/94e61e01f14cfcd9ced0e7ac1bbdeee55ff4bf68f09d8f244fd7d73f97b106f35d10cba3fe7a0132464c312206f2eee9e83b16a8d761101b61da053890062858 + checksum: 10/3594149dd67dfac884a90f8b6a35687cdddd2f5f764562819bf7b66ae2eacfd4aa5e8914155deb4082fbe5a3792dced2fd7e59a948ffafe67acba4d2229dfe5f languageName: node linkType: hard -"@vitest/mocker@npm:2.1.3": - version: 2.1.3 - resolution: "@vitest/mocker@npm:2.1.3" +"@vitest/mocker@npm:2.1.8": + version: 2.1.8 + resolution: "@vitest/mocker@npm:2.1.8" dependencies: - "@vitest/spy": "npm:2.1.3" + "@vitest/spy": "npm:2.1.8" estree-walker: "npm:^3.0.3" - magic-string: "npm:^0.30.11" + magic-string: "npm:^0.30.12" peerDependencies: - "@vitest/spy": 2.1.3 - msw: ^2.3.5 + msw: ^2.4.9 vite: ^5.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - checksum: 10/84be8830d6e965109730257d7a84b3d7594db0998ae55decdbfc304857c1c7d29b49f1f5b23f2addcbce1bd7e8bb33832407737a9bb3f95cb3bf7bb312db4d9d + checksum: 10/f04060f42102caa4cca72059e63c1ecae8b8e091aaa61a2d4a914b129fc711ada4ad117eb0184e49e363757784ed1117fdbf9f4a81a45fe575fd92769740a970 languageName: node linkType: hard @@ -2777,33 +2810,33 @@ __metadata: languageName: node linkType: hard -"@vitest/pretty-format@npm:2.1.3, @vitest/pretty-format@npm:^2.1.3": - version: 2.1.3 - resolution: "@vitest/pretty-format@npm:2.1.3" +"@vitest/pretty-format@npm:2.1.8, @vitest/pretty-format@npm:^2.1.8": + version: 2.1.8 + resolution: "@vitest/pretty-format@npm:2.1.8" dependencies: tinyrainbow: "npm:^1.2.0" - checksum: 10/d9382ee93f0f32e2ef8fe03bda818e5277f052a50ddb05b6a6cf0864b2ccb228484f12f130c05faf62dc2140292ffafc213f2941b0fa24058b3ee2943daa286c + checksum: 10/f0f60c007424194887ad398d202867d58d850154de327993925041e2972357544eea95a22e0bb3a62a470b006ff8de5f691d2078708dcd7f625e24f8a06b26e7 languageName: node linkType: hard -"@vitest/runner@npm:2.1.3": - version: 2.1.3 - resolution: "@vitest/runner@npm:2.1.3" +"@vitest/runner@npm:2.1.8": + version: 2.1.8 + resolution: "@vitest/runner@npm:2.1.8" dependencies: - "@vitest/utils": "npm:2.1.3" + "@vitest/utils": "npm:2.1.8" pathe: "npm:^1.1.2" - checksum: 10/cdf9b82d388c1cc148753f4a8632dfcadf9c4a1c0e065fdcd485d5af824af62507fd7eab9efb21244009775c05773ccb59547043af522a5ab6d216433321066e + checksum: 10/27f265a3ab1e20297b948b06232bfa4dc9fda44d1f9bb6206baa9e6fa643b71143ebfd2d1771570296b7ee74a12d684e529a830f545ad61235cefb454e94a8e9 languageName: node linkType: hard -"@vitest/snapshot@npm:2.1.3": - version: 2.1.3 - resolution: "@vitest/snapshot@npm:2.1.3" +"@vitest/snapshot@npm:2.1.8": + version: 2.1.8 + resolution: "@vitest/snapshot@npm:2.1.8" dependencies: - "@vitest/pretty-format": "npm:2.1.3" - magic-string: "npm:^0.30.11" + "@vitest/pretty-format": "npm:2.1.8" + magic-string: "npm:^0.30.12" pathe: "npm:^1.1.2" - checksum: 10/2c0c4ad8abb758f2f76d1d6094f8928360437e09d0a59e0c6a85a544c892cc41a5324ebbc5657a66c8a3793e51cbf58e357c7f71e899f4e5c5eb76e8c9745abf + checksum: 10/71edf4f574d317579c605ed0a7ecab7ee96fddcebc777bd130774a770ddc692c538f9f5b3dfde89af83ecb36f7338fe880943c83cede58f55e3556768a1a0749 languageName: node linkType: hard @@ -2816,12 +2849,29 @@ __metadata: languageName: node linkType: hard -"@vitest/spy@npm:2.1.3": - version: 2.1.3 - resolution: "@vitest/spy@npm:2.1.3" +"@vitest/spy@npm:2.1.8": + version: 2.1.8 + resolution: "@vitest/spy@npm:2.1.8" dependencies: - tinyspy: "npm:^3.0.0" - checksum: 10/94d6f1bc34da5d0c973d9382c133b938e555fcf2d238edf0aaad3de1a98dd57ebf7c104ba229c6beec48122d2e6f55386d8d2cf96a5804dc95ac683a54754cc7 + tinyspy: "npm:^3.0.2" + checksum: 10/9a1cb9cf6b23c122681469b5890d91ca26fc8d74953b3d46d293a5d2a4944490106891f6a178cd732ab7a8abbda339f43681c81d1594565ecc3bf3e7f9b7735f + languageName: node + linkType: hard + +"@vitest/ui@npm:^2.1.3": + version: 2.1.8 + resolution: "@vitest/ui@npm:2.1.8" + dependencies: + "@vitest/utils": "npm:2.1.8" + fflate: "npm:^0.8.2" + flatted: "npm:^3.3.1" + pathe: "npm:^1.1.2" + sirv: "npm:^3.0.0" + tinyglobby: "npm:^0.2.10" + tinyrainbow: "npm:^1.2.0" + peerDependencies: + vitest: 2.1.8 + checksum: 10/7ff0532b3b0e3f93c037cad3528b8fde8a93188f3222b92faae42e0fdd996e9284b362c4e5e9d725ce0a019870d3b0b7ad80c1874f74b43ffc5a1d703803fdd8 languageName: node linkType: hard @@ -2837,14 +2887,14 @@ __metadata: languageName: node linkType: hard -"@vitest/utils@npm:2.1.3, @vitest/utils@npm:^2.1.1": - version: 2.1.3 - resolution: "@vitest/utils@npm:2.1.3" +"@vitest/utils@npm:2.1.8, @vitest/utils@npm:^2.1.1": + version: 2.1.8 + resolution: "@vitest/utils@npm:2.1.8" dependencies: - "@vitest/pretty-format": "npm:2.1.3" - loupe: "npm:^3.1.1" + "@vitest/pretty-format": "npm:2.1.8" + loupe: "npm:^3.1.2" tinyrainbow: "npm:^1.2.0" - checksum: 10/f064e6634cb84c925a17d8937df7441d150c3e24fa5bbd6304151d11dab6cdeb0cb3d5a95a9aacb8b416c87fb0d9aa8c6b9cc5e174191784231e8345948d6d18 + checksum: 10/be1f4254347199fb5c1d9de8e4537dad4af3f434c033e7cd023165bd4b7e9de16fa0f86664256ab331120585df95ed6be8eea58b209b510651b49f6482051733 languageName: node linkType: hard @@ -3455,16 +3505,16 @@ __metadata: languageName: node linkType: hard -"chai@npm:^5.1.1": - version: 5.1.1 - resolution: "chai@npm:5.1.1" +"chai@npm:^5.1.1, chai@npm:^5.1.2": + version: 5.1.2 + resolution: "chai@npm:5.1.2" dependencies: assertion-error: "npm:^2.0.1" check-error: "npm:^2.1.1" deep-eql: "npm:^5.0.1" loupe: "npm:^3.1.0" pathval: "npm:^2.0.0" - checksum: 10/ee67279a5613bd36dc1dc13660042429ae2f1dc5a9030a6abcf381345866dfb5bce7bc10b9d74c8de86b6f656489f654bbbef3f3361e06925591e6a00c72afff + checksum: 10/e8c2bbc83cb5a2f87130d93056d4cfbbe04106e12aa798b504816dbe3fa538a9f68541b472e56cbf0f54558b501d7e31867d74b8218abcd5a8cc8ba536fba46c languageName: node linkType: hard @@ -3695,10 +3745,10 @@ __metadata: languageName: node linkType: hard -"cookie@npm:^0.5.0": - version: 0.5.0 - resolution: "cookie@npm:0.5.0" - checksum: 10/aae7911ddc5f444a9025fbd979ad1b5d60191011339bce48e555cb83343d0f98b865ff5c4d71fecdfb8555a5cafdc65632f6fce172f32aaf6936830a883a0380 +"cookie@npm:^0.7.2": + version: 0.7.2 + resolution: "cookie@npm:0.7.2" + checksum: 10/24b286c556420d4ba4e9bc09120c9d3db7d28ace2bd0f8ccee82422ce42322f73c8312441271e5eefafbead725980e5996cc02766dbb89a90ac7f5636ede608f languageName: node linkType: hard @@ -3853,7 +3903,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.6": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.7": version: 4.3.7 resolution: "debug@npm:4.3.7" dependencies: @@ -4174,6 +4224,13 @@ __metadata: languageName: node linkType: hard +"es-module-lexer@npm:^1.5.4": + version: 1.5.4 + resolution: "es-module-lexer@npm:1.5.4" + checksum: 10/f29c7c97a58eb17640dcbd71bd6ef754ad4f58f95c3073894573d29dae2cad43ecd2060d97ed5b866dfb7804d5590fb7de1d2c5339a5fceae8bd60b580387fc5 + languageName: node + linkType: hard + "esbuild-register@npm:^3.5.0": version: 3.6.0 resolution: "esbuild-register@npm:3.6.0" @@ -4634,6 +4691,13 @@ __metadata: languageName: node linkType: hard +"expect-type@npm:^1.1.0": + version: 1.1.0 + resolution: "expect-type@npm:1.1.0" + checksum: 10/05fca80ddc7d493a89361f783c6b000750fa04a8226bc24701f3b90adb0efc2fb467f2a0baaed4015a02d8b9034ef5bb87521df9dba980f50b1105bd596ef833 + languageName: node + linkType: hard + "expect@npm:^29.7.0": version: 29.7.0 resolution: "expect@npm:29.7.0" @@ -4753,6 +4817,25 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.4.2": + version: 6.4.2 + resolution: "fdir@npm:6.4.2" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10/5ff80d1d2034e75cc68be175401c9f64c4938a6b2c1e9a0c27f2d211ffbe491fd86d29e4576825d9da8aff9bd465f0283427c2dddc11653457906c46d3bbc448 + languageName: node + linkType: hard + +"fflate@npm:^0.8.2": + version: 0.8.2 + resolution: "fflate@npm:0.8.2" + checksum: 10/2bd26ba6d235d428de793c6a0cd1aaa96a06269ebd4e21b46c8fd1bd136abc631acf27e188d47c3936db090bf3e1ede11d15ce9eae9bffdc4bfe1b9dc66ca9cb + languageName: node + linkType: hard + "figures@npm:^3.2.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -4818,6 +4901,13 @@ __metadata: languageName: node linkType: hard +"flatted@npm:^3.3.1": + version: 3.3.2 + resolution: "flatted@npm:3.3.2" + checksum: 10/ac3c159742e01d0e860a861164bcfd35bb567ccbebb8a0dd041e61cf3c64a435b917dd1e7ed1c380c2ebca85735fb16644485ec33665bc6aafc3b316aa1eed44 + languageName: node + linkType: hard + "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -5030,7 +5120,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2, glob@npm:^10.3.10": +"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.4.1": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -5726,7 +5816,7 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0, istanbul-lib-coverage@npm:^3.2.2": version: 3.2.2 resolution: "istanbul-lib-coverage@npm:3.2.2" checksum: 10/40bbdd1e937dfd8c830fa286d0f665e81b7a78bdabcd4565f6d5667c99828bda3db7fb7ac6b96a3e2e8a2461ddbc5452d9f8bc7d00cb00075fa6a3e99f5b6a81 @@ -5759,7 +5849,7 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-report@npm:^3.0.0": +"istanbul-lib-report@npm:^3.0.0, istanbul-lib-report@npm:^3.0.1": version: 3.0.1 resolution: "istanbul-lib-report@npm:3.0.1" dependencies: @@ -5781,7 +5871,18 @@ __metadata: languageName: node linkType: hard -"istanbul-reports@npm:^3.1.3": +"istanbul-lib-source-maps@npm:^5.0.6": + version: 5.0.6 + resolution: "istanbul-lib-source-maps@npm:5.0.6" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.23" + debug: "npm:^4.1.1" + istanbul-lib-coverage: "npm:^3.0.0" + checksum: 10/569dd0a392ee3464b1fe1accbaef5cc26de3479eacb5b91d8c67ebb7b425d39fd02247d85649c3a0e9c29b600809fa60b5af5a281a75a89c01f385b1e24823a2 + languageName: node + linkType: hard + +"istanbul-reports@npm:^3.1.3, istanbul-reports@npm:^3.1.7": version: 3.1.7 resolution: "istanbul-reports@npm:3.1.7" dependencies: @@ -6593,7 +6694,7 @@ __metadata: languageName: node linkType: hard -"loupe@npm:^3.1.0, loupe@npm:^3.1.1": +"loupe@npm:^3.1.0, loupe@npm:^3.1.1, loupe@npm:^3.1.2": version: 3.1.2 resolution: "loupe@npm:3.1.2" checksum: 10/8f5734e53fb64cd914aa7d986e01b6d4c2e3c6c56dcbd5428d71c2703f0ab46b5ab9f9eeaaf2b485e8a1c43f865bdd16ec08ae1a661c8f55acdbd9f4d59c607a @@ -6634,7 +6735,7 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.0, magic-string@npm:^0.30.11": +"magic-string@npm:^0.30.0": version: 0.30.12 resolution: "magic-string@npm:0.30.12" dependencies: @@ -6643,6 +6744,26 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.12": + version: 0.30.14 + resolution: "magic-string@npm:0.30.14" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.0" + checksum: 10/8ca0f8937c2824e48ebc70e7e065a193c467713639cc6e5972aaba0fa5417b375a6f62c383410a19a66e618c386bb7253fbd3ccbfb0144bb310f0ba772121f12 + languageName: node + linkType: hard + +"magicast@npm:^0.3.5": + version: 0.3.5 + resolution: "magicast@npm:0.3.5" + dependencies: + "@babel/parser": "npm:^7.25.4" + "@babel/types": "npm:^7.25.4" + source-map-js: "npm:^1.2.0" + checksum: 10/3a2dba6b0bdde957797361d09c7931ebdc1b30231705360eeb40ed458d28e1c3112841c3ed4e1b87ceb28f741e333c7673cd961193aa9fdb4f4946b202e6205a + languageName: node + linkType: hard + "make-dir@npm:^4.0.0": version: 4.0.0 resolution: "make-dir@npm:4.0.0" @@ -6892,15 +7013,16 @@ __metadata: languageName: node linkType: hard -"msw@npm:^2.3.5": - version: 2.4.11 - resolution: "msw@npm:2.4.11" +"msw@npm:^2.6.4": + version: 2.6.6 + resolution: "msw@npm:2.6.6" dependencies: - "@bundled-es-modules/cookie": "npm:^2.0.0" + "@bundled-es-modules/cookie": "npm:^2.0.1" "@bundled-es-modules/statuses": "npm:^1.0.1" "@bundled-es-modules/tough-cookie": "npm:^0.1.6" - "@inquirer/confirm": "npm:^3.0.0" - "@mswjs/interceptors": "npm:^0.35.8" + "@inquirer/confirm": "npm:^5.0.0" + "@mswjs/interceptors": "npm:^0.37.0" + "@open-draft/deferred-promise": "npm:^2.2.0" "@open-draft/until": "npm:^2.1.0" "@types/cookie": "npm:^0.6.0" "@types/statuses": "npm:^2.0.4" @@ -6920,14 +7042,14 @@ __metadata: optional: true bin: msw: cli/index.js - checksum: 10/d073ede4bfc7f1f41f7a0cb05b3d20d9befc1658e53faacd3f217a7cb78e3e748a3ee8e937e2a4d93fd09f16b35cba00d71df767736dd567ac15fd8e01aa7d6e + checksum: 10/7762ba5f1570789328af27167e03c2b8eb4981faa476ae47d74c125c90ddc1792bc28b9ce1100bbc4e105b55e3e7d65e7cae8d27fa7677b6516e42a63c38b7a3 languageName: node linkType: hard -"mute-stream@npm:^1.0.0": - version: 1.0.0 - resolution: "mute-stream@npm:1.0.0" - checksum: 10/36fc968b0e9c9c63029d4f9dc63911950a3bdf55c9a87f58d3a266289b67180201cade911e7699f8b2fa596b34c9db43dad37649e3f7fdd13c3bb9edb0017ee7 +"mute-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "mute-stream@npm:2.0.0" + checksum: 10/d2e4fd2f5aa342b89b98134a8d899d8ef9b0a6d69274c4af9df46faa2d97aeb1f2ce83d867880d6de63643c52386579b99139801e24e7526c3b9b0a6d1e18d6c languageName: node linkType: hard @@ -7298,6 +7420,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^4.0.2": + version: 4.0.2 + resolution: "picomatch@npm:4.0.2" + checksum: 10/ce617b8da36797d09c0baacb96ca8a44460452c89362d7cb8f70ca46b4158ba8bc3606912de7c818eb4a939f7f9015cef3c766ec8a0c6bfc725fdc078e39c717 + languageName: node + linkType: hard + "pify@npm:^2.2.0": version: 2.3.0 resolution: "pify@npm:2.3.0" @@ -7380,6 +7509,8 @@ __metadata: "@typescript-eslint/parser": "npm:^6.21.0" "@vitejs/plugin-react": "npm:^4.2.1" "@vitest/browser": "npm:^2.1.3" + "@vitest/coverage-v8": "npm:^2.1.3" + "@vitest/ui": "npm:^2.1.3" cypress: "npm:^13.6.4" eslint: "npm:^8.56.0" eslint-plugin-react-hooks: "npm:^4.6.0" @@ -7991,14 +8122,14 @@ __metadata: languageName: node linkType: hard -"sirv@npm:^2.0.4": - version: 2.0.4 - resolution: "sirv@npm:2.0.4" +"sirv@npm:^3.0.0": + version: 3.0.0 + resolution: "sirv@npm:3.0.0" dependencies: "@polka/url": "npm:^1.0.0-next.24" mrmime: "npm:^2.0.0" totalist: "npm:^3.0.0" - checksum: 10/24f42cf06895017e589c9d16fc3f1c6c07fe8b0dbafce8a8b46322cfba67b7f2498610183954cb0e9d089c8cb60002a7ee7e8bca6a91a0d7042bfbc3473c95c3 + checksum: 10/94dbd5df7cf4965f7c5941767117cbf9709e1d25de1d619a114c3f77fc63c124b5a5255717af2a0de637bb83d0b0defd0822d01420764b56432b53281b1d675d languageName: node linkType: hard @@ -8066,7 +8197,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.2.1": +"source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" checksum: 10/ff9d8c8bf096d534a5b7707e0382ef827b4dd360a577d3f34d2b9f48e12c9d230b5747974ee7c607f0df65113732711bb701fe9ece3c7edbd43cb2294d707df3 @@ -8157,10 +8288,10 @@ __metadata: languageName: node linkType: hard -"std-env@npm:^3.7.0": - version: 3.7.0 - resolution: "std-env@npm:3.7.0" - checksum: 10/6ee0cca1add3fd84656b0002cfbc5bfa20340389d9ba4720569840f1caa34bce74322aef4c93f046391583e50649d0cf81a5f8fe1d411e50b659571690a45f12 +"std-env@npm:^3.8.0": + version: 3.8.0 + resolution: "std-env@npm:3.8.0" + checksum: 10/034176196cfcaaab16dbdd96fc9e925a9544799fb6dc5a3e36fe43270f3a287c7f779d785b89edaf22cef2b5f1dcada2aae67430b8602e785ee74bdb3f671768 languageName: node linkType: hard @@ -8359,6 +8490,17 @@ __metadata: languageName: node linkType: hard +"test-exclude@npm:^7.0.1": + version: 7.0.1 + resolution: "test-exclude@npm:7.0.1" + dependencies: + "@istanbuljs/schema": "npm:^0.1.2" + glob: "npm:^10.4.1" + minimatch: "npm:^9.0.4" + checksum: 10/e6f6f4e1df2e7810e082e8d7dfc53be51a931e6e87925f5e1c2ef92cc1165246ba3bf2dae6b5d86251c16925683dba906bd41e40169ebc77120a2d1b5a0dbbe0 + languageName: node + linkType: hard + "text-table@npm:^0.2.0": version: 0.2.0 resolution: "text-table@npm:0.2.0" @@ -8394,17 +8536,27 @@ __metadata: languageName: node linkType: hard -"tinyexec@npm:^0.3.0": +"tinyexec@npm:^0.3.1": version: 0.3.1 resolution: "tinyexec@npm:0.3.1" checksum: 10/0537c70590d52d354f40c0255ff0f654a3d18ddb3812b440ddf9d436edf516c8057838ad5a38744c0c59670ec03e3cf23fbe04ae3d49f031d948274e99002569 languageName: node linkType: hard -"tinypool@npm:^1.0.0": - version: 1.0.1 - resolution: "tinypool@npm:1.0.1" - checksum: 10/eaceb93784b8e27e60c0e3e2c7d11c29e1e79b2a025b2c232215db73b90fe22bd4753ad53fc8e801c2b5a63b94a823af549555d8361272bc98271de7dd4a9925 +"tinyglobby@npm:^0.2.10": + version: 0.2.10 + resolution: "tinyglobby@npm:0.2.10" + dependencies: + fdir: "npm:^6.4.2" + picomatch: "npm:^4.0.2" + checksum: 10/10c976866d849702edc47fc3fef27d63f074c40f75ef17171ecc1452967900699fa1e62373681dd58e673ddff2e3f6094bcd0a2101e3e4b30f4c2b9da41397f2 + languageName: node + linkType: hard + +"tinypool@npm:^1.0.1": + version: 1.0.2 + resolution: "tinypool@npm:1.0.2" + checksum: 10/6109322f14b3763f65c8fa49fddab72cd3edd96b82dd50e05e63de74867329ff5353bff4377281ec963213d9314f37f4a353e9ee34bbac85fd4c1e4a568d6076 languageName: node linkType: hard @@ -8415,7 +8567,7 @@ __metadata: languageName: node linkType: hard -"tinyspy@npm:^3.0.0": +"tinyspy@npm:^3.0.0, tinyspy@npm:^3.0.2": version: 3.0.2 resolution: "tinyspy@npm:3.0.2" checksum: 10/5db671b2ff5cd309de650c8c4761ca945459d7204afb1776db9a04fb4efa28a75f08517a8620c01ee32a577748802231ad92f7d5b194dc003ee7f987a2a06337 @@ -8759,17 +8911,18 @@ __metadata: languageName: node linkType: hard -"vite-node@npm:2.1.3": - version: 2.1.3 - resolution: "vite-node@npm:2.1.3" +"vite-node@npm:2.1.8": + version: 2.1.8 + resolution: "vite-node@npm:2.1.8" dependencies: cac: "npm:^6.7.14" - debug: "npm:^4.3.6" + debug: "npm:^4.3.7" + es-module-lexer: "npm:^1.5.4" pathe: "npm:^1.1.2" vite: "npm:^5.0.0" bin: vite-node: vite-node.mjs - checksum: 10/8ba6b145cbb02a492c7bb1f0490d02383000462f234ed61d24f650547163825c16f14e6908ee1eb661403bd0a7a3fb3cdbedf116cc015b1e5cdf7bb992872a01 + checksum: 10/0ff0ed7a6fb234d3ddc4946e4c1150229980cac9f34fb4bd7f443aab0aae2da5b73ac20ff68af1df476545807dc23189247194e8cea0dcdfa394311c73f04429 languageName: node linkType: hard @@ -8833,33 +8986,34 @@ __metadata: linkType: hard "vitest@npm:^2.1.3": - version: 2.1.3 - resolution: "vitest@npm:2.1.3" - dependencies: - "@vitest/expect": "npm:2.1.3" - "@vitest/mocker": "npm:2.1.3" - "@vitest/pretty-format": "npm:^2.1.3" - "@vitest/runner": "npm:2.1.3" - "@vitest/snapshot": "npm:2.1.3" - "@vitest/spy": "npm:2.1.3" - "@vitest/utils": "npm:2.1.3" - chai: "npm:^5.1.1" - debug: "npm:^4.3.6" - magic-string: "npm:^0.30.11" + version: 2.1.8 + resolution: "vitest@npm:2.1.8" + dependencies: + "@vitest/expect": "npm:2.1.8" + "@vitest/mocker": "npm:2.1.8" + "@vitest/pretty-format": "npm:^2.1.8" + "@vitest/runner": "npm:2.1.8" + "@vitest/snapshot": "npm:2.1.8" + "@vitest/spy": "npm:2.1.8" + "@vitest/utils": "npm:2.1.8" + chai: "npm:^5.1.2" + debug: "npm:^4.3.7" + expect-type: "npm:^1.1.0" + magic-string: "npm:^0.30.12" pathe: "npm:^1.1.2" - std-env: "npm:^3.7.0" + std-env: "npm:^3.8.0" tinybench: "npm:^2.9.0" - tinyexec: "npm:^0.3.0" - tinypool: "npm:^1.0.0" + tinyexec: "npm:^0.3.1" + tinypool: "npm:^1.0.1" tinyrainbow: "npm:^1.2.0" vite: "npm:^5.0.0" - vite-node: "npm:2.1.3" + vite-node: "npm:2.1.8" why-is-node-running: "npm:^2.3.0" peerDependencies: "@edge-runtime/vm": "*" "@types/node": ^18.0.0 || >=20.0.0 - "@vitest/browser": 2.1.3 - "@vitest/ui": 2.1.3 + "@vitest/browser": 2.1.8 + "@vitest/ui": 2.1.8 happy-dom: "*" jsdom: "*" peerDependenciesMeta: @@ -8877,7 +9031,7 @@ __metadata: optional: true bin: vitest: vitest.mjs - checksum: 10/f6079a88583045b551e6526c08774aeac4a9cf85b132793a03f9470c013326abd7fce3985e3c2217dc0dac2fadeee3506e3dc51e215f10862b2fe9da9289af0f + checksum: 10/c2552c068f6faac82eb4e6debb9ed505c0e8016fd6e0a0f0e0dbb5b5417922fbcde80c54af0d3b5a5503a5d6ad6862b6e95b9b59b8b7e98bb553217b9c6fc227 languageName: node linkType: hard