From 1528ca59f4d9734b712f9c0fb9e7fc94eb5b48e0 Mon Sep 17 00:00:00 2001 From: Krotov Petr <83280920+KrotovPetr@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:53:27 +0300 Subject: [PATCH 1/2] feat: support rspack for storybook --- src/common/webpack/config.ts | 2 +- src/common/webpack/storybook.ts | 238 +++++++++++++++++++++++++------- src/index.ts | 2 + 3 files changed, 194 insertions(+), 48 deletions(-) diff --git a/src/common/webpack/config.ts b/src/common/webpack/config.ts index 1232d30..e9f0d91 100644 --- a/src/common/webpack/config.ts +++ b/src/common/webpack/config.ts @@ -1674,7 +1674,7 @@ export function configureOptimization(helperOptions: HelperOptions): Optimizatio return optimization; } -function configureRspackOptimization( +export function configureRspackOptimization( helperOptions: HelperOptions, ): NonNullable { const {config, isSsr} = helperOptions; diff --git a/src/common/webpack/storybook.ts b/src/common/webpack/storybook.ts index aa8c1c7..7e4754d 100644 --- a/src/common/webpack/storybook.ts +++ b/src/common/webpack/storybook.ts @@ -3,26 +3,60 @@ import {createRequire} from 'node:module'; import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; import MiniCSSExtractPlugin from 'mini-css-extract-plugin'; import OptimizeCSSAssetsPlugin from 'css-minimizer-webpack-plugin'; +import {rspack} from '@rspack/core'; import { WebpackMode, configureModuleRules, configureOptimization, configureResolve, + configureRspackOptimization, } from './config.js'; import {getProjectConfig, normalizeConfig} from '../config.js'; import {isLibraryConfig} from '../models/index.js'; import paths from '../paths.js'; import type {HelperOptions} from './config.js'; -import type {ClientCommonConfig, ClientWebpackConfig} from '../models/index.js'; +import type { + ClientCommonConfig, + ClientRspackConfig, + ClientWebpackConfig, + LibraryConfig, + NormalizedClientConfig, +} from '../models/index.js'; import type * as Webpack from 'webpack'; +import type * as Rspack from '@rspack/core'; import {getNormalizedWorkerOption} from './utils.js'; const require = createRequire(import.meta.url); type Mode = `${WebpackMode}`; +function configureLibraryForStorybook(config: LibraryConfig): ClientCommonConfig { + const libBabelPlugins = config.lib?.babelPlugins || []; + + return { + includes: ['src'], + babel: (babelConfig) => ({ + ...babelConfig, + plugins: [...(babelConfig.plugins || []), ...libBabelPlugins], + }), + }; +} + +function getStorybookHelperOptions(mode: Mode, config: NormalizedClientConfig): HelperOptions { + return { + isEnvDevelopment: mode === WebpackMode.Dev, + isEnvProduction: mode === WebpackMode.Prod, + config, + configType: mode, + buildDirectory: config.outputPath || paths.appBuild, + entriesDirectory: paths.appEntry, + isSsr: false, + webWorkerHandle: getNormalizedWorkerOption(config), + }; +} + export async function configureServiceWebpackConfig( mode: Mode, storybookConfig: Webpack.Configuration, @@ -32,33 +66,18 @@ export async function configureServiceWebpackConfig( }); let options: ClientCommonConfig & ClientWebpackConfig = {}; if (isLibraryConfig(serviceConfig)) { - const libBabelPlugins = serviceConfig.lib?.babelPlugins || []; options = { - includes: ['src'], - babel: (babelConfig) => ({ - ...babelConfig, - plugins: [...(babelConfig.plugins || []), ...libBabelPlugins], - }), + ...configureLibraryForStorybook(serviceConfig), + bundler: 'webpack', }; } else if (serviceConfig.client.bundler === 'rspack') { - const { - rspack: _, - bundler: __, - detectCircularDependencies: ___, - ...rest - } = serviceConfig.client; - - options = rest; + throw new Error( + 'Rspack project config cannot be used by the Webpack Storybook builder. Use configureServiceRspackConfig instead.', + ); } else { options = serviceConfig.client; } - options = { - ...options, - // TODO support rspack for storybook - bundler: 'webpack', - }; - const webpackConfig = await configureWebpackConfigForStorybook( mode, options, @@ -107,41 +126,103 @@ export async function configureServiceWebpackConfig( }; } -type ModuleRule = NonNullable['rules']>[number]; +/** + * Applies the service Rspack settings to the config created by Storybook's Rsbuild builder. + * + * @param mode Storybook build mode. + * @param storybookConfig Rspack config created by the Storybook builder. + * @returns The Storybook config extended with app-builder settings. + */ +export async function configureServiceRspackConfig( + mode: Mode, + storybookConfig: Rspack.Configuration, +): Promise { + const serviceConfig = await getProjectConfig(mode === WebpackMode.Prod ? 'build' : 'dev', { + storybook: true, + }); + + let options: ClientCommonConfig & ClientRspackConfig; + if (isLibraryConfig(serviceConfig)) { + options = { + ...configureLibraryForStorybook(serviceConfig), + bundler: 'rspack', + }; + } else if (serviceConfig.client.bundler === 'rspack') { + options = serviceConfig.client; + } else { + throw new Error( + 'Webpack project config cannot be used by the Rspack Storybook builder. Use configureServiceWebpackConfig instead.', + ); + } + + const rspackConfig = await configureRspackConfigForStorybook( + mode, + options, + storybookConfig.module?.rules, + ); + + let devtool = storybookConfig.devtool; + if (mode === WebpackMode.Prod && devtool) { + devtool = 'source-map'; + } + + return { + ...storybookConfig, + devtool, + plugins: [...(storybookConfig.plugins ?? []), ...rspackConfig.plugins], + resolve: { + ...storybookConfig.resolve, + ...rspackConfig.resolve, + alias: { + ...storybookConfig.resolve?.alias, + ...rspackConfig.resolve.alias, + }, + modules: [ + ...(storybookConfig.resolve?.modules || []), + ...(rspackConfig.resolve.modules || []), + ], + extensions: [ + ...(storybookConfig.resolve?.extensions ?? []), + ...(rspackConfig.resolve.extensions || []), + ], + fallback: { + ...storybookConfig.resolve?.fallback, + ...rspackConfig.resolve.fallback, + }, + }, + module: { + ...storybookConfig.module, + rules: rspackConfig.module.rules, + }, + optimization: { + ...storybookConfig.optimization, + ...rspackConfig.optimization, + }, + }; +} + +type WebpackModuleRule = NonNullable['rules']>[number]; export async function configureWebpackConfigForStorybook( mode: Mode, userConfig: ClientCommonConfig & ClientWebpackConfig = {}, - storybookModuleRules: ModuleRule[] = [], + storybookModuleRules: WebpackModuleRule[] = [], ) { - const isEnvDevelopment = mode === WebpackMode.Dev; - const isEnvProduction = mode === WebpackMode.Prod; - const config = await normalizeConfig({ client: { ...userConfig, - // TODO support rspack for storybook bundler: 'webpack', includes: (userConfig.includes ?? []).concat(['.storybook']), }, }); - const helperOptions: HelperOptions = { - isEnvDevelopment, - isEnvProduction, - config: config.client, - configType: mode, - buildDirectory: config.client.outputPath || paths.appBuild, - entriesDirectory: paths.appEntry, - isSsr: false, - webWorkerHandle: getNormalizedWorkerOption(config.client), - }; + const helperOptions = getStorybookHelperOptions(mode, config.client); return { module: { rules: await configureModuleRules( helperOptions, storybookModuleRules.filter((rule) => rule !== '...') as Exclude< - ModuleRule, + WebpackModuleRule, '...' >[], ), @@ -154,6 +235,42 @@ export async function configureWebpackConfigForStorybook( }; } +type RspackModuleRule = NonNullable['rules']>[number]; + +export async function configureRspackConfigForStorybook( + mode: Mode, + userConfig: ClientCommonConfig & ClientRspackConfig = {bundler: 'rspack'}, + storybookModuleRules: RspackModuleRule[] = [], +) { + const config = await normalizeConfig({ + client: { + ...userConfig, + bundler: 'rspack', + ssr: undefined, + includes: (userConfig.includes ?? []).concat(['.storybook']), + }, + }); + + const helperOptions = getStorybookHelperOptions(mode, config.client); + const additionalRules = storybookModuleRules.filter( + (rule) => rule !== '...', + ) as unknown as NonNullable; + + return { + module: { + rules: (await configureModuleRules( + helperOptions, + additionalRules, + )) as Rspack.RuleSetRules, + }, + resolve: configureResolve(helperOptions) as NonNullable, + plugins: configureRspackPlugins(helperOptions), + optimization: { + minimizer: configureRspackOptimization(helperOptions).minimizer, + }, + }; +} + function configurePlugins({isEnvDevelopment, isEnvProduction, config}: HelperOptions) { const plugins: Webpack.Configuration['plugins'] = []; @@ -169,15 +286,7 @@ function configurePlugins({isEnvDevelopment, isEnvProduction, config}: HelperOpt } if (config.monaco) { - const MonacoEditorWebpackPlugin = require('monaco-editor-webpack-plugin'); - plugins.push( - new MonacoEditorWebpackPlugin({ - ...config.monaco, - // currently, workers located on cdn are not working properly, so we are enforcing loading workers from - // service instead - publicPath: '/', - }), - ); + plugins.push(createStorybookMonacoPlugin(config)); } if (isEnvDevelopment && config.reactRefresh !== false) { @@ -209,3 +318,38 @@ function configurePlugins({isEnvDevelopment, isEnvProduction, config}: HelperOpt return plugins; } + +function configureRspackPlugins({config}: HelperOptions) { + const plugins: NonNullable = []; + + if (config.definitions) { + const definitions = {...config.definitions} as unknown as Rspack.DefinePluginOptions; + plugins.push(new rspack.DefinePlugin(definitions)); + } + + if (config.monaco) { + plugins.push(createStorybookMonacoPlugin(config) as unknown as Rspack.Plugin); + } + + if (config.bundler === 'rspack' && config.detectCircularDependencies) { + const options: Rspack.CircularDependencyRspackPluginOptions = + typeof config.detectCircularDependencies === 'object' + ? config.detectCircularDependencies + : {exclude: /node_modules/}; + + plugins.push(new rspack.CircularDependencyRspackPlugin(options)); + } + + return plugins; +} + +function createStorybookMonacoPlugin(config: NormalizedClientConfig) { + const MonacoEditorWebpackPlugin = require('monaco-editor-webpack-plugin'); + + return new MonacoEditorWebpackPlugin({ + ...config.monaco, + // currently, workers located on cdn are not working properly, so we are enforcing loading workers from + // service instead + publicPath: '/', + }); +} diff --git a/src/index.ts b/src/index.ts index c1c0463..a0f37af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ export { + configureRspackConfigForStorybook, + configureServiceRspackConfig, configureWebpackConfigForStorybook, configureServiceWebpackConfig, } from './common/webpack/storybook.js'; From f8a7b240e5c602f5fc8eb37e808ffc7ec57af382 Mon Sep 17 00:00:00 2001 From: Krotov Petr <83280920+KrotovPetr@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:44:38 +0300 Subject: [PATCH 2/2] refactor: narrow storybook rspack changes --- src/common/webpack/storybook.ts | 113 +++++++++++++++++--------------- 1 file changed, 60 insertions(+), 53 deletions(-) diff --git a/src/common/webpack/storybook.ts b/src/common/webpack/storybook.ts index 7e4754d..0a2d63b 100644 --- a/src/common/webpack/storybook.ts +++ b/src/common/webpack/storybook.ts @@ -17,13 +17,7 @@ import {isLibraryConfig} from '../models/index.js'; import paths from '../paths.js'; import type {HelperOptions} from './config.js'; -import type { - ClientCommonConfig, - ClientRspackConfig, - ClientWebpackConfig, - LibraryConfig, - NormalizedClientConfig, -} from '../models/index.js'; +import type {ClientCommonConfig, ClientRspackConfig, ClientWebpackConfig} from '../models/index.js'; import type * as Webpack from 'webpack'; import type * as Rspack from '@rspack/core'; import {getNormalizedWorkerOption} from './utils.js'; @@ -32,31 +26,6 @@ const require = createRequire(import.meta.url); type Mode = `${WebpackMode}`; -function configureLibraryForStorybook(config: LibraryConfig): ClientCommonConfig { - const libBabelPlugins = config.lib?.babelPlugins || []; - - return { - includes: ['src'], - babel: (babelConfig) => ({ - ...babelConfig, - plugins: [...(babelConfig.plugins || []), ...libBabelPlugins], - }), - }; -} - -function getStorybookHelperOptions(mode: Mode, config: NormalizedClientConfig): HelperOptions { - return { - isEnvDevelopment: mode === WebpackMode.Dev, - isEnvProduction: mode === WebpackMode.Prod, - config, - configType: mode, - buildDirectory: config.outputPath || paths.appBuild, - entriesDirectory: paths.appEntry, - isSsr: false, - webWorkerHandle: getNormalizedWorkerOption(config), - }; -} - export async function configureServiceWebpackConfig( mode: Mode, storybookConfig: Webpack.Configuration, @@ -66,9 +35,13 @@ export async function configureServiceWebpackConfig( }); let options: ClientCommonConfig & ClientWebpackConfig = {}; if (isLibraryConfig(serviceConfig)) { + const libBabelPlugins = serviceConfig.lib?.babelPlugins || []; options = { - ...configureLibraryForStorybook(serviceConfig), - bundler: 'webpack', + includes: ['src'], + babel: (babelConfig) => ({ + ...babelConfig, + plugins: [...(babelConfig.plugins || []), ...libBabelPlugins], + }), }; } else if (serviceConfig.client.bundler === 'rspack') { throw new Error( @@ -143,9 +116,14 @@ export async function configureServiceRspackConfig( let options: ClientCommonConfig & ClientRspackConfig; if (isLibraryConfig(serviceConfig)) { + const libBabelPlugins = serviceConfig.lib?.babelPlugins || []; options = { - ...configureLibraryForStorybook(serviceConfig), bundler: 'rspack', + includes: ['src'], + babel: (babelConfig) => ({ + ...babelConfig, + plugins: [...(babelConfig.plugins || []), ...libBabelPlugins], + }), }; } else if (serviceConfig.client.bundler === 'rspack') { options = serviceConfig.client; @@ -201,12 +179,15 @@ export async function configureServiceRspackConfig( }; } -type WebpackModuleRule = NonNullable['rules']>[number]; +type ModuleRule = NonNullable['rules']>[number]; export async function configureWebpackConfigForStorybook( mode: Mode, userConfig: ClientCommonConfig & ClientWebpackConfig = {}, - storybookModuleRules: WebpackModuleRule[] = [], + storybookModuleRules: ModuleRule[] = [], ) { + const isEnvDevelopment = mode === WebpackMode.Dev; + const isEnvProduction = mode === WebpackMode.Prod; + const config = await normalizeConfig({ client: { ...userConfig, @@ -215,14 +196,23 @@ export async function configureWebpackConfigForStorybook( }, }); - const helperOptions = getStorybookHelperOptions(mode, config.client); + const helperOptions: HelperOptions = { + isEnvDevelopment, + isEnvProduction, + config: config.client, + configType: mode, + buildDirectory: config.client.outputPath || paths.appBuild, + entriesDirectory: paths.appEntry, + isSsr: false, + webWorkerHandle: getNormalizedWorkerOption(config.client), + }; return { module: { rules: await configureModuleRules( helperOptions, storybookModuleRules.filter((rule) => rule !== '...') as Exclude< - WebpackModuleRule, + ModuleRule, '...' >[], ), @@ -242,6 +232,9 @@ export async function configureRspackConfigForStorybook( userConfig: ClientCommonConfig & ClientRspackConfig = {bundler: 'rspack'}, storybookModuleRules: RspackModuleRule[] = [], ) { + const isEnvDevelopment = mode === WebpackMode.Dev; + const isEnvProduction = mode === WebpackMode.Prod; + const config = await normalizeConfig({ client: { ...userConfig, @@ -251,7 +244,16 @@ export async function configureRspackConfigForStorybook( }, }); - const helperOptions = getStorybookHelperOptions(mode, config.client); + const helperOptions: HelperOptions = { + isEnvDevelopment, + isEnvProduction, + config: config.client, + configType: mode, + buildDirectory: config.client.outputPath || paths.appBuild, + entriesDirectory: paths.appEntry, + isSsr: false, + webWorkerHandle: getNormalizedWorkerOption(config.client), + }; const additionalRules = storybookModuleRules.filter( (rule) => rule !== '...', ) as unknown as NonNullable; @@ -286,7 +288,15 @@ function configurePlugins({isEnvDevelopment, isEnvProduction, config}: HelperOpt } if (config.monaco) { - plugins.push(createStorybookMonacoPlugin(config)); + const MonacoEditorWebpackPlugin = require('monaco-editor-webpack-plugin'); + plugins.push( + new MonacoEditorWebpackPlugin({ + ...config.monaco, + // currently, workers located on cdn are not working properly, so we are enforcing loading workers from + // service instead + publicPath: '/', + }), + ); } if (isEnvDevelopment && config.reactRefresh !== false) { @@ -328,7 +338,15 @@ function configureRspackPlugins({config}: HelperOptions) { } if (config.monaco) { - plugins.push(createStorybookMonacoPlugin(config) as unknown as Rspack.Plugin); + const MonacoEditorWebpackPlugin = require('monaco-editor-webpack-plugin'); + plugins.push( + new MonacoEditorWebpackPlugin({ + ...config.monaco, + // currently, workers located on cdn are not working properly, so we are enforcing loading workers from + // service instead + publicPath: '/', + }) as unknown as Rspack.Plugin, + ); } if (config.bundler === 'rspack' && config.detectCircularDependencies) { @@ -342,14 +360,3 @@ function configureRspackPlugins({config}: HelperOptions) { return plugins; } - -function createStorybookMonacoPlugin(config: NormalizedClientConfig) { - const MonacoEditorWebpackPlugin = require('monaco-editor-webpack-plugin'); - - return new MonacoEditorWebpackPlugin({ - ...config.monaco, - // currently, workers located on cdn are not working properly, so we are enforcing loading workers from - // service instead - publicPath: '/', - }); -}