diff --git a/.gitignore b/.gitignore index 8630dc138669..29c025aadf26 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,5 @@ code/playwright-results/ code/playwright-report/ code/playwright/.cache/ code/bench-results/ + +/packs \ No newline at end of file diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 039c76a34456..c0ecfbb41227 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,17 @@ +## 7.4.0-alpha.2 + +- Addon-docs: Resolve `mdx-react-shim` & `@storybook/global` correctly - [#23941](https://github.com/storybookjs/storybook/pull/23941), thanks [@ndelangen](https://github.com/ndelangen)! +- Addons: Fix key is not a prop warning - [#23935](https://github.com/storybookjs/storybook/pull/23935), thanks [@kasperpeulen](https://github.com/kasperpeulen)! +- CLI: Pass package manager to postinstall - [#23913](https://github.com/storybookjs/storybook/pull/23913), thanks [@Integrayshaun](https://github.com/Integrayshaun)! +- CLI: Provide guidance for users who try to initialize Storybook on an empty dir - [#23874](https://github.com/storybookjs/storybook/pull/23874), thanks [@yannbf](https://github.com/yannbf)! +- Logger: Fix double error messages/stack - [#23919](https://github.com/storybookjs/storybook/pull/23919), thanks [@ndelangen](https://github.com/ndelangen)! +- Maintenance: Categorize server errors - [#23912](https://github.com/storybookjs/storybook/pull/23912), thanks [@yannbf](https://github.com/yannbf)! +- Maintenance: Remove need for `react` as peerDependency - [#23897](https://github.com/storybookjs/storybook/pull/23897), thanks [@ndelangen](https://github.com/ndelangen)! +- Maintenance: Remove sourcemaps generation - [#23936](https://github.com/storybookjs/storybook/pull/23936), thanks [@ndelangen](https://github.com/ndelangen)! +- Preset: Add common preset overrides mechanism - [#23915](https://github.com/storybookjs/storybook/pull/23915), thanks [@yannbf](https://github.com/yannbf)! +- UI: Add an experimental API for adding sidebar bottom toolbar - [#23778](https://github.com/storybookjs/storybook/pull/23778), thanks [@ndelangen](https://github.com/ndelangen)! +- UI: Add an experimental API for adding sidebar top toolbar - [#23811](https://github.com/storybookjs/storybook/pull/23811), thanks [@ndelangen](https://github.com/ndelangen)! + ## 7.4.0-alpha.1 - Build: Migrate @storybook/scripts to strict-ts - [#23818](https://github.com/storybookjs/storybook/pull/23818), thanks [@stilt0n](https://github.com/stilt0n)! diff --git a/code/addons/docs/src/preset.ts b/code/addons/docs/src/preset.ts index 462e990e8e16..1931f0bcc32c 100644 --- a/code/addons/docs/src/preset.ts +++ b/code/addons/docs/src/preset.ts @@ -1,4 +1,5 @@ import fs from 'fs-extra'; +import { dirname, join } from 'path'; import remarkSlug from 'remark-slug'; import remarkExternalLinks from 'remark-external-links'; import { dedent } from 'ts-dedent'; @@ -50,7 +51,10 @@ async function webpack( skipCsf: true, ...mdxPluginOptions, mdxCompileOptions: { - providerImportSource: '@storybook/addon-docs/mdx-react-shim', + providerImportSource: join( + dirname(require.resolve('@storybook/addon-docs/package.json')), + '/dist/shims/mdx-react-shim' + ), ...mdxPluginOptions.mdxCompileOptions, remarkPlugins: [remarkSlug, remarkExternalLinks].concat( mdxPluginOptions?.mdxCompileOptions?.remarkPlugins ?? [] diff --git a/code/addons/essentials/src/docs/preset.ts b/code/addons/essentials/src/docs/preset.ts index af022cfece92..1faed598e0b1 100644 --- a/code/addons/essentials/src/docs/preset.ts +++ b/code/addons/essentials/src/docs/preset.ts @@ -1,8 +1,13 @@ -/* eslint-disable import/export */ +import { dirname, join } from 'path'; + +// eslint-disable-next-line import/export export * from '@storybook/addon-docs/dist/preset'; export const mdxLoaderOptions = async (config: any) => { // eslint-disable-next-line no-param-reassign - config.mdxCompileOptions.providerImportSource = '@storybook/addon-essentials/docs/mdx-react-shim'; + config.mdxCompileOptions.providerImportSource = join( + dirname(require.resolve('@storybook/addon-docs/package.json')), + '/dist/shims/mdx-react-shim' + ); return config; }; diff --git a/code/addons/themes/package.json b/code/addons/themes/package.json index 1d5a3857a9f8..5e690f76608e 100644 --- a/code/addons/themes/package.json +++ b/code/addons/themes/package.json @@ -43,7 +43,8 @@ "require": "./dist/preview.js", "import": "./dist/preview.mjs" }, - "./package.json": "./package.json" + "./package.json": "./package.json", + "./postinstall": "./postinstall.js" }, "main": "dist/index.js", "module": "dist/index.mjs", diff --git a/code/addons/themes/postinstall.js b/code/addons/themes/postinstall.js new file mode 100644 index 000000000000..c84a4e88e4b4 --- /dev/null +++ b/code/addons/themes/postinstall.js @@ -0,0 +1,17 @@ +const { spawn } = require('child_process'); + +const PACKAGE_MANAGER_TO_COMMAND = { + npm: 'npx', + yarn1: 'yarn dlx', + yarn2: 'yarn dlx', + pnpm: 'pnpm dlx', +}; + +module.exports = function postinstall(options) { + const command = PACKAGE_MANAGER_TO_COMMAND[options.packageManager]; + + spawn(command, ['@storybook/auto-config', 'themes'], { + stdio: 'inherit', + cwd: process.cwd(), + }); +}; diff --git a/code/builders/builder-manager/src/index.ts b/code/builders/builder-manager/src/index.ts index 40e180ada28c..c13f0daa87f1 100644 --- a/code/builders/builder-manager/src/index.ts +++ b/code/builders/builder-manager/src/index.ts @@ -72,7 +72,7 @@ export const getConfig: ManagerBuilder['getConfig'] = async (options) => { platform: 'browser', bundle: true, minify: true, - sourcemap: true, + sourcemap: false, conditions: ['browser', 'module', 'default'], jsxFactory: 'React.createElement', diff --git a/code/builders/builder-vite/src/plugins/mdx-plugin.ts b/code/builders/builder-vite/src/plugins/mdx-plugin.ts index 4f50a0b38159..8e4b51f16d6e 100644 --- a/code/builders/builder-vite/src/plugins/mdx-plugin.ts +++ b/code/builders/builder-vite/src/plugins/mdx-plugin.ts @@ -3,6 +3,7 @@ import type { Plugin } from 'vite'; import remarkSlug from 'remark-slug'; import remarkExternalLinks from 'remark-external-links'; import { createFilter } from 'vite'; +import { dirname, join } from 'path'; const isStorybookMdx = (id: string) => id.endsWith('stories.mdx') || id.endsWith('story.mdx'); @@ -33,7 +34,10 @@ export async function mdxPlugin(options: Options): Promise { const mdxLoaderOptions = await options.presets.apply('mdxLoaderOptions', { ...mdxPluginOptions, mdxCompileOptions: { - providerImportSource: '@storybook/addon-docs/mdx-react-shim', + providerImportSource: join( + dirname(require.resolve('@storybook/addon-docs/package.json')), + '/dist/shims/mdx-react-shim' + ), ...mdxPluginOptions?.mdxCompileOptions, remarkPlugins: [remarkSlug, remarkExternalLinks].concat( mdxPluginOptions?.mdxCompileOptions?.remarkPlugins ?? [] diff --git a/code/builders/builder-webpack5/package.json b/code/builders/builder-webpack5/package.json index f5d7ffa80971..8f61972eb1a0 100644 --- a/code/builders/builder-webpack5/package.json +++ b/code/builders/builder-webpack5/package.json @@ -56,23 +56,15 @@ "prep": "../../../scripts/prepare/bundle.ts" }, "dependencies": { - "@babel/core": "^7.22.9", - "@storybook/addons": "workspace:*", + "@babel/core": "^7.22.0", "@storybook/channels": "workspace:*", - "@storybook/client-api": "workspace:*", "@storybook/client-logger": "workspace:*", - "@storybook/components": "workspace:*", "@storybook/core-common": "workspace:*", "@storybook/core-events": "workspace:*", "@storybook/core-webpack": "workspace:*", - "@storybook/global": "^5.0.0", - "@storybook/manager-api": "workspace:*", "@storybook/node-logger": "workspace:*", "@storybook/preview": "workspace:*", "@storybook/preview-api": "workspace:*", - "@storybook/router": "workspace:*", - "@storybook/store": "workspace:*", - "@storybook/theming": "workspace:*", "@swc/core": "^1.3.49", "@types/node": "^16.0.0", "@types/semver": "^7.3.4", @@ -110,10 +102,6 @@ "slash": "^5.0.0", "typescript": "~4.9.3" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, "peerDependenciesMeta": { "typescript": { "optional": true 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 d2f6ec6affbc..01b294f48fb0 100644 --- a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts +++ b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts @@ -29,24 +29,35 @@ import { createBabelLoader, createSWCLoader } from './loaders'; const getAbsolutePath = (input: I): I => dirname(require.resolve(join(input, 'package.json'))) as any; +const maybeGetAbsolutePath = (input: I): I | false => { + try { + return getAbsolutePath(input); + } catch (e) { + return false; + } +}; +const managerAPIPath = maybeGetAbsolutePath(`@storybook/manager-api`); +const componentsPath = maybeGetAbsolutePath(`@storybook/components`); +const globalPath = maybeGetAbsolutePath(`@storybook/global`); +const routerPath = maybeGetAbsolutePath(`@storybook/router`); +const themingPath = maybeGetAbsolutePath(`@storybook/theming`); + +// these packages are not pre-bundled because of react dependencies. +// these are not dependencies of the builder anymore, thus resolving them can fail. +// we should remove the aliases in 8.0, I'm not sure why they are here in the first place. const storybookPaths: Record = { - ...[ - // these packages are not pre-bundled because of react dependencies - 'components', - 'global', - 'manager-api', - 'router', - 'theming', - ].reduce( - (acc, sbPackage) => ({ - ...acc, - [`@storybook/${sbPackage}`]: getAbsolutePath(`@storybook/${sbPackage}`), - }), - {} - ), - // deprecated, remove in 8.0 - [`@storybook/api`]: getAbsolutePath(`@storybook/manager-api`), + ...(managerAPIPath + ? { + // deprecated, remove in 8.0 + [`@storybook/api`]: managerAPIPath, + [`@storybook/manager-api`]: managerAPIPath, + } + : {}), + ...(componentsPath ? { [`@storybook/components`]: componentsPath } : {}), + ...(globalPath ? { [`@storybook/global`]: globalPath } : {}), + ...(routerPath ? { [`@storybook/router`]: routerPath } : {}), + ...(themingPath ? { [`@storybook/theming`]: themingPath } : {}), }; export default async ( diff --git a/code/frameworks/angular/src/builders/utils/error-handler.ts b/code/frameworks/angular/src/builders/utils/error-handler.ts index f2ff150495cf..2673dbfd0b87 100644 --- a/code/frameworks/angular/src/builders/utils/error-handler.ts +++ b/code/frameworks/angular/src/builders/utils/error-handler.ts @@ -12,7 +12,7 @@ export const printErrorDetails = (error: any): void => { } else if ((error as any).stats && (error as any).stats.compilation.errors) { (error as any).stats.compilation.errors.forEach((e: any) => logger.plain(e)); } else { - logger.error(error as any); + logger.error(error); } } else if (error.compilation?.errors) { error.compilation.errors.forEach((e: any) => logger.plain(e)); diff --git a/code/lib/cli/src/add.ts b/code/lib/cli/src/add.ts index 2b8565085dab..c5ddd1b1c995 100644 --- a/code/lib/cli/src/add.ts +++ b/code/lib/cli/src/add.ts @@ -1,5 +1,6 @@ import { getStorybookInfo } from '@storybook/core-common'; import { readConfig, writeConfig } from '@storybook/csf-tools'; +import SemVer from 'semver'; import { JsPackageManagerFactory, @@ -10,7 +11,11 @@ import { getStorybookVersion } from './utils'; const logger = console; -const postinstallAddon = async (addonName: string) => { +interface PostinstallOptions { + packageManager: PackageManagerName; +} + +const postinstallAddon = async (addonName: string, options: PostinstallOptions) => { try { const modulePath = require.resolve(`${addonName}/postinstall`, { paths: [process.cwd()] }); // eslint-disable-next-line import/no-dynamic-require, global-require @@ -18,7 +23,7 @@ const postinstallAddon = async (addonName: string) => { try { logger.log(`Running postinstall script for ${addonName}`); - await postinstall(); + await postinstall(options); } catch (e) { logger.error(`Error running postinstall script for ${addonName}`); logger.error(e); @@ -73,7 +78,9 @@ export async function add( const isStorybookAddon = addonName.startsWith('@storybook/'); const storybookVersion = await getStorybookVersion(packageManager); const version = versionSpecifier || (isStorybookAddon ? storybookVersion : latestVersion); - const addonWithVersion = `${addonName}@^${version}`; + const addonWithVersion = SemVer.valid(version) + ? `${addonName}@^${version}` + : `${addonName}@${version}`; logger.log(`Installing ${addonWithVersion}`); await packageManager.addDependencies({ installAsDevDependencies: true }, [addonWithVersion]); @@ -83,6 +90,6 @@ export async function add( await writeConfig(main); if (!options.skipPostinstall && isStorybookAddon) { - await postinstallAddon(addonName); + await postinstallAddon(addonName, { packageManager: pkgMgr }); } } diff --git a/code/lib/cli/src/detect.ts b/code/lib/cli/src/detect.ts index c1b9317bc556..b62288bc82e5 100644 --- a/code/lib/cli/src/detect.ts +++ b/code/lib/cli/src/detect.ts @@ -15,6 +15,7 @@ import { } from './project_types'; import { commandLog, isNxProject } from './helpers'; import type { JsPackageManager, PackageJsonWithMaybeDeps } from './js-package-manager'; +import { HandledError } from './HandledError'; const viteConfigFiles = ['vite.config.ts', 'vite.config.js', 'vite.config.mjs']; const webpackConfigFiles = ['webpack.config.js']; @@ -135,16 +136,23 @@ export async function detectBuilder(packageManager: JsPackageManager, projectTyp return CoreBuilder.Webpack5; default: // eslint-disable-next-line no-case-declarations - const { builder } = await prompts({ - type: 'select', - name: 'builder', - message: - 'We were not able to detect the right builder for your project. Please select one:', - choices: [ - { title: 'Vite', value: CoreBuilder.Vite }, - { title: 'Webpack 5', value: CoreBuilder.Webpack5 }, - ], - }); + const { builder } = await prompts( + { + type: 'select', + name: 'builder', + message: + '\nWe were not able to detect the right builder for your project. Please select one:', + choices: [ + { title: 'Vite', value: CoreBuilder.Vite }, + { title: 'Webpack 5', value: CoreBuilder.Webpack5 }, + ], + }, + { + onCancel: () => { + throw new HandledError('Canceled by the user'); + }, + } + ); return builder; } diff --git a/code/lib/cli/src/generators/EMBER/index.ts b/code/lib/cli/src/generators/EMBER/index.ts index 255409fd7c48..313dcf8691fd 100644 --- a/code/lib/cli/src/generators/EMBER/index.ts +++ b/code/lib/cli/src/generators/EMBER/index.ts @@ -1,16 +1,23 @@ +import { CoreBuilder } from '../../project_types'; import { baseGenerator } from '../baseGenerator'; import type { Generator } from '../types'; const generator: Generator = async (packageManager, npmOptions, options) => { - await baseGenerator(packageManager, npmOptions, options, 'ember', { - extraPackages: [ - // babel-plugin-ember-modules-api-polyfill is a peerDep of @storybook/ember - 'babel-plugin-ember-modules-api-polyfill', - // babel-plugin-htmlbars-inline-precompile is a peerDep of @storybook/ember - 'babel-plugin-htmlbars-inline-precompile', - ], - staticDir: 'dist', - }); + await baseGenerator( + packageManager, + npmOptions, + { ...options, builder: CoreBuilder.Webpack5 }, + 'ember', + { + extraPackages: [ + // babel-plugin-ember-modules-api-polyfill is a peerDep of @storybook/ember + 'babel-plugin-ember-modules-api-polyfill', + // babel-plugin-htmlbars-inline-precompile is a peerDep of @storybook/ember + 'babel-plugin-htmlbars-inline-precompile', + ], + staticDir: 'dist', + } + ); }; export default generator; diff --git a/code/lib/cli/src/generators/NEXTJS/index.ts b/code/lib/cli/src/generators/NEXTJS/index.ts index ef3afea02d60..2588b387312a 100644 --- a/code/lib/cli/src/generators/NEXTJS/index.ts +++ b/code/lib/cli/src/generators/NEXTJS/index.ts @@ -1,3 +1,4 @@ +import { CoreBuilder } from '../../project_types'; import { baseGenerator } from '../baseGenerator'; import type { Generator } from '../types'; @@ -5,7 +6,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => { await baseGenerator( packageManager, npmOptions, - options, + { ...options, builder: CoreBuilder.Webpack5 }, 'react', { extraAddons: ['@storybook/addon-onboarding'], diff --git a/code/lib/cli/src/generators/REACT_SCRIPTS/index.ts b/code/lib/cli/src/generators/REACT_SCRIPTS/index.ts index 4059beedec0e..5a0300c25b5a 100644 --- a/code/lib/cli/src/generators/REACT_SCRIPTS/index.ts +++ b/code/lib/cli/src/generators/REACT_SCRIPTS/index.ts @@ -26,25 +26,15 @@ const generator: Generator = async (packageManager, npmOptions, options) => { : {}; const craVersion = await packageManager.getPackageVersion('react-scripts'); - const isCra5OrHigher = craVersion && semver.gte(craVersion, '5.0.0'); - const updatedOptions = isCra5OrHigher ? { ...options, builder: CoreBuilder.Webpack5 } : options; - const extraPackages = []; - if (isCra5OrHigher) { - extraPackages.push('webpack'); - // Miscellaneous dependency used in `babel-preset-react-app` but not listed as dep there - extraPackages.push('babel-plugin-named-exports-order'); - // Miscellaneous dependency to add to be sure Storybook + CRA is working fine with Yarn PnP mode - extraPackages.push('prop-types'); + if (craVersion === null) { + throw new Error(dedent` + It looks like you're trying to initialize Storybook in a CRA project that does not have react-scripts installed. + Please install it and make sure it's of version 5 or higher, which are the versions supported by Storybook 7.0+. + `); } - const version = versions['@storybook/preset-create-react-app']; - const extraAddons = [ - `@storybook/preset-create-react-app@${version}`, - '@storybook/addon-onboarding', - ]; - - if (!isCra5OrHigher) { + if (!craVersion && semver.gte(craVersion, '5.0.0')) { throw new Error(dedent` Storybook 7.0+ doesn't support react-scripts@<5.0.0. @@ -52,13 +42,32 @@ const generator: Generator = async (packageManager, npmOptions, options) => { `); } - await baseGenerator(packageManager, npmOptions, updatedOptions, 'react', { - extraAddons, - extraPackages, - staticDir: fs.existsSync(path.resolve('./public')) ? 'public' : undefined, - skipBabel: true, - extraMain, - }); + const extraPackages = []; + extraPackages.push('webpack'); + // Miscellaneous dependency used in `babel-preset-react-app` but not listed as dep there + extraPackages.push('babel-plugin-named-exports-order'); + // Miscellaneous dependency to add to be sure Storybook + CRA is working fine with Yarn PnP mode + extraPackages.push('prop-types'); + + const version = versions['@storybook/preset-create-react-app']; + const extraAddons = [ + `@storybook/preset-create-react-app@${version}`, + '@storybook/addon-onboarding', + ]; + + await baseGenerator( + packageManager, + npmOptions, + { ...options, builder: CoreBuilder.Webpack5 }, + 'react', + { + extraAddons, + extraPackages, + staticDir: fs.existsSync(path.resolve('./public')) ? 'public' : undefined, + skipBabel: true, + extraMain, + } + ); }; export default generator; diff --git a/code/lib/cli/src/generators/SERVER/index.ts b/code/lib/cli/src/generators/SERVER/index.ts index 96032e2c88e8..966efee90898 100755 --- a/code/lib/cli/src/generators/SERVER/index.ts +++ b/code/lib/cli/src/generators/SERVER/index.ts @@ -1,10 +1,17 @@ +import { CoreBuilder } from '../../project_types'; import { baseGenerator } from '../baseGenerator'; import type { Generator } from '../types'; const generator: Generator = async (packageManager, npmOptions, options) => { - await baseGenerator(packageManager, npmOptions, options, 'server', { - extensions: ['json', 'yaml', 'yml'], - }); + await baseGenerator( + packageManager, + npmOptions, + { ...options, builder: CoreBuilder.Vite }, + 'server', + { + extensions: ['json', 'yaml', 'yml'], + } + ); }; export default generator; diff --git a/code/lib/cli/src/generators/SOLID/index.ts b/code/lib/cli/src/generators/SOLID/index.ts index 7dc517f667bb..21347d057682 100644 --- a/code/lib/cli/src/generators/SOLID/index.ts +++ b/code/lib/cli/src/generators/SOLID/index.ts @@ -1,8 +1,16 @@ +import { CoreBuilder } from '../../project_types'; import { baseGenerator } from '../baseGenerator'; import type { Generator } from '../types'; const generator: Generator = async (packageManager, npmOptions, options) => { - await baseGenerator(packageManager, npmOptions, options, 'solid', {}, 'solid'); + await baseGenerator( + packageManager, + npmOptions, + { ...options, builder: CoreBuilder.Vite }, + 'solid', + {}, + 'solid' + ); }; export default generator; diff --git a/code/lib/cli/src/generators/SVELTEKIT/index.ts b/code/lib/cli/src/generators/SVELTEKIT/index.ts index 21565e6a45cc..856d1d04c76b 100644 --- a/code/lib/cli/src/generators/SVELTEKIT/index.ts +++ b/code/lib/cli/src/generators/SVELTEKIT/index.ts @@ -1,8 +1,16 @@ +import { CoreBuilder } from '../../project_types'; import { baseGenerator } from '../baseGenerator'; import type { Generator } from '../types'; const generator: Generator = async (packageManager, npmOptions, options) => { - await baseGenerator(packageManager, npmOptions, options, 'svelte', undefined, 'sveltekit'); + await baseGenerator( + packageManager, + npmOptions, + { ...options, builder: CoreBuilder.Vite }, + 'svelte', + undefined, + 'sveltekit' + ); }; export default generator; diff --git a/code/lib/cli/src/generators/VUE/index.ts b/code/lib/cli/src/generators/VUE/index.ts index 02878a42c112..c1869a539695 100644 --- a/code/lib/cli/src/generators/VUE/index.ts +++ b/code/lib/cli/src/generators/VUE/index.ts @@ -3,9 +3,10 @@ import { baseGenerator } from '../baseGenerator'; import type { Generator } from '../types'; const generator: Generator = async (packageManager, npmOptions, options) => { - const extraPackages = options.builder === CoreBuilder.Webpack5 ? ['vue-loader@^15.7.0'] : []; await baseGenerator(packageManager, npmOptions, options, 'vue', { - extraPackages, + extraPackages: async ({ builder }) => { + return builder === CoreBuilder.Webpack5 ? ['vue-loader@^15.7.0'] : []; + }, }); }; diff --git a/code/lib/cli/src/generators/VUE3/index.ts b/code/lib/cli/src/generators/VUE3/index.ts index fa08aa327bdf..63dbddede7b5 100644 --- a/code/lib/cli/src/generators/VUE3/index.ts +++ b/code/lib/cli/src/generators/VUE3/index.ts @@ -3,12 +3,12 @@ import { baseGenerator } from '../baseGenerator'; import type { Generator } from '../types'; const generator: Generator = async (packageManager, npmOptions, options) => { - const extraPackages = - options.builder === CoreBuilder.Webpack5 - ? ['vue-loader@^17.0.0', '@vue/compiler-sfc@^3.2.0'] - : []; await baseGenerator(packageManager, npmOptions, options, 'vue3', { - extraPackages, + extraPackages: async ({ builder }) => { + return builder === CoreBuilder.Webpack5 + ? ['vue-loader@^17.0.0', '@vue/compiler-sfc@^3.2.0'] + : []; + }, }); }; diff --git a/code/lib/cli/src/generators/baseGenerator.ts b/code/lib/cli/src/generators/baseGenerator.ts index 4ac1cd2f8f6f..48b2f8affd5b 100644 --- a/code/lib/cli/src/generators/baseGenerator.ts +++ b/code/lib/cli/src/generators/baseGenerator.ts @@ -17,6 +17,7 @@ import { extractEslintInfo, suggestESLintPlugin, } from '../automigrate/helpers/eslintPlugin'; +import { detectBuilder } from '../detect'; const logger = console; @@ -175,10 +176,11 @@ export async function baseGenerator( npmOptions: NpmOptions, { language, - builder = CoreBuilder.Webpack5, + builder, pnp, frameworkPreviewParts, yes: skipPrompts, + projectType, }: GeneratorOptions, renderer: SupportedRenderers, options: FrameworkOptions = defaultOptions, @@ -187,6 +189,11 @@ export async function baseGenerator( const isStorybookInMonorepository = packageManager.isStorybookInMonorepo(); const shouldApplyRequireWrapperOnPackageNames = isStorybookInMonorepository || pnp; + if (!builder) { + // eslint-disable-next-line no-param-reassign + builder = await detectBuilder(packageManager, projectType); + } + const { extraAddons: extraAddonPackages, extraPackages, @@ -219,19 +226,28 @@ export async function baseGenerator( shouldApplyRequireWrapperOnPackageNames ); + const extraAddonsToInstall = + typeof extraAddonPackages === 'function' + ? await extraAddonPackages({ + builder: builder || builderInclude, + framework: framework || frameworkInclude, + }) + : extraAddonPackages; + // added to main.js const addons = [ '@storybook/addon-links', '@storybook/addon-essentials', - ...stripVersions(extraAddonPackages), - ]; + ...stripVersions(extraAddonsToInstall), + ].filter(Boolean); + // added to package.json const addonPackages = [ '@storybook/addon-links', '@storybook/addon-essentials', '@storybook/blocks', - ...extraAddonPackages, - ]; + ...extraAddonsToInstall, + ].filter(Boolean); if (hasInteractiveStories(rendererId)) { addons.push('@storybook/addon-interactions'); @@ -265,12 +281,20 @@ export async function baseGenerator( ); } + const extraPackagesToInstall = + typeof extraPackages === 'function' + ? await extraPackages({ + builder: builder || builderInclude, + framework: framework || frameworkInclude, + }) + : extraPackages; + const allPackages = [ 'storybook', getExternalFramework(rendererId) ? undefined : `@storybook/${rendererId}`, ...frameworkPackages, ...addonPackages, - ...extraPackages, + ...extraPackagesToInstall, ].filter(Boolean); const packages = [...new Set(allPackages)].filter( diff --git a/code/lib/cli/src/generators/types.ts b/code/lib/cli/src/generators/types.ts index c4298cc85529..1711505e0bdc 100644 --- a/code/lib/cli/src/generators/types.ts +++ b/code/lib/cli/src/generators/types.ts @@ -8,14 +8,17 @@ export type GeneratorOptions = { builder: Builder; linkable: boolean; pnp: boolean; + projectType: ProjectType; frameworkPreviewParts?: FrameworkPreviewParts; // skip prompting the user yes: boolean; }; export interface FrameworkOptions { - extraPackages?: string[]; - extraAddons?: string[]; + extraPackages?: + | string[] + | ((details: { framework: string; builder: string }) => Promise); + extraAddons?: string[] | ((details: { framework: string; builder: string }) => Promise); staticDir?: string; addScripts?: boolean; addMainFile?: boolean; diff --git a/code/lib/cli/src/initiate.ts b/code/lib/cli/src/initiate.ts index 5f84963bfc05..e2966a0c1798 100644 --- a/code/lib/cli/src/initiate.ts +++ b/code/lib/cli/src/initiate.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-param-reassign */ import type { PackageJson } from 'read-pkg-up'; import chalk from 'chalk'; import prompts from 'prompts'; @@ -7,14 +8,9 @@ import { NxProjectDetectedError } from '@storybook/core-events/server-errors'; import dedent from 'ts-dedent'; import boxen from 'boxen'; +import { readdirSync } from 'fs-extra'; import { installableProjectTypes, ProjectType } from './project_types'; -import { - detect, - isStorybookInstantiated, - detectLanguage, - detectBuilder, - detectPnp, -} from './detect'; +import { detect, isStorybookInstantiated, detectLanguage, detectPnp } from './detect'; import { commandLog, codeLog, paddedLog } from './helpers'; import angularGenerator from './generators/ANGULAR'; import emberGenerator from './generators/EMBER'; @@ -34,10 +30,10 @@ import qwikGenerator from './generators/QWIK'; import svelteKitGenerator from './generators/SVELTEKIT'; import solidGenerator from './generators/SOLID'; import serverGenerator from './generators/SERVER'; -import type { JsPackageManager } from './js-package-manager'; +import type { JsPackageManager, PackageManagerName } from './js-package-manager'; import { JsPackageManagerFactory, useNpmWarning } from './js-package-manager'; import type { NpmOptions } from './NpmOptions'; -import type { CommandOptions } from './generators/types'; +import type { CommandOptions, GeneratorOptions } from './generators/types'; import { HandledError } from './HandledError'; const logger = console; @@ -55,12 +51,13 @@ const installStorybook = async ( const language = await detectLanguage(packageManager); const pnp = await detectPnp(); - const generatorOptions = { + const generatorOptions: GeneratorOptions = { language, - builder: options.builder || (await detectBuilder(packageManager, projectType)), + builder: options.builder, linkable: !!options.linkable, pnp: pnp || options.usePnp, yes: options.yes, + projectType: options.type, }; const runGenerator: () => Promise = async () => { @@ -236,6 +233,45 @@ const projectTypeInquirer = async ( process.exit(0); }; +const getEmptyDirMessage = (packageManagerType: PackageManagerName) => { + const generatorCommandsMap = { + vite: { + npm: 'npm create vite@latest', + yarn1: 'yarn create vite', + yarn2: 'yarn create vite', + pnpm: 'pnpm create vite', + }, + angular: { + npm: 'npx -p @angular/cli ng new my-project --package-manager=npm', + yarn1: 'npx -p @angular/cli ng new my-project --package-manager=yarn', + yarn2: 'npx -p @angular/cli ng new my-project --package-manager=yarn', + pnpm: 'npx -p @angular/cli ng new my-project --package-manager=pnpm', + }, + }; + + return dedent` + Storybook cannot be installed into an empty project. We recommend creating a new project with the following: + + 📦 Vite CLI for React/Vue/Web Components => ${chalk.green( + generatorCommandsMap.vite[packageManagerType] + )} + See ${chalk.yellowBright('https://vitejs.dev/guide/#scaffolding-your-first-vite-project')} + + 📦 Angular CLI => ${chalk.green(generatorCommandsMap.angular[packageManagerType])} + See ${chalk.yellowBright('https://angular.io/cli/new')} + + 📦 Any other tooling of your choice + + Once you've created a project, please re-run ${chalk.green( + 'npx storybook@latest init' + )} inside the project root. For more information, see ${chalk.yellowBright( + 'https://storybook.js.org/docs' + )} + + Good luck! 🚀 + `; +}; + async function doInitiate( options: CommandOptions, pkg: PackageJson @@ -254,6 +290,11 @@ async function doInitiate( pkgMgr = 'npm'; } + + const cwdFolderEntries = readdirSync(process.cwd()); + const isEmptyDir = + cwdFolderEntries.length === 0 || cwdFolderEntries.every((entry) => entry.startsWith('.')); + const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); const welcomeMessage = 'storybook init - the simplest way to add a Storybook to your project.'; logger.log(chalk.inverse(`\n ${welcomeMessage} \n`)); @@ -265,6 +306,17 @@ async function doInitiate( updateCheckInterval: 1000 * 60 * 60, // every hour (we could increase this later on.) }); + if (options.force !== true && isEmptyDir) { + logger.log( + boxen(getEmptyDirMessage(packageManager.type), { + borderStyle: 'round', + padding: 1, + borderColor: '#F1618C', + }) + ); + throw new HandledError('Project was initialized in an empty directory.'); + } + let projectType: ProjectType; const projectTypeProvided = options.type; const infoText = projectTypeProvided @@ -307,7 +359,6 @@ async function doInitiate( logger.log(); if (force) { - // eslint-disable-next-line no-param-reassign options.force = true; } else { process.exit(0); diff --git a/code/lib/cli/src/project_types.ts b/code/lib/cli/src/project_types.ts index 17bc9c27a25d..25c058dee818 100644 --- a/code/lib/cli/src/project_types.ts +++ b/code/lib/cli/src/project_types.ts @@ -275,7 +275,11 @@ export const unsupportedTemplate: TemplateConfiguration = { }, }; -const notInstallableProjectTypes: ProjectType[] = [ProjectType.UNDETECTED, ProjectType.UNSUPPORTED]; +const notInstallableProjectTypes: ProjectType[] = [ + ProjectType.UNDETECTED, + ProjectType.UNSUPPORTED, + ProjectType.NX, +]; export const installableProjectTypes = Object.values(ProjectType) .filter((type) => !notInstallableProjectTypes.includes(type)) diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json index 4b67418e0d1c..17a477dc70f3 100644 --- a/code/lib/codemod/package.json +++ b/code/lib/codemod/package.json @@ -40,10 +40,6 @@ }, "main": "dist/index.js", "types": "dist/index.d.ts", - "scripts": { - "check": "../../../scripts/prepare/check.ts", - "prep": "../../../scripts/prepare/bundle.ts" - }, "files": [ "dist/**/*", "README.md", @@ -51,6 +47,10 @@ "*.d.ts", "!src/**/*" ], + "scripts": { + "check": "../../../scripts/prepare/check.ts", + "prep": "../../../scripts/prepare/bundle.ts" + }, "dependencies": { "@babel/core": "^7.22.9", "@babel/preset-env": "^7.22.9", diff --git a/code/lib/core-common/src/utils/validate-config.ts b/code/lib/core-common/src/utils/validate-config.ts index 3c5d7201724c..39d3f5ff44dc 100644 --- a/code/lib/core-common/src/utils/validate-config.ts +++ b/code/lib/core-common/src/utils/validate-config.ts @@ -1,5 +1,9 @@ import { join } from 'path'; -import { dedent } from 'ts-dedent'; +import { + CouldNotEvaluateFrameworkError, + MissingFrameworkFieldError, + InvalidFrameworkNameError, +} from '@storybook/core-events/server-errors'; import { frameworkPackages } from './get-storybook-info'; const renderers = ['html', 'preact', 'react', 'server', 'svelte', 'vue', 'vue3', 'web-components']; @@ -9,28 +13,15 @@ const rendererNames = [...renderers, ...renderers.map((renderer) => `@storybook/ export function validateFrameworkName( frameworkName: string | undefined ): asserts frameworkName is string { - const automigrateMessage = `Please run 'npx storybook@next automigrate' to automatically fix your config. - - See the migration guide for more information: - https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#new-framework-api\n`; - // Throw error if there is no framework field // TODO: maybe this error should not be thrown if we allow empty Storybooks that only use "refs" for composition if (!frameworkName) { - throw new Error(dedent` - Could not find a 'framework' field in Storybook config. - - ${automigrateMessage} - `); + throw new MissingFrameworkFieldError(); } // Account for legacy scenario where the framework was referring to a renderer if (rendererNames.includes(frameworkName)) { - throw new Error(dedent` - Invalid value of '${frameworkName}' in the 'framework' field of Storybook config. - - ${automigrateMessage} - `); + throw new InvalidFrameworkNameError({ frameworkName }); } // If we know about the framework, we don't need to validate it @@ -42,9 +33,6 @@ export function validateFrameworkName( try { require.resolve(join(frameworkName, 'preset')); } catch (err) { - throw new Error(dedent` - Could not evaluate the ${frameworkName} package from the 'framework' field of Storybook config. - - Are you sure it's a valid package and is installed?`); + throw new CouldNotEvaluateFrameworkError({ frameworkName }); } } diff --git a/code/lib/core-events/src/errors/server-errors.ts b/code/lib/core-events/src/errors/server-errors.ts index 695b4cb09306..f12926d0f4a9 100644 --- a/code/lib/core-events/src/errors/server-errors.ts +++ b/code/lib/core-events/src/errors/server-errors.ts @@ -44,3 +44,78 @@ export class NxProjectDetectedError extends StorybookError { `; } } + +export class MissingFrameworkFieldError extends StorybookError { + readonly category = Category.CORE_COMMON; + + readonly code = 1; + + public readonly documentation = + 'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#new-framework-api'; + + template() { + return dedent` + Could not find a 'framework' field in Storybook config. + + Please run 'npx storybook@next automigrate' to automatically fix your config. + `; + } +} + +export class InvalidFrameworkNameError extends StorybookError { + readonly category = Category.CORE_COMMON; + + readonly code = 2; + + public readonly documentation = + 'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#new-framework-api'; + + constructor(public data: { frameworkName: string }) { + super(); + } + + template() { + return dedent` + Invalid value of '${this.data.frameworkName}' in the 'framework' field of Storybook config. + + Please run 'npx storybook@next automigrate' to automatically fix your config. + `; + } +} + +export class CouldNotEvaluateFrameworkError extends StorybookError { + readonly category = Category.CORE_COMMON; + + readonly code = 3; + + constructor(public data: { frameworkName: string }) { + super(); + } + + template() { + return dedent` + Could not evaluate the '${this.data.frameworkName}' package from the 'framework' field of Storybook config. + + Are you sure it's a valid package and is installed? + `; + } +} + +export class ConflictingStaticDirConfigError extends StorybookError { + readonly category = Category.CORE_SERVER; + + readonly code = 1; + + public readonly documentation = + 'https://storybook.js.org/docs/react/configure/images-and-assets#serving-static-files-via-storybook-configuration'; + + template() { + return dedent` + Storybook encountered a conflict when trying to serve statics. You have configured both: + * Storybook's option in the config file: 'staticDirs' + * Storybook's (deprecated) CLI flag: '--staticDir' or '-s' + + Please remove the CLI flag from your storybook script and use only the 'staticDirs' option instead. + `; + } +} diff --git a/code/lib/core-events/src/errors/storybook-error.test.ts b/code/lib/core-events/src/errors/storybook-error.test.ts index 328c27a827e4..dc26a50f9679 100644 --- a/code/lib/core-events/src/errors/storybook-error.test.ts +++ b/code/lib/core-events/src/errors/storybook-error.test.ts @@ -26,16 +26,35 @@ describe('StorybookError', () => { const error = new TestError(); error.documentation = true; const expectedMessage = - 'This is a test error.\n\nMore info: https://storybook.js.org/error/SB_TEST_CATEGORY_0123'; + 'This is a test error.\n\nMore info: https://storybook.js.org/error/SB_TEST_CATEGORY_0123\n'; expect(error.message).toBe(expectedMessage); }); it('should generate the correct message with external documentation link', () => { const error = new TestError(); error.documentation = 'https://example.com/docs/test-error'; - const expectedMessage = - 'This is a test error.\n\nMore info: https://example.com/docs/test-error'; - expect(error.message).toBe(expectedMessage); + expect(error.message).toMatchInlineSnapshot(` + "This is a test error. + + More info: https://example.com/docs/test-error + " + `); + }); + + it('should generate the correct message with multiple external documentation links', () => { + const error = new TestError(); + error.documentation = [ + 'https://example.com/docs/first-error', + 'https://example.com/docs/second-error', + ]; + expect(error.message).toMatchInlineSnapshot(` + "This is a test error. + + More info: + - https://example.com/docs/first-error + - https://example.com/docs/second-error + " + `); }); it('should have default documentation value of false', () => { diff --git a/code/lib/core-events/src/errors/storybook-error.ts b/code/lib/core-events/src/errors/storybook-error.ts index 3a0abbf463f5..40158190d93a 100644 --- a/code/lib/core-events/src/errors/storybook-error.ts +++ b/code/lib/core-events/src/errors/storybook-error.ts @@ -26,7 +26,7 @@ export abstract class StorybookError extends Error { * - If a string, uses the provided URL for documentation (external or FAQ links). * - If `false` (default), no documentation link is added. */ - public documentation: boolean | string = false; + public documentation: boolean | string | string[] = false; /** * Flag used to easily determine if the error originates from Storybook. @@ -51,8 +51,10 @@ export abstract class StorybookError extends Error { page = `https://storybook.js.org/error/${this.name}`; } else if (typeof this.documentation === 'string') { page = this.documentation; + } else if (Array.isArray(this.documentation)) { + page = `\n${this.documentation.map((doc) => `\t- ${doc}`).join('\n')}`; } - return this.template() + (page != null ? `\n\nMore info: ${page}` : ''); + return this.template() + (page != null ? `\n\nMore info: ${page}\n` : ''); } } diff --git a/code/lib/core-server/package.json b/code/lib/core-server/package.json index 235266fcfdda..b61bdda65d98 100644 --- a/code/lib/core-server/package.json +++ b/code/lib/core-server/package.json @@ -36,6 +36,11 @@ "node": "./dist/presets/common-preset.js", "require": "./dist/presets/common-preset.js" }, + "./dist/presets/common-override-preset": { + "types": "./dist/presets/common-override-preset.d.ts", + "node": "./dist/presets/common-override-preset.js", + "require": "./dist/presets/common-override-preset.js" + }, "./public/favicon.svg": "./public/favicon.svg", "./package.json": "./package.json" }, @@ -119,7 +124,8 @@ "entries": [ "./src/index.ts", "./src/presets/babel-cache-preset.ts", - "./src/presets/common-preset.ts" + "./src/presets/common-preset.ts", + "./src/presets/common-override-preset.ts" ], "platform": "node" }, diff --git a/code/lib/core-server/src/build-dev.ts b/code/lib/core-server/src/build-dev.ts index ee045cde1a3d..7785afdee7b7 100644 --- a/code/lib/core-server/src/build-dev.ts +++ b/code/lib/core-server/src/build-dev.ts @@ -80,7 +80,9 @@ export async function buildDevStandalone( // We hope to remove this in SB8 let presets = await loadAllPresets({ corePresets, - overridePresets: [], + overridePresets: [ + require.resolve('@storybook/core-server/dist/presets/common-override-preset'), + ], ...options, }); @@ -112,7 +114,10 @@ export async function buildDevStandalone( ...corePresets, require.resolve('@storybook/core-server/dist/presets/babel-cache-preset'), ], - overridePresets: previewBuilder.overridePresets ?? [], + overridePresets: [ + ...(previewBuilder.overridePresets || []), + require.resolve('@storybook/core-server/dist/presets/common-override-preset'), + ], ...options, }); diff --git a/code/lib/core-server/src/build-static.ts b/code/lib/core-server/src/build-static.ts index 4f22e5c8ec11..810c871724fd 100644 --- a/code/lib/core-server/src/build-static.ts +++ b/code/lib/core-server/src/build-static.ts @@ -1,9 +1,7 @@ import chalk from 'chalk'; import { copy, emptyDir, ensureDir } from 'fs-extra'; import { dirname, isAbsolute, join, resolve } from 'path'; -import { dedent } from 'ts-dedent'; import { global } from '@storybook/global'; - import { logger } from '@storybook/node-logger'; import { telemetry, getPrecedingUpgrade } from '@storybook/telemetry'; import type { @@ -22,6 +20,7 @@ import { normalizeStories, resolveAddonName, } from '@storybook/core-common'; +import { ConflictingStaticDirConfigError } from '@storybook/core-events/server-errors'; import isEqual from 'lodash/isEqual.js'; import { outputStats } from './utils/output-stats'; @@ -85,7 +84,9 @@ export async function buildStaticStandalone(options: BuildStaticStandaloneOption require.resolve('@storybook/core-server/dist/presets/common-preset'), ...corePresets, ], - overridePresets: [], + overridePresets: [ + require.resolve('@storybook/core-server/dist/presets/common-override-preset'), + ], ...options, }); @@ -103,7 +104,10 @@ export async function buildStaticStandalone(options: BuildStaticStandaloneOption ...corePresets, require.resolve('@storybook/core-server/dist/presets/babel-cache-preset'), ], - overridePresets: previewBuilder.overridePresets || [], + overridePresets: [ + ...(previewBuilder.overridePresets || []), + require.resolve('@storybook/core-server/dist/presets/common-override-preset'), + ], ...options, }); @@ -125,13 +129,7 @@ export async function buildStaticStandalone(options: BuildStaticStandaloneOption }; if (options.staticDir && !isEqual(staticDirs, defaultStaticDirs)) { - throw new Error(dedent` - Conflict when trying to read staticDirs: - * Storybook's configuration option: 'staticDirs' - * Storybook's CLI flag: '--staticDir' or '-s' - - Choose one of them, but not both. - `); + throw new ConflictingStaticDirConfigError(); } const effects: Promise[] = []; diff --git a/code/lib/core-server/src/presets/common-override-preset.ts b/code/lib/core-server/src/presets/common-override-preset.ts new file mode 100644 index 000000000000..55a55bb0f117 --- /dev/null +++ b/code/lib/core-server/src/presets/common-override-preset.ts @@ -0,0 +1,14 @@ +import type { PresetProperty, StorybookConfig } from '@storybook/types'; + +export const framework: PresetProperty<'framework', StorybookConfig> = async (config) => { + // This will get called with the values from the user's main config, but before + // framework preset from framework packages e.g. react-webpack5 gets called. + // This means we can add default values to the framework config, before it's requested by other packages. + const name = typeof config === 'string' ? config : config?.name; + const options = typeof config === 'string' ? {} : config?.options || {}; + + return { + name, + options, + }; +}; diff --git a/code/lib/core-server/src/utils/server-statics.ts b/code/lib/core-server/src/utils/server-statics.ts index 609384692943..b2d5a5e3cbce 100644 --- a/code/lib/core-server/src/utils/server-statics.ts +++ b/code/lib/core-server/src/utils/server-statics.ts @@ -1,6 +1,7 @@ import { logger } from '@storybook/node-logger'; import type { Options, StorybookConfig } from '@storybook/types'; import { getDirectoryFromWorkingDir } from '@storybook/core-common'; +import { ConflictingStaticDirConfigError } from '@storybook/core-events/server-errors'; import chalk from 'chalk'; import express from 'express'; import { pathExists } from 'fs-extra'; @@ -17,13 +18,7 @@ export async function useStatics(router: any, options: Options) { const faviconPath = await options.presets.apply('favicon'); if (options.staticDir && !isEqual(staticDirs, defaultStaticDirs)) { - throw new Error(dedent` - Conflict when trying to read staticDirs: - * Storybook's configuration option: 'staticDirs' - * Storybook's CLI flag: '--staticDir' or '-s' - - Choose one of them, but not both. - `); + throw new ConflictingStaticDirConfigError(); } const statics = [ diff --git a/code/lib/core-server/src/withTelemetry.ts b/code/lib/core-server/src/withTelemetry.ts index 49b8c6c79699..0baebb8d97cb 100644 --- a/code/lib/core-server/src/withTelemetry.ts +++ b/code/lib/core-server/src/withTelemetry.ts @@ -151,7 +151,7 @@ export async function withTelemetry( try { return await run(); - } catch (error) { + } catch (error: any) { if (canceled) { return undefined; } diff --git a/code/lib/manager-api/src/lib/addons.ts b/code/lib/manager-api/src/lib/addons.ts index abaf7101d5a0..1145ab1826f5 100644 --- a/code/lib/manager-api/src/lib/addons.ts +++ b/code/lib/manager-api/src/lib/addons.ts @@ -13,6 +13,8 @@ import type { Addon_Types, Addon_TypesMapping, Addon_WrapperType, + Addon_SidebarBottomType, + Addon_SidebarTopType, } from '@storybook/types'; import { Addon_TypesEnum } from '@storybook/types'; import { logger } from '@storybook/client-logger'; @@ -97,9 +99,13 @@ export class AddonStore { this.serverChannel = channel; }; - getElements( - type: T - ): Addon_Collection { + getElements< + T extends + | Addon_Types + | Addon_TypesEnum.experimental_PAGE + | Addon_TypesEnum.experimental_SIDEBAR_BOTTOM + | Addon_TypesEnum.experimental_SIDEBAR_TOP + >(type: T): Addon_Collection { if (!this.elements[type]) { this.elements[type] = {}; } @@ -141,6 +147,8 @@ export class AddonStore { id: string, addon: | Addon_BaseType + | (Omit & DeprecatedAddonWithId) + | (Omit & DeprecatedAddonWithId) | (Omit & DeprecatedAddonWithId) | (Omit & DeprecatedAddonWithId) ): void { diff --git a/code/lib/manager-api/src/modules/addons.ts b/code/lib/manager-api/src/modules/addons.ts index 84fd51d7f206..0e1d69ca39e5 100644 --- a/code/lib/manager-api/src/modules/addons.ts +++ b/code/lib/manager-api/src/modules/addons.ts @@ -23,7 +23,13 @@ export interface SubAPI { * @param {Addon_Types | Addon_TypesEnum.experimental_PAGE} type - The type of the elements to retrieve. * @returns {API_Collection} - A collection of elements of the specified type. */ - getElements: ( + getElements: < + T extends + | Addon_Types + | Addon_TypesEnum.experimental_PAGE + | Addon_TypesEnum.experimental_SIDEBAR_BOTTOM + | Addon_TypesEnum.experimental_SIDEBAR_TOP = Addon_Types + >( type: T ) => Addon_Collection; /** diff --git a/code/lib/node-logger/src/index.test.ts b/code/lib/node-logger/src/index.test.ts index 40188dbc8ceb..3cda41501ed0 100644 --- a/code/lib/node-logger/src/index.test.ts +++ b/code/lib/node-logger/src/index.test.ts @@ -1,12 +1,28 @@ -import { info, warn, error } from 'npmlog'; +import { info, warn } from 'npmlog'; import { logger } from '.'; +globalThis.console = { log: jest.fn() } as any; + jest.mock('npmlog', () => ({ info: jest.fn(), warn: jest.fn(), error: jest.fn(), + levels: { + silly: -Infinity, + verbose: 1000, + info: 2000, + timing: 2500, + http: 3000, + notice: 3500, + warn: 4000, + error: 5000, + silent: Infinity, + }, + level: 'info', })); +// + describe('node-logger', () => { it('should have an info method', () => { const message = 'information'; @@ -21,6 +37,13 @@ describe('node-logger', () => { it('should have an error method', () => { const message = 'error message'; logger.error(message); - expect(error).toHaveBeenCalledWith('', message); + expect(globalThis.console.log).toHaveBeenCalledWith(expect.stringMatching('message')); + }); + it('should format errors', () => { + const message = new Error('A complete disaster'); + logger.error(message); + expect(globalThis.console.log).toHaveBeenCalledWith( + expect.stringMatching('A complete disaster') + ); }); }); diff --git a/code/lib/node-logger/src/index.ts b/code/lib/node-logger/src/index.ts index 09e0f2828783..4ca3550cfba2 100644 --- a/code/lib/node-logger/src/index.ts +++ b/code/lib/node-logger/src/index.ts @@ -25,13 +25,28 @@ export const logger = { plain: (message: string): void => console.log(message), line: (count = 1): void => console.log(`${Array(count - 1).fill('\n')}`), warn: (message: string): void => npmLog.warn('', message), - // npmLog supports anything we log, it will just stringify it - error: (message: unknown): void => npmLog.error('', message as string), trace: ({ message, time }: { message: string; time: [number, number] }): void => npmLog.info('', `${message} (${colors.purple(prettyTime(time))})`), setLevel: (level = 'info'): void => { npmLog.level = level; }, + error: (message: Error | string): void => { + if (npmLog.levels[npmLog.level] < npmLog.levels.error) { + let msg: string; + + if (message instanceof Error && message.stack) { + msg = message.stack.toString(); + } else { + msg = message.toString(); + } + + console.log( + msg + .replace(message.toString(), chalk.red(message.toString())) + .replaceAll(process.cwd(), '.') + ); + } + }, }; export { npmLog as instance }; diff --git a/code/lib/preview/src/globals/runtime.ts b/code/lib/preview/src/globals/runtime.ts index 6aec0edd155c..ad078c4afa8d 100644 --- a/code/lib/preview/src/globals/runtime.ts +++ b/code/lib/preview/src/globals/runtime.ts @@ -4,6 +4,7 @@ import * as CHANNELS from '@storybook/channels'; import * as CLIENT_LOGGER from '@storybook/client-logger'; import * as CORE_EVENTS from '@storybook/core-events'; import * as PREVIEW_API from '@storybook/preview-api'; +import * as GLOBAL from '@storybook/global'; // DEPRECATED, remove in 8.0 import * as ADDONS from '@storybook/preview-api/dist/addons'; @@ -22,6 +23,7 @@ export const values: Required> = { '@storybook/client-logger': CLIENT_LOGGER, '@storybook/core-events': CORE_EVENTS, '@storybook/preview-api': PREVIEW_API, + '@storybook/global': GLOBAL, // DEPRECATED, remove in 8.0 '@storybook/addons': ADDONS, diff --git a/code/lib/preview/src/globals/types.ts b/code/lib/preview/src/globals/types.ts index afd90b8bf564..40ed603cd598 100644 --- a/code/lib/preview/src/globals/types.ts +++ b/code/lib/preview/src/globals/types.ts @@ -1,6 +1,7 @@ // Here we map the name of a module to their NAME in the global scope. export const globals = { '@storybook/addons': '__STORYBOOK_MODULE_ADDONS__', + '@storybook/global': '__STORYBOOK_MODULE_GLOBAL__', '@storybook/channel-postmessage': '__STORYBOOK_MODULE_CHANNEL_POSTMESSAGE__', // @deprecated: remove in 8.0 '@storybook/channel-websocket': '__STORYBOOK_MODULE_CHANNEL_WEBSOCKET__', // @deprecated: remove in 8.0 '@storybook/channels': '__STORYBOOK_MODULE_CHANNELS__', diff --git a/code/lib/types/src/modules/addons.ts b/code/lib/types/src/modules/addons.ts index 7c6a7987a2e3..e5ec1bb2c3f3 100644 --- a/code/lib/types/src/modules/addons.ts +++ b/code/lib/types/src/modules/addons.ts @@ -29,7 +29,12 @@ import type { } from './csf'; import type { IndexEntry } from './indexer'; -export type Addon_Types = Exclude; +export type Addon_Types = Exclude< + Addon_TypesEnum, + | Addon_TypesEnum.experimental_PAGE + | Addon_TypesEnum.experimental_SIDEBAR_BOTTOM + | Addon_TypesEnum.experimental_SIDEBAR_TOP +>; export interface Addon_ArgType extends InputType { defaultValue?: TArg; @@ -324,7 +329,12 @@ export type ReactJSXElement = { key: any; }; -export type Addon_Type = Addon_BaseType | Addon_PageType | Addon_WrapperType; +export type Addon_Type = + | Addon_BaseType + | Addon_PageType + | Addon_WrapperType + | Addon_SidebarBottomType + | Addon_SidebarTopType; export interface Addon_BaseType { /** * The title of the addon. @@ -335,7 +345,13 @@ export interface Addon_BaseType { * The type of the addon. * @example Addon_TypesEnum.PANEL */ - type: Exclude; + type: Exclude< + Addon_Types, + | Addon_TypesEnum.PREVIEW + | Addon_TypesEnum.experimental_PAGE + | Addon_TypesEnum.experimental_SIDEBAR_BOTTOM + | Addon_TypesEnum.experimental_SIDEBAR_TOP + >; /** * The unique id of the addon. * @warn This will become non-optional in 8.0 @@ -448,15 +464,43 @@ export interface Addon_WrapperType { }> >; } +export interface Addon_SidebarBottomType { + type: Addon_TypesEnum.experimental_SIDEBAR_BOTTOM; + /** + * The unique id of the tool. + */ + id: string; + /** + * A React.FunctionComponent. + */ + render: FCWithoutChildren; +} + +export interface Addon_SidebarTopType { + type: Addon_TypesEnum.experimental_SIDEBAR_TOP; + /** + * The unique id of the tool. + */ + id: string; + /** + * A React.FunctionComponent. + */ + render: FCWithoutChildren; +} type Addon_TypeBaseNames = Exclude< Addon_TypesEnum, - Addon_TypesEnum.PREVIEW | Addon_TypesEnum.experimental_PAGE + | Addon_TypesEnum.PREVIEW + | Addon_TypesEnum.experimental_PAGE + | Addon_TypesEnum.experimental_SIDEBAR_BOTTOM + | Addon_TypesEnum.experimental_SIDEBAR_TOP >; export interface Addon_TypesMapping extends Record { [Addon_TypesEnum.PREVIEW]: Addon_WrapperType; [Addon_TypesEnum.experimental_PAGE]: Addon_PageType; + [Addon_TypesEnum.experimental_SIDEBAR_BOTTOM]: Addon_SidebarBottomType; + [Addon_TypesEnum.experimental_SIDEBAR_TOP]: Addon_SidebarTopType; } export type Addon_Loader = (api: API) => void; @@ -510,6 +554,16 @@ export enum Addon_TypesEnum { * @unstable */ experimental_PAGE = 'page', + /** + * This adds items in the bottom of the sidebar. + * @unstable + */ + experimental_SIDEBAR_BOTTOM = 'sidebar-bottom', + /** + * This adds items in the top of the sidebar. + * @unstable This will get replaced with a new API in 8.0, use at your own risk. + */ + experimental_SIDEBAR_TOP = 'sidebar-top', /** * @deprecated This property does nothing, and will be removed in Storybook 8.0. diff --git a/code/package.json b/code/package.json index e7e8f58081c5..3a3fba278a58 100644 --- a/code/package.json +++ b/code/package.json @@ -327,5 +327,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "7.4.0-alpha.2" } diff --git a/code/ui/.storybook/manager.tsx b/code/ui/.storybook/manager.tsx index 64691f64f4a3..7cdbda8d32e9 100644 --- a/code/ui/.storybook/manager.tsx +++ b/code/ui/.storybook/manager.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { addons, types } from '@storybook/manager-api'; import startCase from 'lodash/startCase.js'; diff --git a/code/ui/components/src/components/tabs/tabs.helpers.tsx b/code/ui/components/src/components/tabs/tabs.helpers.tsx index 024765fd5cf3..fe41cc47be91 100644 --- a/code/ui/components/src/components/tabs/tabs.helpers.tsx +++ b/code/ui/components/src/components/tabs/tabs.helpers.tsx @@ -29,8 +29,8 @@ export const childrenToList = (children: TabsProps['children']) => const render: FC = ( typeof content === 'function' ? content - : ({ active, key }: any) => ( - + : ({ active }) => ( + {content} ) diff --git a/code/ui/manager/src/components/layout/app.mockdata.tsx b/code/ui/manager/src/components/layout/app.mockdata.tsx index 1cb665508b7b..f41596303ed7 100644 --- a/code/ui/manager/src/components/layout/app.mockdata.tsx +++ b/code/ui/manager/src/components/layout/app.mockdata.tsx @@ -57,6 +57,7 @@ const realSidebarProps: SidebarProps = { refs: {}, status: {}, previewInitialized: true, + extra: [], }; const PlaceholderBlock = styled.div(({ color }) => ({ diff --git a/code/ui/manager/src/components/sidebar/Heading.stories.tsx b/code/ui/manager/src/components/sidebar/Heading.stories.tsx index d86cd02f9825..b958ebe2e1b1 100644 --- a/code/ui/manager/src/components/sidebar/Heading.stories.tsx +++ b/code/ui/manager/src/components/sidebar/Heading.stories.tsx @@ -27,7 +27,9 @@ const menuItems = [ { title: 'Menu Item 3', onClick: action('onActivateMenuItem'), id: '3' }, ]; -export const MenuHighlighted: Story = () => ; +export const MenuHighlighted: Story = () => ( + +); export const standardData = { menu: menuItems }; @@ -45,7 +47,7 @@ export const Standard: Story = () => { }, }} > - + ); }; @@ -64,7 +66,7 @@ export const StandardNoLink: Story = () => { }, }} > - + ); }; @@ -83,7 +85,7 @@ export const LinkAndText: Story = () => { }, }} > - + ); }; @@ -102,7 +104,7 @@ export const OnlyText: Story = () => { }, }} > - + ); }; @@ -121,7 +123,7 @@ export const LongText: Story = () => { }, }} > - + ); }; @@ -140,7 +142,7 @@ export const CustomTitle: Story = () => { }, }} > - + ); }; @@ -159,7 +161,7 @@ export const CustomBrandImage: Story = () => { }, }} > - + ); }; @@ -178,7 +180,7 @@ export const CustomBrandImageTall: Story = () => { }, }} > - + ); }; @@ -197,7 +199,7 @@ export const CustomBrandImageUnsizedSVG: Story = () => { }, }} > - + ); }; @@ -216,13 +218,18 @@ export const NoBrand: Story = () => { }, }} > - + ); }; export const SkipToCanvasLinkFocused: ComponentStoryObj = { - args: { menu: menuItems, skipLinkHref: '#storybook-preview-wrapper' }, + args: { + menu: menuItems, + skipLinkHref: '#storybook-preview-wrapper', + extra: [], + isLoading: false, + }, parameters: { layout: 'padded', chromatic: { delay: 300 } }, play: () => { // focus each instance for chromatic/storybook's stacked theme diff --git a/code/ui/manager/src/components/sidebar/Heading.tsx b/code/ui/manager/src/components/sidebar/Heading.tsx index 8d11af4374f6..552e1859af69 100644 --- a/code/ui/manager/src/components/sidebar/Heading.tsx +++ b/code/ui/manager/src/components/sidebar/Heading.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { styled } from '@storybook/theming'; import { Button } from '@storybook/components'; +import type { Addon_SidebarTopType } from '@storybook/types'; import { Brand } from './Brand'; import type { MenuList } from './Menu'; import { SidebarMenu } from './Menu'; @@ -10,7 +11,9 @@ import { SidebarMenu } from './Menu'; export interface HeadingProps { menuHighlighted?: boolean; menu: MenuList; + extra: Addon_SidebarTopType[]; skipLinkHref?: string; + isLoading: boolean; } const BrandArea = styled.div(({ theme }) => ({ @@ -23,6 +26,9 @@ const BrandArea = styled.div(({ theme }) => ({ alignItems: 'center', minHeight: 22, + '& > * > *': { + maxWidth: '100%', + }, '& > *': { maxWidth: '100%', height: 'auto', @@ -73,6 +79,8 @@ export const Heading: FC> = menuHighlighted = false, menu, skipLinkHref, + extra, + isLoading, ...props }) => { return ( @@ -87,6 +95,7 @@ export const Heading: FC> = + {isLoading ? null : extra.map(({ id, render: Render }) => )} ); diff --git a/code/ui/manager/src/components/sidebar/RefBlocks.tsx b/code/ui/manager/src/components/sidebar/RefBlocks.tsx index ddea8ed11aa6..52d1d335a5dd 100644 --- a/code/ui/manager/src/components/sidebar/RefBlocks.tsx +++ b/code/ui/manager/src/components/sidebar/RefBlocks.tsx @@ -145,6 +145,7 @@ export const EmptyBlock: FC = ({ isMain }) => ( The glob specified in main.js isn't correct.
  • No stories are defined in your story files.
  • +
  • You're using filter-functions, and all stories are filtered away.
  • {' '} ) : ( diff --git a/code/ui/manager/src/components/sidebar/Sidebar.stories.tsx b/code/ui/manager/src/components/sidebar/Sidebar.stories.tsx index 66fe63fbe43b..7c6dfea9a7ab 100644 --- a/code/ui/manager/src/components/sidebar/Sidebar.stories.tsx +++ b/code/ui/manager/src/components/sidebar/Sidebar.stories.tsx @@ -1,8 +1,10 @@ import React from 'react'; -import type { IndexHash, State } from 'lib/manager-api/src'; +import type { IndexHash, State } from '@storybook/manager-api'; +import { types } from '@storybook/manager-api'; import type { StoryObj, Meta } from '@storybook/react'; import { within, userEvent } from '@storybook/testing-library'; +import { Button, IconButton, Icons } from '@storybook/components'; import { Sidebar, DEFAULT_REF_ID } from './Sidebar'; import { standardData as standardHeaderData } from './Heading.stories'; import * as ExplorerStories from './Explorer.stories'; @@ -60,6 +62,7 @@ export const Simple: Story = { ( - + ), }; @@ -84,6 +95,7 @@ export const Empty: Story = { ( + ( + + ), + }, + { + id: '2', + type: types.experimental_SIDEBAR_BOTTOM, + render: () => ( + + ), + }, + { + id: '3', + type: types.experimental_SIDEBAR_BOTTOM, + render: () => ( + + {' '} + + + ), + }, + ]} + /> + ), +}; diff --git a/code/ui/manager/src/components/sidebar/Sidebar.tsx b/code/ui/manager/src/components/sidebar/Sidebar.tsx index f8daa49b726a..23aacc19f099 100644 --- a/code/ui/manager/src/components/sidebar/Sidebar.tsx +++ b/code/ui/manager/src/components/sidebar/Sidebar.tsx @@ -4,7 +4,11 @@ import { styled } from '@storybook/theming'; import { ScrollArea, Spaced } from '@storybook/components'; import type { State } from '@storybook/manager-api'; -import type { API_LoadedRefData } from 'lib/types/src'; +import type { + Addon_SidebarBottomType, + Addon_SidebarTopType, + API_LoadedRefData, +} from '@storybook/types'; import { Heading } from './Heading'; // eslint-disable-next-line import/no-cycle @@ -27,12 +31,28 @@ const Container = styled.nav({ right: 0, width: '100%', height: '100%', + display: 'flex', + flexDirection: 'column', }); -const StyledSpaced = styled(Spaced)({ - paddingBottom: '2.5rem', +const Top = styled(Spaced)({ + padding: 20, + flex: 1, }); +const Bottom = styled.div(({ theme }) => ({ + borderTop: `1px solid ${theme.appBorderColor}`, + padding: theme.layoutMargin / 2, + display: 'flex', + flexWrap: 'wrap', + gap: theme.layoutMargin / 2, + backgroundColor: theme.barBg, + + '&:empty': { + display: 'none', + }, +})); + const CustomScrollArea = styled(ScrollArea)({ '&&&&& .os-scrollbar-handle:before': { left: -12, @@ -40,7 +60,6 @@ const CustomScrollArea = styled(ScrollArea)({ '&&&&& .os-scrollbar-vertical': { right: 5, }, - padding: 20, }); const Swap = React.memo(function Swap({ @@ -82,6 +101,8 @@ export interface SidebarProps extends API_LoadedRefData { refs: State['refs']; status: State['status']; menu: any[]; + extra: Addon_SidebarTopType[]; + bottom?: Addon_SidebarBottomType[]; storyId?: string; refId?: string; menuHighlighted?: boolean; @@ -96,6 +117,8 @@ export const Sidebar = React.memo(function Sidebar({ status, previewInitialized, menu, + extra, + bottom = [], menuHighlighted = false, enableShortcuts = true, refs = {}, @@ -108,12 +131,14 @@ export const Sidebar = React.memo(function Sidebar({ return ( - + )} - + + {isLoading ? null : ( + + {bottom.map(({ id, render: Render }) => ( + + ))} + + )} ); }); diff --git a/code/ui/manager/src/components/sidebar/__tests__/Sidebar.test.tsx b/code/ui/manager/src/components/sidebar/__tests__/Sidebar.test.tsx index 26fd8750f301..78ebfb697ac7 100644 --- a/code/ui/manager/src/components/sidebar/__tests__/Sidebar.test.tsx +++ b/code/ui/manager/src/components/sidebar/__tests__/Sidebar.test.tsx @@ -16,7 +16,15 @@ const factory = (props: Partial): RenderResult => { return render( - + ); }; diff --git a/code/ui/manager/src/containers/sidebar.tsx b/code/ui/manager/src/containers/sidebar.tsx index c4beb5bda4c4..5a8b52870f82 100755 --- a/code/ui/manager/src/containers/sidebar.tsx +++ b/code/ui/manager/src/containers/sidebar.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import type { Combo, StoriesHash } from '@storybook/manager-api'; -import { Consumer } from '@storybook/manager-api'; +import { types, Consumer } from '@storybook/manager-api'; import { Sidebar as SidebarComponent } from '../components/sidebar/Sidebar'; import { useMenu } from './menu'; @@ -36,6 +36,10 @@ const Sidebar = React.memo(function Sideber() { const whatsNewNotificationsEnabled = state.whatsNewData?.status === 'SUCCESS' && !state.disableWhatsNewNotifications; + const items = api.getElements(types.experimental_SIDEBAR_BOTTOM); + const bottom = useMemo(() => Object.values(items), [items]); + const top = useMemo(() => Object.values(api.getElements(types.experimental_SIDEBAR_TOP)), []); + return { title: name, url, @@ -50,6 +54,8 @@ const Sidebar = React.memo(function Sideber() { menu, menuHighlighted: whatsNewNotificationsEnabled && api.isWhatsNewUnread(), enableShortcuts, + bottom, + extra: top, }; }; return ( diff --git a/code/yarn.lock b/code/yarn.lock index 799a4a106cbe..f6d64d965987 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -453,7 +453,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.0, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.19.6, @babel/core@npm:^7.20.12, @babel/core@npm:^7.22.1, @babel/core@npm:^7.22.9, @babel/core@npm:^7.3.4, @babel/core@npm:^7.7.5": +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.0, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.19.6, @babel/core@npm:^7.20.12, @babel/core@npm:^7.22.0, @babel/core@npm:^7.22.1, @babel/core@npm:^7.22.9, @babel/core@npm:^7.3.4, @babel/core@npm:^7.7.5": version: 7.22.9 resolution: "@babel/core@npm:7.22.9" dependencies: @@ -6466,23 +6466,15 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/builder-webpack5@workspace:builders/builder-webpack5" dependencies: - "@babel/core": ^7.22.9 - "@storybook/addons": "workspace:*" + "@babel/core": ^7.22.0 "@storybook/channels": "workspace:*" - "@storybook/client-api": "workspace:*" "@storybook/client-logger": "workspace:*" - "@storybook/components": "workspace:*" "@storybook/core-common": "workspace:*" "@storybook/core-events": "workspace:*" "@storybook/core-webpack": "workspace:*" - "@storybook/global": ^5.0.0 - "@storybook/manager-api": "workspace:*" "@storybook/node-logger": "workspace:*" "@storybook/preview": "workspace:*" "@storybook/preview-api": "workspace:*" - "@storybook/router": "workspace:*" - "@storybook/store": "workspace:*" - "@storybook/theming": "workspace:*" "@swc/core": ^1.3.49 "@types/node": ^16.0.0 "@types/pretty-hrtime": ^1.0.0 @@ -6517,9 +6509,6 @@ __metadata: webpack-dev-middleware: ^6.1.1 webpack-hot-middleware: ^2.25.1 webpack-virtual-modules: ^0.5.0 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: typescript: optional: true diff --git a/docs/addons/addons-api.md b/docs/addons/addons-api.md index 787bcbe4fdd8..17c05b0a50d5 100644 --- a/docs/addons/addons-api.md +++ b/docs/addons/addons-api.md @@ -277,7 +277,7 @@ To help streamline addon development and reduce boilerplate code, the API expose ### useStorybookState -It allows access to Storybook's internal state. Similar to the [`useglobals`](#useglobals) hook, we recommend optimizing your addon to rely on [`React.memo`](https://reactjs.org/docs/react-api.html#reactmemo), or the following hooks; [`useMemo`](https://reactjs.org/docs/hooks-reference.html#usememo), [`useCallback`](https://reactjs.org/docs/hooks-reference.html#usecallback) to prevent a high volume of re-render cycles. +It allows access to Storybook's internal state. Similar to the [`useglobals`](#useglobals) hook, we recommend optimizing your addon to rely on [`React.memo`](https://react.dev/reference/react/memo), or the following hooks; [`useMemo`](https://react.dev/reference/react/useMemo), [`useCallback`](https://react.dev/reference/react/useCallback) to prevent a high volume of re-render cycles. @@ -349,7 +349,7 @@ The `useParameter` retrieves the current story's parameters. If the parameter's ### useGlobals -Extremely useful hook for addons that rely on Storybook [Globals](../essentials/toolbars-and-globals.md). It allows you to obtain and update `global` values. We also recommend optimizing your addon to rely on [`React.memo`](https://reactjs.org/docs/react-api.html#reactmemo), or the following hooks; [`useMemo`](https://reactjs.org/docs/hooks-reference.html#usememo), [`useCallback`](https://reactjs.org/docs/hooks-reference.html#usecallback) to prevent a high volume of re-render cycles. +Extremely useful hook for addons that rely on Storybook [Globals](../essentials/toolbars-and-globals.md). It allows you to obtain and update `global` values. We also recommend optimizing your addon to rely on [`React.memo`](https://react.dev/reference/react/memo), or the following hooks; [`useMemo`](https://react.dev/reference/react/useMemo), [`useCallback`](https://react.dev/reference/react/useCallback) to prevent a high volume of re-render cycles. diff --git a/docs/snippets/react/button-story.with-hooks.ts-4-9.mdx b/docs/snippets/react/button-story.with-hooks.ts-4-9.mdx index e17ee32637c8..bbe75ff837b2 100644 --- a/docs/snippets/react/button-story.with-hooks.ts-4-9.mdx +++ b/docs/snippets/react/button-story.with-hooks.ts-4-9.mdx @@ -2,6 +2,7 @@ // Button.stories.ts|tsx import React, { useState } from 'react'; + import { Meta, StoryObj } from '@storybook/react'; import { Button } from './Button'; @@ -9,8 +10,8 @@ import { Button } from './Button'; const meta = { component: Button, } satisfies Meta; -export default meta; +export default meta; type Story = StoryObj; /* diff --git a/docs/snippets/react/button-story.with-hooks.ts.mdx b/docs/snippets/react/button-story.with-hooks.ts.mdx index 9c9f93ca87c7..338480ef0749 100644 --- a/docs/snippets/react/button-story.with-hooks.ts.mdx +++ b/docs/snippets/react/button-story.with-hooks.ts.mdx @@ -1,7 +1,8 @@ -```js +```tsx // Button.stories.ts|tsx import React, { useState } from 'react'; + import { Meta, StoryObj } from '@storybook/react'; import { Button } from './Button'; @@ -9,8 +10,8 @@ import { Button } from './Button'; const meta: Meta = { component: Button, }; -export default meta; +export default meta; type Story = StoryObj; /* diff --git a/docs/snippets/solid/button-story.with-hooks.js.mdx b/docs/snippets/solid/button-story.with-hooks.js.mdx index f9e278a50171..b41881651f14 100644 --- a/docs/snippets/solid/button-story.with-hooks.js.mdx +++ b/docs/snippets/solid/button-story.with-hooks.js.mdx @@ -1,5 +1,5 @@ ```js -// Button.stories.js|ts|jsx|tsx +// Button.stories.js|jsx import { createSignal } from 'solid-js'; diff --git a/docs/snippets/solid/button-story.with-hooks.ts-4-9.mdx b/docs/snippets/solid/button-story.with-hooks.ts-4-9.mdx new file mode 100644 index 000000000000..60422480efc3 --- /dev/null +++ b/docs/snippets/solid/button-story.with-hooks.ts-4-9.mdx @@ -0,0 +1,39 @@ +```tsx +// Button.stories.ts|tsx + +import type { Meta, StoryObj } from 'storybook-solidjs'; + +import { createSignal } from 'solid-js'; + +import { Button } from './Button'; + +const meta = { + component: Button, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/* + * Example Button story with Solid Hooks. + * See note below related to this example. + */ +const ButtonWithHooks = () => { + // Sets the hooks for both the label and primary props + const [value, setValue] = createSignal('Secondary'); + const [isPrimary, setIsPrimary] = createSignal(false); + + // Sets a click handler to change the label's value + const handleOnChange = () => { + if (!isPrimary()) { + setIsPrimary(true); + setValue('Primary'); + } + }; + return