Source

本章节描述了 Rsbuild 中与源代码解析、编译方式相关的配置。

source.alias

  • 类型:
type Alias = Record<string, string | false | (string | false)[]> | Function;
  • 默认值: undefined

设置文件引用的别名,对应 Rspack 的 resolve.alias 配置。

TIP

对于 TypeScript 项目,你只需要在 tsconfig.json 中配置 compilerOptions.paths 即可,Rsbuild 会自动识别它,不需要额外配置 source.alias 字段,详见 「路径别名」

Object 类型

alias 的值可以定义为 Object 类型,其中的相对路径会自动被 Rsbuild 转换为绝对路径。

export default {
  source: {
    alias: {
      '@common': './src/common',
    },
  },
};

以上配置完成后,如果你在代码中引用 @common/Foo.tsx, 则会映射到 <project>/src/common/Foo.tsx 路径上。

Function 类型

alias 的值定义为函数时,可以接受预设的 alias 对象,并对其进行修改。

export default {
  source: {
    alias: (alias) => {
      alias['@common'] = './src/common';
    },
  },
};

也可以在函数中返回一个新对象作为最终结果,新对象会覆盖预设的 alias 对象。

export default {
  source: {
    alias: (alias) => {
      return {
        '@common': './src/common',
      };
    },
  },
};

精确匹配

默认情况,source.alias 会自动匹配子路径,比如以下配置:

import path from 'path';

export default {
  source: {
    alias: {
      '@common': './src/common',
    },
  },
};

它的匹配结果如下:

import a from '@common'; // 解析为 `./src/common`
import b from '@common/util'; // 解析为 `./src/common/util`

你可以添加 $ 符号来开启精确匹配,开启后将不会自动匹配子路径。

import path from 'path';

export default {
  source: {
    alias: {
      '@common$': './src/common',
    },
  },
};

它的匹配结果如下:

import a from '@common'; // 解析为 `./src/common`
import b from '@common/util'; // 保持 `@common/util` 不变

处理 npm 包

你可以使用 alias 将某个 npm 包指向统一的目录。

比如项目中安装了多份 react,你可以将 react 统一指向根目录的 node_modules 中安装的版本,避免出现打包多份 React 代码的问题。

import path from 'path';

export default {
  source: {
    alias: {
      react: path.resolve(__dirname, './node_modules/react'),
    },
  },
};

当你在使用 alias 处理 npm 包时,请留意项目中是否使用了这个包不同的 major 版本。

比如你的项目中某个模块或 npm 依赖使用了 React 18 的 API,如果你将 React alias 到 17 版本,就会导致该模块无法引用到 React 18 的 API,导致代码异常。

source.aliasStrategy

  • 类型: 'prefer-tsconfig' | 'prefer-alias'
  • 默认值: 'prefer-tsconfig'

source.aliasStrategy 用于控制 tsconfig.json 中的 paths 选项与打包工具的 alias 选项的优先级。

prefer-tsconfig

source.aliasStrategy 默认为 'prefer-tsconfig',此时 tsconfig.json 中的 paths 选项和打包工具的 alias 选项都会生效,但 tsconfig paths 选项的优先级更高。

比如同时配置以下内容:

  • tsconfig paths:
tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@common/*": ["./src/common-1/*"]
    }
  }
}
  • source.alias:
export default {
  source: {
    alias: {
      '@common': './src/common-2',
      '@utils': './src/utils',
    },
  },
};

由于 tsconfig paths 的优先级更高,所以:

  • @common 会使用 tsconfig paths 定义的值,指向 ./src/common-1
  • @utils 会使用 source.alias 定义的值,指向 ./src/utils

prefer-alias

source.aliasStrategy 的值为 prefer-alias 时,tsconfig.json 中的 paths 选项只用于提供 TypeScript 类型定义,而不会对打包结果产生任何影响。此时,构建工具只会读取 alias 选项作为路径别名。

export default {
  source: {
    aliasStrategy: 'prefer-alias',
  },
};

比如同时配置以下内容:

  • tsconfig paths:
tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@common/*": ["./src/common-1/*"],
      "@utils/*": ["./src/utils/*"]
    }
  }
}
  • source.alias:
export default {
  source: {
    alias: {
      '@common': './src/common-2',
    },
  },
};

由于 tsconfig paths 只用于提供类型,所以最终只有 @common 别名生效,并指向 ./src/common-2 目录。

大部分情况下你不需要使用 prefer-alias,但当你需要动态生成一些别名配置时,可以考虑使用它。比如,基于环境变量来生成 alias 选项:

export default {
  source: {
    alias: {
      '@common':
        process.env.NODE_ENV === 'production'
          ? './src/common-prod'
          : './src/common-dev',
    },
  },
};

source.include

  • 类型: Array<string | RegExp>
  • 默认值:
const defaultInclude = [
  {
    and: [rootPath, { not: /[\\/]node_modules[\\/]/ }],
  },
  /\.(ts|tsx|jsx|mts|cts)$/,
];

source.include 用于指定额外需要编译的 JavaScript 文件。

为了避免二次编译,默认情况下,Rsbuild 只会编译当前目录下的 JavaScript 文件,以及所有目录下的 TypeScript 和 JSX 文件,不会编译 node_modules 下的 JavaScript 文件。

通过 source.include 配置项,可以指定需要 Rsbuild 额外进行编译的目录或模块。source.include 的用法与 Rspack 中的 Rule.include 一致,支持传入字符串或正则表达式来匹配模块的路径。

比如:

import path from 'path';

export default {
  source: {
    include: [path.resolve(__dirname, '../other-dir')],
  },
};

编译 npm 包

比较典型的使用场景是编译 node_modules 下的 npm 包,因为某些第三方依赖存在 ESNext 的语法,这可能导致在低版本浏览器上无法运行,你可以通过该选项指定需要编译的依赖,从而解决此类问题。

query-string 为例,你可以做如下的配置:

import path from 'path';

export default {
  source: {
    include: [
      // 方法一:
      // 先通过 require.resolve 来获取模块的路径
      // 再通过 path.dirname 来指向对应的目录
      path.dirname(require.resolve('query-string')),
      // 方法二:
      // 通过正则表达式进行匹配
      // 所有包含 `/node_modules/query-string/` 的路径都会被匹配到
      /\/node_modules\/query-string\//,
    ],
  },
};

上述两种方法分别通过 "路径前缀" 和 "正则表达式" 来匹配文件的绝对路径,值得留意的是,项目中所有被引用的模块都会经过匹配,因此你不能使用过于松散的值进行匹配,避免造成编译性能问题或编译异常。

编译 npm 包的子依赖

当你通过 source.include 编译一个 npm 包时,Rsbuild 默认只会编译匹配到的模块,不会编译对应模块的子依赖

query-string 为例,它依赖的 decode-uri-component 包中同样存在 ESNext 代码,因此你需要将 decode-uri-component 也加入到 source.include 中:

export default {
  source: {
    include: [
      /\/node_modules\/query-string\//,
      /\/node_modules\/decode-uri-component\//,
    ],
  },
};

编译 Monorepo 中的其他库

在 Monorepo 中进行开发时,如果需要引用 Monorepo 中其他库的 JavaScript 代码,也可以直接在 source.include 进行配置:

import path from 'path';

export default {
  source: {
    include: [
      // 方法一:
      // 编译 Monorepo 的 package 目录下的所有文件
      path.resolve(__dirname, '../../packages'),

      // 方法二:
      // 编译 Monorepo 的 package 目录里某个包的源代码
      // 这种写法匹配的范围更加精准,对整体编译性能的影响更小
      path.resolve(__dirname, '../../packages/xxx/src'),
    ],
  },
};

如果你匹配的模块是通过 symlink 链接到当前项目中的,那么需要匹配这个模块的真实路径,而不是 symlink 后的路径。

比如,你将 Monorepo 中的 packages/foo 路径 symlink 到当前项目的 node_modules/foo 路径下,则需要去匹配 packages/foo 路径,而不是 node_modules/foo 路径。

注意事项

注意,source.include 不应该被用于编译整个 node_modules 目录,比如下面的写法是错误的:

export default {
  source: {
    include: [/\/node_modules\//],
  },
};

如果你对整个 node_modules 进行编译,不仅会使编译时间大幅度增加,并且可能会产生不可预期的错误。因为 node_modules 中的大部分 npm 包发布的已经是编译后的产物,通常没必要经过二次编译。此外,core-js 等 npm 包被编译后可能会出现异常。

source.entry

  • 类型:
type Entry = Record<string, string | string[]>;
  • 默认值:
const defaultEntry = {
  index: 'src/index.(js|ts|jsx|tsx)',
};

用于设置构建的入口模块,用法与 Rspack 的 entry 选项一致。

  • 示例:
export default {
  source: {
    entry: {
      foo: './src/pages/foo/index.ts',
      bar: './src/pages/bar/index.ts',
    },
  },
};

source.exclude

  • 类型: Array<string | RegExp>
  • 默认值: []

指定不需要编译的 JavaScript/TypeScript 文件。用法与 Rspack 中的 Rule.exclude 一致,支持传入字符串或正则表达式来匹配模块的路径。

比如:

import path from 'path';

export default {
  source: {
    exclude: [path.resolve(__dirname, 'src/module-a'), /src\/module-b/],
  },
};

source.define

  • 类型: Record<string, unknown>
  • 默认值: {}

构建时将代码中的变量替换成其它值或者表达式,可以用于在代码逻辑中区分开发环境与生产环境等场景。

传入的配置对象的键名是需要替换变量的名称,或者是用 . 连接的多个标识符,配置项的值则根据类型进行不同的处理:

  • 字符串会被当作代码片段。
  • 包括函数在内的其他类型会被转换成字符串。
  • 嵌套对象的父子键名之间会用 . 连接作为需要替换的变量名。
  • 以 typeof 开头的键名会用来替换 typeof 调用。

更多细节参考 Rspack - DefinePlugin

示例

export default {
  source: {
    define: {
      PRODUCTION: JSON.stringify(true),
      VERSION: JSON.stringify('5fa3b9'),
      BROWSER_SUPPORTS_HTML5: true,
      TWO: '1 + 1',
      'typeof window': JSON.stringify('object'),
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      'import.meta': { test: undefined },
    },
  },
};

表达式会被替换为对应的代码段:

const foo = TWO;

// ⬇️ Turn into being...
const foo = 1 + 1;

source.transformImport

用于按需引入组件库的代码和样式,能力等价于 babel-plugin-import

它与 babel-plugin-import 的区别在于,source.transformImport 不与 Babel 耦合。Rsbuild 会自动识别当前使用的编译工具是 Babel、SWC 还是 Rspack,并添加相应的按需引入配置。

  • 类型:
type Config =
  | false
  | Array<{
      libraryName: string;
      libraryDirectory?: string;
      style?: string | boolean;
      styleLibraryDirectory?: string;
      camelToDashComponentName?: boolean;
      transformToDefaultImport?: boolean;
      customName?: ((member: string) => string | undefined) | string;
      customStyleName?: ((member: string) => string | undefined) | string;
    }>;

示例

当使用上述 antd 默认配置:

export default {
  source: {
    transformImport: [
      {
        libraryName: 'antd',
        libraryDirectory: 'es',
        style: true,
      },
    ],
  },
};

源代码如下:

import { Button } from 'antd';

会被转换成:

import Button from 'antd/es/button';
import 'antd/es/button/style';

禁用默认配置

你可以手动设置 transformImport: false 来关掉 transformImport 的默认行为。

export default {
  source: {
    transformImport: false,
  },
};

比如,当你使用了 externals 来避免打包 antd 时,由于 transformImport 默认会转换 antd 的引用路径,导致匹配的路径发生了变化,因此 externals 无法正确生效,此时你可以设置关闭 transformImport 来避免该问题。

配置

libraryName

  • 类型: string

用于指定需要按需加载的模块名称。当 Rsbuild 遍历代码时,如果遇到了对应模块的 import 语句,则会对其进行转换。

libraryDirectory

  • 类型: string
  • 默认值: 'lib'

用于拼接转换后的路径,拼接规则为 ${libraryName}/${libraryDirectory}/${member},其中 member 为引入成员。

示例:

import { Button } from 'foo';

转换结果:

import Button from 'foo/lib/button';

style

  • 类型: boolean
  • 默认值: undefined

确定是否需要引入相关样式,若为 true,则会引入路径 ${libraryName}/${libraryDirectory}/${member}/style。 若为 falseundefined 则不会引入样式。

当配置为 true 时:

import { Button } from 'foo';

转换结果:

import Button from 'foo/lib/button';
import 'foo/lib/button/style';

styleLibraryDirectory

  • 类型: string
  • 默认值: undefined

该配置用于拼接引入样式时的引入路径,若该配置被指定,则 style 配置项会被忽略。拼接引入路径为 ${libraryName}/${styleLibraryDirectory}/${member}

当配置为 styles 时:

import { Button } from 'foo';

转换结果:

import Button from 'foo/lib/button';
import 'foo/styles/button';

camelToDashComponentName

  • 类型: boolean
  • 默认值: true

是否需要将 camelCase 的引入转换成 kebab-case。

示例:

import { ButtonGroup } from 'foo';

转换结果:

// 设置成 true:
import ButtonGroup from 'foo/button-group';
// 设置成 false:
import ButtonGroup from 'foo/ButtonGroup';

transformToDefaultImport

  • 类型: boolean
  • 默认值: true

是否将导入语句转换成默认导入。

示例:

import { Button } from 'foo';

转换结果:

// 设置成 true:
import Button from 'foo/button';
// 设置成 false:
import { Button } from 'foo/button';

customName

  • 类型: ((member: string) => string | undefined) | string
  • 默认值: undefined
注意
  • 函数类型的配置只能在 Webpack 构建中使用。
  • 模版类型的配置只能在 Rspack 构建或者使用了 SWC 的 Webpack 构建中使用。

自定义转换后的导入路径,输入是引入的成员,例如配置成 (member) => `my-lib/${member}` ,会将 import { foo } from 'bar' 转换成 import foo from 'my-lib/foo'

在使用 Rspack 构建时,不能使用函数配置,但可以使用 handlebars 模版字符串,对于上面的函数配置,在使用模版字符串时可以用以下模版代替 my-lib/{{ member }},也可以使用一些内置帮助方法,例如 my-lib/{{ kebabCase member }} 来转换成 kebab-case 格式,除了 kebabCase 以外还有 camelCase,snakeCase,upperCase,lowerCase 可以使用。

customStyleName

  • 类型: ((member: string) => string | undefined) | string
  • 默认值: undefined
注意
  • 函数类型的配置只能在 Webpack 构建中使用。
  • 模版类型的配置只能在 Rspack 构建或者使用了 SWC 的 Webpack 构建中使用。

自定义转换后的样式导入路径,输入是引入的成员,例如配置成 (member) => `my-lib/${member}` ,会将 import { foo } from 'bar' 转换成 import foo from 'my-lib/foo'

在使用 Rspack 构建时,不能使用函数配置,但可以使用 handlebars 模版字符串,对于上面的函数配置,在使用模版字符串时可以用以下模版代替 my-lib/{{ member }},也可以使用一些内置帮助方法,例如 my-lib/{{ kebabCase member }} 来转换成 kebab-case 格式,除了 kebabCase 以外还有 camelCase,snakeCase,upperCase,lowerCase 可以使用。

source.preEntry

  • 类型: string | string[]
  • 默认值: undefined

在每个页面的入口文件前添加一段代码,这段代码会早于页面的代码执行,因此可以用于执行一些全局的代码逻辑,比如注入 polyfill、设置全局样式等。

添加单个脚本

首先创建一个 src/polyfill.ts 文件:

console.log('I am a polyfill');

然后将 src/polyfill.ts 配置到 source.preEntry 上:

export default {
  source: {
    preEntry: './src/polyfill.ts',
  },
};

重新运行编译并访问任意页面,可以看到 src/polyfill.ts 中的代码已经执行,并在 console 中输出了对应的内容。

添加全局样式

你也可以通过 source.preEntry 来配置全局样式,这段 CSS 代码会早于页面代码加载,比如引入一个 normalize.css 文件:

export default {
  source: {
    preEntry: './src/normalize.css',
  },
};

添加多个脚本

你可以将 preEntry 设置为数组来添加多个脚本,它们会按数组顺序执行:

export default {
  source: {
    preEntry: ['./src/polyfill-a.ts', './src/polyfill-b.ts'],
  },
};

source.resolveExtensionPrefix

  • 类型: string | Record<RsbuildTarget, string>
  • 默认值: undefined

用于为 resolve.extensions 添加统一的前缀。

如果多个文件拥有相同的名称,但具有不同的文件后缀,Rsbuild 会根据 extensions 数组的顺序进行识别,解析数组中第一个被识别的文件,并跳过其余文件。

示例

下面是配置 .web 前缀的例子。

export default {
  source: {
    resolveExtensionPrefix: '.web',
  },
};

配置完成后,extensions 数组会发生以下变化:

// 配置前
const extensions = ['.js', '.ts', ...];

// 配置后
const extensions = ['.web.js', '.js', '.web.ts' , '.ts', ...];

在代码中 import './foo' 时,会优先识别 foo.web.js 文件,再识别 foo.js 文件。

根据产物类型设置

当你同时构建多种类型的产物时,你可以为不同的产物类型设置不同的 extension 前缀。此时,你需要把 resolveExtensionPrefix 设置为一个对象,对象的 key 为对应的产物类型。

比如为 webnode 设置不同的 extension 前缀:

export default {
  output: {
    source: {
      resolveExtensionPrefix: {
        web: '.web',
        node: '.node',
      },
    },
  },
};

在代码中 import './foo' 时,对于 node 产物,会优先识别 foo.node.js 文件,而对于 web 产物,则会优先识别 foo.web.js 文件。

source.resolveMainFields

  • 类型:
type Fields = (string | string[])[];

type ResolveMainFields = Fields | Record<RsbuildTarget, Fields>;
  • 默认值: undefined

该配置项将决定你使用 package.json 哪个字段导入 npm 模块。对应 webpack 的 resolve.mainFields 配置。

示例

export default {
  source: {
    resolveMainFields: ['main', 'browser', 'exports'],
  },
};

根据产物类型设置

当你同时构建多种类型的产物时,你可以为不同的产物类型设置不同的 mainFields。此时,你需要把 resolveMainFields 设置为一个对象,对象的 key 为对应的产物类型。

比如为 webnode 设置不同的 mainFields:

export default {
  output: {
    source: {
      resolveMainFields: {
        web: ['main', 'browser', 'exports'],
        node: ['main', 'node', 'exports'],
      },
    },
  },
};