storybook/core-webpackStorybook 中 Webpack 构建器的共享工具库【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybookstorybook/core-webpack是 Storybook 单仓库monorepo内部的一个基础包它把跨storybook/core-servermanager UI 配置与storybook/builder-webpack{4,5}preview 配置反复使用的 Webpack 相关工具函数抽取成了统一实现。本文以该包的 README 为主线结合其源码逐一走读这个包公开的全部工具版本校验、自定义配置加载、配置合并、stories 动态导入生成以及配套的 TypeScript 类型定义帮助你理解 Storybook 的 Webpack 构建器是如何拼装用户配置与默认配置的。一、包的定位为什么会有 core-webpackREADME 中对该包的定位非常直白Common utilities used acrossstorybook/core-server(manager UI configuration) andstorybook/builder-webpack{4,5}(preview configuration).也就是说它是 manager 端与 preview 端两条配置链路的公共依赖。README 同时交代了这个包诞生的历史原因——它不是因为设计上天然应该抽象而是因为 Storybook 需要同时支持多个 Webpack 版本Webpack 4 与 Webpack 5导致大量本不该通用的代码被复制了两份于是这些重复代码被集中抽取到这个包里This is a lot of code extracted for convenience, not because it made sense... Supporting multiple version of webpack and this duplicating a large portion of code that was never meant to be generic caused this.README 还坦承这属于历史债未来可能会重构这个包其中可能有相当一部分代码已经死掉或几乎不再使用。这一点对阅读源码时的判断很重要——看到某个工具函数时应结合当前 builder 的实际调用去确认它是否仍在使用。从 package.json 可以看到该包的关键元信息包名storybook/core-webpack当前仓库中版本为10.6.0-beta.1type: module纯 ESM 包exports只暴露一个入口./src/index.ts源码与./dist/index.js产物外加类型声明运行时依赖只有一个轻量库ts-dedent用于源码中的多行字符串模板peer 依赖是 monorepo 内的storybook包。在仓库中实际消费这个包的典型位置是 Webpack 5 构建器例如 builder-webpack5 入口 中的import { checkWebpackVersion } from storybook/core-webpack下文会结合调用点展开。二、公共 API 总览包的全部公共接口由 src/index.ts 一次性导出对应五个模块导出来源文件职责WebpackConfiguration、StorybookConfig、Options等类型types.tsWebpack 配置与.storybook/main.ts的类型契约loadCustomWebpackConfigload-custom-webpack-config.ts从配置目录加载用户的自定义 Webpack 配置文件checkWebpackVersioncheck-webpack-version.ts校验实际加载的 Webpack 版本是否符合预期mergeConfigsmerge-webpack-config.ts将 Storybook 默认配置与用户/插件配置按字段合并toImportFn、toImportFnPart、webpackIncludeRegexpto-importFn.ts生成 stories 的动态import()代码toRequireContext、toRequireContextStringto-require-context.ts生成 Webpack 4 风格的require.context参数下面逐个展开其实现细节。三、checkWebpackVersion防止版本混用check-webpack-version.ts 实现了一个很小的但很实用的防护函数export const checkWebpackVersion ( webpack: { version?: string }, specifier: string, caption: string ) { if (!webpack.version) { logger.info(Skipping webpack version check, no version available); return; } if (webpack.version ! specifier) { logger.warn(dedent Unexpected webpack version in ${caption}: - Received ${webpack.version} - Expected ${specifier} ... ); } };行为规则有三点值得注意缺版本则静默跳过如果加载到的 webpack 模块没有version字段例如被 mock 或非标准安装只打一条 info 日志不做阻断——这是为了兼容测试与特殊环境严格字符串相等比较webpack.version ! specifier即期望值是调用方传入的精确版本字符串如5而不是范围只告警不抛错版本不一致时输出带格式的logger.warn提示中会指出当前版本与期望版本并指向 Webpack 5 迁移指南对应仓库根目录的 MIGRATION.md。实际调用方在 builder-webpack5/src/index.ts 中构建器启动时会用该函数核对项目里真实加载到的 Webpack 版本避免用户装了错误大版本后出现难以排查的构建错误。四、loadCustomWebpackConfig查找并加载用户的 Webpack 配置load-custom-webpack-config.ts 全文只有几行却定义了 Storybook 识别用户自定义 Webpack 配置的文件约定const webpackConfigs [webpack.config, webpackfile]; export const loadCustomWebpackConfig async (configDir: string) serverRequire(webpackConfigs.map((configName) resolve(configDir, configName)));要点解析文件约定依次尝试configDir/webpack.config.ts|js|mjs|cjs与configDir/webpackfile前缀匹配由serverRequire内部解析扩展名。这里的configDir就是 Storybook 配置目录默认.storybook多候选一次性探测把两个候选绝对路径一起交给serverRequire来自storybook/internal/common由它负责按顺序解析扩展名、加载 ESM/CJS 模块并取第一个成功者找不到时返回空构建器随后只使用内置默认配置服务端加载使用serverRequire而非普通require保证与 Storybook 服务端的模块解析环境一致例如 monorepo 工作区场景下的解析行为。这个函数在构建器侧的消费点在 builder-webpack5/src/presets/custom-webpack-preset.ts它把加载结果作为 preset 注入后续的配置管道。五、mergeConfigsStorybook 配置与用户配置的合并规则这是整个包中最核心的一块逻辑。merge-webpack-config.ts 导出的mergeConfigs(config, customConfig)实现了Storybook 默认配置 用户/插件配置的字段级合并配套的类型定义在 types.tsexport interface WebpackConfiguration { plugins?: any[]; module?: ModuleConfig; // { rules?: RulesConfig[] } resolve?: ResolveConfig; // { extensions?, mainFields?, alias? } optimization?: any; devtool?: false | string | { type: all | javascript | css; use: any }[]; }合并策略按字段区分这正是理解我的自定义配置会如何影响最终构建的关键字段合并策略源码函数plugins数组拼接默认插件在前自定义插件追加在后mergePluginsFieldmodule.rules数组拼接默认 loader 规则在前自定义规则在后即自定义 rule 不会覆盖同名默认 rule而是并列mergeRulesFieldresolve.alias对象浅合并用户别名后展开同名键覆盖默认值mergeAliasFieldresolve.extensions数组拼接两者并集顺序为默认在前、自定义在后mergeExtensionsFieldoptimization对象浅合并自定义字段覆盖同名默认字段mergeOptimizationFielddevtool用户优先customConfig.devtool \|\| config.devtoolmergeConfigs主体mergeConfigs的主体逻辑export function mergeConfigs(config: Configuration, customConfig: Configuration): Configuration { return { // Well always load our configurations after the custom config. // So, well always load the stuff we need. ...customConfig, ...config, devtool: customConfig.devtool || config.devtool, plugins: mergePluginsField(config.plugins, customConfig.plugins), module: mergeModuleField(config.module || {}, customConfig.module || {}), resolve: mergeResolveField(config, customConfig), optimization: mergeOptimizationField(config, customConfig), }; }可以注意源码注释里解释的设计意图Storybook 的配置总在自定义配置之后加载因此用...customConfig打底、...config覆盖的方式保证Storybook 自身必需的字段不会被用户配置意外抹掉同时对plugins、rules这类必须累积的字段做显式拼接。该模块带有完整的单元测试 merge-webpack-config.test.ts 与快照文件 merge-webpack-config.test.ts.snap可以查看具体合并用例的预期输出。这一合并策略直接对应文档层面.storybook/main.ts的webpack/webpackFinal钩子语义见下文类型定义前者在插件链中运行、后者在所有插件之后运行但无论哪一层最终都会经过这套规则落到真实构建配置上。六、stories 动态导入toImportFn 与 importPipelineStorybook 需要把main.stories里的 glob如../src/**/*.stories.tsx翻译成运行时的模块加载代码。to-importFn.ts 负责生成这段虚拟模块源码。6.1 webpackIncludeRegexp生成 Webpack 的 include 魔法注释webpackIncludeRegexp(specifier)接收一个已规范化的 stories 描述{ directory, files }产出webpackInclude魔法注释要用到的正则const webpackIncludeGlob [., ..].includes(directory) ? files : ${directoryWithoutLeadingDots}/${files}; const webpackIncludeRegexpWithCaret webpackIncludeGlob.includes(node_modules) ? globToRegexp(webpackIncludeGlob) : adjustRegexToExcludeNodeModules(globToRegexp(webpackIncludeGlob));两个细节设计默认排除 node_modulesadjustRegexToExcludeNodeModules会给生成的正则注入(?!.*node_modules)负向前瞻避免src/**/Button.stories.tsx这类 glob 把node_modules里的同名文件也拖进 bundle只有 glob 本身就显式包含node_modules时才跳过该保护去锚点源码注释说明 Webpack 传给 matcher 的是某种类似绝对路径的字符串而picomatch经globToRegexp生成的是精确匹配所以最后把正则开头的^去掉退化为后缀匹配以适应 Webpack 实际传入的路径前缀不确定这一现实。6.2 toImportFn拼装 importers 数组toImportFn把多个 stories 描述拼成一段可直接注入预览虚拟模块的 ESM 源码return dedent ${pipelinedImport} const importers [ ${stories.map(toImportFnPart).join(,\n)} ]; export async function importFn(path) { for (let i 0; i importers.length; i) { const moduleExports await pipeline(() importersi); if (moduleExports) { return moduleExports; } } } ;其中每个 importer 由toImportFnPart生成核心是利用 Webpack 的两个魔法注释约束动态 import 的模块集合return import( /* webpackChunkName: [request] */ /* webpackInclude: 正则 */ ${directory}/ pathRemainder );webpackChunkName: [request]让每个 stories 模块拥有独立、可读的 chunk 名利于懒加载与调试webpackInclude正则告诉 Webpack 只把匹配到的文件纳入该动态表达式从而让main.stories的 glob 语义真正生效。6.3 importPipeline避免热更新时的重编译竞争toImportFn接受{ needPipelinedImport }选项开启后会内联注入 importPipeline.ts 中的门闩函数export function importPipeline() { let importGate: Promisevoid Promise.resolve(); return async (importFn: () PromiseModuleExports) { await importGate; const moduleExportsPromise importFn(); importGate importGate.then(async () { await moduleExportsPromise; }); return moduleExportsPromise; }; };源码注释解释了动机如果一个import()还在飞行途中又有新的import()发起Webpack 可能在首次编译尚未完成时启动第二次编译造成热更新场景下的竞争问题对应上游 Webpack 社区的已知问题讨论。该管道的行为是首个 import 排队等待后续同批 import 在同一 tick 内一起放行既串行化了会互相干扰的连续编译又不会让无并发的批量导入被人为串行拖慢。它同样附有独立的单元测试 importPipeline.test.ts。消费方在 builder-webpack5/src/preview/virtual-module-mapping.ts构建器把toImportFn生成的源码映射为一个虚拟模块供预览 iframe 里的运行时按路径加载 stories。七、toRequireContext面向 Webpack 4 时代的等价实现to-require-context.ts 提供与toImportFn语义等价的旧式 APIexport const toRequireContext (specifier: NormalizedStoriesSpecifier) { const { directory, files } specifier; const match globToRegexp(./${files}); return { path: directory, recursive: files.includes(**) || files.split(/).length 1, match, }; };path是 stories 所在目录recursive的判定规则是glob 中含**或路径段超过一层才递归扫描match是./前缀的文件 glob 经globToRegexp得到的正则toRequireContextString进一步把它渲染成require.context(${p}, ${r}, ${m})的可执行字符串。从源码结构看这两个工具与toImportFn并存正体现了 README 所述同时支持多个 Webpack 版本的历史包袱——新版预览管线走动态import()旧版路径走require.context。八、类型契约WebpackConfiguration 与 webpack/webpackFinal 钩子types.ts 除了上面提到的WebpackConfiguration还重导出并扩展了StorybookConfigexport type StorybookConfigTWebpackConfiguration WebpackConfiguration StorybookConfigBase { /** * Modify or return a custom Webpack config after the Storybooks default configuration has run * (mostly used by addons). */ webpack?: (config, options) TWebpackConfiguration | PromiseTWebpackConfiguration; /** Modify or return a custom Webpack config after every addon has run. */ webpackFinal?: (config, options) TWebpackConfiguration | PromiseTWebpackConfiguration; };这两个钩子就是用户在.storybook/main.ts中编写 Webpack 定制代码的类型来源其执行顺序语义在 JSDoc 中写得很明确webpack在 Storybook 默认配置跑完之后、供插件与框架层扩展使用webpackFinal在所有插件都执行完之后运行是用户做最终覆盖的落点两者均支持同步或Promise返回值可异步读取配置文件后返回。同文件还定义了BuilderOptionsfsCache、lazyCompilation两个布尔选项供 builder-webpack5 的 iframe 构建配置 等模块声明可选的构建增强项Options、Preset、BuilderResult、TypescriptOptions等类型则从storybook/internal/types重导出供各框架包共用同一套契约。九、小结这个包在构建链路中的位置综合 README 的自我描述与源码证据storybook/core-webpack在 Storybook 构建链路中的角色可以概括为三点配置合并层mergeConfigs定义了默认配置 插件配置 用户配置如何字段级叠加plugins/rules 累积、alias/extensions 合并、devtool 用户优先是所有 Webpack 构建器共享的合并规则实现见 merge-webpack-config.tsstories 装载层toImportFn/toRequireContext把main.stories的 glob 翻译成 Webpack 可执行的动态导入或require.context代码并借助importPipeline规避热更新竞争实现见 to-importFn.ts 与 importPipeline.ts防护与探测层checkWebpackVersion校验 Webpack 版本、loadCustomWebpackConfig探测.storybook目录下的自定义配置文件实现见 check-webpack-version.ts 与 load-custom-webpack-config.ts。同时README 也提醒使用者这个包是为抽取而抽取的历史产物其中可能存在不再活跃的代码——在实际引用或迁移时应以 builder-webpack5 等当前构建器的真实调用点为准来判断某个工具是否仍在主链路上生效。【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考