diff --git a/.circleci/config.yml b/.circleci/config.yml index 5e7cc4a22d24..617cdac42c28 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -609,22 +609,22 @@ workflows: requires: - build - create-sandboxes: - parallelism: 35 + parallelism: 34 requires: - build # - smoke-test-sandboxes: # disabled for now # requires: # - create-sandboxes - build-sandboxes: - parallelism: 35 + parallelism: 34 requires: - create-sandboxes - chromatic-sandboxes: - parallelism: 32 + parallelism: 31 requires: - build-sandboxes - e2e-production: - parallelism: 32 + parallelism: 31 requires: - build-sandboxes - e2e-dev: @@ -632,7 +632,7 @@ workflows: requires: - create-sandboxes - test-runner-production: - parallelism: 32 + parallelism: 31 requires: - build-sandboxes # TODO: reenable once we find out the source of flakyness diff --git a/.github/workflows/canary-release-pr.yml b/.github/workflows/canary-release-pr.yml index 4b1504cfd1bf..827f24a2c5f8 100644 --- a/.github/workflows/canary-release-pr.yml +++ b/.github/workflows/canary-release-pr.yml @@ -58,8 +58,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: '16' - + node-version-file: '.nvmrc' - name: Cache dependencies uses: actions/cache@v3 with: diff --git a/.github/workflows/danger-js.yml b/.github/workflows/danger-js.yml index f83519953b4b..eddb5dee1fe7 100644 --- a/.github/workflows/danger-js.yml +++ b/.github/workflows/danger-js.yml @@ -21,10 +21,10 @@ jobs: name: Danger JS runs-on: ubuntu-latest steps: + - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: '16' - - uses: actions/checkout@v3 + node-version-file: '.nvmrc' - name: Danger JS uses: danger/danger-js@11.2.6 env: diff --git a/.github/workflows/prepare-patch-release.yml b/.github/workflows/prepare-patch-release.yml index c88022c7ea01..e4f8e38df502 100644 --- a/.github/workflows/prepare-patch-release.yml +++ b/.github/workflows/prepare-patch-release.yml @@ -33,7 +33,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: '16' + node-version-file: '.nvmrc' - name: Cache dependencies uses: actions/cache@v3 diff --git a/.github/workflows/prepare-prerelease.yml b/.github/workflows/prepare-prerelease.yml index 1250aedcfaa3..e68a7e1ef63a 100644 --- a/.github/workflows/prepare-prerelease.yml +++ b/.github/workflows/prepare-prerelease.yml @@ -54,7 +54,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: '16' + node-version-file: '.nvmrc' - name: Cache dependencies uses: actions/cache@v3 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 18e23c174162..863b4e9ae7e2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -45,7 +45,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: '16' + node-version-file: '.nvmrc' - name: Cache dependencies uses: actions/cache@v3 diff --git a/.gitignore b/.gitignore index 29c025aadf26..63f9445af854 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,6 @@ coverage/ /**/LICENSE code/docs/public package-lock.json -.nvmrc storybook-static .jest-test-results.json *.jar diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000000..59ea99ee63cb --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +16.20 diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index cd2a36896cbb..b6d8e6878186 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,19 @@ +## 7.5.0-alpha.2 + +- Angular: Categorize legacy build options error - [#24014](https://github.com/storybookjs/storybook/pull/24014), thanks [@yannbf](https://github.com/yannbf)! +- Builder-Webpack5: Categorize builder error - [#24031](https://github.com/storybookjs/storybook/pull/24031), thanks [@yannbf](https://github.com/yannbf)! +- CI: Inform the user how to dedupe and strip color from info command - [#24087](https://github.com/storybookjs/storybook/pull/24087), thanks [@kasperpeulen](https://github.com/kasperpeulen)! +- CLI: Fix packageManager handling in `sb add` - [#24079](https://github.com/storybookjs/storybook/pull/24079), thanks [@Integrayshaun](https://github.com/Integrayshaun)! +- CLI: Improve sanitization logic in crash reports - [#24028](https://github.com/storybookjs/storybook/pull/24028), thanks [@yannbf](https://github.com/yannbf)! +- Maintenance: Add more context to explanation in core-events errors - [#24063](https://github.com/storybookjs/storybook/pull/24063), thanks [@yannbf](https://github.com/yannbf)! +- Monorepo: Fix `svelte-vite` detection - [#24085](https://github.com/storybookjs/storybook/pull/24085), thanks [@legnaleurc](https://github.com/legnaleurc)! +- NextJS: Fix Image Context reuse (ensure singleton by externalizing it) - [#23881](https://github.com/storybookjs/storybook/pull/23881), thanks [@martinnabhan](https://github.com/martinnabhan)! +- Source-loader: Fix property key validation - [#24068](https://github.com/storybookjs/storybook/pull/24068), thanks [@MrZillaGold](https://github.com/MrZillaGold)! +- Svelte: Fix generated properties on Svelte event handler - [#24020](https://github.com/storybookjs/storybook/pull/24020), thanks [@j3rem1e](https://github.com/j3rem1e)! +- Telemetry: Add platform info to telemetry event - [#24081](https://github.com/storybookjs/storybook/pull/24081), thanks [@yannbf](https://github.com/yannbf)! +- UI: Fix target id in searchfield label - [#23464](https://github.com/storybookjs/storybook/pull/23464), thanks [@plumpNation](https://github.com/plumpNation)! +- Vue3: Remove console.log in sourceDecorator - [#24062](https://github.com/storybookjs/storybook/pull/24062), thanks [@oruman](https://github.com/oruman)! + ## 7.5.0-alpha.1 - Core: Add CJS entrypoints to errors in core events - [#24038](https://github.com/storybookjs/storybook/pull/24038), thanks [@yannbf](https://github.com/yannbf)! diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc26a6bff211..78993afd8386 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,13 +1,24 @@ # Getting started -Storybook is developed against a specific node version. We recommend using [Volta](https://volta.sh/) as it will automatically install the correct node and yarn version when you first use the repo. If you chose not to use Volta please ensure you you have node version 16 installed (suggestion: v16.5) +Storybook is developed against a specific node version which is defined in an `.nvmrc` file. You can use any Node version manager that uses the `.nvmrc` configuration file (we recommend [fnm](https://fnm.vercel.app/)). + +## Using fnm as a Node version manager + +- Install fnm [as per instructions](https://github.com/Schniz/fnm/tree/master#installation) +- In your shell setup include the `use-on-cd`, `corepack-enabled` and `version-file-strategy recursive` parameters in the `fnm env` command, e.g. + + ```sh + eval "$(fnm env --use-on-cd --corepack-enabled --version-file-strategy recursive)" + ``` + +## Running the local development environment - Ensure if you are using Windows to use the Windows Subsystem for Linux (WSL). - Run `yarn start` in the root directory to run a basic test Storybook "sandbox". The `yarn start` script will generate a React Vite TypeScript sandbox with a set of test stories inside it, as well as taking all steps required to get it running (building the various packages we need etc). There is no need to run `yarn` or `yarn install` as `yarn start` will do this for you. -## Issues +### Issues If you run `yarn start` and encounter the following error, try rerunning `yarn start` a second time: diff --git a/MIGRATION.md b/MIGRATION.md index 5022eefb59bd..d7c47af924b3 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1044,7 +1044,11 @@ Starting in 7.0, we drop support for Angular < 14 #### Angular: Drop support for calling Storybook directly -In Storybook 6.4 we have deprecated calling Storybook directly (`npm run storybook`) for Angular. In Storybook 7.0, we've removed it entirely. Instead you have to set up the Storybook builder in your `angular.json` and execute `ng run :storybook` to start Storybook. Please visit https://github.com/storybookjs/storybook/tree/next/code/frameworks/angular to set up Storybook for Angular correctly. +_Has automigration_ + +In Storybook 6.4 we deprecated calling Storybook directly (e.g. `npm run storybook`) for Angular. In Storybook 7.0, we've removed it entirely. Instead, you have to set up the Storybook builder in your `angular.json` and execute `ng run :storybook` to start Storybook. + +You can run `npx storybook@next automigrate` to automatically fix your configuration, or visit https://github.com/storybookjs/storybook/tree/next/code/frameworks/angular/README.md#how-do-i-migrate-to-an-angular-storybook-builder for instructions on how to set up Storybook for Angular manually. #### Angular: Application providers and ModuleWithProviders diff --git a/code/addons/themes/postinstall.js b/code/addons/themes/postinstall.js index c84a4e88e4b4..01a9b3151e89 100644 --- a/code/addons/themes/postinstall.js +++ b/code/addons/themes/postinstall.js @@ -2,7 +2,7 @@ const { spawn } = require('child_process'); const PACKAGE_MANAGER_TO_COMMAND = { npm: 'npx', - yarn1: 'yarn dlx', + yarn1: 'npx', yarn2: 'yarn dlx', pnpm: 'pnpm dlx', }; diff --git a/code/builders/builder-webpack5/src/index.ts b/code/builders/builder-webpack5/src/index.ts index 0106cfea091d..582c3156c71b 100644 --- a/code/builders/builder-webpack5/src/index.ts +++ b/code/builders/builder-webpack5/src/index.ts @@ -9,6 +9,11 @@ import { dirname, join, parse } from 'path'; import express from 'express'; import fs from 'fs-extra'; import { PREVIEW_BUILDER_PROGRESS } from '@storybook/core-events'; +import { + WebpackCompilationError, + WebpackInvocationError, + WebpackMissingStatsError, +} from '@storybook/core-events/server-errors'; import prettyTime from 'pretty-hrtime'; @@ -117,21 +122,19 @@ const starter: StarterFunction = async function* starterGeneratorFn({ yield; const config = await getConfig(options); + + if (config.stats === 'none' || config.stats === 'summary') { + throw new WebpackMissingStatsError(); + } yield; + const compiler = webpackInstance(config); if (!compiler) { - const err = `${config.name}: missing webpack compiler at runtime!`; - logger.error(err); - return { - bail, - totalTime: process.hrtime(startTime), - stats: { - hasErrors: () => true, - hasWarnings: () => false, - toJson: () => ({ warnings: [] as any[], errors: [err] }), - } as any as Stats, - }; + throw new WebpackInvocationError({ + // eslint-disable-next-line local-rules/no-uncategorized-errors + error: new Error(`Missing Webpack compiler at runtime!`), + }); } yield; @@ -172,6 +175,7 @@ const starter: StarterFunction = async function* starterGeneratorFn({ const middlewareOptions: Parameters[1] = { publicPath: config.output?.publicPath as string, writeToDisk: true, + stats: 'errors-only', }; compilation = webpackDevMiddleware(compiler, middlewareOptions); @@ -184,19 +188,24 @@ const starter: StarterFunction = async function* starterGeneratorFn({ router.use(compilation); router.use(webpackHotMiddleware(compiler, { log: false })); - const stats = await new Promise((ready, stop) => { - compilation?.waitUntilValid(ready as any); - reject = stop; + const stats = await new Promise((res, rej) => { + compilation?.waitUntilValid(res as any); + reject = rej; }); yield; if (!stats) { - throw new Error('no stats after building preview'); + throw new WebpackMissingStatsError(); } - if (stats.hasErrors()) { - // eslint-disable-next-line @typescript-eslint/no-throw-literal - throw stats; + const { warnings, errors } = getWebpackStats({ config, stats }); + + if (warnings.length > 0) { + warnings?.forEach((e) => logger.error(e.message)); + } + + if (errors.length > 0) { + throw new WebpackCompilationError({ errors }); } return { @@ -206,6 +215,22 @@ const starter: StarterFunction = async function* starterGeneratorFn({ }; }; +function getWebpackStats({ config, stats }: { config: Configuration; stats: Stats }) { + const statsOptions = + typeof config.stats === 'string' + ? config.stats + : { + ...(config.stats as StatsOptions), + warnings: true, + errors: true, + }; + const { warnings = [], errors = [] } = stats?.toJson(statsOptions) || {}; + return { + warnings, + errors, + }; +} + /** * This function is a generator so that we can abort it mid process * in case of failure coming from other processes e.g. manager builder @@ -215,73 +240,47 @@ const starter: StarterFunction = async function* starterGeneratorFn({ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime, options }) { const webpackInstance = await executor.get(options); yield; - logger.info('=> Compiling preview..'); const config = await getConfig(options); + + if (config.stats === 'none' || config.stats === 'summary') { + throw new WebpackMissingStatsError(); + } yield; const compiler = webpackInstance(config); if (!compiler) { - const err = `${config.name}: missing webpack compiler at runtime!`; - logger.error(err); - return { - hasErrors: () => true, - hasWarnings: () => false, - toJson: () => ({ warnings: [] as any[], errors: [err] }), - } as any as Stats; + throw new WebpackInvocationError({ + // eslint-disable-next-line local-rules/no-uncategorized-errors + error: new Error(`Missing Webpack compiler at runtime!`), + }); } const webpackCompilation = new Promise((succeed, fail) => { compiler.run((error, stats) => { - if (error || !stats || stats.hasErrors()) { - logger.error('=> Failed to build the preview'); - process.exitCode = 1; - - if (error) { - logger.error(error.message); + if (error) { + compiler.close(() => fail(new WebpackInvocationError({ error }))); + return; + } - compiler.close(() => fail(error)); + if (!stats) { + throw new WebpackMissingStatsError(); + } - return; - } + const { warnings, errors } = getWebpackStats({ config, stats }); - if (stats && (stats.hasErrors() || stats.hasWarnings())) { - const { warnings = [], errors = [] } = stats.toJson( - typeof config.stats === 'string' - ? config.stats - : { - warnings: true, - errors: true, - ...(config.stats as StatsOptions), - } - ); - - errors.forEach((e) => logger.error(e.message)); - warnings.forEach((e) => logger.error(e.message)); - - compiler.close(() => - options.debugWebpack - ? fail(stats) - : fail(new Error('=> Webpack failed, learn more with --debug-webpack')) - ); - - return; - } + if (warnings.length > 0) { + warnings?.forEach((e) => logger.error(e.message)); } - logger.trace({ message: '=> Preview built', time: process.hrtime(startTime) }); - if (stats && stats.hasWarnings()) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know it has warnings because of hasWarnings() - stats - .toJson({ warnings: true } as StatsOptions) - .warnings!.forEach((e) => logger.warn(e.message)); + if (errors.length > 0) { + compiler.close(() => fail(new WebpackCompilationError({ errors }))); + return; } - // https://webpack.js.org/api/node/#run - // #15227 compiler.close((closeErr) => { if (closeErr) { - return fail(closeErr); + return fail(new WebpackInvocationError({ error: closeErr })); } return succeed(stats as Stats); diff --git a/code/frameworks/angular/README.md b/code/frameworks/angular/README.md index 3158060f3f9a..d97e1ab93f2a 100644 --- a/code/frameworks/angular/README.md +++ b/code/frameworks/angular/README.md @@ -10,7 +10,7 @@ - [moduleMetadata decorator](#modulemetadata-decorator) - [applicationConfig decorator](#applicationconfig-decorator) - [FAQ](#faq) - - [How do I migrate to a Angular Storybook builder?](#how-do-i-migrate-to-a-angular-storybook-builder) + - [How do I migrate to an Angular Storybook builder?](#how-do-i-migrate-to-an-angular-storybook-builder) - [Do you have only one Angular project in your workspace?](#do-you-have-only-one-angular-project-in-your-workspace) - [Adjust your `package.json`](#adjust-your-packagejson) - [I have multiple projects in my Angular workspace](#i-have-multiple-projects-in-my-angular-workspace) @@ -252,10 +252,12 @@ export const WithCustomApplicationProvider: Story = { ## FAQ -### How do I migrate to a Angular Storybook builder? +### How do I migrate to an Angular Storybook builder? The Storybook [Angular builder](https://angular.io/guide/glossary#builder) is a new way to run Storybook in an Angular workspace. It is a drop-in replacement for running `storybook dev` and `storybook build` directly. +You can run `npx storybook@next automigrate` to try let Storybook detect and automatically fix your configuration. Otherwise, you can follow the next steps to manually adjust your configuration. + #### Do you have only one Angular project in your workspace? In this case go to your `angular.json` and add `storybook` and `build-storybook` entries in `architect` section of your project like shown above. diff --git a/code/frameworks/angular/src/server/framework-preset-angular-cli.ts b/code/frameworks/angular/src/server/framework-preset-angular-cli.ts index f8f78996c366..059d8b30f4d1 100644 --- a/code/frameworks/angular/src/server/framework-preset-angular-cli.ts +++ b/code/frameworks/angular/src/server/framework-preset-angular-cli.ts @@ -1,10 +1,10 @@ import webpack from 'webpack'; import { logger } from '@storybook/node-logger'; +import { AngularLegacyBuildOptionsError } from '@storybook/core-events/server-errors'; import { BuilderContext, targetFromTargetString } from '@angular-devkit/architect'; import { sync as findUpSync } from 'find-up'; -import { dedent } from 'ts-dedent'; - import { JsonObject, logging } from '@angular-devkit/core'; + import { getWebpackConfig as getCustomWebpackConfig } from './angular-cli-webpack'; import { moduleIsAvailable } from './utils/module-is-available'; import { PresetOptions } from './preset-options'; @@ -85,13 +85,6 @@ async function getBuilderOptions( return builderOptions; } -export const migrationToBuilderReferrenceMessage = dedent`Your Storybook startup uses a solution that is not supported. - You must use angular builder to have an explicit configuration on the project used in angular.json - Read more at: - - https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#sb-angular-builder) - - https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#angular13) - `; - /** * Checks if using legacy configuration that doesn't use builder and logs message referring to migration docs. */ @@ -101,7 +94,5 @@ function checkForLegacyBuildOptions(options: PresetOptions) { return; } - logger.error(migrationToBuilderReferrenceMessage); - - throw Error('angularBrowserTarget is undefined.'); + throw new AngularLegacyBuildOptionsError(); } diff --git a/code/frameworks/nextjs/README.md b/code/frameworks/nextjs/README.md index 4d9ea8542470..756c93732e23 100644 --- a/code/frameworks/nextjs/README.md +++ b/code/frameworks/nextjs/README.md @@ -295,7 +295,7 @@ export const Example = { parameters: { nextjs: { router: { - path: '/profile/[id]', + pathname: '/profile/[id]', asPath: '/profile/1', query: { id: '1', @@ -316,7 +316,7 @@ Global defaults can be set in [preview.js](https://storybook.js.org/docs/react/c export const parameters = { nextjs: { router: { - path: '/some-default-path', + pathname: '/some-default-path', asPath: '/some-default-path', query: {}, }, diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json index c7898a217097..82220e09a260 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -27,6 +27,16 @@ "require": "./dist/index.js", "import": "./dist/index.mjs" }, + "./image-context": { + "types": "./dist/image-context.d.ts", + "require": "./dist/image-context.js", + "import": "./dist/image-context.mjs" + }, + "./dist/image-context": { + "types": "./dist/image-context.d.ts", + "require": "./dist/image-context.js", + "import": "./dist/image-context.mjs" + }, "./preset": { "types": "./dist/preset.d.ts", "require": "./dist/preset.js" @@ -46,6 +56,16 @@ "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/index.d.ts" + ], + "dist/image-context": [ + "dist/image-context.d.ts" + ] + } + }, "files": [ "dist/**/*", "template/cli/**/*", @@ -136,11 +156,12 @@ }, "bundler": { "entries": [ + "./src/image-context.ts", "./src/index.ts", "./src/preset.ts", "./src/preview.tsx", "./src/next-image-loader-stub.ts", - "./src/images/context.ts", + "./src/images/decorator.tsx", "./src/images/next-future-image.tsx", "./src/images/next-legacy-image.tsx", "./src/images/next-image.tsx", diff --git a/code/frameworks/nextjs/src/images/context.ts b/code/frameworks/nextjs/src/image-context.ts similarity index 100% rename from code/frameworks/nextjs/src/images/context.ts rename to code/frameworks/nextjs/src/image-context.ts diff --git a/code/frameworks/nextjs/src/images/decorator.tsx b/code/frameworks/nextjs/src/images/decorator.tsx index f0917b3a3b50..342f49d32b9a 100644 --- a/code/frameworks/nextjs/src/images/decorator.tsx +++ b/code/frameworks/nextjs/src/images/decorator.tsx @@ -1,6 +1,13 @@ import * as React from 'react'; import type { Addon_StoryContext } from '@storybook/types'; -import { ImageContext } from './context'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore-error (this only errors during compilation for production) +// eslint-disable-next-line import/no-extraneous-dependencies +import { ImageContext as ImageContextValue } from '@storybook/nextjs/dist/image-context'; +import { type ImageContext as ImageContextType } from '../image-context'; + +const ImageContext = ImageContextValue as typeof ImageContextType; export const ImageDecorator = ( Story: React.FC, diff --git a/code/frameworks/nextjs/src/images/next-future-image.tsx b/code/frameworks/nextjs/src/images/next-future-image.tsx index 559d2e857d39..306518079b38 100644 --- a/code/frameworks/nextjs/src/images/next-future-image.tsx +++ b/code/frameworks/nextjs/src/images/next-future-image.tsx @@ -3,18 +3,21 @@ import type * as _NextImage from 'next/image'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import is aliased in webpack config import OriginalNextFutureImage from 'sb-original/next/future/image'; -import { ImageContext } from './context'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore-error (this only errors during compilation for production) +// eslint-disable-next-line import/no-extraneous-dependencies +import { ImageContext as ImageContextValue } from '@storybook/nextjs/dist/image-context'; +import { type ImageContext as ImageContextType } from '../image-context'; import { defaultLoader } from './next-image-default-loader'; -function NextFutureImage(props: _NextImage.ImageProps) { +const ImageContext = ImageContextValue as typeof ImageContextType; + +function NextFutureImage({ loader, ...props }: _NextImage.ImageProps) { const imageParameters = React.useContext(ImageContext); return ( - + ); } diff --git a/code/frameworks/nextjs/src/images/next-image.tsx b/code/frameworks/nextjs/src/images/next-image.tsx index 74e252d93a7d..8fc964785f6b 100644 --- a/code/frameworks/nextjs/src/images/next-image.tsx +++ b/code/frameworks/nextjs/src/images/next-image.tsx @@ -3,15 +3,20 @@ import OriginalNextImage from 'sb-original/next/image'; import type * as _NextImage from 'next/image'; import React from 'react'; -import { ImageContext } from './context'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore-error (this only errors during compilation for production) +// eslint-disable-next-line import/no-extraneous-dependencies +import { ImageContext as ImageContextValue } from '@storybook/nextjs/dist/image-context'; +import { type ImageContext as ImageContextType } from '../image-context'; import { defaultLoader } from './next-image-default-loader'; -const MockedNextImage = (props: _NextImage.ImageProps) => { +const ImageContext = ImageContextValue as typeof ImageContextType; + +const MockedNextImage = ({ loader, ...props }: _NextImage.ImageProps) => { const imageParameters = React.useContext(ImageContext); - return ( - - ); + return ; }; export default MockedNextImage; diff --git a/code/frameworks/nextjs/src/images/next-legacy-image.tsx b/code/frameworks/nextjs/src/images/next-legacy-image.tsx index 8d899cc341a5..33dfc0e0068a 100644 --- a/code/frameworks/nextjs/src/images/next-legacy-image.tsx +++ b/code/frameworks/nextjs/src/images/next-legacy-image.tsx @@ -3,18 +3,21 @@ import OriginalNextLegacyImage from 'sb-original/next/legacy/image'; import type * as _NextLegacyImage from 'next/legacy/image'; import React from 'react'; -import { ImageContext } from './context'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore-error (this only errors during compilation for production) +// eslint-disable-next-line import/no-extraneous-dependencies +import { ImageContext as ImageContextValue } from '@storybook/nextjs/dist/image-context'; +import { type ImageContext as ImageContextType } from '../image-context'; import { defaultLoader } from './next-image-default-loader'; -function NextLegacyImage(props: _NextLegacyImage.ImageProps) { +const ImageContext = ImageContextValue as typeof ImageContextType; + +function NextLegacyImage({ loader, ...props }: _NextLegacyImage.ImageProps) { const imageParameters = React.useContext(ImageContext); return ( - + ); } diff --git a/code/frameworks/svelte-vite/src/utils.ts b/code/frameworks/svelte-vite/src/utils.ts index 87f2129aeb9f..514dada509a3 100644 --- a/code/frameworks/svelte-vite/src/utils.ts +++ b/code/frameworks/svelte-vite/src/utils.ts @@ -23,7 +23,7 @@ export async function handleSvelteKit(plugins: PluginOption[], options: Options) 'vite-plugin-sveltekit-compile', ]); - if (hasSvelteKitPlugins && framework !== '@storybook/sveltekit') { + if (hasSvelteKitPlugins && !framework.includes('@storybook/sveltekit')) { throw new Error(dedent` We've detected a SvelteKit project using the @storybook/svelte-vite framework, which is not supported in Storybook 7.0 Please use the @storybook/sveltekit framework instead. diff --git a/code/lib/cli/src/add.ts b/code/lib/cli/src/add.ts index a163e1f1e34f..71437b314b1c 100644 --- a/code/lib/cli/src/add.ts +++ b/code/lib/cli/src/add.ts @@ -91,6 +91,6 @@ export async function add( await writeConfig(main); if (!options.skipPostinstall && isStorybookAddon) { - await postinstallAddon(addonName, { packageManager: pkgMgr }); + await postinstallAddon(addonName, { packageManager: packageManager.type }); } } diff --git a/code/lib/cli/src/automigrate/fixes/angular-builders.ts b/code/lib/cli/src/automigrate/fixes/angular-builders.ts index 626723f091ff..3bdc9da5062c 100644 --- a/code/lib/cli/src/automigrate/fixes/angular-builders.ts +++ b/code/lib/cli/src/automigrate/fixes/angular-builders.ts @@ -59,14 +59,14 @@ export const angularBuilders: Fix = { prompt() { return dedent` - We have detected that your project does not use the Storybook Angular builder yet. In Storybook 6.4 we have deprecated calling Storybook directly (npm run storybook) for Angular. In Storybook 7.0, we've removed it entirely. + We have detected that your project does not use the Storybook Angular builder yet. In Storybook 6.4 we deprecated calling Storybook directly (npm run storybook) for Angular. In Storybook 7.0, we've removed it entirely. In order to use the Storybook Angular builder, we need to add a few entries to your angular.json file. Additionally, we will add the @compodoc/compodoc package to your devDependencies if you want and we will add a few scripts to your package.json file. Also feel free to remove the Compodoc script from your package.json file if you don't use it apart from Storybook anymore. Storybook uses Compodoc internally and you don't have to call in separately anymore. Read more about the Angular builder here: ${chalk.yellow( - 'https://storybook.js.org/docs/angular/configure/storybook-builders' + 'https://github.com/storybookjs/storybook/tree/next/code/frameworks/angular#how-do-i-migrate-to-an-angular-storybook-builder' )} `; }, diff --git a/code/lib/cli/src/automigrate/helpers/getMigrationSummary.test.ts b/code/lib/cli/src/automigrate/helpers/getMigrationSummary.test.ts index 2b95a6658217..f5569ae2f49d 100644 --- a/code/lib/cli/src/automigrate/helpers/getMigrationSummary.test.ts +++ b/code/lib/cli/src/automigrate/helpers/getMigrationSummary.test.ts @@ -32,6 +32,7 @@ describe('getMigrationSummary', () => { }, dependencies: {}, infoCommand: 'yarn why', + dedupeCommand: 'yarn dedupe', }; const logFile = '/path/to/log/file'; @@ -146,7 +147,9 @@ describe('getMigrationSummary', () => { @storybook/addon-essentials: 7.0.0, 7.1.0 - You can find more information for a given dependency by running yarn why " + You can find more information for a given dependency by running yarn why + + Please try de-duplicating these dependencies by running yarn dedupe" `); }); diff --git a/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts b/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts index 9e9d60df35c7..6a16e2ae3291 100644 --- a/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts +++ b/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts @@ -184,6 +184,11 @@ function getWarnings(installationMetadata: InstallationMetadata) { `${installationMetadata.infoCommand} ` )}` ); + messages.push( + `Please try de-duplicating these dependencies by running ${chalk.cyan( + `${installationMetadata.dedupeCommand}` + )}` + ); return messages; } diff --git a/code/lib/cli/src/generators/ANGULAR/helpers.ts b/code/lib/cli/src/generators/ANGULAR/helpers.ts index d1774e583017..b2394797909c 100644 --- a/code/lib/cli/src/generators/ANGULAR/helpers.ts +++ b/code/lib/cli/src/generators/ANGULAR/helpers.ts @@ -1,6 +1,8 @@ import fs from 'fs'; +import { join } from 'path'; import prompts from 'prompts'; import dedent from 'ts-dedent'; +import { MissingAngularJsonError } from '@storybook/core-events/server-errors'; export const ANGULAR_JSON_PATH = 'angular.json'; @@ -27,9 +29,7 @@ export class AngularJSON { constructor() { if (!fs.existsSync(ANGULAR_JSON_PATH)) { - throw new Error( - 'An angular.json file was not found in the current working directory. Storybook needs it to work properly, so please rerun the command at the root of your project, where the angular.json file is located. More info: https://storybook.js.org/docs/angular/faq#error-no-angularjson-file-found' - ); + throw new MissingAngularJsonError({ path: join(process.cwd(), ANGULAR_JSON_PATH) }); } const jsonContent = fs.readFileSync(ANGULAR_JSON_PATH, 'utf8'); diff --git a/code/lib/cli/src/js-package-manager/NPMProxy.test.ts b/code/lib/cli/src/js-package-manager/NPMProxy.test.ts index 667f173f99e8..60ba9e3e9a4a 100644 --- a/code/lib/cli/src/js-package-manager/NPMProxy.test.ts +++ b/code/lib/cli/src/js-package-manager/NPMProxy.test.ts @@ -379,6 +379,7 @@ describe('NPM Proxy', () => { expect(installations).toMatchInlineSnapshot(` Object { + "dedupeCommand": "npm dedupe", "dependencies": Object { "@storybook/addon-interactions": Array [ Object { diff --git a/code/lib/cli/src/js-package-manager/NPMProxy.ts b/code/lib/cli/src/js-package-manager/NPMProxy.ts index 519030d34a59..1613bac9bb5d 100644 --- a/code/lib/cli/src/js-package-manager/NPMProxy.ts +++ b/code/lib/cli/src/js-package-manager/NPMProxy.ts @@ -142,6 +142,9 @@ export class NPMProxy extends JsPackageManager { args: ['ls', '--json', '--depth=99', pipeToNull], // ignore errors, because npm ls will exit with code 1 if there are e.g. unmet peer dependencies ignoreError: true, + env: { + FORCE_COLOR: 'false', + }, }); try { @@ -272,6 +275,7 @@ export class NPMProxy extends JsPackageManager { dependencies: acc, duplicatedDependencies, infoCommand: 'npm ls --depth=1', + dedupeCommand: 'npm dedupe', }; } diff --git a/code/lib/cli/src/js-package-manager/PNPMProxy.test.ts b/code/lib/cli/src/js-package-manager/PNPMProxy.test.ts index e59bd7354bcc..94ba1d1e7ed7 100644 --- a/code/lib/cli/src/js-package-manager/PNPMProxy.test.ts +++ b/code/lib/cli/src/js-package-manager/PNPMProxy.test.ts @@ -316,6 +316,7 @@ describe('PNPM Proxy', () => { expect(installations).toMatchInlineSnapshot(` Object { + "dedupeCommand": "pnpm dedupe", "dependencies": Object { "@storybook/addon-interactions": Array [ Object { diff --git a/code/lib/cli/src/js-package-manager/PNPMProxy.ts b/code/lib/cli/src/js-package-manager/PNPMProxy.ts index d435ec397a05..57fb2ae9b075 100644 --- a/code/lib/cli/src/js-package-manager/PNPMProxy.ts +++ b/code/lib/cli/src/js-package-manager/PNPMProxy.ts @@ -101,6 +101,9 @@ export class PNPMProxy extends JsPackageManager { const commandResult = await this.executeCommand({ command: 'pnpm', args: ['list', pattern.map((p) => `"${p}"`).join(' '), '--json', '--depth=99'], + env: { + FORCE_COLOR: 'false', + }, }); try { @@ -287,6 +290,7 @@ export class PNPMProxy extends JsPackageManager { dependencies: acc, duplicatedDependencies, infoCommand: 'pnpm list --depth=1', + dedupeCommand: 'pnpm dedupe', }; } diff --git a/code/lib/cli/src/js-package-manager/Yarn1Proxy.test.ts b/code/lib/cli/src/js-package-manager/Yarn1Proxy.test.ts index 3edf536f2a96..799fe8fb55cd 100644 --- a/code/lib/cli/src/js-package-manager/Yarn1Proxy.test.ts +++ b/code/lib/cli/src/js-package-manager/Yarn1Proxy.test.ts @@ -241,6 +241,7 @@ describe('Yarn 1 Proxy', () => { expect(installations).toMatchInlineSnapshot(` Object { + "dedupeCommand": "yarn dedupe", "dependencies": Object { "@storybook/addon-interactions": Array [ Object { diff --git a/code/lib/cli/src/js-package-manager/Yarn1Proxy.ts b/code/lib/cli/src/js-package-manager/Yarn1Proxy.ts index 3793b7f54528..8d24e3676f4b 100644 --- a/code/lib/cli/src/js-package-manager/Yarn1Proxy.ts +++ b/code/lib/cli/src/js-package-manager/Yarn1Proxy.ts @@ -91,6 +91,9 @@ export class Yarn1Proxy extends JsPackageManager { const commandResult = await this.executeCommand({ command: 'yarn', args: ['list', '--pattern', pattern.map((p) => `"${p}"`).join(' '), '--recursive', '--json'], + env: { + FORCE_COLOR: 'false', + }, }); try { @@ -215,6 +218,7 @@ export class Yarn1Proxy extends JsPackageManager { dependencies: acc, duplicatedDependencies, infoCommand: 'yarn why', + dedupeCommand: 'yarn dedupe', }; } diff --git a/code/lib/cli/src/js-package-manager/Yarn2Proxy.test.ts b/code/lib/cli/src/js-package-manager/Yarn2Proxy.test.ts index 51e5c1235cd4..0e1bb1276a30 100644 --- a/code/lib/cli/src/js-package-manager/Yarn2Proxy.test.ts +++ b/code/lib/cli/src/js-package-manager/Yarn2Proxy.test.ts @@ -219,6 +219,7 @@ describe('Yarn 2 Proxy', () => { expect(installations).toMatchInlineSnapshot(` Object { + "dedupeCommand": "yarn dedupe", "dependencies": Object { "@storybook/global": Array [ Object { diff --git a/code/lib/cli/src/js-package-manager/Yarn2Proxy.ts b/code/lib/cli/src/js-package-manager/Yarn2Proxy.ts index a4d4484f554e..ebf7928d8aea 100644 --- a/code/lib/cli/src/js-package-manager/Yarn2Proxy.ts +++ b/code/lib/cli/src/js-package-manager/Yarn2Proxy.ts @@ -114,6 +114,9 @@ export class Yarn2Proxy extends JsPackageManager { pattern.map((p) => `"${p}"`).join(' '), `"${pattern}"`, ], + env: { + FORCE_COLOR: 'false', + }, }); try { @@ -286,6 +289,7 @@ export class Yarn2Proxy extends JsPackageManager { dependencies: acc, duplicatedDependencies, infoCommand: 'yarn why', + dedupeCommand: 'yarn dedupe', }; } diff --git a/code/lib/cli/src/js-package-manager/types.ts b/code/lib/cli/src/js-package-manager/types.ts index 165540de7a9d..ae8ad155669a 100644 --- a/code/lib/cli/src/js-package-manager/types.ts +++ b/code/lib/cli/src/js-package-manager/types.ts @@ -3,4 +3,5 @@ export type InstallationMetadata = { dependencies: Record; duplicatedDependencies: Record; infoCommand: string; + dedupeCommand: string; }; diff --git a/code/lib/cli/src/sandbox-templates.ts b/code/lib/cli/src/sandbox-templates.ts index 9d06a8054953..b91296c5025f 100644 --- a/code/lib/cli/src/sandbox-templates.ts +++ b/code/lib/cli/src/sandbox-templates.ts @@ -296,6 +296,8 @@ const baseTemplates = { builder: '@storybook/builder-webpack5', }, skipTasks: ['e2e-tests-dev', 'bench'], + // TODO: Can be enabled once we re-revert this PR: https://github.com/storybookjs/storybook/pull/24033 + inDevelopment: true, }, 'angular-cli/default-ts': { name: 'Angular CLI Latest (Webpack | TypeScript)', diff --git a/code/lib/core-events/manager-errors.js b/code/lib/core-events/manager-errors.js index 6a1ce1522c1b..f94cf725de3c 100644 --- a/code/lib/core-events/manager-errors.js +++ b/code/lib/core-events/manager-errors.js @@ -1,4 +1,6 @@ -// This is required for projects that require paths such as `@storybook/core-events/manager-errors` -// but in CJS, while not in ESM mode. Else an error like this will occur: +// This is required for compatibility in projects that don't support the exports map field (e.g. Jest 27), +// so when require paths such as `@storybook/core-events/manager-errors`, +// An error like this will occur: // ENOENT: no such file or directory, open '/xyz/node_modules/@storybook/core-events/manager-errors.js' +// https://github.com/storybookjs/storybook/pull/24038#issuecomment-1704684432 module.exports = require('./dist/errors/manager-errors'); diff --git a/code/lib/core-events/preview-errors.js b/code/lib/core-events/preview-errors.js index a98055ba796d..1a144b6c7e20 100644 --- a/code/lib/core-events/preview-errors.js +++ b/code/lib/core-events/preview-errors.js @@ -1,4 +1,6 @@ -// This is required for projects that require paths such as `@storybook/core-events/preview-errors` -// but in CJS, while not in ESM mode. Else an error like this will occur: +// This is required for compatibility in projects that don't support the exports map field (e.g. Jest 27), +// so when require paths such as `@storybook/core-events/preview-errors`, +// An error like this will occur: // ENOENT: no such file or directory, open '/xyz/node_modules/@storybook/core-events/preview-errors.js' +// https://github.com/storybookjs/storybook/pull/24038#issuecomment-1704684432 module.exports = require('./dist/errors/preview-errors'); diff --git a/code/lib/core-events/server-errors.js b/code/lib/core-events/server-errors.js index 5f4eb31a408c..6aa7a95fd223 100644 --- a/code/lib/core-events/server-errors.js +++ b/code/lib/core-events/server-errors.js @@ -1,4 +1,6 @@ -// This is required for projects that require paths such as `@storybook/core-events/server-errors` -// but in CJS, while not in ESM mode. Else an error like this will occur: +// This is required for compatibility in projects that don't support the exports map field (e.g. Jest 27), +// so when require paths such as `@storybook/core-events/server-errors`, +// An error like this will occur: // ENOENT: no such file or directory, open '/xyz/node_modules/@storybook/core-events/server-errors.js' +// https://github.com/storybookjs/storybook/pull/24038#issuecomment-1704684432 module.exports = require('./dist/errors/server-errors'); diff --git a/code/lib/core-events/src/errors/manager-errors.ts b/code/lib/core-events/src/errors/manager-errors.ts index a97c5f8d7035..a025b708e0a3 100644 --- a/code/lib/core-events/src/errors/manager-errors.ts +++ b/code/lib/core-events/src/errors/manager-errors.ts @@ -35,9 +35,13 @@ export class UncaughtManagerError extends StorybookError { readonly code = 1; - constructor(public error: Error) { - super(error.message); - this.stack = error.stack; + constructor( + public data: { + error: Error; + } + ) { + super(data.error.message); + this.stack = data.error.stack; } template() { diff --git a/code/lib/core-events/src/errors/preview-errors.ts b/code/lib/core-events/src/errors/preview-errors.ts index 3a2fe93aecf5..84161db505b8 100644 --- a/code/lib/core-events/src/errors/preview-errors.ts +++ b/code/lib/core-events/src/errors/preview-errors.ts @@ -19,25 +19,6 @@ export enum Category { PREVIEW_REACT_DOM_SHIM = 'PREVIEW_REACT-DOM-SHIM', PREVIEW_ROUTER = 'PREVIEW_ROUTER', PREVIEW_THEMING = 'PREVIEW_THEMING', - FRAMEWORK_ANGULAR = 'FRAMEWORK_ANGULAR', - FRAMEWORK_EMBER = 'FRAMEWORK_EMBER', - FRAMEWORK_HTML_VITE = 'FRAMEWORK_HTML-VITE', - FRAMEWORK_HTML_WEBPACK5 = 'FRAMEWORK_HTML-WEBPACK5', - FRAMEWORK_NEXTJS = 'FRAMEWORK_NEXTJS', - FRAMEWORK_PREACT_VITE = 'FRAMEWORK_PREACT-VITE', - FRAMEWORK_PREACT_WEBPACK5 = 'FRAMEWORK_PREACT-WEBPACK5', - FRAMEWORK_REACT_VITE = 'FRAMEWORK_REACT-VITE', - FRAMEWORK_REACT_WEBPACK5 = 'FRAMEWORK_REACT-WEBPACK5', - FRAMEWORK_SERVER_WEBPACK5 = 'FRAMEWORK_SERVER-WEBPACK5', - FRAMEWORK_SVELTE_VITE = 'FRAMEWORK_SVELTE-VITE', - FRAMEWORK_SVELTE_WEBPACK5 = 'FRAMEWORK_SVELTE-WEBPACK5', - FRAMEWORK_SVELTEKIT = 'FRAMEWORK_SVELTEKIT', - FRAMEWORK_VUE_VITE = 'FRAMEWORK_VUE-VITE', - FRAMEWORK_VUE_WEBPACK5 = 'FRAMEWORK_VUE-WEBPACK5', - FRAMEWORK_VUE3_VITE = 'FRAMEWORK_VUE3-VITE', - FRAMEWORK_VUE3_WEBPACK5 = 'FRAMEWORK_VUE3-WEBPACK5', - FRAMEWORK_WEB_COMPONENTS_VITE = 'FRAMEWORK_WEB-COMPONENTS-VITE', - FRAMEWORK_WEB_COMPONENTS_WEBPACK5 = 'FRAMEWORK_WEB-COMPONENTS-WEBPACK5', RENDERER_HTML = 'RENDERER_HTML', RENDERER_PREACT = 'RENDERER_PREACT', RENDERER_REACT = 'RENDERER_REACT', diff --git a/code/lib/core-events/src/errors/server-errors.test.ts b/code/lib/core-events/src/errors/server-errors.test.ts new file mode 100644 index 000000000000..0eb5fa4ad21d --- /dev/null +++ b/code/lib/core-events/src/errors/server-errors.test.ts @@ -0,0 +1,16 @@ +/* eslint-disable local-rules/no-uncategorized-errors */ +import { WebpackCompilationError } from './server-errors'; + +describe('WebpackCompilationError', () => { + it('should correctly handle error with stats.compilation.errors', () => { + const errors = [ + new Error('Error 1 \u001B[4mmessage\u001B[0m'), + new Error('\u001B[4mError\u001B[0m 2 message'), + ]; + + const webpackError = new WebpackCompilationError({ errors }); + + expect(webpackError.data.errors[0].message).toEqual('Error 1 message'); + expect(webpackError.data.errors[1].message).toEqual('Error 2 message'); + }); +}); diff --git a/code/lib/core-events/src/errors/server-errors.ts b/code/lib/core-events/src/errors/server-errors.ts index 93f7355a3024..f773a2fbf69e 100644 --- a/code/lib/core-events/src/errors/server-errors.ts +++ b/code/lib/core-events/src/errors/server-errors.ts @@ -28,6 +28,25 @@ export enum Category { POSTINSTALL = 'POSTINSTALL', DOCS_TOOLS = 'DOCS-TOOLS', CORE_WEBPACK = 'CORE-WEBPACK', + FRAMEWORK_ANGULAR = 'FRAMEWORK_ANGULAR', + FRAMEWORK_EMBER = 'FRAMEWORK_EMBER', + FRAMEWORK_HTML_VITE = 'FRAMEWORK_HTML-VITE', + FRAMEWORK_HTML_WEBPACK5 = 'FRAMEWORK_HTML-WEBPACK5', + FRAMEWORK_NEXTJS = 'FRAMEWORK_NEXTJS', + FRAMEWORK_PREACT_VITE = 'FRAMEWORK_PREACT-VITE', + FRAMEWORK_PREACT_WEBPACK5 = 'FRAMEWORK_PREACT-WEBPACK5', + FRAMEWORK_REACT_VITE = 'FRAMEWORK_REACT-VITE', + FRAMEWORK_REACT_WEBPACK5 = 'FRAMEWORK_REACT-WEBPACK5', + FRAMEWORK_SERVER_WEBPACK5 = 'FRAMEWORK_SERVER-WEBPACK5', + FRAMEWORK_SVELTE_VITE = 'FRAMEWORK_SVELTE-VITE', + FRAMEWORK_SVELTE_WEBPACK5 = 'FRAMEWORK_SVELTE-WEBPACK5', + FRAMEWORK_SVELTEKIT = 'FRAMEWORK_SVELTEKIT', + FRAMEWORK_VUE_VITE = 'FRAMEWORK_VUE-VITE', + FRAMEWORK_VUE_WEBPACK5 = 'FRAMEWORK_VUE-WEBPACK5', + FRAMEWORK_VUE3_VITE = 'FRAMEWORK_VUE3-VITE', + FRAMEWORK_VUE3_WEBPACK5 = 'FRAMEWORK_VUE3-WEBPACK5', + FRAMEWORK_WEB_COMPONENTS_VITE = 'FRAMEWORK_WEB-COMPONENTS-VITE', + FRAMEWORK_WEB_COMPONENTS_WEBPACK5 = 'FRAMEWORK_WEB-COMPONENTS-WEBPACK5', } export class NxProjectDetectedError extends StorybookError { @@ -137,3 +156,126 @@ export class InvalidStoriesEntryError extends StorybookError { `; } } + +export class WebpackMissingStatsError extends StorybookError { + readonly category = Category.BUILDER_WEBPACK5; + + readonly code = 1; + + public documentation = [ + 'https://webpack.js.org/configuration/stats/', + 'https://storybook.js.org/docs/react/builders/webpack#configure', + ]; + + template() { + return dedent` + No Webpack stats found. Did you turn off stats reporting in your webpack config? + Storybook needs Webpack stats (including errors) in order to build correctly. + `; + } +} + +export class WebpackInvocationError extends StorybookError { + readonly category = Category.BUILDER_WEBPACK5; + + readonly code = 2; + + private errorMessage = ''; + + constructor( + public data: { + error: Error; + } + ) { + super(); + this.errorMessage = data.error.message; + } + + template() { + return this.errorMessage.trim(); + } +} + +function removeAnsiEscapeCodes(input = '') { + // eslint-disable-next-line no-control-regex + return input.replace(/\u001B\[[0-9;]*m/g, ''); +} + +export class WebpackCompilationError extends StorybookError { + readonly category = Category.BUILDER_WEBPACK5; + + readonly code = 3; + + constructor( + public data: { + errors: { + message: string; + stack?: string; + name?: string; + }[]; + } + ) { + super(); + + this.data.errors = data.errors.map((err) => { + return { + ...err, + message: removeAnsiEscapeCodes(err.message), + stack: removeAnsiEscapeCodes(err.stack), + name: err.name, + }; + }); + } + + template() { + // This error message is a followup of errors logged by Webpack to the user + return dedent` + There were problems when compiling your code with Webpack. + Run Storybook with --debug-webpack for more information. + `; + } +} + +export class MissingAngularJsonError extends StorybookError { + readonly category = Category.CLI_INIT; + + readonly code = 2; + + public readonly documentation = + 'https://storybook.js.org/docs/angular/faq#error-no-angularjson-file-found'; + + constructor( + public data: { + path: string; + } + ) { + super(); + } + + template() { + return dedent` + An angular.json file was not found in the current working directory: ${this.data.path} + Storybook needs it to work properly, so please rerun the command at the root of your project, where the angular.json file is located. + `; + } +} + +export class AngularLegacyBuildOptionsError extends StorybookError { + readonly category = Category.FRAMEWORK_ANGULAR; + + readonly code = 1; + + public readonly documentation = [ + 'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#angular-drop-support-for-calling-storybook-directly', + 'https://github.com/storybookjs/storybook/tree/next/code/frameworks/angular#how-do-i-migrate-to-an-angular-storybook-builder', + ]; + + template() { + return dedent` + Your Storybook startup script uses a solution that is not supported anymore. + You must use Angular builder to have an explicit configuration on the project used in angular.json. + + Please run 'npx storybook@next automigrate' to automatically fix your config. + `; + } +} diff --git a/code/lib/core-server/src/build-static.ts b/code/lib/core-server/src/build-static.ts index 810c871724fd..b8ca4a95fa6c 100644 --- a/code/lib/core-server/src/build-static.ts +++ b/code/lib/core-server/src/build-static.ts @@ -199,23 +199,33 @@ export async function buildStaticStandalone(options: BuildStaticStandaloneOption if (options.ignorePreview) { logger.info(`=> Not building preview`); + } else { + logger.info('=> Building preview..'); } + const startTime = process.hrtime(); await Promise.all([ ...(options.ignorePreview ? [] : [ previewBuilder .build({ - startTime: process.hrtime(), + startTime, options: fullOptions, }) .then(async (previewStats) => { + logger.trace({ message: '=> Preview built', time: process.hrtime(startTime) }); + if (options.webpackStatsJson) { const target = options.webpackStatsJson === true ? options.outputDir : options.webpackStatsJson; await outputStats(target, previewStats); } + }) + .catch((error) => { + logger.error('=> Failed to build the preview'); + process.exitCode = 1; + throw error; }), ]), ...effects, diff --git a/code/lib/core-server/src/dev-server.ts b/code/lib/core-server/src/dev-server.ts index 2eb1ea420cda..50d4c8dd4cdc 100644 --- a/code/lib/core-server/src/dev-server.ts +++ b/code/lib/core-server/src/dev-server.ts @@ -6,6 +6,7 @@ import type { CoreConfig, Options, StorybookConfig } from '@storybook/types'; import { logConfig } from '@storybook/core-common'; +import { logger } from '@storybook/node-logger'; import { getMiddleware } from './utils/middleware'; import { getServerAddresses } from './utils/server-address'; import { getServer } from './utils/server-init'; @@ -90,6 +91,7 @@ export async function storybookDevServer(options: Options) { let previewStarted: Promise = Promise.resolve(); if (!options.ignorePreview) { + logger.info('=> Starting preview..'); previewStarted = previewBuilder .start({ startTime: process.hrtime(), @@ -99,6 +101,9 @@ export async function storybookDevServer(options: Options) { channel: serverChannel, }) .catch(async (e: any) => { + logger.error('=> Failed to build the preview'); + process.exitCode = 1; + await managerBuilder?.bail().catch(); // For some reason, even when Webpack fails e.g. wrong main.js config, // the preview may continue to print to stdout, which can affect output diff --git a/code/lib/core-server/src/withTelemetry.test.ts b/code/lib/core-server/src/withTelemetry.test.ts index 519de40c1458..edcf7ddde90c 100644 --- a/code/lib/core-server/src/withTelemetry.test.ts +++ b/code/lib/core-server/src/withTelemetry.test.ts @@ -1,10 +1,11 @@ +/* eslint-disable local-rules/no-uncategorized-errors */ /// ; import prompts from 'prompts'; import { loadAllPresets, cache } from '@storybook/core-common'; -import { telemetry } from '@storybook/telemetry'; +import { telemetry, oneWayHash } from '@storybook/telemetry'; -import { withTelemetry } from './withTelemetry'; +import { getErrorLevel, sendTelemetryError, withTelemetry } from './withTelemetry'; jest.mock('prompts'); jest.mock('@storybook/core-common'); @@ -12,222 +13,463 @@ jest.mock('@storybook/telemetry'); const cliOptions = {}; -it('works in happy path', async () => { - const run = jest.fn(); +describe('withTelemetry', () => { + it('works in happy path', async () => { + const run = jest.fn(); - await withTelemetry('dev', { cliOptions }, run); + await withTelemetry('dev', { cliOptions }, run); - expect(telemetry).toHaveBeenCalledTimes(1); - expect(telemetry).toHaveBeenCalledWith('boot', { eventType: 'dev' }, { stripMetadata: true }); -}); - -it('does not send boot when cli option is passed', async () => { - const run = jest.fn(); + expect(telemetry).toHaveBeenCalledTimes(1); + expect(telemetry).toHaveBeenCalledWith('boot', { eventType: 'dev' }, { stripMetadata: true }); + }); - await withTelemetry('dev', { cliOptions: { disableTelemetry: true } }, run); + it('does not send boot when cli option is passed', async () => { + const run = jest.fn(); - expect(telemetry).toHaveBeenCalledTimes(0); -}); + await withTelemetry('dev', { cliOptions: { disableTelemetry: true } }, run); -describe('when command fails', () => { - const error = new Error('An Error!'); - const run = jest.fn(async () => { - throw error; + expect(telemetry).toHaveBeenCalledTimes(0); }); - it('sends boot message', async () => { - await expect(async () => withTelemetry('dev', { cliOptions }, run)).rejects.toThrow(error); + describe('when command fails', () => { + const error = new Error('An Error!'); + const run = jest.fn(async () => { + throw error; + }); - expect(telemetry).toHaveBeenCalledWith('boot', { eventType: 'dev' }, { stripMetadata: true }); - }); + it('sends boot message', async () => { + await expect(async () => + withTelemetry('dev', { cliOptions, printError: jest.fn() }, run) + ).rejects.toThrow(error); - it('does not send boot when cli option is passed', async () => { - await expect(async () => - withTelemetry('dev', { cliOptions: { disableTelemetry: true } }, run) - ).rejects.toThrow(error); + expect(telemetry).toHaveBeenCalledWith('boot', { eventType: 'dev' }, { stripMetadata: true }); + }); - expect(telemetry).toHaveBeenCalledTimes(0); + it('does not send boot when cli option is passed', async () => { + await expect(async () => + withTelemetry('dev', { cliOptions: { disableTelemetry: true }, printError: jest.fn() }, run) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(0); + }); + + it('sends error message when no options are passed', async () => { + await expect(async () => + withTelemetry('dev', { cliOptions, printError: jest.fn() }, run) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev', error }), + expect.objectContaining({}) + ); + }); + + it('does not send error message when cli opt out is passed', async () => { + await expect(async () => + withTelemetry('dev', { cliOptions: { disableTelemetry: true }, printError: jest.fn() }, run) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(0); + expect(telemetry).not.toHaveBeenCalledWith( + 'error', + expect.objectContaining({}), + expect.objectContaining({}) + ); + }); + + it('does not send full error message when crash reports are disabled', async () => { + jest.mocked(loadAllPresets).mockResolvedValueOnce({ + apply: async () => ({ enableCrashReports: false } as any), + }); + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev' }), + expect.objectContaining({}) + ); + }); + + it('does send error message when crash reports are enabled', async () => { + jest.mocked(loadAllPresets).mockResolvedValueOnce({ + apply: async () => ({ enableCrashReports: true } as any), + }); + + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev', error }), + expect.objectContaining({}) + ); + }); + + it('does not send any error message when telemetry is disabled', async () => { + jest.mocked(loadAllPresets).mockResolvedValueOnce({ + apply: async () => ({ disableTelemetry: true } as any), + }); + + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(1); + expect(telemetry).not.toHaveBeenCalledWith( + 'error', + expect.objectContaining({}), + expect.objectContaining({}) + ); + }); + + it('does send error messages when telemetry is disabled, but crash reports are enabled', async () => { + jest.mocked(loadAllPresets).mockResolvedValueOnce({ + apply: async () => ({ disableTelemetry: true, enableCrashReports: true } as any), + }); + + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev', error }), + expect.objectContaining({}) + ); + }); + + it('does not send full error messages when disabled crash reports are cached', async () => { + jest.mocked(loadAllPresets).mockResolvedValueOnce({ + apply: async () => ({} as any), + }); + jest.mocked(cache.get).mockResolvedValueOnce(false); + + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev' }), + expect.objectContaining({}) + ); + }); + + it('does send error messages when enabled crash reports are cached', async () => { + jest.mocked(loadAllPresets).mockResolvedValueOnce({ + apply: async () => ({} as any), + }); + jest.mocked(cache.get).mockResolvedValueOnce(true); + + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev', error }), + expect.objectContaining({}) + ); + }); + + it('does not send full error messages when disabled crash reports are prompted', async () => { + jest.mocked(loadAllPresets).mockResolvedValueOnce({ + apply: async () => ({} as any), + }); + jest.mocked(cache.get).mockResolvedValueOnce(undefined); + jest.mocked(prompts).mockResolvedValueOnce({ enableCrashReports: false }); + + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev' }), + expect.objectContaining({}) + ); + }); + + it('does send error messages when enabled crash reports are prompted', async () => { + jest.mocked(loadAllPresets).mockResolvedValueOnce({ + apply: async () => ({} as any), + }); + jest.mocked(cache.get).mockResolvedValueOnce(undefined); + jest.mocked(prompts).mockResolvedValueOnce({ enableCrashReports: true }); + + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev', error }), + expect.objectContaining({}) + ); + }); + + // if main.js has errors, we have no way to tell if they've disabled error reporting, + // so we assume they have. + it('does not send full error messages when presets fail to evaluate', async () => { + jest.mocked(loadAllPresets).mockRejectedValueOnce(error); + + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev' }), + expect.objectContaining({}) + ); + }); }); +}); + +describe('sendTelemetryError', () => { + it('handles error instances and sends telemetry', async () => { + const options: any = { + cliOptions: {}, + skipPrompt: false, + }; + const mockError = new Error('Test error'); + const eventType: any = 'testEventType'; + + jest.mocked(oneWayHash).mockReturnValueOnce('some-hash'); - it('sends error message when no options are passed', async () => { - await expect(async () => withTelemetry('dev', { cliOptions }, run)).rejects.toThrow(error); + await sendTelemetryError(mockError, eventType, options); - expect(telemetry).toHaveBeenCalledTimes(2); expect(telemetry).toHaveBeenCalledWith( 'error', - { eventType: 'dev', error }, - expect.objectContaining({}) + expect.objectContaining({ + error: mockError, + eventType, + isErrorInstance: true, + errorHash: 'some-hash', + }), + expect.any(Object) ); }); - it('does not send error message when cli opt out is passed', async () => { - await expect(async () => - withTelemetry('dev', { cliOptions: { disableTelemetry: true } }, run) - ).rejects.toThrow(error); + it('handles non-error instances and sends telemetry with no-message hash', async () => { + const options: any = { + cliOptions: {}, + skipPrompt: false, + }; + const mockError = { error: new Error('Test error') }; + const eventType: any = 'testEventType'; - expect(telemetry).toHaveBeenCalledTimes(0); - expect(telemetry).not.toHaveBeenCalledWith( + await sendTelemetryError(mockError, eventType, options); + + expect(telemetry).toHaveBeenCalledWith( 'error', - expect.objectContaining({}), - expect.objectContaining({}) + expect.objectContaining({ + error: mockError, + eventType, + isErrorInstance: false, + errorHash: 'NO_MESSAGE', + }), + expect.any(Object) ); }); - it('does not send full error message when crash reports are disabled', async () => { - jest.mocked(loadAllPresets).mockResolvedValueOnce({ - apply: async () => ({ enableCrashReports: false } as any), - }); - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); + it('handles error with empty message and sends telemetry with empty-message hash', async () => { + const options: any = { + cliOptions: {}, + skipPrompt: false, + }; + const mockError = new Error(); + const eventType: any = 'testEventType'; + + await sendTelemetryError(mockError, eventType, options); - expect(telemetry).toHaveBeenCalledTimes(2); expect(telemetry).toHaveBeenCalledWith( 'error', - { eventType: 'dev' }, - expect.objectContaining({}) + expect.objectContaining({ + error: mockError, + eventType, + isErrorInstance: true, + errorHash: 'EMPTY_MESSAGE', + }), + expect.any(Object) ); }); +}); - it('does send error message when crash reports are enabled', async () => { - jest.mocked(loadAllPresets).mockResolvedValueOnce({ - apply: async () => ({ enableCrashReports: true } as any), - }); +describe('getErrorLevel', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); + it('returns "none" when cliOptions.disableTelemetry is true', async () => { + const options: any = { + cliOptions: { + disableTelemetry: true, + }, + presetOptions: undefined, + skipPrompt: false, + }; - expect(telemetry).toHaveBeenCalledTimes(2); - expect(telemetry).toHaveBeenCalledWith( - 'error', - { eventType: 'dev', error }, - expect.objectContaining({}) - ); + const errorLevel = await getErrorLevel(options); + + expect(errorLevel).toBe('none'); }); - it('does not send any error message when telemetry is disabled', async () => { - jest.mocked(loadAllPresets).mockResolvedValueOnce({ - apply: async () => ({ disableTelemetry: true } as any), - }); + it('returns "full" when presetOptions is not provided', async () => { + const options: any = { + cliOptions: { + disableTelemetry: false, + }, + presetOptions: undefined, + skipPrompt: false, + }; - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); + const errorLevel = await getErrorLevel(options); - expect(telemetry).toHaveBeenCalledTimes(1); - expect(telemetry).not.toHaveBeenCalledWith( - 'error', - expect.objectContaining({}), - expect.objectContaining({}) - ); + expect(errorLevel).toBe('full'); }); - it('does send error messages when telemetry is disabled, but crash reports are enabled', async () => { + it('returns "full" when core.enableCrashReports is true', async () => { + const options: any = { + cliOptions: { + disableTelemetry: false, + }, + presetOptions: {}, + skipPrompt: false, + }; + jest.mocked(loadAllPresets).mockResolvedValueOnce({ - apply: async () => ({ disableTelemetry: true, enableCrashReports: true } as any), + apply: async () => ({ enableCrashReports: true } as any), }); + jest.mocked(cache.get).mockResolvedValueOnce(false); - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); + const errorLevel = await getErrorLevel(options); - expect(telemetry).toHaveBeenCalledTimes(2); - expect(telemetry).toHaveBeenCalledWith( - 'error', - { eventType: 'dev', error }, - expect.objectContaining({}) - ); + expect(errorLevel).toBe('full'); }); - it('does not send full error messages when disabled crash reports are cached', async () => { + it('returns "error" when core.enableCrashReports is false', async () => { + const options: any = { + cliOptions: { + disableTelemetry: false, + }, + presetOptions: {}, + skipPrompt: false, + }; + jest.mocked(loadAllPresets).mockResolvedValueOnce({ - apply: async () => ({} as any), + apply: async () => ({ enableCrashReports: false } as any), }); jest.mocked(cache.get).mockResolvedValueOnce(false); - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); + const errorLevel = await getErrorLevel(options); - expect(telemetry).toHaveBeenCalledTimes(2); - expect(telemetry).toHaveBeenCalledWith( - 'error', - { eventType: 'dev' }, - expect.objectContaining({}) - ); + expect(errorLevel).toBe('error'); }); - it('does send error messages when enabled crash reports are cached', async () => { + it('returns "none" when core.disableTelemetry is true', async () => { + const options: any = { + cliOptions: { + disableTelemetry: false, + }, + presetOptions: {}, + skipPrompt: false, + }; + jest.mocked(loadAllPresets).mockResolvedValueOnce({ - apply: async () => ({} as any), + apply: async () => ({ disableTelemetry: true } as any), }); - jest.mocked(cache.get).mockResolvedValueOnce(true); + jest.mocked(cache.get).mockResolvedValueOnce(false); - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); + const errorLevel = await getErrorLevel(options); - expect(telemetry).toHaveBeenCalledTimes(2); - expect(telemetry).toHaveBeenCalledWith( - 'error', - { eventType: 'dev', error }, - expect.objectContaining({}) - ); + expect(errorLevel).toBe('none'); }); - it('does not send full error messages when disabled crash reports are prompted', async () => { + it('returns "full" if cache contains crashReports true', async () => { + const options: any = { + cliOptions: { + disableTelemetry: false, + }, + presetOptions: {}, + skipPrompt: false, + }; + + jest.mocked(cache.get).mockResolvedValueOnce(true); jest.mocked(loadAllPresets).mockResolvedValueOnce({ apply: async () => ({} as any), }); - jest.mocked(cache.get).mockResolvedValueOnce(undefined); - jest.mocked(prompts).mockResolvedValueOnce({ enableCrashReports: false }); - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); + const errorLevel = await getErrorLevel(options); - expect(telemetry).toHaveBeenCalledTimes(2); - expect(telemetry).toHaveBeenCalledWith( - 'error', - { eventType: 'dev' }, - expect.objectContaining({}) - ); + expect(errorLevel).toBe('full'); }); - it('does send error messages when enabled crash reports are prompted', async () => { + it('returns "error" when skipPrompt is true', async () => { + const options: any = { + cliOptions: { + disableTelemetry: false, + }, + presetOptions: {}, + skipPrompt: true, + }; + jest.mocked(loadAllPresets).mockResolvedValueOnce({ apply: async () => ({} as any), }); jest.mocked(cache.get).mockResolvedValueOnce(undefined); - jest.mocked(prompts).mockResolvedValueOnce({ enableCrashReports: true }); - - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); - - expect(telemetry).toHaveBeenCalledTimes(2); - expect(telemetry).toHaveBeenCalledWith( - 'error', - { eventType: 'dev', error }, - expect.objectContaining({}) - ); - }); - - // if main.js has errors, we have no way to tell if they've disabled error reporting, - // so we assume they have. - it('does not send full error messages when presets fail to evaluate', async () => { - jest.mocked(loadAllPresets).mockRejectedValueOnce(error); - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); + const errorLevel = await getErrorLevel(options); - expect(telemetry).toHaveBeenCalledTimes(2); - expect(telemetry).toHaveBeenCalledWith( - 'error', - { eventType: 'dev' }, - expect.objectContaining({}) - ); + expect(errorLevel).toBe('error'); }); }); diff --git a/code/lib/core-server/src/withTelemetry.ts b/code/lib/core-server/src/withTelemetry.ts index 0baebb8d97cb..21a33636d77a 100644 --- a/code/lib/core-server/src/withTelemetry.ts +++ b/code/lib/core-server/src/withTelemetry.ts @@ -4,7 +4,6 @@ import { loadAllPresets, cache } from '@storybook/core-common'; import { telemetry, getPrecedingUpgrade, oneWayHash } from '@storybook/telemetry'; import type { EventType } from '@storybook/telemetry'; import { logger } from '@storybook/node-logger'; -import invariant from 'tiny-invariant'; type TelemetryOptions = { cliOptions: CLIOptions; @@ -32,7 +31,7 @@ const promptCrashReports = async () => { type ErrorLevel = 'none' | 'error' | 'full'; -async function getErrorLevel({ +export async function getErrorLevel({ cliOptions, presetOptions, skipPrompt, @@ -67,7 +66,7 @@ async function getErrorLevel({ } export async function sendTelemetryError( - error: unknown, + _error: unknown, eventType: EventType, options: TelemetryOptions ) { @@ -81,37 +80,28 @@ export async function sendTelemetryError( if (errorLevel !== 'none') { const precedingUpgrade = await getPrecedingUpgrade(); - invariant( - error instanceof Error, - 'The error passed to sendTelemetryError was not an Error, please only send Errors' - ); + const error = _error as Error & Record; - let storybookErrorProperties = {}; - // if it's an UNCATEGORIZED error, it won't have a coded name, so we just pass the category and source - if ((error as any).category) { - const { category } = error as any; - storybookErrorProperties = { - category, - }; - } - - if ((error as any).fromStorybook) { - const { code, name } = error as any; - storybookErrorProperties = { - ...storybookErrorProperties, - code, - name, - }; + let errorHash; + if ('message' in error) { + errorHash = error.message ? oneWayHash(error.message) : 'EMPTY_MESSAGE'; + } else { + errorHash = 'NO_MESSAGE'; } + const { code, name, category } = error; await telemetry( 'error', { + code, + name, + category, eventType, precedingUpgrade, error: errorLevel === 'full' ? error : undefined, - errorHash: oneWayHash(error.message), - ...storybookErrorProperties, + errorHash, + // if we ever end up sending a non-error instance, we'd like to know + isErrorInstance: error instanceof Error, }, { immediate: true, diff --git a/code/lib/preview/package.json b/code/lib/preview/package.json index fdabb3dfec22..cb5dcfd3cfa0 100644 --- a/code/lib/preview/package.json +++ b/code/lib/preview/package.json @@ -61,6 +61,7 @@ "@storybook/core-events": "workspace:*", "@storybook/global": "^5.0.0", "@storybook/preview-api": "workspace:*", + "browser-dtector": "^3.4.0", "typescript": "~4.9.3" }, "publishConfig": { diff --git a/code/lib/preview/src/runtime.ts b/code/lib/preview/src/runtime.ts index 7785e42df8c9..603c586b9fe3 100644 --- a/code/lib/preview/src/runtime.ts +++ b/code/lib/preview/src/runtime.ts @@ -3,6 +3,7 @@ import { global } from '@storybook/global'; import { values } from './globals/runtime'; import { globals } from './globals/types'; +import { prepareForTelemetry } from './utils'; const getKeys = Object.keys as (obj: T) => Array; @@ -13,7 +14,7 @@ getKeys(globals).forEach((key) => { global.sendTelemetryError = (error: any) => { const channel = global.__STORYBOOK_ADDONS_CHANNEL__; - channel.emit(TELEMETRY_ERROR, error); + channel.emit(TELEMETRY_ERROR, prepareForTelemetry(error)); }; // handle all uncaught StorybookError at the root of the application and log to telemetry if applicable diff --git a/code/lib/preview/src/utils.ts b/code/lib/preview/src/utils.ts new file mode 100644 index 000000000000..094e19fba6d4 --- /dev/null +++ b/code/lib/preview/src/utils.ts @@ -0,0 +1,29 @@ +import { global } from '@storybook/global'; +import type { BrowserInfo } from 'browser-dtector'; +import BrowserDetector from 'browser-dtector'; + +let browserInfo: BrowserInfo | undefined; + +function getBrowserInfo() { + if (!browserInfo) { + browserInfo = new BrowserDetector(global.navigator?.userAgent).getBrowserInfo(); + } + + return browserInfo; +} + +export function prepareForTelemetry( + error: Error & { + fromStorybook?: boolean; + category?: string; + target?: any; + currentTarget?: any; + srcElement?: any; + browserInfo?: BrowserInfo; + } +) { + // eslint-disable-next-line no-param-reassign + error.browserInfo = getBrowserInfo(); + + return error; +} diff --git a/code/lib/source-loader/src/abstract-syntax-tree/traverse-helpers.js b/code/lib/source-loader/src/abstract-syntax-tree/traverse-helpers.js index 798e8a1ca7cf..a908bf618c9a 100644 --- a/code/lib/source-loader/src/abstract-syntax-tree/traverse-helpers.js +++ b/code/lib/source-loader/src/abstract-syntax-tree/traverse-helpers.js @@ -239,7 +239,7 @@ export function popParametersObjectFromDefaultExport(source, ast) { (targetNode.properties || []).length ) { const parametersProperty = targetNode.properties.find( - (p) => p.key.name === 'parameters' && p.value.type === 'ObjectExpression' + (p) => p.key && p.key.name === 'parameters' && p.value.type === 'ObjectExpression' ); foundParametersProperty = !!parametersProperty; diff --git a/code/lib/telemetry/src/sanitize.test.ts b/code/lib/telemetry/src/sanitize.test.ts index f5b3a742d3e6..934695e1e70b 100644 --- a/code/lib/telemetry/src/sanitize.test.ts +++ b/code/lib/telemetry/src/sanitize.test.ts @@ -1,7 +1,23 @@ +/* eslint-disable local-rules/no-uncategorized-errors */ import { sanitizeError, cleanPaths } from './sanitize'; describe(`Errors Helpers`, () => { describe(`sanitizeError`, () => { + it(`Sanitizes ansi codes in error`, () => { + const errorMessage = `\u001B[4mStorybook\u001B[0m`; + let e: any; + try { + throw new Error(errorMessage); + } catch (error) { + e = error; + } + + const sanitizedError = sanitizeError(e); + + expect(sanitizedError.message).toEqual('Storybook'); + expect(sanitizedError.stack).toContain('Error: Storybook'); + }); + it(`Sanitizes current path from error stacktraces`, () => { const errorMessage = `this is a test`; let e: any; @@ -69,14 +85,12 @@ describe(`Errors Helpers`, () => { `should clean path on unix: %s`, (filePath) => { const cwdMockPath = `/Users/username/storybook-app`; - const fullPath = `${cwdMockPath}/${filePath}`; - const mockCwd = jest.spyOn(process, `cwd`).mockImplementation(() => cwdMockPath); - const errorMessage = `This path should be sanitized ${fullPath}`; + const errorMessage = `Path 1 /Users/Username/storybook-app/${filePath} Path 2 /Users/username/storybook-app/${filePath}`; expect(cleanPaths(errorMessage, `/`)).toBe( - `This path should be sanitized $SNIP/${filePath}` + `Path 1 $SNIP/${filePath} Path 2 $SNIP/${filePath}` ); mockCwd.mockRestore(); } @@ -86,14 +100,12 @@ describe(`Errors Helpers`, () => { `should clean path on windows: %s`, (filePath) => { const cwdMockPath = `C:\\Users\\username\\storybook-app`; - const fullPath = `${cwdMockPath}\\${filePath}`; - - const mockCwd = jest.spyOn(process, `cwd`).mockImplementation(() => cwdMockPath); - const errorMessage = `This path should be sanitized ${fullPath}`; + const mockCwd = jest.spyOn(process, `cwd`).mockImplementationOnce(() => cwdMockPath); + const errorMessage = `Path 1 C:\\Users\\username\\storybook-app\\${filePath} Path 2 c:\\Users\\username\\storybook-app\\${filePath}`; expect(cleanPaths(errorMessage, `\\`)).toBe( - `This path should be sanitized $SNIP\\${filePath}` + `Path 1 $SNIP\\${filePath} Path 2 $SNIP\\${filePath}` ); mockCwd.mockRestore(); } diff --git a/code/lib/telemetry/src/sanitize.ts b/code/lib/telemetry/src/sanitize.ts index 4c68ed50db94..77e0c1fbda0e 100644 --- a/code/lib/telemetry/src/sanitize.ts +++ b/code/lib/telemetry/src/sanitize.ts @@ -12,6 +12,11 @@ function regexpEscape(str: string): string { return str.replace(/[-[/{}()*+?.\\^$|]/g, `\\$&`); } +export function removeAnsiEscapeCodes(input = ''): string { + // eslint-disable-next-line no-control-regex + return input.replace(/\u001B\[[0-9;]*m/g, ''); +} + export function cleanPaths(str: string, separator: string = sep): string { if (!str) return str; @@ -19,11 +24,11 @@ export function cleanPaths(str: string, separator: string = sep): string { while (stack.length > 1) { const currentPath = stack.join(separator); - const currentRegex = new RegExp(regexpEscape(currentPath), `g`); + const currentRegex = new RegExp(regexpEscape(currentPath), `gi`); str = str.replace(currentRegex, `$SNIP`); const currentPath2 = stack.join(separator + separator); - const currentRegex2 = new RegExp(regexpEscape(currentPath2), `g`); + const currentRegex2 = new RegExp(regexpEscape(currentPath2), `gi`); str = str.replace(currentRegex2, `$SNIP`); stack.pop(); @@ -34,8 +39,13 @@ export function cleanPaths(str: string, separator: string = sep): string { // Takes an Error and returns a sanitized JSON String export function sanitizeError(error: Error, pathSeparator: string = sep) { try { - // Hack because Node - error = JSON.parse(JSON.stringify(error, Object.getOwnPropertyNames(error))); + error = { + ...JSON.parse(JSON.stringify(error)), + message: removeAnsiEscapeCodes(error.message), + stack: removeAnsiEscapeCodes(error.stack), + cause: error.cause, + name: error.name, + }; // Removes all user paths const errorString = cleanPaths(JSON.stringify(error), pathSeparator); diff --git a/code/lib/telemetry/src/telemetry.ts b/code/lib/telemetry/src/telemetry.ts index 090e58cf1240..15899abbd323 100644 --- a/code/lib/telemetry/src/telemetry.ts +++ b/code/lib/telemetry/src/telemetry.ts @@ -1,5 +1,6 @@ /// +import * as os from 'os'; import originalFetch from 'node-fetch'; import retry from 'fetch-retry'; import { nanoid } from 'nanoid'; @@ -18,12 +19,33 @@ export const addToGlobalContext = (key: string, value: any) => { globalContext[key] = value; }; +const getOperatingSystem = (): 'Windows' | 'macOS' | 'Linux' | `Other: ${string}` | 'Unknown' => { + try { + const platform = os.platform(); + + if (platform === 'win32') { + return 'Windows'; + } + if (platform === 'darwin') { + return 'macOS'; + } + if (platform === 'linux') { + return 'Linux'; + } + + return `Other: ${platform}`; + } catch (_err) { + return 'Unknown'; + } +}; + // context info sent with all events, provided // by the app. currently: // - cliVersion const globalContext = { inCI: Boolean(process.env.CI), isTTY: process.stdout.isTTY, + platform: getOperatingSystem(), } as Record; const prepareRequest = async (data: TelemetryData, context: Record, options: any) => { diff --git a/code/package.json b/code/package.json index c73247e6eef5..f41b68e1e742 100644 --- a/code/package.json +++ b/code/package.json @@ -327,5 +327,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "7.5.0-alpha.2" } diff --git a/code/renderers/svelte/src/docs/sourceDecorator.test.ts b/code/renderers/svelte/src/docs/sourceDecorator.test.ts index 86b30788f462..6123b45d8b94 100644 --- a/code/renderers/svelte/src/docs/sourceDecorator.test.ts +++ b/code/renderers/svelte/src/docs/sourceDecorator.test.ts @@ -7,6 +7,9 @@ expect.addSnapshotSerializer({ test: (val: unknown) => typeof val === 'string', }); +const loremIpsum = 'Lorem ipsum dolor sit amet'; +const lotOfProperties = { property1: loremIpsum, property2: loremIpsum, property3: loremIpsum }; + function generateForArgs(args: Args, slotProperty: string | null = null) { return generateSvelteSource({ name: 'Component' }, args, {}, slotProperty); } @@ -35,6 +38,14 @@ describe('generateSvelteSource', () => { test('multiple properties', () => { expect(generateForArgs({ a: 1, b: 2 })).toMatchInlineSnapshot(``); }); + test('lot of properties', () => { + expect(generateForArgs(lotOfProperties)).toMatchInlineSnapshot(` + + `); + }); test('slot property', () => { expect(generateForArgs({ content: 'xyz', myProp: 'abc' }, 'content')).toMatchInlineSnapshot(` @@ -42,7 +53,32 @@ describe('generateSvelteSource', () => { `); }); + test('slot property with lot of properties', () => { + expect(generateForArgs({ content: 'xyz', ...lotOfProperties }, 'content')) + .toMatchInlineSnapshot(` + + xyz + + `); + }); test('component is not set', () => { expect(generateSvelteSource(null, {}, {}, null)).toBeNull(); }); + test('Skip event property', () => { + expect( + generateSvelteSource( + { name: 'Component' }, + { event_click: () => {} }, + { event_click: { action: 'click' } } + ) + ).toMatchInlineSnapshot(``); + }); + test('Property value is a function', () => { + expect( + generateSvelteSource({ name: 'Component' }, { myHandler: () => {} }, {}) + ).toMatchInlineSnapshot(`}/>`); + }); }); diff --git a/code/renderers/svelte/src/docs/sourceDecorator.ts b/code/renderers/svelte/src/docs/sourceDecorator.ts index 9f16caf80162..7b61eea6a7e9 100644 --- a/code/renderers/svelte/src/docs/sourceDecorator.ts +++ b/code/renderers/svelte/src/docs/sourceDecorator.ts @@ -38,8 +38,15 @@ function toSvelteProperty(key: string, value: any, argTypes: ArgTypes): string | return null; } + const argType = argTypes[key]; + // default value ? - if (argTypes[key] && argTypes[key].defaultValue === value) { + if (argType && argType.defaultValue === value) { + return null; + } + + // event should be skipped + if (argType && argType.action) { return null; } @@ -51,6 +58,11 @@ function toSvelteProperty(key: string, value: any, argTypes: ArgTypes): string | return `${key}=${JSON.stringify(value)}`; } + // handle function + if (typeof value === 'function') { + return `${key}={}`; + } + return `${key}={${JSON.stringify(value)}}`; } @@ -98,19 +110,21 @@ export function generateSvelteSource( return null; } - const props = Object.entries(args) + const propsArray = Object.entries(args) .filter(([k]) => k !== slotProperty) .map(([k, v]) => toSvelteProperty(k, v, argTypes)) - .filter((p) => p) - .join(' '); + .filter((p) => p); + const props = propsArray.join(' '); + + const multiline = props.length > 50; const slotValue = slotProperty ? args[slotProperty] : null; + const srcStart = multiline ? `<${name}\n ${propsArray.join('\n ')}` : `<${name} ${props}`; if (slotValue) { - return `<${name} ${props}>\n ${slotValue}\n`; + return `${srcStart}>\n ${slotValue}\n`; } - - return `<${name} ${props}/>`; + return `${srcStart}/>`; } /** diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 5c1e9e51b0e5..f0e254f082ae 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -247,7 +247,6 @@ export function generateTemplateSource( .map((child) => child.content) .join('') : ''; - console.log(' vnode ', vnode, ' childSources ', childSources, ' attributes ', attributes); const name = typeof type === 'string' ? type diff --git a/code/ui/.storybook/main.ts b/code/ui/.storybook/main.ts index 5f979898a5c2..190a5e4b84da 100644 --- a/code/ui/.storybook/main.ts +++ b/code/ui/.storybook/main.ts @@ -51,6 +51,14 @@ const config: StorybookConfig = { '@storybook/addon-interactions', '@storybook/addon-storysource', '@storybook/addon-designs', + { + name: '@chromaui/addon-visual-tests', + options: { + projectId: 'Project:635781f3500dd2c49e189caf', + projectToken: '80b312430ec4', + buildScriptName: 'storybook:ui:build', + }, + }, ], framework: { name: '@storybook/react-vite', diff --git a/code/ui/components/package.json b/code/ui/components/package.json index d53e46686f3a..75782892247c 100644 --- a/code/ui/components/package.json +++ b/code/ui/components/package.json @@ -71,6 +71,7 @@ "util-deprecate": "^1.0.2" }, "devDependencies": { + "@chromaui/addon-visual-tests": "^0.0.49", "@popperjs/core": "^2.6.0", "@storybook/icons": "^1.1.6", "@types/react-syntax-highlighter": "11.0.5", diff --git a/code/ui/manager/package.json b/code/ui/manager/package.json index fbcfb8bce2f2..5794551ae38c 100644 --- a/code/ui/manager/package.json +++ b/code/ui/manager/package.json @@ -65,6 +65,7 @@ "@storybook/types": "workspace:*", "@testing-library/react": "^11.2.2", "@types/semver": "^7.3.4", + "browser-dtector": "^3.4.0", "copy-to-clipboard": "^3.3.1", "downshift": "^6.0.15", "fs-extra": "^11.1.0", diff --git a/code/ui/manager/src/components/sidebar/Search.tsx b/code/ui/manager/src/components/sidebar/Search.tsx index af2c858efdf5..d9d03217db38 100644 --- a/code/ui/manager/src/components/sidebar/Search.tsx +++ b/code/ui/manager/src/components/sidebar/Search.tsx @@ -348,8 +348,9 @@ export const Search = React.memo<{ } } + const inputId = 'storybook-explorer-searchfield'; const inputProps = getInputProps({ - id: 'storybook-explorer-searchfield', + id: inputId, ref: inputRef, required: true, type: 'search', @@ -361,9 +362,13 @@ export const Search = React.memo<{ onBlur: () => setPlaceholder('Find components'), }); + const labelProps = getLabelProps({ + htmlFor: inputId, + }); + return ( <> - Search for components + Search for components { global[Keys[key]] = values[key]; }); -function preprocessError( - originalError: Error & { - fromStorybook?: boolean; - category?: string; - target?: any; - currentTarget?: any; - srcElement?: any; - } -) { - let error = originalError; - - if (!originalError.fromStorybook) { - error = new UncaughtManagerError(originalError); - } - - // DOM manipulation errors and other similar errors are not serializable as they contain - // circular references to the window object. If that's the case, we make a simplified copy - if (error.target === window || error.currentTarget === window || error.srcElement === window) { - error = new Error(originalError.message); - error.name = originalError.name || error.name; - error.category = originalError.category; - } - - return error; -} - global.sendTelemetryError = (error) => { const channel = global.__STORYBOOK_ADDONS_CHANNEL__; - channel.emit(TELEMETRY_ERROR, preprocessError(error)); + channel.emit(TELEMETRY_ERROR, prepareForTelemetry(error)); }; // handle all uncaught errors at the root of the application and log to telemetry diff --git a/code/ui/manager/src/utils/prepareForTelemetry.ts b/code/ui/manager/src/utils/prepareForTelemetry.ts new file mode 100644 index 000000000000..2ae99ba431a1 --- /dev/null +++ b/code/ui/manager/src/utils/prepareForTelemetry.ts @@ -0,0 +1,47 @@ +/* eslint-disable local-rules/no-uncategorized-errors */ +import { UncaughtManagerError } from '@storybook/core-events/manager-errors'; +import { global } from '@storybook/global'; +import type { BrowserInfo } from 'browser-dtector'; +import BrowserDetector from 'browser-dtector'; + +let browserInfo: BrowserInfo | undefined; + +function getBrowserInfo() { + if (!browserInfo) { + browserInfo = new BrowserDetector(global.navigator?.userAgent).getBrowserInfo(); + } + + return browserInfo; +} + +export function prepareForTelemetry( + originalError: Error & { + fromStorybook?: boolean; + category?: string; + target?: any; + currentTarget?: any; + srcElement?: any; + browserInfo?: BrowserInfo; + } +) { + let error = originalError; + + // DOM manipulation errors and other similar errors are not serializable as they contain + // circular references to the window object. If that's the case, we make a simplified copy + if ( + originalError.target === global || + originalError.currentTarget === global || + originalError.srcElement === global + ) { + error = new Error(originalError.message); + error.name = originalError.name || error.name; + } + + if (!originalError.fromStorybook) { + error = new UncaughtManagerError({ error }); + } + + error.browserInfo = getBrowserInfo(); + + return error; +} diff --git a/code/yarn.lock b/code/yarn.lock index 3969bd8d5185..7e1747b11551 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -5,6 +5,18 @@ __metadata: version: 6 cacheKey: 8c0 +"@0no-co/graphql.web@npm:^1.0.1": + version: 1.0.4 + resolution: "@0no-co/graphql.web@npm:1.0.4" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + graphql: + optional: true + checksum: bf63cb5b017063363c9a9e06dc17532abc1c2da402c7ebcbc7b5ab2a0601ec93b02de93af9e50d9daffb3b747eddcf0b1e5418a46d1182c5b8087b7d7a1768ad + languageName: node + linkType: hard + "@aashutoshrathi/word-wrap@npm:^1.2.3": version: 1.2.6 resolution: "@aashutoshrathi/word-wrap@npm:1.2.6" @@ -2238,6 +2250,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.1.2": + version: 7.22.10 + resolution: "@babel/runtime@npm:7.22.10" + dependencies: + regenerator-runtime: ^0.14.0 + checksum: d3a006fe2cbaf4048b935fb18f55d9ed52c26292182537b442cee57bf524dbb483367c57f464b1a5a96648d9d8d0fdcda848d58a8a09e18ed3f8971dcd684c6c + languageName: node + linkType: hard + "@babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.8, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.6, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2": version: 7.22.6 resolution: "@babel/runtime@npm:7.22.6" @@ -2321,6 +2342,38 @@ __metadata: languageName: node linkType: hard +"@chromaui/addon-visual-tests@npm:^0.0.49": + version: 0.0.49 + resolution: "@chromaui/addon-visual-tests@npm:0.0.49" + dependencies: + "@storybook/csf-tools": 7.4.0 + "@storybook/design-system": ^7.15.15 + chromatic: 6.24.0 + date-fns: ^2.30.0 + pluralize: ^8.0.0 + ts-dedent: ^2.2.0 + urql: ^4.0.3 + uuid: ^9.0.0 + peerDependencies: + "@storybook/blocks": ^7.2.0 + "@storybook/client-logger": ^7.2.0 + "@storybook/components": ^7.2.0 + "@storybook/core-events": ^7.2.0 + "@storybook/manager-api": ^7.2.0 + "@storybook/preview-api": ^7.2.0 + "@storybook/theming": ^7.2.0 + "@storybook/types": ^7.2.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: af5860520f2652d866017ca24d11fc38c18030c7452ba430c2f399c41ef5808af9187ded1f1fdcf936e97c68e821f8cce53495a54d8f40a75b07dfc7b94da3c4 + languageName: node + linkType: hard + "@colors/colors@npm:1.5.0": version: 1.5.0 resolution: "@colors/colors@npm:1.5.0" @@ -2614,7 +2667,7 @@ __metadata: languageName: node linkType: hard -"@emotion/weak-memoize@npm:^0.3.1": +"@emotion/weak-memoize@npm:^0.3.0, @emotion/weak-memoize@npm:^0.3.1": version: 0.3.1 resolution: "@emotion/weak-memoize@npm:0.3.1" checksum: ed514b3cb94bbacece4ac2450d98898066c0a0698bdeda256e312405ca53634cb83c75889b25cd8bbbe185c80f4c05a1f0a0091e1875460ba6be61d0334f0b8a @@ -3347,6 +3400,19 @@ __metadata: languageName: node linkType: hard +"@hypnosphi/create-react-context@npm:^0.3.1": + version: 0.3.1 + resolution: "@hypnosphi/create-react-context@npm:0.3.1" + dependencies: + gud: ^1.0.0 + warning: ^4.0.3 + peerDependencies: + prop-types: ^15.0.0 + react: ">=0.14.0" + checksum: e8072221f9f9c2c47c3ebc5bc6079f9a71938e181d2b4aa3e1d3922707bc097336d5260dad088cf47c1d6e1ff34839fa21f2505a95bddda0d7548c5a955b5691 + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -6535,6 +6601,20 @@ __metadata: languageName: unknown linkType: soft +"@storybook/channels@npm:7.4.0": + version: 7.4.0 + resolution: "@storybook/channels@npm:7.4.0" + dependencies: + "@storybook/client-logger": 7.4.0 + "@storybook/core-events": 7.4.0 + "@storybook/global": ^5.0.0 + qs: ^6.10.0 + telejson: ^7.2.0 + tiny-invariant: ^1.3.1 + checksum: 5f12fe715b33aaf1889a51ed2c68d37c286d0c6a1d546606451d421bf4514a66757db8171a69f09d454cf875f7c9857245939a7a70a5612517b9e88f168bbcfa + languageName: node + linkType: hard + "@storybook/channels@workspace:*, @storybook/channels@workspace:lib/channels": version: 0.0.0-use.local resolution: "@storybook/channels@workspace:lib/channels" @@ -6617,6 +6697,15 @@ __metadata: languageName: unknown linkType: soft +"@storybook/client-logger@npm:7.4.0": + version: 7.4.0 + resolution: "@storybook/client-logger@npm:7.4.0" + dependencies: + "@storybook/global": ^5.0.0 + checksum: 00d73356f506dbc85616739fd7ebbc175990664aee7e0ca0e2f2a7244337ba2ea8f0a86d7003b461548b31a514268fea32a34ea74a18299060a62e0825ceb402 + languageName: node + linkType: hard + "@storybook/client-logger@workspace:*, @storybook/client-logger@workspace:lib/client-logger": version: 0.0.0-use.local resolution: "@storybook/client-logger@workspace:lib/client-logger" @@ -6666,6 +6755,7 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/components@workspace:ui/components" dependencies: + "@chromaui/addon-visual-tests": ^0.0.49 "@popperjs/core": ^2.6.0 "@radix-ui/react-select": ^1.2.2 "@radix-ui/react-toolbar": ^1.0.4 @@ -6741,6 +6831,15 @@ __metadata: languageName: unknown linkType: soft +"@storybook/core-events@npm:7.4.0": + version: 7.4.0 + resolution: "@storybook/core-events@npm:7.4.0" + dependencies: + ts-dedent: ^2.0.0 + checksum: 6a80acb1c333363b03f69f22e5d98a50b94f11f9743cc075d0442e0153aaf2cb7fc37baee3c4b480b89e5f1f98d997e866db2800286f5252ee78dfa9f116bd6d + languageName: node + linkType: hard + "@storybook/core-events@workspace:*, @storybook/core-events@workspace:lib/core-events": version: 0.0.0-use.local resolution: "@storybook/core-events@workspace:lib/core-events" @@ -6835,6 +6934,23 @@ __metadata: languageName: unknown linkType: soft +"@storybook/csf-tools@npm:7.4.0": + version: 7.4.0 + resolution: "@storybook/csf-tools@npm:7.4.0" + dependencies: + "@babel/generator": ^7.22.9 + "@babel/parser": ^7.22.7 + "@babel/traverse": ^7.22.8 + "@babel/types": ^7.22.5 + "@storybook/csf": ^0.1.0 + "@storybook/types": 7.4.0 + fs-extra: ^11.1.0 + recast: ^0.23.1 + ts-dedent: ^2.0.0 + checksum: 85ec0953ed82dfcade9eab296e55479a71f965f740bf3befc9eb155e161e510b9ee78c8a89f729bd91887f3f988b78f03076655f26444d62155d5c6c35daefe6 + languageName: node + linkType: hard + "@storybook/csf-tools@workspace:*, @storybook/csf-tools@workspace:lib/csf-tools": version: 0.0.0-use.local resolution: "@storybook/csf-tools@workspace:lib/csf-tools" @@ -6873,6 +6989,32 @@ __metadata: languageName: node linkType: hard +"@storybook/design-system@npm:^7.15.15": + version: 7.15.15 + resolution: "@storybook/design-system@npm:7.15.15" + dependencies: + "@emotion/weak-memoize": ^0.3.0 + "@storybook/theming": ^7.0.2 + "@types/pluralize": ^0.0.29 + "@types/prismjs": ^1.16.6 + "@types/react-modal": ^3.12.1 + "@types/uuid": ^8.3.1 + copy-to-clipboard: ^3.3.1 + human-format: ^0.11.0 + pluralize: ^8.0.0 + polished: ^3.6.4 + prismjs: 1.25.0 + react-github-button: ^0.1.11 + react-modal: ^3.11.2 + react-popper-tooltip: ^2.11.1 + uuid: ^3.3.3 + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 3f2ea63556aed966e906b8e56fa0ef12fd67f919426a63350a7ecba5a6cff277b8e18362d680530f7fdfa7240c8ceb890984a0594f5129f2b9d972e991b297cf + languageName: node + linkType: hard + "@storybook/docs-mdx@npm:^0.1.0": version: 0.1.0 resolution: "@storybook/docs-mdx@npm:0.1.0" @@ -7107,6 +7249,7 @@ __metadata: "@storybook/types": "workspace:*" "@testing-library/react": ^11.2.2 "@types/semver": ^7.3.4 + browser-dtector: ^3.4.0 copy-to-clipboard: ^3.3.1 downshift: ^6.0.15 fs-extra: ^11.1.0 @@ -7508,6 +7651,7 @@ __metadata: "@storybook/core-events": "workspace:*" "@storybook/global": ^5.0.0 "@storybook/preview-api": "workspace:*" + browser-dtector: ^3.4.0 typescript: ~4.9.3 languageName: unknown linkType: soft @@ -8005,6 +8149,21 @@ __metadata: languageName: node linkType: hard +"@storybook/theming@npm:^7.0.2": + version: 7.4.0 + resolution: "@storybook/theming@npm:7.4.0" + dependencies: + "@emotion/use-insertion-effect-with-fallbacks": ^1.0.0 + "@storybook/client-logger": 7.4.0 + "@storybook/global": ^5.0.0 + memoizerific: ^1.11.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: ff67a20407d239347219017e04c8b883f120c2f3af94d1eb47992711e9578ec923c2bbd058c4c229a62bee729e33e49e9ae58681fac2d1ed5e2e38f7ec396d04 + languageName: node + linkType: hard + "@storybook/theming@workspace:*, @storybook/theming@workspace:lib/theming": version: 0.0.0-use.local resolution: "@storybook/theming@workspace:lib/theming" @@ -8030,6 +8189,19 @@ __metadata: languageName: unknown linkType: soft +"@storybook/types@npm:7.4.0": + version: 7.4.0 + resolution: "@storybook/types@npm:7.4.0" + dependencies: + "@storybook/channels": 7.4.0 + "@types/babel__core": ^7.0.0 + "@types/express": ^4.7.0 + "@types/react": ^16.14.34 + file-system-cache: 2.3.0 + checksum: 982a21466def5312dd87078abdd83c85d58445e00ea3aed7dcdf27279721eee67b390e41b91c886bb8d4364f675bfd1333d3812d1d398e66170eca1fe6015ff7 + languageName: node + linkType: hard + "@storybook/types@workspace:*, @storybook/types@workspace:lib/types": version: 0.0.0-use.local resolution: "@storybook/types@workspace:lib/types" @@ -9194,6 +9366,13 @@ __metadata: languageName: node linkType: hard +"@types/pluralize@npm:^0.0.29": + version: 0.0.29 + resolution: "@types/pluralize@npm:0.0.29" + checksum: 840796fa1db158eb4d9787758d134736e29d9a8035f5b0cbad06e3801fc64b79112ba944c83f9a1a5b94da08703f505b8315b7e0f28bfc0f8e9e1ccfead7b083 + languageName: node + linkType: hard + "@types/prettier@npm:2.7.2": version: 2.7.2 resolution: "@types/prettier@npm:2.7.2" @@ -9215,6 +9394,13 @@ __metadata: languageName: node linkType: hard +"@types/prismjs@npm:^1.16.6": + version: 1.26.0 + resolution: "@types/prismjs@npm:1.26.0" + checksum: dce1388a626c20b95fa2715917deef5a401eec33e9e181f202840ee3b3c7d8a84d5558c834af4c29b8e007741a6a18639b074db8ecccdd6e7de15280fc4dfdd2 + languageName: node + linkType: hard + "@types/prompts@npm:^2.0.9": version: 2.4.4 resolution: "@types/prompts@npm:2.4.4" @@ -9289,6 +9475,15 @@ __metadata: languageName: node linkType: hard +"@types/react-modal@npm:^3.12.1": + version: 3.16.0 + resolution: "@types/react-modal@npm:3.16.0" + dependencies: + "@types/react": "*" + checksum: ee65eb9f47e6c953bed23d716442ee1fcac3c2d586409924317043c8df2e38475c2232ef04e3c57d7d7387d7a319a97732278e92864d7363596e54fc5ebcd0cb + languageName: node + linkType: hard + "@types/react-syntax-highlighter@npm:11.0.5": version: 11.0.5 resolution: "@types/react-syntax-highlighter@npm:11.0.5" @@ -9501,6 +9696,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:^8.3.1": + version: 8.3.4 + resolution: "@types/uuid@npm:8.3.4" + checksum: b9ac98f82fcf35962317ef7dc44d9ac9e0f6fdb68121d384c88fe12ea318487d5585d3480fa003cf28be86a3bbe213ca688ba786601dce4a97724765eb5b1cf2 + languageName: node + linkType: hard + "@types/uuid@npm:^9.0.1": version: 9.0.2 resolution: "@types/uuid@npm:9.0.2" @@ -9744,6 +9946,16 @@ __metadata: languageName: node linkType: hard +"@urql/core@npm:^4.1.0": + version: 4.1.1 + resolution: "@urql/core@npm:4.1.1" + dependencies: + "@0no-co/graphql.web": ^1.0.1 + wonka: ^6.3.2 + checksum: 1f2077a0ce6cc7e34f03107f6e51f7b4ae77a9ef8f81ac37a81d61828b8ec4fa7b937eb315ead84108164ecd01e442b21f8ad3701fb8df881fa1c63cba114e68 + languageName: node + linkType: hard + "@vitejs/plugin-basic-ssl@npm:1.0.1": version: 1.0.1 resolution: "@vitejs/plugin-basic-ssl@npm:1.0.1" @@ -12074,6 +12286,13 @@ __metadata: languageName: node linkType: hard +"browser-dtector@npm:^3.4.0": + version: 3.4.0 + resolution: "browser-dtector@npm:3.4.0" + checksum: b2586d2fdccd9ab992b6cc254a65f10d54137b50edfd70bf80ecf80e8e7761e78482e10d7c3874609ab9b602bc6da7466a01b254d40ec721d341c723589aa288 + languageName: node + linkType: hard + "browser-process-hrtime@npm:^1.0.0": version: 1.0.0 resolution: "browser-process-hrtime@npm:1.0.0" @@ -12722,6 +12941,17 @@ __metadata: languageName: node linkType: hard +"chromatic@npm:6.24.0": + version: 6.24.0 + resolution: "chromatic@npm:6.24.0" + bin: + chroma: dist/bin.js + chromatic: dist/bin.js + chromatic-cli: dist/bin.js + checksum: e5ebc1ff78076e5112d79fd5f4f133febf789cb4713149507f755741cd069daf3f0a2c7f13b7b176becde7734cf010d1d7628a22f3e28087a6faa17f28ffd798 + languageName: node + linkType: hard + "chrome-trace-event@npm:^1.0.2": version: 1.0.3 resolution: "chrome-trace-event@npm:1.0.3" @@ -13967,7 +14197,7 @@ __metadata: languageName: node linkType: hard -"date-fns@npm:^2.0.1": +"date-fns@npm:^2.0.1, date-fns@npm:^2.30.0": version: 2.30.0 resolution: "date-fns@npm:2.30.0" dependencies: @@ -14076,6 +14306,20 @@ __metadata: languageName: node linkType: hard +"deep-equal@npm:^1.1.1": + version: 1.1.1 + resolution: "deep-equal@npm:1.1.1" + dependencies: + is-arguments: ^1.0.4 + is-date-object: ^1.0.1 + is-regex: ^1.0.4 + object-is: ^1.0.1 + object-keys: ^1.1.1 + regexp.prototype.flags: ^1.2.0 + checksum: 473d5dd1d707afd5ad3068864765590591b049d0e0d9a01931599dbbd820e35f09d0a42faa6e4644deb7cf6b7dc90f7bfdf5559f42279d67f714209b62036212 + languageName: node + linkType: hard + "deep-equal@npm:^2.0.5": version: 2.2.2 resolution: "deep-equal@npm:2.2.2" @@ -16156,6 +16400,13 @@ __metadata: languageName: node linkType: hard +"exenv@npm:^1.2.0": + version: 1.2.2 + resolution: "exenv@npm:1.2.2" + checksum: 4e96b355a6b9b9547237288ca779dd673b2e698458b409e88b50df09feb7c85ef94c07354b6b87bc3ed0193a94009a6f7a3c71956da12f45911c0d0f5aa3caa0 + languageName: node + linkType: hard + "exit@npm:^0.1.2": version: 0.1.2 resolution: "exit@npm:0.1.2" @@ -17827,6 +18078,13 @@ __metadata: languageName: node linkType: hard +"gud@npm:^1.0.0": + version: 1.0.0 + resolution: "gud@npm:1.0.0" + checksum: a4db6edc18e2c4e3a22dc9e639e40a4e5650d53dae9cf384a96d5380dfa17ddda376cf6b7797a5c30d140d2532e5a69d167bdb70c2c151dd673253bac6b027f3 + languageName: node + linkType: hard + "gunzip-maybe@npm:^1.4.2": version: 1.4.2 resolution: "gunzip-maybe@npm:1.4.2" @@ -18497,6 +18755,13 @@ __metadata: languageName: node linkType: hard +"human-format@npm:^0.11.0": + version: 0.11.0 + resolution: "human-format@npm:0.11.0" + checksum: 83cc87af67036b4abb6dc585533fcc232279373f8a3a7a4fc1f6d988f6aa35664f986adb818d04d9de3dee240648ec94a9944a8ab1852df21eb67c254e991ea7 + languageName: node + linkType: hard + "human-signals@npm:^2.1.0": version: 2.1.0 resolution: "human-signals@npm:2.1.0" @@ -19360,7 +19625,7 @@ __metadata: languageName: node linkType: hard -"is-regex@npm:^1.0.3, is-regex@npm:^1.0.5, is-regex@npm:^1.1.0, is-regex@npm:^1.1.4": +"is-regex@npm:^1.0.3, is-regex@npm:^1.0.4, is-regex@npm:^1.0.5, is-regex@npm:^1.1.0, is-regex@npm:^1.1.4": version: 1.1.4 resolution: "is-regex@npm:1.1.4" dependencies: @@ -25163,6 +25428,15 @@ __metadata: languageName: node linkType: hard +"polished@npm:^3.6.4": + version: 3.7.2 + resolution: "polished@npm:3.7.2" + dependencies: + "@babel/runtime": ^7.12.5 + checksum: c36439946b5bfbac16c06dd7b00a89f45e07410427344e909c540ce3ddeb9b44d2ae9cc035a9d77f4551e07b9803419ae77767aec85958a0978158a95c0115d8 + languageName: node + linkType: hard + "polished@npm:^4.2.2": version: 4.2.2 resolution: "polished@npm:4.2.2" @@ -25172,6 +25446,13 @@ __metadata: languageName: node linkType: hard +"popper.js@npm:^1.14.4": + version: 1.16.1 + resolution: "popper.js@npm:1.16.1" + checksum: 1c1a826f757edb5b8c2049dfd7a9febf6ae1e9d0e51342fc715b49a0c1020fced250d26484619883651e097c5764bbcacd2f31496e3646027f079dd83e072681 + languageName: node + linkType: hard + "portfinder@npm:^1.0.28": version: 1.0.32 resolution: "portfinder@npm:1.0.32" @@ -25521,6 +25802,13 @@ __metadata: languageName: node linkType: hard +"prismjs@npm:1.25.0": + version: 1.25.0 + resolution: "prismjs@npm:1.25.0" + checksum: 0c3853a6c815b23a07bef77700f60a40b2a12018a383a75cd7d108718717b73927c809e7dd08ac0ae9f16fbe1e005b337262bc95952cf9cfc91914f986b07bd3 + languageName: node + linkType: hard + "prismjs@npm:^1.27.0": version: 1.29.0 resolution: "prismjs@npm:1.29.0" @@ -25663,7 +25951,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:^15.5.10, prop-types@npm:^15.6.1, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -26248,6 +26536,15 @@ __metadata: languageName: node linkType: hard +"react-github-button@npm:^0.1.11": + version: 0.1.11 + resolution: "react-github-button@npm:0.1.11" + dependencies: + prop-types: ^15.5.10 + checksum: e00fa4f3b2dee74f45fff0c9d68d7d75eefa495d27a56ef2e2391f9600623d16b8a9f99c1d35a7b4f620dfb95dd8ed0b1a76fbbfece4be0843cd507c17a37dfa + languageName: node + linkType: hard + "react-helmet-async@npm:^1.0.7": version: 1.3.0 resolution: "react-helmet-async@npm:1.3.0" @@ -26301,6 +26598,13 @@ __metadata: languageName: node linkType: hard +"react-lifecycles-compat@npm:^3.0.0": + version: 3.0.4 + resolution: "react-lifecycles-compat@npm:3.0.4" + checksum: 1d0df3c85af79df720524780f00c064d53a9dd1899d785eddb7264b378026979acbddb58a4b7e06e7d0d12aa1494fd5754562ee55d32907b15601068dae82c27 + languageName: node + linkType: hard + "react-merge-refs@npm:^1.0.0": version: 1.1.0 resolution: "react-merge-refs@npm:1.1.0" @@ -26308,6 +26612,34 @@ __metadata: languageName: node linkType: hard +"react-modal@npm:^3.11.2": + version: 3.16.1 + resolution: "react-modal@npm:3.16.1" + dependencies: + exenv: ^1.2.0 + prop-types: ^15.7.2 + react-lifecycles-compat: ^3.0.0 + warning: ^4.0.3 + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 + react-dom: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 + checksum: 7b56e2c505b2b924736c471a34754a4211df40ac2d6fb0949cf095aea5e65d3326bd9f111fa7898acf40afa54f526809ad8aa47e02b8328663d11422568dc7b1 + languageName: node + linkType: hard + +"react-popper-tooltip@npm:^2.11.1": + version: 2.11.1 + resolution: "react-popper-tooltip@npm:2.11.1" + dependencies: + "@babel/runtime": ^7.9.2 + react-popper: ^1.3.7 + peerDependencies: + react: ^16.6.0 + react-dom: ^16.6.0 + checksum: f81278f1ea87899ffa57fed85c2531fa583ebca424ae5522e3a1b05c5635c014b3467391e77fb9c48bbc8e7b9f1050fa9302e8ee6134a9333858b5a6e0ae1b49 + languageName: node + linkType: hard + "react-popper-tooltip@npm:^4.4.2": version: 4.4.2 resolution: "react-popper-tooltip@npm:4.4.2" @@ -26322,6 +26654,23 @@ __metadata: languageName: node linkType: hard +"react-popper@npm:^1.3.7": + version: 1.3.11 + resolution: "react-popper@npm:1.3.11" + dependencies: + "@babel/runtime": ^7.1.2 + "@hypnosphi/create-react-context": ^0.3.1 + deep-equal: ^1.1.1 + popper.js: ^1.14.4 + prop-types: ^15.6.1 + typed-styles: ^0.0.7 + warning: ^4.0.2 + peerDependencies: + react: 0.14.x || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: d5dd1d0d4b5a3407134681b42a079fce525c94bce892ad177515d54a8cf64203eecbc30231476367e916aaff91221f5b6abd5afc207a86c698f35b7254178488 + languageName: node + linkType: hard + "react-popper@npm:^2.3.0": version: 2.3.0 resolution: "react-popper@npm:2.3.0" @@ -26816,6 +27165,13 @@ __metadata: languageName: node linkType: hard +"regenerator-runtime@npm:^0.14.0": + version: 0.14.0 + resolution: "regenerator-runtime@npm:0.14.0" + checksum: e25f062c1a183f81c99681691a342760e65c55e8d3a4d4fe347ebe72433b123754b942b70b622959894e11f8a9131dc549bd3c9a5234677db06a4af42add8d12 + languageName: node + linkType: hard + "regenerator-transform@npm:^0.15.1": version: 0.15.1 resolution: "regenerator-transform@npm:0.15.1" @@ -26842,7 +27198,7 @@ __metadata: languageName: node linkType: hard -"regexp.prototype.flags@npm:^1.4.3, regexp.prototype.flags@npm:^1.5.0": +"regexp.prototype.flags@npm:^1.2.0, regexp.prototype.flags@npm:^1.4.3, regexp.prototype.flags@npm:^1.5.0": version: 1.5.0 resolution: "regexp.prototype.flags@npm:1.5.0" dependencies: @@ -30163,6 +30519,13 @@ __metadata: languageName: node linkType: hard +"typed-styles@npm:^0.0.7": + version: 0.0.7 + resolution: "typed-styles@npm:0.0.7" + checksum: ec159f0e538364750cf9b8f19136375df64ad364fda355e6f7a7216ebffc67f18b436722c5c6853c89f70e6507eb69e5061ceb9334fa1f54902c0f6be1b985fe + languageName: node + linkType: hard + "typedarray@npm:^0.0.6": version: 0.0.6 resolution: "typedarray@npm:0.0.6" @@ -30724,6 +31087,18 @@ __metadata: languageName: node linkType: hard +"urql@npm:^4.0.3": + version: 4.0.5 + resolution: "urql@npm:4.0.5" + dependencies: + "@urql/core": ^4.1.0 + wonka: ^6.3.2 + peerDependencies: + react: ">= 16.8.0" + checksum: 9560d04b3c2fe72c921bdb21e969039b776e07999704d23bce35f815eb537c9357b6c7322a1b8cd43957550798c30cd15f5130ddd054dfd8a890d17d2be85282 + languageName: node + linkType: hard + "use-callback-ref@npm:^1.3.0": version: 1.3.0 resolution: "use-callback-ref@npm:1.3.0" @@ -30868,6 +31243,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^3.3.3": + version: 3.4.0 + resolution: "uuid@npm:3.4.0" + bin: + uuid: ./bin/uuid + checksum: 1c13950df865c4f506ebfe0a24023571fa80edf2e62364297a537c80af09c618299797bbf2dbac6b1f8ae5ad182ba474b89db61e0e85839683991f7e08795347 + languageName: node + linkType: hard + "uuid@npm:^9.0.0": version: 9.0.0 resolution: "uuid@npm:9.0.0" @@ -31481,7 +31865,7 @@ __metadata: languageName: node linkType: hard -"warning@npm:^4.0.2": +"warning@npm:^4.0.2, warning@npm:^4.0.3": version: 4.0.3 resolution: "warning@npm:4.0.3" dependencies: @@ -31953,6 +32337,13 @@ __metadata: languageName: node linkType: hard +"wonka@npm:^6.3.2": + version: 6.3.4 + resolution: "wonka@npm:6.3.4" + checksum: 77329eea673da07717476e1b8f1a22f1e1a4f261bb9a58fa446c03d3da13dbd5b254664f8aded5928d953f33ee5b399a17a4f70336e8b236e478209c0e78cda4 + languageName: node + linkType: hard + "wordwrap@npm:^1.0.0": version: 1.0.0 resolution: "wordwrap@npm:1.0.0" diff --git a/docs/configure/frameworks.md b/docs/configure/frameworks.md index 80ebc224932e..fc9010251423 100644 --- a/docs/configure/frameworks.md +++ b/docs/configure/frameworks.md @@ -64,7 +64,7 @@ Storybook is a framework-agnostic tool. It can be used with any framework. Howev ### Legacy framework support -We're deprecating support for several frameworks, including [Aurelia](https://github.com/aurelia/framework), [Marionette](https://github.com/marionettejs/backbone.marionette), [Mithril](https://github.com/MithrilJS/mithril.js), [Rax](https://github.com/alibaba/rax), and [Riot](https://github.com/riot/riot). Nevertheless, we're always looking for help maintaining these frameworks. If you're working with one of them and you want to continue supporting them, visit the dedicated [Storybook End-of-Life repository](https://github.com/storybook-eol) to learn more about the sunsetting process and for instructions on how to contribute our visit our [Discord server](https://discord.gg/storybook). +We're deprecating support for several frameworks, including [Aurelia](https://github.com/aurelia/framework), [Marionette](https://github.com/marionettejs/backbone.marionette), [Mithril](https://github.com/MithrilJS/mithril.js), [Rax](https://github.com/alibaba/rax), and [Riot](https://github.com/riot/riot). Nevertheless, we're always looking for help maintaining these frameworks. If you're working with one of them and you want to continue supporting them, visit the dedicated [Storybook End-of-Life repository](https://github.com/storybook-eol) to learn more about the sunsetting process and for instructions on how to contribute visit our [Discord server](https://discord.gg/storybook). ### Learn about configuring Storybook diff --git a/docs/essentials/actions.md b/docs/essentials/actions.md index 130b05a2e307..2ffc18444046 100644 --- a/docs/essentials/actions.md +++ b/docs/essentials/actions.md @@ -2,6 +2,8 @@ title: 'Actions' --- + + The actions addon is used to display data received by event handler (callback) arguments in your stories.