diff --git a/.github/workflows/generate-sandboxes-main.yml b/.github/workflows/generate-sandboxes-main.yml index 66cd1b800d37..474542495848 100644 --- a/.github/workflows/generate-sandboxes-main.yml +++ b/.github/workflows/generate-sandboxes-main.yml @@ -43,7 +43,7 @@ jobs: run: yarn wait-on http://localhost:6001 working-directory: ./code - name: Generate - run: yarn generate-sandboxes --local-registry + run: yarn generate-sandboxes --local-registry --exclude=angular-cli/prerelease working-directory: ./code - name: Publish run: yarn publish-sandboxes --remote=https://storybook-bot:${{ secrets.PAT_STORYBOOK_BOT}}@github.com/storybookjs/sandboxes.git --push --branch=main diff --git a/.github/workflows/generate-sandboxes-next.yml b/.github/workflows/generate-sandboxes-next.yml index f22e7cb4a50b..f6ab2f7c822f 100644 --- a/.github/workflows/generate-sandboxes-next.yml +++ b/.github/workflows/generate-sandboxes-next.yml @@ -43,7 +43,7 @@ jobs: run: yarn wait-on http://localhost:6001 working-directory: ./code - name: Generate - run: yarn generate-sandboxes --local-registry + run: yarn generate-sandboxes --local-registry --exclude=angular-cli/prerelease working-directory: ./code - name: Publish run: yarn publish-sandboxes --remote=https://storybook-bot:${{ secrets.PAT_STORYBOOK_BOT}}@github.com/storybookjs/sandboxes.git --push --branch=next diff --git a/code/frameworks/nextjs/README.md b/code/frameworks/nextjs/README.md index 756c93732e23..01b2efd88862 100644 --- a/code/frameworks/nextjs/README.md +++ b/code/frameworks/nextjs/README.md @@ -19,6 +19,7 @@ - [next/font/google](#nextfontgoogle) - [next/font/local](#nextfontlocal) - [Not supported features of next/font](#not-supported-features-of-nextfont) + - [Mocking fonts during testing](#mocking-fonts-during-testing) - [Next.js Routing](#nextjs-routing) - [Overriding defaults](#overriding-defaults) - [Global Defaults](#global-defaults) @@ -271,6 +272,51 @@ The following features are not supported (yet). Support for these features might - [preload](https://nextjs.org/docs/api-reference/next/font#preload) option gets ignored. Storybook handles Font loading its own way. - [display](https://nextjs.org/docs/api-reference/next/font#display) option gets ignored. All fonts are loaded with display set to "block" to make Storybook load the font properly. +#### Mocking fonts during testing + +Occasionally fetching fonts from Google may fail as part of your Storybook build step. It is highly recommended to mock these requests, as those failures can cause your pipeline to fail as well. Next.js [supports mocking fonts](https://github.com/vercel/next.js/blob/725ddc7371f80cca273779d37f961c3e20356f95/packages/font/src/google/fetch-css-from-google-fonts.ts#L36) via a JavaScript module located where the env var `NEXT_FONT_GOOGLE_MOCKED_RESPONSES` references. + +For example, using [GitHub Actions](https://www.chromatic.com/docs/github-actions): + +```shell + - uses: chromaui/action@v1 + env: + #👇 the location of mocked fonts to use + NEXT_FONT_GOOGLE_MOCKED_RESPONSES: ${{ github.workspace }}/mocked-google-fonts.js + with: + projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} +``` + +Your mocked fonts will look something like this: + +```js +// mocked-google-fonts.js +//👇 Mocked responses of google fonts with the URL as the key +module.exports = { + 'https://fonts.googleapis.com/css?family=Inter:wght@400;500;600;800&display=block': ` + /* cyrillic-ext */ + @font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: block; + src: url(https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZJhiJ-Ek-_EeAmM.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; + } + /* more font declarations go here */ + /* latin */ + @font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: block; + src: url(https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiJ-Ek-_EeA.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + }`, +}; +``` + ### Next.js Routing [Next.js's router](https://nextjs.org/docs/routing/introduction) is automatically stubbed for you so that when the router is interacted with, all of its interactions are automatically logged to the Actions ctions panel if you have the [Storybook actions addon](https://storybook.js.org/docs/react/essentials/actions). diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json index d2209cc030a3..a2b64993a8e5 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -95,6 +95,7 @@ "@storybook/addon-actions": "workspace:*", "@storybook/builder-webpack5": "workspace:*", "@storybook/core-common": "workspace:*", + "@storybook/core-events": "workspace:*", "@storybook/node-logger": "workspace:*", "@storybook/preset-react-webpack": "workspace:*", "@storybook/preview-api": "workspace:*", diff --git a/code/frameworks/nextjs/src/font/webpack/loader/google/get-font-face-declarations.ts b/code/frameworks/nextjs/src/font/webpack/loader/google/get-font-face-declarations.ts index 9526072fe0bf..0f8bfb1957f8 100644 --- a/code/frameworks/nextjs/src/font/webpack/loader/google/get-font-face-declarations.ts +++ b/code/frameworks/nextjs/src/font/webpack/loader/google/get-font-face-declarations.ts @@ -1,6 +1,10 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error import loaderUtils from 'next/dist/compiled/loader-utils3'; +import { + GoogleFontsDownloadError, + GoogleFontsLoadingError, +} from '@storybook/core-events/server-errors'; import type { LoaderOptions } from '../types'; const cssCache = new Map>(); @@ -33,7 +37,10 @@ export async function getFontFaceDeclarations(options: LoaderOptions) { cssCache.delete(url); } if (fontFaceCSS === null) { - throw Error(`Failed to fetch \`${fontFamily}\` from Google Fonts.`); + throw new GoogleFontsDownloadError({ + fontFamily, + url, + }); } return { @@ -45,6 +52,6 @@ export async function getFontFaceDeclarations(options: LoaderOptions) { variable, }; } catch (error) { - throw new Error("Google Fonts couldn't be loaded."); + throw new GoogleFontsLoadingError({ error, url }); } } diff --git a/code/lib/cli/src/sandbox-templates.ts b/code/lib/cli/src/sandbox-templates.ts index b91296c5025f..69e858f1d14f 100644 --- a/code/lib/cli/src/sandbox-templates.ts +++ b/code/lib/cli/src/sandbox-templates.ts @@ -296,7 +296,7 @@ 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 + // TODO: Should be removed after we merge this PR: https://github.com/storybookjs/storybook/pull/24188 inDevelopment: true, }, 'angular-cli/default-ts': { @@ -586,7 +586,8 @@ export const merged: TemplateKey[] = [ ]; export const daily: TemplateKey[] = [ ...merged, - 'angular-cli/prerelease', + // TODO: Should be re-added after we merge this PR: https://github.com/storybookjs/storybook/pull/24188 + // 'angular-cli/prerelease', 'cra/default-js', 'react-vite/default-js', 'vue3-vite/default-js', diff --git a/code/lib/core-events/src/errors/server-errors.ts b/code/lib/core-events/src/errors/server-errors.ts index fdfb28fb7f0b..951a482e9570 100644 --- a/code/lib/core-events/src/errors/server-errors.ts +++ b/code/lib/core-events/src/errors/server-errors.ts @@ -328,3 +328,43 @@ export class MissingBuilderError extends StorybookError { `; } } + +export class GoogleFontsDownloadError extends StorybookError { + readonly category = Category.FRAMEWORK_NEXTJS; + + readonly code = 1; + + public readonly documentation = + 'https://github.com/storybookjs/storybook/blob/next/code/frameworks/nextjs/README.md#nextjs-font-optimization'; + + constructor(public data: { fontFamily: string; url: string }) { + super(); + } + + template() { + return dedent` + Failed to fetch \`${this.data.fontFamily}\` from Google Fonts with URL: \`${this.data.url}\` + `; + } +} + +export class GoogleFontsLoadingError extends StorybookError { + readonly category = Category.FRAMEWORK_NEXTJS; + + readonly code = 2; + + public readonly documentation = + 'https://github.com/storybookjs/storybook/blob/next/code/frameworks/nextjs/README.md#nextjs-font-optimization'; + + constructor(public data: { error: unknown | Error; url: string }) { + super(); + } + + template() { + return dedent` + An error occurred when trying to load Google Fonts with URL \`${this.data.url}\`. + + ${this.data.error instanceof Error ? this.data.error.message : ''} + `; + } +} diff --git a/code/lib/types/src/modules/api-stories.ts b/code/lib/types/src/modules/api-stories.ts index 45acc19e35c7..b6c150242a2a 100644 --- a/code/lib/types/src/modules/api-stories.ts +++ b/code/lib/types/src/modules/api-stories.ts @@ -183,7 +183,7 @@ export interface API_StatusObject { } export type API_StatusState = Record>; -export type API_StatusUpdate = Record; +export type API_StatusUpdate = Record; export type API_FilterFunction = ( item: API_PreparedIndexEntry & { status: Record } diff --git a/code/ui/manager/src/runtime.ts b/code/ui/manager/src/runtime.ts index 21c6922ddf7e..efa348ac47e0 100644 --- a/code/ui/manager/src/runtime.ts +++ b/code/ui/manager/src/runtime.ts @@ -11,7 +11,7 @@ import { renderStorybookUI } from './index'; import { values } from './globals/runtime'; import { Keys } from './globals/types'; -import { prepareForTelemetry } from './utils/prepareForTelemetry'; +import { prepareForTelemetry, shouldSkipError } from './utils/prepareForTelemetry'; const { FEATURES, CONFIG_TYPE } = global; @@ -63,8 +63,10 @@ Object.keys(Keys).forEach((key: keyof typeof Keys) => { }); global.sendTelemetryError = (error) => { - const channel = global.__STORYBOOK_ADDONS_CHANNEL__; - channel.emit(TELEMETRY_ERROR, prepareForTelemetry(error)); + if (!shouldSkipError(error)) { + const channel = global.__STORYBOOK_ADDONS_CHANNEL__; + 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 index 2ae99ba431a1..3b28b8c42506 100644 --- a/code/ui/manager/src/utils/prepareForTelemetry.ts +++ b/code/ui/manager/src/utils/prepareForTelemetry.ts @@ -14,6 +14,19 @@ function getBrowserInfo() { return browserInfo; } +// If you're adding errors to filter, please explain why they should be filtered. +const errorMessages = [ + // It's a harmless issue with react-resize-detector that supposedly will be gone when we move to React 18. + // https://github.com/maslianok/react-resize-detector/issues/45#issuecomment-1500958024 + 'ResizeObserver loop completed with undelivered notifications.', + 'ResizeObserver loop limit exceeded', + // Safari does not seem to provide any helpful info on window.onerror + // https://bugs.webkit.org/show_bug.cgi?id=132945 + 'Script error.', +]; + +export const shouldSkipError = (error: Error) => errorMessages.includes(error?.message); + export function prepareForTelemetry( originalError: Error & { fromStorybook?: boolean; diff --git a/code/yarn.lock b/code/yarn.lock index e58d7b11388e..3aa75ca60e2f 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -7347,6 +7347,7 @@ __metadata: "@storybook/addon-actions": "workspace:*" "@storybook/builder-webpack5": "workspace:*" "@storybook/core-common": "workspace:*" + "@storybook/core-events": "workspace:*" "@storybook/node-logger": "workspace:*" "@storybook/preset-react-webpack": "workspace:*" "@storybook/preview-api": "workspace:*" diff --git a/docs/builders/builder-api.md b/docs/builders/builder-api.md index ae71323218a0..c359f7c6df6d 100644 --- a/docs/builders/builder-api.md +++ b/docs/builders/builder-api.md @@ -17,6 +17,7 @@ To opt into a builder, the user must add it as a dependency and then edit their diff --git a/docs/builders/vite.md b/docs/builders/vite.md index bb56004bb49c..d964931f66ab 100644 --- a/docs/builders/vite.md +++ b/docs/builders/vite.md @@ -18,6 +18,7 @@ Run the following command to install the builder. diff --git a/docs/snippets/common/storybook-vite-builder-install.pnpm.js.mdx b/docs/snippets/common/storybook-vite-builder-install.pnpm.js.mdx new file mode 100644 index 000000000000..d9a19eb5601e --- /dev/null +++ b/docs/snippets/common/storybook-vite-builder-install.pnpm.js.mdx @@ -0,0 +1,3 @@ +```shell +pnpm add --save-dev @storybook/builder-vite +``` diff --git a/scripts/sandbox/generate.ts b/scripts/sandbox/generate.ts index b74ae5a28d03..0e477b1a6220 100755 --- a/scripts/sandbox/generate.ts +++ b/scripts/sandbox/generate.ts @@ -202,11 +202,16 @@ const runGenerators = async ( }; export const options = createOptions({ - template: { - type: 'string', - description: 'Which template would you like to create?', + templates: { + type: 'string[]', + description: 'Which templates would you like to create?', values: Object.keys(sandboxTemplates), }, + exclude: { + type: 'string[]', + description: 'Space-delimited list of templates to exclude. Takes precedence over --templates', + promptType: false, + }, localRegistry: { type: 'boolean', description: 'Generate reproduction from local registry?', @@ -220,7 +225,8 @@ export const options = createOptions({ }); export const generate = async ({ - template, + templates, + exclude, localRegistry, debug, }: OptionValues) => { @@ -230,11 +236,11 @@ export const generate = async ({ ...configuration, })) .filter(({ dirName }) => { - if (template) { - return dirName === template; + let include = Array.isArray(templates) ? templates.includes(dirName) : true; + if (Array.isArray(exclude) && include) { + include = !exclude.includes(dirName); } - - return true; + return include; }); await runGenerators(generatorConfigs, localRegistry, debug); @@ -243,7 +249,11 @@ export const generate = async ({ if (require.main === module) { program .description('Generate sandboxes from a set of possible templates') - .option('--template