diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 90f09cc9f0d3..a31684580dca 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,7 +1,7 @@ name: Bug report 🐞 description: >- Something is broken and you have a reliable reproduction? Let us know here. - For questions, please use "Question" below. + For questions, please post in GitHub Discussion. title: '[Bug]: ' labels: - needs triage @@ -14,16 +14,26 @@ body: description: A clear and concise description of what the bug is validations: required: true - - type: textarea - id: reproduce + - type: input + id: repro-link attributes: - label: To Reproduce + label: Reproduction link description: >- - Due to the high volume of reports we receive, we can only prioritize bug reports that include a clear reproduction of the problem. Please use [storybook.new](https://storybook.new) to create one, and consult our [documentation](https://storybook.js.org/docs/react/contribute/how-to-reproduce) for guidance. Thank you for your understanding! - placeholder: >- - Please provide a link to your reproduction here. If creating a reproduction really isn't feasible, let us know and be sure to include as much detail as you can to help us understand the issue. + Please provide a link to a reproduction of the issue. We accept reproductions hosted on GitHub, CodeSandbox, and StackBlitz. Due to the high volume of reports, we prioritize those with clear reproductions. The easiest way to create a reproduction is to use [storybook.new](https://storybook.new). For detailed guidance, please refer to our [documentation](https://storybook.js.org/docs/react/contribute/how-to-reproduce). + + Important: If the provided URL is invalid (e.g., 404 error or private repository), we may close the issue. Thank you for your understanding! validations: required: true + - type: textarea + id: repro-steps + attributes: + label: Reproduction steps + description: >- + Include the steps to reproduce the issue using the provided link. Additionally, provide a clear and concise description of what you expected to happen. + placeholder: >- + 1. Go to above link + 2. Click on '....' + 3. ... - type: textarea id: system attributes: diff --git a/.github/comments/good-first-issue.md b/.github/comments/good-first-issue.md new file mode 100644 index 000000000000..ff2aa8c0873f --- /dev/null +++ b/.github/comments/good-first-issue.md @@ -0,0 +1,12 @@ +The issue was marked with the `good first issue` label by a maintainer. + +This means that it is a good candidate for someone interested in contributing to the project, but does not know where to start. + +To get started, read the [Contributing Guide](https://storybook.js.org/docs/contribute/how-to-contribute). When you are ready, open a PR and link back to this issue in the form of adding `Fixes #1234` to the PR description, where `1234` is the issue number. This will automatically close the issue when the PR gets merged, making it easier for us to keep track of what has been fixed. + +Please remember to add tests to confirm your code changes will fix the issue and we do not regress in the future. + +If you have any questions, feel free to ask below or hop onto the [Storybook Discord](https://discord.gg/storybook) and ask in the #contributing channel. We're looking forward to your contribution! ✨ + +> [!NOTE] +> There is no need to ask to be assigned or for permission (e.g. "can I work on this?"). Please, go ahead if there is no linked PR. :slightly_smiling_face: diff --git a/.github/comments/invalid-link.md b/.github/comments/invalid-link.md new file mode 100644 index 000000000000..51b084dfe35d --- /dev/null +++ b/.github/comments/invalid-link.md @@ -0,0 +1,50 @@ +We could not detect a valid reproduction link. **Make sure to follow the bug report template carefully.** + +### Why was this issue closed? + +To be able to investigate, we need access to a reproduction to identify what triggered the issue. We need a link to a **public** GitHub repository, Stackblitz or CodeSandbox. The easiest way to create a reproduction is with [storybook.new](https://storybook.new). + +The bug template that you filled out has a section called "To reproduce", which is where you should provide the link to the reproduction. + +- If you did not provide a link or the link you provided is not valid, we will close the issue. +- If you provide a link to a private repository, we will close the issue. +- If you provide a link to a repository but not in the correct section, we will close the issue. + +### What should I do? + +Depending on the reason the issue was closed, you can do the following: + +- If you did not provide a link, please open a new issue with a link to a reproduction. +- If you provided a link to a private repository, please open a new issue with a link to a public repository. +- If you provided a link to a repository but not in the correct section, please open a new issue with a link to a reproduction in the correct section. + +**In general, assume that we should not go through a lengthy onboarding process at your company code only to be able to verify an issue.** + +### My repository is private and cannot be public + +In most cases, a private repo will not be a sufficient **minimal reproduction**, as this codebase might contain a lot of unrelated parts that would make our investigation take longer. Please do **not** make it public. Instead, create a new repository using the templates above, adding the relevant code to reproduce the issue. Common things to look out for: + +- Remove any code that is not related to the issue. (pages, API routes, irrelevant components, etc.) +- Remove any dependencies that are not related to the issue. +- Remove any third-party service that would require us to sign up for an account to reproduce the issue. +- Remove any environment variables that are not related to the issue. +- Remove private packages that we do not have access to. +- If the issue is not related to a monorepo specifically, try to reproduce the issue without a complex monorepo setup + +### I did not open this issue, but it is relevant to me, what can I do to help? + +Anyone experiencing the same issue is welcome to provide a minimal reproduction following the above steps by opening a new issue. + +### I think my reproduction is good enough, why aren't you looking into it quickly? + +We look into every Storybook issue and constantly monitor open issues for new comments. + +However, sometimes we might miss one or two due to the popularity/high traffic of the repository. We apologize, and kindly ask you to refrain from tagging core maintainers, as that will usually not result in increased priority. + +Upvoting issues to show your interest will help us prioritize and address them as quickly as possible. That said, every issue is important to us, and if an issue gets closed by accident, we encourage you to open a new one linking to the old issue and we will look into it. + +### Useful Resources + +- [Create a Storybook reproduction](https://storybook.js.org/docs/react/contribute/how-to-reproduce) +- [How to create a Minimal, Complete, and Verifiable example](https://stackoverflow.com/help/mcve) +- [Contributing to Storybook](https://storybook.js.org/docs/contribute/how-to-contribute) diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml new file mode 100644 index 000000000000..adf6ad3b9bfc --- /dev/null +++ b/.github/workflows/triage.yml @@ -0,0 +1,30 @@ +name: Triage issues + +on: + issues: + types: [opened, labeled] + issue_comment: + types: [created] + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +permissions: + issues: write + +jobs: + triage: + name: Nissuer + runs-on: ubuntu-latest + steps: + - uses: balazsorban44/nissuer@1.10.0 + with: + label-comments: | + { + "good first issue": ".github/comments/good-first-issue.md" + } + reproduction-comment: ".github/comments/invalid-link.md" + reproduction-hosts: "github.com,codesandbox.io,stackblitz.com" + reproduction-link-section: "### Reproduction link(.*)### Reproduction steps" + reproduction-invalid-label: "needs reproduction" + reproduction-issue-labels: "bug,needs triage" diff --git a/CHANGELOG.md b/CHANGELOG.md index d94e871aa45c..0fdbefeb9821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 8.1.1 + +- Docgen: Only add react-docgen info when a component is defined in the file - [#26967](https://github.com/storybookjs/storybook/pull/26967), thanks @glenjamin! +- Docs: Fix MDX Stories block tag-filtering behavior - [#27144](https://github.com/storybookjs/storybook/pull/27144), thanks @shilman! +- Docs: Fix Subtitle block when no `of` prop passed - [#27147](https://github.com/storybookjs/storybook/pull/27147), thanks @JReinhold! +- Next.js: Add typing for NextImage to main framework options type - [#27105](https://github.com/storybookjs/storybook/pull/27105), thanks @valentinpalkovic! +- Next.js: Avoid conflicts with the raw loader - [#27093](https://github.com/storybookjs/storybook/pull/27093), thanks @seanparmelee! +- Types: Fix typing for main.framework/builder fields - [#27088](https://github.com/storybookjs/storybook/pull/27088), thanks @valentinpalkovic! + ## 8.1.0 Storybook 8.1 is here with a tone of new features and bug fixes: diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 9fd08c30bb5d..11b40449bfff 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,11 @@ +## 8.2.0-alpha.2 + +- Angular: Cleanup types - [#27189](https://github.com/storybookjs/storybook/pull/27189), thanks @valentinpalkovic! +- Angular: Fix filtering of workspace config styles - [#27108](https://github.com/storybookjs/storybook/pull/27108), thanks @valentinpalkovic! +- Controls: Fix grouped Radio controls to have the same name - [#23374](https://github.com/storybookjs/storybook/pull/23374), thanks @srapilly! +- Controls: Throttling makes Color control lagging - [#22615](https://github.com/storybookjs/storybook/pull/22615), thanks @gitstart! +- Docs: Fix `Typeset` Doc block `fontSizes` type - [#26475](https://github.com/storybookjs/storybook/pull/26475), thanks @noranda! + ## 8.2.0-alpha.1 - CLI: Add optional `--dev` and `--no-dev` options to `storybook init` CLI - [#26918](https://github.com/storybookjs/storybook/pull/26918), thanks @fastfrwrd! diff --git a/code/addons/docs/docs/docspage.md b/code/addons/docs/docs/docspage.md index 603652331eb6..ea50050f6b94 100644 --- a/code/addons/docs/docs/docspage.md +++ b/code/addons/docs/docs/docspage.md @@ -166,9 +166,9 @@ You can override this default behavior in `.storybook/preview.js` (or in any of export const parameters = { docs: { canvas: { - sourceState: 'shown' - } - } + sourceState: 'shown', + }, + }, }; ``` diff --git a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts index ef510ef2378f..6eca9b6a3dff 100644 --- a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts +++ b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts @@ -2,6 +2,7 @@ import { dirname, join, resolve } from 'path'; import { DefinePlugin, HotModuleReplacementPlugin, ProgressPlugin, ProvidePlugin } from 'webpack'; import type { Configuration } from 'webpack'; import HtmlWebpackPlugin from 'html-webpack-plugin'; + // @ts-expect-error (I removed this on purpose, because it's incorrect) import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin'; import TerserWebpackPlugin from 'terser-webpack-plugin'; @@ -53,7 +54,11 @@ const storybookPaths: Record = { }; export default async ( - options: Options & { typescriptOptions: TypescriptOptions } + options: Options & { + typescriptOptions: TypescriptOptions; + /* Build entries, which should not be linked in the iframe HTML file */ + excludeChunks?: string[]; + } ): Promise => { const { outputDir = join('.', 'public'), @@ -64,6 +69,7 @@ export default async ( previewUrl, typescriptOptions, features, + excludeChunks = [], } = options; const isProd = configType === 'PRODUCTION'; @@ -172,6 +178,7 @@ export default async ( alwaysWriteToDisk: true, inject: false, template, + excludeChunks, templateParameters: { version: packageJson.version, globals: { diff --git a/code/frameworks/angular/src/builders/build-storybook/index.ts b/code/frameworks/angular/src/builders/build-storybook/index.ts index 25f7faeb5268..a41c1490e41a 100644 --- a/code/frameworks/angular/src/builders/build-storybook/index.ts +++ b/code/frameworks/angular/src/builders/build-storybook/index.ts @@ -1,36 +1,22 @@ -import { - BuilderContext, - BuilderHandlerFn, - BuilderOutput, - BuilderOutputLike, - Target, - createBuilder, - targetFromTargetString, -} from '@angular-devkit/architect'; +import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; import { JsonObject } from '@angular-devkit/core'; -import { from, of, throwError } from 'rxjs'; -import { catchError, map, mapTo, switchMap } from 'rxjs/operators'; import { sync as findUpSync } from 'find-up'; import { sync as readUpSync } from 'read-pkg-up'; -import { BrowserBuilderOptions, StylePreprocessorOptions } from '@angular-devkit/build-angular'; import { CLIOptions } from '@storybook/types'; import { getEnvConfig, versions } from '@storybook/core-common'; import { addToGlobalContext } from '@storybook/telemetry'; import { buildStaticStandalone, withTelemetry } from '@storybook/core-server'; -import { - AssetPattern, - SourceMapUnion, - StyleElement, -} from '@angular-devkit/build-angular/src/builders/browser/schema'; +import { StyleClass } from '@angular-devkit/build-angular/src/builders/browser/schema'; import { StandaloneOptions } from '../utils/standalone-options'; import { runCompodoc } from '../utils/run-compodoc'; import { errorSummary, printErrorDetails } from '../utils/error-handler'; +import { AngularBuilderOptions, setup } from '../utils/setup'; addToGlobalContext('cliVersion', versions.storybook); -export type StorybookBuilderOptions = JsonObject & { +export type StorybookBuilderOptions = AngularBuilderOptions & { browserTarget?: string | null; tsConfig?: string; test: boolean; @@ -38,10 +24,6 @@ export type StorybookBuilderOptions = JsonObject & { compodoc: boolean; compodocArgs: string[]; enableProdMode?: boolean; - styles?: StyleElement[]; - stylePreprocessorOptions?: StylePreprocessorOptions; - assets?: AssetPattern[]; - sourceMap?: SourceMapUnion; } & Pick< // makes sure the option exists CLIOptions, @@ -59,112 +41,77 @@ export type StorybookBuilderOptions = JsonObject & { export type StorybookBuilderOutput = JsonObject & BuilderOutput & { [key: string]: any }; -type StandaloneBuildOptions = StandaloneOptions & { outputDir: string }; - -const commandBuilder: BuilderHandlerFn = ( - options, - context -): BuilderOutputLike => { - const builder = from(setup(options, context)).pipe( - switchMap(({ tsConfig }) => { - const docTSConfig = findUpSync('tsconfig.doc.json', { cwd: options.configDir }); - const runCompodoc$ = options.compodoc - ? runCompodoc( - { compodocArgs: options.compodocArgs, tsconfig: docTSConfig ?? tsConfig }, - context - ).pipe(mapTo({ tsConfig })) - : of({}); - - return runCompodoc$.pipe(mapTo({ tsConfig })); - }), - map(({ tsConfig }) => { - getEnvConfig(options, { - staticDir: 'SBCONFIG_STATIC_DIR', - outputDir: 'SBCONFIG_OUTPUT_DIR', - configDir: 'SBCONFIG_CONFIG_DIR', - }); - - const { - browserTarget, - stylePreprocessorOptions, - styles, - configDir, - docs, - loglevel, - test, - outputDir, - quiet, - enableProdMode = true, - webpackStatsJson, - statsJson, - debugWebpack, - disableTelemetry, - assets, - previewUrl, - sourceMap = false, - } = options; - - const standaloneOptions: StandaloneBuildOptions = { - packageJson: readUpSync({ cwd: __dirname }).packageJson, - configDir, - ...(docs ? { docs } : {}), - loglevel, - outputDir, - test, - quiet, - enableProdMode, - disableTelemetry, - angularBrowserTarget: browserTarget, - angularBuilderContext: context, - angularBuilderOptions: { - ...(stylePreprocessorOptions ? { stylePreprocessorOptions } : {}), - ...(styles ? { styles } : {}), - ...(assets ? { assets } : {}), - sourceMap, - }, - tsConfig, - webpackStatsJson, - statsJson, - debugWebpack, - previewUrl, - }; - - return standaloneOptions; - }), - switchMap((standaloneOptions) => runInstance({ ...standaloneOptions, mode: 'static' })), - map(() => { - return { success: true }; - }) - ); - - return builder as any as BuilderOutput; -}; +type StandaloneBuildOptions = StandaloneOptions & { outputDir: string; excludeChunks: string[] }; -export default createBuilder(commandBuilder); +const commandBuilder = async ( + options: StorybookBuilderOptions, + context: BuilderContext +): Promise => { + const { tsConfig, angularBuilderContext, angularBuilderOptions } = await setup(options, context); -async function setup(options: StorybookBuilderOptions, context: BuilderContext) { - let browserOptions: (JsonObject & BrowserBuilderOptions) | undefined; - let browserTarget: Target | undefined; + const docTSConfig = findUpSync('tsconfig.doc.json', { cwd: options.configDir }); - if (options.browserTarget) { - browserTarget = targetFromTargetString(options.browserTarget); - browserOptions = await context.validateOptions( - await context.getTargetOptions(browserTarget), - await context.getBuilderNameForTarget(browserTarget) + if (options.compodoc) { + await runCompodoc( + { compodocArgs: options.compodocArgs, tsconfig: docTSConfig ?? tsConfig }, + context ); } - return { - tsConfig: - options.tsConfig ?? - findUpSync('tsconfig.json', { cwd: options.configDir }) ?? - browserOptions.tsConfig, + getEnvConfig(options, { + staticDir: 'SBCONFIG_STATIC_DIR', + outputDir: 'SBCONFIG_OUTPUT_DIR', + configDir: 'SBCONFIG_CONFIG_DIR', + }); + + const { + configDir, + docs, + loglevel, + test, + outputDir, + quiet, + enableProdMode = true, + webpackStatsJson, + statsJson, + debugWebpack, + disableTelemetry, + previewUrl, + } = options; + + const standaloneOptions: StandaloneBuildOptions = { + packageJson: readUpSync({ cwd: __dirname }).packageJson, + configDir, + ...(docs ? { docs } : {}), + excludeChunks: angularBuilderOptions.styles + ?.filter((style) => typeof style !== 'string' && style.inject === false) + .map((s: StyleClass) => s.bundleName), + loglevel, + outputDir, + test, + quiet, + enableProdMode, + disableTelemetry, + angularBrowserTarget: options.browserTarget, + angularBuilderContext, + angularBuilderOptions, + tsConfig, + webpackStatsJson, + statsJson, + debugWebpack, + previewUrl, }; -} -function runInstance(options: StandaloneBuildOptions) { - return from( - withTelemetry( + await runInstance({ ...standaloneOptions, mode: 'static' }); + + return { success: true }; +}; + +export default createBuilder(commandBuilder); + +async function runInstance(options: StandaloneBuildOptions) { + try { + await withTelemetry( 'build', { cliOptions: options, @@ -172,6 +119,8 @@ function runInstance(options: StandaloneBuildOptions) { printError: printErrorDetails, }, () => buildStaticStandalone(options) - ) - ).pipe(catchError((error: any) => throwError(errorSummary(error)))); + ); + } catch (error) { + throw new Error(errorSummary(error)); + } } diff --git a/code/frameworks/angular/src/builders/start-storybook/index.ts b/code/frameworks/angular/src/builders/start-storybook/index.ts index 2ecbb63c8a0f..194674b0ffd5 100644 --- a/code/frameworks/angular/src/builders/start-storybook/index.ts +++ b/code/frameworks/angular/src/builders/start-storybook/index.ts @@ -1,15 +1,6 @@ -import { - BuilderContext, - BuilderHandlerFn, - BuilderOutput, - Target, - createBuilder, - targetFromTargetString, -} from '@angular-devkit/architect'; +import { BuilderHandlerFn, BuilderOutput, createBuilder } from '@angular-devkit/architect'; import { JsonObject } from '@angular-devkit/core'; -import { BrowserBuilderOptions, StylePreprocessorOptions } from '@angular-devkit/build-angular'; -import { from, Observable, of } from 'rxjs'; -import { map, switchMap, mapTo } from 'rxjs/operators'; +import { StylePreprocessorOptions } from '@angular-devkit/build-angular'; import { sync as findUpSync } from 'find-up'; import { sync as readUpSync } from 'read-pkg-up'; @@ -20,24 +11,22 @@ import { buildDevStandalone, withTelemetry } from '@storybook/core-server'; import { AssetPattern, SourceMapUnion, + StyleClass, StyleElement, } from '@angular-devkit/build-angular/src/builders/browser/schema'; import { StandaloneOptions } from '../utils/standalone-options'; import { runCompodoc } from '../utils/run-compodoc'; import { printErrorDetails, errorSummary } from '../utils/error-handler'; +import { AngularBuilderOptions, setup } from '../utils/setup'; addToGlobalContext('cliVersion', versions.storybook); -export type StorybookBuilderOptions = JsonObject & { +export type StorybookBuilderOptions = AngularBuilderOptions & { browserTarget?: string | null; tsConfig?: string; compodoc: boolean; compodocArgs: string[]; enableProdMode?: boolean; - styles?: StyleElement[]; - stylePreprocessorOptions?: StylePreprocessorOptions; - assets?: AssetPattern[]; - sourceMap?: SourceMapUnion; } & Pick< // makes sure the option exists CLIOptions, @@ -64,131 +53,96 @@ export type StorybookBuilderOptions = JsonObject & { export type StorybookBuilderOutput = JsonObject & BuilderOutput & {}; -const commandBuilder: BuilderHandlerFn = (options, context) => { - const builder = from(setup(options, context)).pipe( - switchMap(({ tsConfig }) => { - const docTSConfig = findUpSync('tsconfig.doc.json', { cwd: options.configDir }); - - const runCompodoc$ = options.compodoc - ? runCompodoc( - { - compodocArgs: [...options.compodocArgs, ...(options.quiet ? ['--silent'] : [])], - tsconfig: docTSConfig ?? tsConfig, - }, - context - ).pipe(mapTo({ tsConfig })) - : of({}); +const commandBuilder: BuilderHandlerFn = async (options, context) => { + const { tsConfig, angularBuilderContext, angularBuilderOptions } = await setup(options, context); - return runCompodoc$.pipe(mapTo({ tsConfig })); - }), - map(({ tsConfig }) => { - getEnvConfig(options, { - port: 'SBCONFIG_PORT', - host: 'SBCONFIG_HOSTNAME', - staticDir: 'SBCONFIG_STATIC_DIR', - configDir: 'SBCONFIG_CONFIG_DIR', - ci: 'CI', - }); + const docTSConfig = findUpSync('tsconfig.doc.json', { cwd: options.configDir }); - options.port = parseInt(`${options.port}`, 10); + if (options.compodoc) { + await runCompodoc( + { + compodocArgs: [...options.compodocArgs, ...(options.quiet ? ['--silent'] : [])], + tsconfig: docTSConfig ?? tsConfig, + }, + context + ); + } - const { - browserTarget, - stylePreprocessorOptions, - styles, - ci, - configDir, - docs, - host, - https, - port, - quiet, - enableProdMode = false, - smokeTest, - sslCa, - sslCert, - sslKey, - disableTelemetry, - assets, - initialPath, - open, - debugWebpack, - loglevel, - webpackStatsJson, - statsJson, - previewUrl, - sourceMap = false, - } = options; + getEnvConfig(options, { + port: 'SBCONFIG_PORT', + host: 'SBCONFIG_HOSTNAME', + staticDir: 'SBCONFIG_STATIC_DIR', + configDir: 'SBCONFIG_CONFIG_DIR', + ci: 'CI', + }); - const standaloneOptions: StandaloneOptions = { - packageJson: readUpSync({ cwd: __dirname }).packageJson, - ci, - configDir, - ...(docs ? { docs } : {}), - host, - https, - port, - quiet, - enableProdMode, - smokeTest, - sslCa, - sslCert, - sslKey, - disableTelemetry, - angularBrowserTarget: browserTarget, - angularBuilderContext: context, - angularBuilderOptions: { - ...(stylePreprocessorOptions ? { stylePreprocessorOptions } : {}), - ...(styles ? { styles } : {}), - ...(assets ? { assets } : {}), - sourceMap, - }, - tsConfig, - initialPath, - open, - debugWebpack, - webpackStatsJson, - statsJson, - loglevel, - previewUrl, - }; + options.port = parseInt(`${options.port}`, 10); + + const { + browserTarget, + ci, + configDir, + docs, + host, + https, + port, + quiet, + enableProdMode = false, + smokeTest, + sslCa, + sslCert, + sslKey, + disableTelemetry, + initialPath, + open, + debugWebpack, + loglevel, + webpackStatsJson, + statsJson, + previewUrl, + } = options; + + const standaloneOptions: StandaloneOptions = { + packageJson: readUpSync({ cwd: __dirname }).packageJson, + ci, + configDir, + ...(docs ? { docs } : {}), + excludeChunks: angularBuilderOptions.styles + ?.filter((style) => typeof style !== 'string' && style.inject === false) + .map((s: StyleClass) => s.bundleName), + host, + https, + port, + quiet, + enableProdMode, + smokeTest, + sslCa, + sslCert, + sslKey, + disableTelemetry, + angularBrowserTarget: browserTarget, + angularBuilderContext, + angularBuilderOptions, + tsConfig, + initialPath, + open, + debugWebpack, + webpackStatsJson, + statsJson, + loglevel, + previewUrl, + }; - return standaloneOptions; - }), - switchMap((standaloneOptions) => runInstance(standaloneOptions)), - map((port: number) => { - return { success: true, info: { port } }; - }) - ); + const devPort = await runInstance(standaloneOptions); - return builder as any as BuilderOutput; + return { success: true, info: { port: devPort } }; }; export default createBuilder(commandBuilder); -async function setup(options: StorybookBuilderOptions, context: BuilderContext) { - let browserOptions: (JsonObject & BrowserBuilderOptions) | undefined; - let browserTarget: Target | undefined; - - if (options.browserTarget) { - browserTarget = targetFromTargetString(options.browserTarget); - browserOptions = await context.validateOptions( - await context.getTargetOptions(browserTarget), - await context.getBuilderNameForTarget(browserTarget) - ); - } - - return { - tsConfig: - options.tsConfig ?? - findUpSync('tsconfig.json', { cwd: options.configDir }) ?? - browserOptions.tsConfig, - }; -} -function runInstance(options: StandaloneOptions) { - return new Observable((observer) => { - // This Observable intentionally never complete, leaving the process running ;) - withTelemetry( +async function runInstance(options: StandaloneOptions): Promise { + try { + const { port } = await withTelemetry( 'dev', { cliOptions: options, @@ -196,10 +150,9 @@ function runInstance(options: StandaloneOptions) { printError: printErrorDetails, }, () => buildDevStandalone(options) - ) - .then(({ port }) => observer.next(port)) - .catch((error) => { - observer.error(errorSummary(error)); - }); - }); + ); + return port; + } catch (error) { + throw new Error(errorSummary(error)); + } } diff --git a/code/frameworks/angular/src/builders/utils/run-compodoc.spec.ts b/code/frameworks/angular/src/builders/utils/run-compodoc.spec.ts index cf0686b11b1d..a8f7428e7cec 100644 --- a/code/frameworks/angular/src/builders/utils/run-compodoc.spec.ts +++ b/code/frameworks/angular/src/builders/utils/run-compodoc.spec.ts @@ -1,7 +1,6 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { vi, describe, afterEach, it, expect } from 'vitest'; import { LoggerApi } from '@angular-devkit/core/src/logger'; -import { take } from 'rxjs/operators'; import { BuilderContext } from '@angular-devkit/architect'; import { runCompodoc } from './run-compodoc'; @@ -37,15 +36,13 @@ describe('runCompodoc', () => { } as BuilderContext; it('should run compodoc with tsconfig from context', async () => { - runCompodoc( + await runCompodoc( { compodocArgs: [], tsconfig: 'path/to/tsconfig.json', }, builderContextMock - ) - .pipe(take(1)) - .subscribe(); + ); expect(mockRunScript).toHaveBeenCalledWith( 'compodoc', @@ -56,15 +53,13 @@ describe('runCompodoc', () => { }); it('should run compodoc with tsconfig from compodocArgs', async () => { - runCompodoc( + await runCompodoc( { compodocArgs: ['-p', 'path/to/tsconfig.stories.json'], tsconfig: 'path/to/tsconfig.json', }, builderContextMock - ) - .pipe(take(1)) - .subscribe(); + ); expect(mockRunScript).toHaveBeenCalledWith( 'compodoc', @@ -75,15 +70,13 @@ describe('runCompodoc', () => { }); it('should run compodoc with default output folder.', async () => { - runCompodoc( + await runCompodoc( { compodocArgs: [], tsconfig: 'path/to/tsconfig.json', }, builderContextMock - ) - .pipe(take(1)) - .subscribe(); + ); expect(mockRunScript).toHaveBeenCalledWith( 'compodoc', @@ -94,15 +87,13 @@ describe('runCompodoc', () => { }); it('should run with custom output folder specified with --output compodocArgs', async () => { - runCompodoc( + await runCompodoc( { compodocArgs: ['--output', 'path/to/customFolder'], tsconfig: 'path/to/tsconfig.json', }, builderContextMock - ) - .pipe(take(1)) - .subscribe(); + ); expect(mockRunScript).toHaveBeenCalledWith( 'compodoc', @@ -113,15 +104,13 @@ describe('runCompodoc', () => { }); it('should run with custom output folder specified with -d compodocArgs', async () => { - runCompodoc( + await runCompodoc( { compodocArgs: ['-d', 'path/to/customFolder'], tsconfig: 'path/to/tsconfig.json', }, builderContextMock - ) - .pipe(take(1)) - .subscribe(); + ); expect(mockRunScript).toHaveBeenCalledWith( 'compodoc', diff --git a/code/frameworks/angular/src/builders/utils/run-compodoc.ts b/code/frameworks/angular/src/builders/utils/run-compodoc.ts index e926c041bfa8..512f884b408d 100644 --- a/code/frameworks/angular/src/builders/utils/run-compodoc.ts +++ b/code/frameworks/angular/src/builders/utils/run-compodoc.ts @@ -1,5 +1,4 @@ import { BuilderContext } from '@angular-devkit/architect'; -import { Observable } from 'rxjs'; import * as path from 'path'; import { JsPackageManagerFactory } from '@storybook/core-common'; @@ -13,34 +12,30 @@ const toRelativePath = (pathToTsConfig: string) => { return path.isAbsolute(pathToTsConfig) ? path.relative('.', pathToTsConfig) : pathToTsConfig; }; -export const runCompodoc = ( +export const runCompodoc = async ( { compodocArgs, tsconfig }: { compodocArgs: string[]; tsconfig: string }, context: BuilderContext -): Observable => { - return new Observable((observer) => { - const tsConfigPath = toRelativePath(tsconfig); - const finalCompodocArgs = [ - ...(hasTsConfigArg(compodocArgs) ? [] : ['-p', tsConfigPath]), - ...(hasOutputArg(compodocArgs) ? [] : ['-d', `${context.workspaceRoot || '.'}`]), - ...compodocArgs, - ]; +): Promise => { + const tsConfigPath = toRelativePath(tsconfig); + const finalCompodocArgs = [ + ...(hasTsConfigArg(compodocArgs) ? [] : ['-p', tsConfigPath]), + ...(hasOutputArg(compodocArgs) ? [] : ['-d', `${context.workspaceRoot || '.'}`]), + ...compodocArgs, + ]; - const packageManager = JsPackageManagerFactory.getPackageManager(); + const packageManager = JsPackageManagerFactory.getPackageManager(); - try { - const stdout = packageManager.runPackageCommandSync( - 'compodoc', - finalCompodocArgs, - context.workspaceRoot, - 'inherit' - ); + try { + const stdout = packageManager.runPackageCommandSync( + 'compodoc', + finalCompodocArgs, + context.workspaceRoot, + 'inherit' + ); - context.logger.info(stdout); - observer.next(); - observer.complete(); - } catch (e) { - context.logger.error(e); - observer.error(); - } - }); + context.logger.info(stdout); + } catch (e) { + context.logger.error(e); + throw e; + } }; diff --git a/code/frameworks/angular/src/builders/utils/setup.ts b/code/frameworks/angular/src/builders/utils/setup.ts new file mode 100644 index 000000000000..976f1115c061 --- /dev/null +++ b/code/frameworks/angular/src/builders/utils/setup.ts @@ -0,0 +1,109 @@ +import { Target, targetFromTargetString } from '@angular-devkit/architect'; +import { BuilderContext } from '@angular-devkit/architect'; +import { JsonObject, logging } from '@angular-devkit/core'; +import { sync as findUpSync } from 'find-up'; +import { BrowserBuilderOptions } from '@angular-devkit/build-angular'; +import { logger } from '@storybook/node-logger'; + +export type AngularBuilderOptions = BrowserBuilderOptions & { + browserTarget?: string | null; + configDir?: string; +}; + +export async function setup( + { stylePreprocessorOptions, styles, assets, sourceMap, ...options }: AngularBuilderOptions, + context: BuilderContext +) { + let browserOptions: BrowserBuilderOptions | undefined; + let browserTarget: Target | undefined; + + if (options.browserTarget) { + browserTarget = targetFromTargetString(options.browserTarget); + browserOptions = await context.validateOptions( + await context.getTargetOptions(browserTarget), + await context.getBuilderNameForTarget(browserTarget) + ); + } + + const tsConfig = + options.tsConfig ?? + findUpSync('tsconfig.json', { cwd: options.configDir }) ?? + browserOptions.tsConfig; + + const angularBuilderContext = getBuilderContext(context); + + const angularBuilderOptions = await getBuilderOptions( + options.browserTarget, + { + ...options, + ...(stylePreprocessorOptions ? { stylePreprocessorOptions } : {}), + ...(styles ? { styles } : {}), + ...(assets ? { assets } : {}), + sourceMap: sourceMap ?? false, + }, + tsConfig, + options.configDir, + angularBuilderContext + ); + + return { + tsConfig, + angularBuilderContext, + angularBuilderOptions, + }; +} + +/** + * Get Builder Context + * If storybook is not start by angular builder create dumb BuilderContext + */ +function getBuilderContext(builderContext: BuilderContext): BuilderContext { + return ( + builderContext ?? + ({ + target: { project: 'noop-project', builder: '', options: {} }, + workspaceRoot: process.cwd(), + getProjectMetadata: () => ({}), + getTargetOptions: () => ({}), + logger: new logging.Logger('Storybook'), + } as unknown as BuilderContext) + ); +} + +/** + * Get builder options + * Merge target options from browser target and from storybook options + */ +async function getBuilderOptions( + angularBrowserTarget: string, + angularBuilderOptions: AngularBuilderOptions, + tsConfig: string, + configDir: string, + builderContext: BuilderContext +): Promise { + /** + * Get Browser Target options + */ + let browserTargetOptions: JsonObject = {}; + + if (angularBrowserTarget) { + const browserTarget = targetFromTargetString(angularBrowserTarget); + + browserTargetOptions = await builderContext.getTargetOptions(browserTarget); + } + + /** + * Merge target options from browser target options and from storybook options + */ + const builderOptions = { + ...browserTargetOptions, + ...angularBuilderOptions, + tsConfig: + tsConfig ?? + findUpSync('tsconfig.json', { cwd: configDir }) ?? + (browserTargetOptions.tsConfig as string), + }; + logger.info(`=> Using angular project with "tsConfig:${builderOptions.tsConfig}"`); + + return builderOptions; +} diff --git a/code/frameworks/angular/src/builders/utils/standalone-options.ts b/code/frameworks/angular/src/builders/utils/standalone-options.ts index ef73d78f01b5..9889797bcbe3 100644 --- a/code/frameworks/angular/src/builders/utils/standalone-options.ts +++ b/code/frameworks/angular/src/builders/utils/standalone-options.ts @@ -1,11 +1,7 @@ import { BuilderContext } from '@angular-devkit/architect'; -import { - AssetPattern, - SourceMapUnion, - StyleElement, - StylePreprocessorOptions, -} from '@angular-devkit/build-angular/src/builders/browser/schema'; + import { LoadOptions, CLIOptions, BuilderOptions } from '@storybook/types'; +import { AngularBuilderOptions } from './setup'; export type StandaloneOptions = CLIOptions & LoadOptions & @@ -13,12 +9,8 @@ export type StandaloneOptions = CLIOptions & mode?: 'static' | 'dev'; enableProdMode: boolean; angularBrowserTarget?: string | null; - angularBuilderOptions?: Record & { - styles?: StyleElement[]; - stylePreprocessorOptions?: StylePreprocessorOptions; - assets?: AssetPattern[]; - sourceMap?: SourceMapUnion; - }; + angularBuilderOptions?: AngularBuilderOptions; angularBuilderContext?: BuilderContext | null; tsConfig?: string; + excludeChunks?: string[]; }; diff --git a/code/frameworks/angular/src/server/angular-cli-webpack.d.ts b/code/frameworks/angular/src/server/angular-cli-webpack.d.ts index b2f79e4d963a..8450a946025a 100644 --- a/code/frameworks/angular/src/server/angular-cli-webpack.d.ts +++ b/code/frameworks/angular/src/server/angular-cli-webpack.d.ts @@ -1,7 +1,7 @@ -import { JsonObject } from '@angular-devkit/core'; import { BuilderContext } from '@angular-devkit/architect'; +import { AngularBuilderOptions } from '../builders/utils/setup'; export declare function getWebpackConfig( baseConfig: any, - options: { builderOptions: JsonObject; builderContext: BuilderContext } + options: { builderOptions: AngularBuilderOptions; builderContext: BuilderContext } ): any; diff --git a/code/frameworks/angular/src/server/angular-cli-webpack.js b/code/frameworks/angular/src/server/angular-cli-webpack.js index 621680125536..32b31c1418a1 100644 --- a/code/frameworks/angular/src/server/angular-cli-webpack.js +++ b/code/frameworks/angular/src/server/angular-cli-webpack.js @@ -56,6 +56,7 @@ exports.getWebpackConfig = async (baseConfig, { builderOptions, builderContext } */ const { getCommonConfig, getStylesConfig, getDevServerConfig, getTypeScriptConfig } = getAngularWebpackUtils(); + const { config: cliConfig } = await generateI18nBrowserWebpackConfigFromContext( { // Default options @@ -65,10 +66,15 @@ exports.getWebpackConfig = async (baseConfig, { builderOptions, builderContext } // Options provided by user ...builderOptions, - styles: builderOptions.styles - ?.map((style) => (typeof style === 'string' ? style : style.input)) - .filter((style) => typeof style === 'string' || style.inject !== false), - + styles: builderOptions.styles?.map((style) => + typeof style === 'string' + ? { + input: style, + inject: true, + bundleName: style.split('/').pop(), + } + : style + ), // Fixed options optimization: false, namedChunks: false, 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 059d8b30f4d1..9c52447b3faf 100644 --- a/code/frameworks/angular/src/server/framework-preset-angular-cli.ts +++ b/code/frameworks/angular/src/server/framework-preset-angular-cli.ts @@ -1,9 +1,6 @@ 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 { JsonObject, logging } from '@angular-devkit/core'; import { getWebpackConfig as getCustomWebpackConfig } from './angular-cli-webpack'; import { moduleIsAvailable } from './utils/module-is-available'; @@ -17,74 +14,15 @@ export async function webpackFinal(baseConfig: webpack.Configuration, options: P checkForLegacyBuildOptions(options); - const builderContext = getBuilderContext(options); - const builderOptions = await getBuilderOptions(options, builderContext); - return getCustomWebpackConfig(baseConfig, { builderOptions: { watch: options.configType === 'DEVELOPMENT', - ...builderOptions, + ...options.angularBuilderOptions, }, - builderContext, + builderContext: options.angularBuilderContext, }); } -/** - * Get Builder Context - * If storybook is not start by angular builder create dumb BuilderContext - */ -function getBuilderContext(options: PresetOptions): BuilderContext { - return ( - options.angularBuilderContext ?? - ({ - target: { project: 'noop-project', builder: '', options: {} }, - workspaceRoot: process.cwd(), - getProjectMetadata: () => ({}), - getTargetOptions: () => ({}), - logger: new logging.Logger('Storybook'), - } as unknown as BuilderContext) - ); -} - -/** - * Get builder options - * Merge target options from browser target and from storybook options - */ -async function getBuilderOptions( - options: PresetOptions, - builderContext: BuilderContext -): Promise { - /** - * Get Browser Target options - */ - let browserTargetOptions: JsonObject = {}; - if (options.angularBrowserTarget) { - const browserTarget = targetFromTargetString(options.angularBrowserTarget); - - logger.info( - `=> Using angular browser target options from "${browserTarget.project}:${ - browserTarget.target - }${browserTarget.configuration ? `:${browserTarget.configuration}` : ''}"` - ); - browserTargetOptions = await builderContext.getTargetOptions(browserTarget); - } - - /** - * Merge target options from browser target options and from storybook options - */ - const builderOptions = { - ...browserTargetOptions, - ...(options.angularBuilderOptions as JsonObject), - tsConfig: - options.tsConfig ?? - findUpSync('tsconfig.json', { cwd: options.configDir }) ?? - browserTargetOptions.tsConfig, - }; - logger.info(`=> Using angular project with "tsConfig:${builderOptions.tsConfig}"`); - - return builderOptions; -} - /** * Checks if using legacy configuration that doesn't use builder and logs message referring to migration docs. */ diff --git a/code/frameworks/angular/src/server/preset-options.ts b/code/frameworks/angular/src/server/preset-options.ts index 5412d5a19482..c854b2834e39 100644 --- a/code/frameworks/angular/src/server/preset-options.ts +++ b/code/frameworks/angular/src/server/preset-options.ts @@ -1,17 +1,13 @@ import { Options as CoreOptions } from '@storybook/types'; import { BuilderContext } from '@angular-devkit/architect'; -import { StylePreprocessorOptions } from '@angular-devkit/build-angular'; -import { StyleElement } from '@angular-devkit/build-angular/src/builders/browser/schema'; +import { AngularBuilderOptions } from '../builders/utils/setup'; export type PresetOptions = CoreOptions & { /* Allow to get the options of a targeted "browser builder" */ angularBrowserTarget?: string | null; /* Defined set of options. These will take over priority from angularBrowserTarget options */ - angularBuilderOptions?: { - styles?: StyleElement[]; - stylePreprocessorOptions?: StylePreprocessorOptions; - }; + angularBuilderOptions?: AngularBuilderOptions; /* Angular context from builder */ angularBuilderContext?: BuilderContext | null; tsConfig?: string; diff --git a/code/lib/core-common/src/utils/get-renderer-name.test.ts b/code/lib/core-common/src/utils/get-renderer-name.test.ts index f5d10959405c..09c31f36b4f1 100644 --- a/code/lib/core-common/src/utils/get-renderer-name.test.ts +++ b/code/lib/core-common/src/utils/get-renderer-name.test.ts @@ -1,15 +1,14 @@ -import { it } from 'node:test'; -import { describe, expect } from 'vitest'; +import { describe, expect, test } from 'vitest'; import { extractProperRendererNameFromFramework } from './get-renderer-name'; describe('get-renderer-name', () => { describe('extractProperRendererNameFromFramework', () => { - it('should return the renderer name for a known framework', async () => { - const renderer = await extractProperRendererNameFromFramework('@storybook/react'); + test('should return the renderer name for a known framework', async () => { + const renderer = await extractProperRendererNameFromFramework('@storybook/react-vite'); expect(renderer).toEqual('react'); }); - it('should return null for an unknown framework', async () => { + test('should return null for an unknown framework', async () => { const renderer = await extractProperRendererNameFromFramework('@third-party/framework'); expect(renderer).toBeNull(); }); diff --git a/code/package.json b/code/package.json index 9295a38225d7..e8d7673ba5bd 100644 --- a/code/package.json +++ b/code/package.json @@ -50,7 +50,7 @@ "storybook:blocks:chromatic": "STORYBOOK_BLOCKS_ONLY=true yarn storybook:ui:chromatic --project-token=${CHROMATIC_TOKEN_STORYBOOK_BLOCKS:-MISSING_PROJECT_TOKEN}", "storybook:ui": "NODE_OPTIONS=\"--preserve-symlinks --preserve-symlinks-main\" ./lib/cli/bin/index.js dev --port 6006 --config-dir ./ui/.storybook", "storybook:ui:build": "NODE_OPTIONS=\"--preserve-symlinks --preserve-symlinks-main\" ./lib/cli/bin/index.js build --config-dir ./ui/.storybook --webpack-stats-json", - "storybook:ui:chromatic": "../scripts/node_modules/.bin/chromatic --build-script-name storybook:ui:build --storybook-base-dir ./ --project-token=${CHROMATIC_TOKEN_STORYBOOK_UI:-MISSING_PROJECT_TOKEN} --only-changed --exit-zero-on-changes --exit-once-uploaded", + "storybook:ui:chromatic": "../scripts/node_modules/.bin/chromatic --build-script-name storybook:ui:build --storybook-base-dir ./ --project-token=${CHROMATIC_TOKEN_STORYBOOK_UI:-MISSING_PROJECT_TOKEN} --exit-zero-on-changes --exit-once-uploaded", "task": "yarn --cwd ../scripts task", "test": "NODE_OPTIONS=--max_old_space_size=4096 vitest run", "test:watch": "NODE_OPTIONS=--max_old_space_size=4096 vitest watch" @@ -299,5 +299,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.2.0-alpha.2" } diff --git a/code/ui/blocks/src/components/Typeset.tsx b/code/ui/blocks/src/components/Typeset.tsx index 8fbfca58b568..76b70c998859 100644 --- a/code/ui/blocks/src/components/Typeset.tsx +++ b/code/ui/blocks/src/components/Typeset.tsx @@ -35,7 +35,7 @@ const Wrapper = styled.div(withReset, ({ theme }) => ({ export interface TypesetProps { fontFamily?: string; - fontSizes: string[]; + fontSizes: (string | number)[]; fontWeight?: number; sampleText?: string; } diff --git a/code/ui/blocks/src/controls/Color.tsx b/code/ui/blocks/src/controls/Color.tsx index b0ab993f3cff..1ac10a7531b2 100644 --- a/code/ui/blocks/src/controls/Color.tsx +++ b/code/ui/blocks/src/controls/Color.tsx @@ -2,7 +2,7 @@ import type { FC, ChangeEvent, FocusEvent } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { HexColorPicker, HslaStringColorPicker, RgbaStringColorPicker } from 'react-colorful'; import convert from 'color-convert'; -import throttle from 'lodash/throttle.js'; +import debounce from 'lodash/debounce.js'; import { styled } from '@storybook/theming'; import { TooltipNote, WithTooltip, Form } from '@storybook/components'; @@ -317,10 +317,10 @@ export const ColorControl: FC = ({ startOpen = false, argType, }) => { - const throttledOnChange = useCallback(throttle(onChange, 200), [onChange]); + const debouncedOnChange = useCallback(debounce(onChange, 200), [onChange]); const { value, realValue, updateValue, color, colorSpace, cycleColorSpace } = useColorInput( initialValue, - throttledOnChange + debouncedOnChange ); const { presets, addPreset } = usePresets(presetColors, color, colorSpace); const Picker = ColorPicker[colorSpace]; diff --git a/code/ui/blocks/src/controls/options/Radio.tsx b/code/ui/blocks/src/controls/options/Radio.tsx index 76c3239e747a..022c23ccb7b3 100644 --- a/code/ui/blocks/src/controls/options/Radio.tsx +++ b/code/ui/blocks/src/controls/options/Radio.tsx @@ -86,7 +86,7 @@ export const RadioControl: FC = ({ onChange(options[e.currentTarget.value])} diff --git a/code/ui/manager/src/components/notifications/NotificationItem.stories.tsx b/code/ui/manager/src/components/notifications/NotificationItem.stories.tsx index a3b87acdcf52..5ed3ee4b25cc 100644 --- a/code/ui/manager/src/components/notifications/NotificationItem.stories.tsx +++ b/code/ui/manager/src/components/notifications/NotificationItem.stories.tsx @@ -99,8 +99,8 @@ export const Clickable: Story = { }, play: async ({ args, canvasElement }) => { const canvas = within(canvasElement); - const [button] = await canvas.findAllByRole('button'); - await userEvent.click(button); + const notification = await canvas.findByText('Storybook cool!'); + await userEvent.click(notification); await expect(args.notification.onClick).toHaveBeenCalledWith({ onDismiss: expect.anything() }); }, }; diff --git a/code/ui/manager/src/components/sidebar/FileSearchList.stories.tsx b/code/ui/manager/src/components/sidebar/FileSearchList.stories.tsx index f2fb798d5992..597b8b82581c 100644 --- a/code/ui/manager/src/components/sidebar/FileSearchList.stories.tsx +++ b/code/ui/manager/src/components/sidebar/FileSearchList.stories.tsx @@ -43,32 +43,38 @@ export const WithResults: Story = { const exportedElement1 = await findByText(canvasElement, 'module-multiple-exports'); fireEvent.click(exportedElement1); - expect(args.onNewStory).toHaveBeenCalledWith({ - selectedItemId: 'src/module-multiple-exports.js_0', - componentExportName: 'default', - componentFilePath: 'src/module-multiple-exports.js', - componentIsDefaultExport: true, - }); + expect(args.onNewStory).toHaveBeenCalledWith( + expect.objectContaining({ + selectedItemId: 'src/module-multiple-exports.js_0', + componentExportName: 'default', + componentFilePath: 'src/module-multiple-exports.js', + componentIsDefaultExport: true, + }) + ); const exportedElement2 = await findByText(canvasElement, 'namedExport'); fireEvent.click(exportedElement2); - expect(args.onNewStory).toHaveBeenCalledWith({ - selectedItemId: 'src/module-multiple-exports.js_1', - componentExportName: 'namedExport', - componentFilePath: 'src/module-multiple-exports.js', - componentIsDefaultExport: false, - }); + expect(args.onNewStory).toHaveBeenCalledWith( + expect.objectContaining({ + selectedItemId: 'src/module-multiple-exports.js_1', + componentExportName: 'namedExport', + componentFilePath: 'src/module-multiple-exports.js', + componentIsDefaultExport: false, + }) + ); const singleExport = await findByText(canvasElement, 'module-single-export.js'); fireEvent.click(singleExport); - expect(args.onNewStory).toHaveBeenCalledWith({ - selectedItemId: 'src/module-single-export.js', - componentExportName: 'default', - componentFilePath: 'src/module-single-export.js', - componentIsDefaultExport: true, - }); + expect(args.onNewStory).toHaveBeenCalledWith( + expect.objectContaining({ + selectedItemId: 'src/module-single-export.js', + componentExportName: 'default', + componentFilePath: 'src/module-single-export.js', + componentIsDefaultExport: true, + }) + ); expect(args.onNewStory).toHaveBeenCalledTimes(3); diff --git a/code/ui/manager/src/components/sidebar/Sidebar.stories.tsx b/code/ui/manager/src/components/sidebar/Sidebar.stories.tsx index 46c3d7c14979..6d49c03923b6 100644 --- a/code/ui/manager/src/components/sidebar/Sidebar.stories.tsx +++ b/code/ui/manager/src/components/sidebar/Sidebar.stories.tsx @@ -3,7 +3,7 @@ import React from 'react'; import type { IndexHash, State } from '@storybook/manager-api'; import { ManagerContext, types } from '@storybook/manager-api'; import type { StoryObj, Meta } from '@storybook/react'; -import { within, userEvent, expect } from '@storybook/test'; +import { within, userEvent, expect, fn } from '@storybook/test'; import type { Addon_SidebarTopType } from '@storybook/types'; import { Button, IconButton } from '@storybook/components'; import { FaceHappyIcon } from '@storybook/icons'; @@ -55,10 +55,13 @@ const meta = { }, }, api: { - emit: () => {}, - on: () => {}, - off: () => {}, - getShortcutKeys: () => ({ search: ['control', 'shift', 's'] }), + emit: fn().mockName('api::emit'), + on: fn().mockName('api::on'), + off: fn().mockName('api::off'), + getShortcutKeys: fn(() => ({ search: ['control', 'shift', 's'] })).mockName( + 'api::getShortcutKeys' + ), + selectStory: fn().mockName('api::selectStory'), }, } as any } @@ -288,7 +291,7 @@ export const Scrolled: Story = { const scrollable = await canvasElement.querySelector('[data-radix-scroll-area-viewport]'); await step('expand component', async () => { const componentNode = await canvas.queryAllByText('Child A2')[1]; - userEvent.click(componentNode); + await userEvent.click(componentNode); }); await wait(100); await step('scroll to bottom', async () => { @@ -296,11 +299,11 @@ export const Scrolled: Story = { }); await step('toggle parent state', async () => { const button = await canvas.findByRole('button', { name: 'Change state' }); - button.click(); + await userEvent.click(button); }); await wait(100); // expect the scrollable to be scrolled to the bottom - expect(scrollable.scrollTop).toBe(scrollable.scrollHeight - scrollable.clientHeight); + await expect(scrollable.scrollTop).toBe(scrollable.scrollHeight - scrollable.clientHeight); }, }; diff --git a/docs/api/doc-block-typeset.md b/docs/api/doc-block-typeset.md index 22933dc5f521..047657cd89f8 100644 --- a/docs/api/doc-block-typeset.md +++ b/docs/api/doc-block-typeset.md @@ -84,7 +84,7 @@ Provides a font family to be displayed. ### `fontSizes` -Type: `number[]` +Type: `(string | number)[]` Provides a list of available font sizes (in `px`). diff --git a/docs/api/portable-stories-playwright.md b/docs/api/portable-stories-playwright.md index ae7d012b8445..38dc1502f0a4 100644 --- a/docs/api/portable-stories-playwright.md +++ b/docs/api/portable-stories-playwright.md @@ -1,5 +1,5 @@ --- -title: 'Portable stories in Playwright' +title: 'Portable stories in Playwright CT' --- export const SUPPORTED_RENDERERS = ['react', 'vue']; @@ -34,6 +34,8 @@ Normally, Storybook composes a story and its [annotations](#annotations) automat +Your project must be using React 18+ to use the portable stories API with Playwright CT. + **Using `Next.js`?** The portable stories API is not yet supported in Next.js with Playwright CT. diff --git a/docs/api/story-pipeline-playwright-ct.png b/docs/api/story-pipeline-playwright-ct.png index 615af79387b9..e90239dce803 100644 Binary files a/docs/api/story-pipeline-playwright-ct.png and b/docs/api/story-pipeline-playwright-ct.png differ diff --git a/docs/api/story-pipeline.png b/docs/api/story-pipeline.png index 21d059741855..a77dc956b313 100644 Binary files a/docs/api/story-pipeline.png and b/docs/api/story-pipeline.png differ diff --git a/docs/versions/next.json b/docs/versions/next.json index 697ac1cd38e6..677eac93350d 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"8.2.0-alpha.1","info":{"plain":"- CLI: Add optional `--dev` and `--no-dev` options to `storybook init` CLI - [#26918](https://github.com/storybookjs/storybook/pull/26918), thanks @fastfrwrd!\n- CLI: Include `@storybook/addon-svelte-csf` when initializing new projects - [#27070](https://github.com/storybookjs/storybook/pull/27070), thanks @benmccann!\n- Dependency: Upgrade `webpack-virtual-modules` to 0.6.0 - [#27102](https://github.com/storybookjs/storybook/pull/27102), thanks @fyodorovandrei!\n- Dependency: bump `markdown-to-jsx` to v7.4.5 - [#26694](https://github.com/storybookjs/storybook/pull/26694), thanks @xyy94813!\n- Docgen: Only add react-docgen info when a component is defined in the file - [#26967](https://github.com/storybookjs/storybook/pull/26967), thanks @glenjamin!\n- Docs: Fix MDX Stories block tag-filtering behavior - [#27144](https://github.com/storybookjs/storybook/pull/27144), thanks @shilman!\n- Docs: Fix Subtitle block when no `of` prop passed - [#27147](https://github.com/storybookjs/storybook/pull/27147), thanks @JReinhold!\n- Next.js: Add typing for NextImage to main framework options type - [#27105](https://github.com/storybookjs/storybook/pull/27105), thanks @valentinpalkovic!\n- Next.js: Avoid conflicts with the raw loader - [#27093](https://github.com/storybookjs/storybook/pull/27093), thanks @seanparmelee!\n- Types: Fix typing for main.framework/builder fields - [#27088](https://github.com/storybookjs/storybook/pull/27088), thanks @valentinpalkovic!"}} +{"version":"8.2.0-alpha.2","info":{"plain":"- Angular: Cleanup types - [#27189](https://github.com/storybookjs/storybook/pull/27189), thanks @valentinpalkovic!\n- Angular: Fix filtering of workspace config styles - [#27108](https://github.com/storybookjs/storybook/pull/27108), thanks @valentinpalkovic!\n- Controls: Fix grouped Radio controls to have the same name - [#23374](https://github.com/storybookjs/storybook/pull/23374), thanks @srapilly!\n- Controls: Throttling makes Color control lagging - [#22615](https://github.com/storybookjs/storybook/pull/22615), thanks @gitstart!\n- Docs: Fix `Typeset` Doc block `fontSizes` type - [#26475](https://github.com/storybookjs/storybook/pull/26475), thanks @noranda!"}} diff --git a/docs/writing-stories/mocking-modules.md b/docs/writing-stories/mocking-modules.md index 0f165546a381..d80c3bfe1241 100644 --- a/docs/writing-stories/mocking-modules.md +++ b/docs/writing-stories/mocking-modules.md @@ -95,6 +95,16 @@ import { getUserFromSession } from '#lib/session'; // ... rest of the file ``` + + +Subpath imports will only be correctly resolved and typed when the [`moduleResolution` property](https://www.typescriptlang.org/tsconfig/#moduleResolution) is set to `'Bundler'`, `'NodeNext'`, or `'Node16'` in your TypeScript configuration. + +If you are currently using `'node'`, that is intended for projects using a Node.js version older than v10. Projects written with modern code likely do not need to use `'node'`. + +Storybook recommends the [TSConfig Cheat Sheet](https://www.totaltypescript.com/tsconfig-cheat-sheet) for guidance on setting up your TypeScript configuration. + + + ## Builder aliases If your project is unable to use [subpath imports](#subpath-imports), you can configure your Storybook builder to alias the module to the mock file. This will instruct the builder to replace the module with the mock file when bundling your Storybook stories. diff --git a/scripts/bench/browse.ts b/scripts/bench/browse.ts index 43d03756411b..f95edea759c5 100644 --- a/scripts/bench/browse.ts +++ b/scripts/bench/browse.ts @@ -37,7 +37,7 @@ async function benchAutodocs(url: string) { const browser = await chromium.launch(/* { headless: false } */); await browser.newContext(); const page = await browser.newPage(); - await page.setDefaultTimeout(40000); + page.setDefaultTimeout(40000); const start = now(); await page.goto(`${url}?path=/docs/example-button--docs`); @@ -45,7 +45,7 @@ async function benchAutodocs(url: string) { const tasks = [ async () => { const previewPage = await getPreviewPage(page); - await previewPage.setDefaultTimeout(40000); + previewPage.setDefaultTimeout(40000); await previewPage.waitForLoadState('load'); await previewPage.getByText('Primary UI component for user interaction'); @@ -72,7 +72,7 @@ async function benchMDX(url: string) { const tasks = [ async () => { const previewPage = await getPreviewPage(page); - await previewPage.setDefaultTimeout(40000); + previewPage.setDefaultTimeout(40000); await previewPage.waitForLoadState('load'); await previewPage.getByText('Configure your project'); @@ -108,7 +108,7 @@ async function benchStory(url: string) { }, async () => { const previewPage = await getPreviewPage(page); - await previewPage.setDefaultTimeout(40000); + previewPage.setDefaultTimeout(40000); await previewPage.waitForLoadState('load'); await previewPage.getByText('Button'); diff --git a/scripts/bench/utils.ts b/scripts/bench/utils.ts index c9c159cb5782..7457cf80e359 100644 --- a/scripts/bench/utils.ts +++ b/scripts/bench/utils.ts @@ -33,9 +33,24 @@ export const loadBench = async (options: SaveBenchOptions): Promise { - return document.querySelector('iframe')?.contentDocument.readyState === 'complete'; - }); - const previewPage = await page.frame({ url: /iframe.html/ }).page(); - return previewPage; + /** + * Fix flakiness in preview iframe retrieval + * Sometimes the iframe is not yet available when we try to access it, + * even after waiting for the readyState to be complete. + * + * This loop will keep trying to access the iframe until it's available. + */ + for (let i = 0; i < 10; i++) { + // eslint-disable-next-line @typescript-eslint/no-loop-func + await page.waitForFunction(() => { + return document.querySelector('iframe')?.contentDocument.readyState === 'complete'; + }); + + const previewPage = page.frame({ url: /iframe.html/ })?.page(); + if (previewPage) { + return previewPage; + } + } + + throw new Error('The preview iframe was never found'); }