Source Config

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

source.alias

  • 类型: Record<string, string | string[]> | Function
  • 默认值: undefined

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

TIP

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

Object 类型

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

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

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 包,因为某些第三方依赖存在 ES6+ 的语法,这可能导致在低版本浏览器上无法运行,你可以通过该选项指定需要编译的依赖,从而解决此类问题。

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 包时,Builder 默认只会编译匹配到的模块,不会编译对应模块的子依赖

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

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

编译 Monorepo 中的其他库

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

import path from 'path';

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

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

编译 CommonJS 模块

Babel 默认无法编译 CommonJS 模块,如果你编译了一个 CommonJS 模块,可能会出现 exports is not defined 的运行时报错信息。

当你需要使用 source.include 来编译 CommonJS 模块时,可以将 Babel 的 sourceType 配置设置为 unambiguous

export default {
  tools: {
    babel(config) {
      config.sourceType = 'unambiguous';
    },
  },
};

sourceType 设置为 unambiguous 可能会产生一些其他影响,请参考 Babel 官方文档

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

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

该行为可以通过 webpack 的 resolve.symlinks 配置项来进行控制。

注意事项

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

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

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

source.exclude

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

指定不需要编译的 JavaScript/TypeScript 文件。用法与 webpack 中的 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 调用。

更多细节参考 webpack - DefinePlugin

TIP

在使用 Rspack 作为打包工具时,支持的类型可参考 Rspack.builtins.define

示例

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.globalVars

  • 类型: Record<string, JSONValue> | Function
  • 默认值:
// Builder 默认会添加环境变量 `process.env.NODE_ENV`,因此你不需要手动添加它。
const defaultGlobalVars = {
  'process.env.NODE_ENV': process.env.NODE_ENV,
};

用于在构建时将类似 process.env.FOO 的全局变量表达式替换为指定的值,比如:

console.log(process.env.NODE_ENV);

// ⬇️ Turn into being...
console.log('development');

示例

在下方示例中,会在代码中注入 ENABLE_VCONSOLEAPP_CONTEXT 两个环境变量:

export default {
  source: {
    globalVars: {
      ENABLE_VCONSOLE: true,
      APP_CONTEXT: { answer: 42 },
    },
  },
};

你可以在代码中直接使用它们:

if (ENABLE_VCONSOLE) {
  // do something
}

console.log(APP_CONTEXT);

函数用法

  • 类型:
type GlobalVarsFn = (
  obj: Record<string, JSONValue>,
  utils: { env: NodeEnv; target: BuilderTarget },
) => Record<string, JSONValue> | void;

你可以将 source.globalVars 设置为一个函数,从而动态设置一些环境变量的值。

比如,根据当前的构建产物类型进行动态设置:

export default {
  source: {
    globalVars(obj, { target }) {
      obj['MY_TARGET'] = target === 'node' ? 'server' : 'client';
    },
  },
};

与 define 的区别

source.globalVarssource.define 的一个语法糖,它们之间唯一的区别是,source.globalVars 会自动将传入的值进行 JSON 序列化处理,这使得设置全局变量的值更加方便。注意 globalVars 的每个值都需要是可以被 JSON 序列化的值。

export default {
  source: {
    globalVars: {
      'process.env.BUILD_VERSION': '0.0.1',
      'import.meta.foo': { bar: 42 },
      'import.meta.baz': false,
    },
    define: {
      'process.env.BUILD_VERSION': JSON.stringify('0.0.1'),
      'import.meta': {
        foo: JSON.stringify({ bar: 42 }),
        baz: JSON.stringify(false),
      },
    },
  },
};

注意事项

source.globalVars 是通过字符串替换的形式来注入环境变量的,因此它无法对「解构赋值」等动态写法生效。

在使用解构赋值时,Builder 将无法判断变量 NODE_ENV 是否与要替换的表达式 process.env.NODE_ENV 存在关联,因此以下使用方式是无效的:

const { NODE_ENV } = process.env;
console.log(NODE_ENV);
// ❌ Won't get a string.

source.moduleScopes

  • 类型: Array<string | Regexp> | Function
  • 默认值: undefined
  • 打包工具: 仅支持 webpack

限制源代码的引用路径。配置该选项后,所有源文件只能从约定的目录下引用代码,从其他目录引用代码会产生对应的报错提示。

示例

首先我们配置 moduleScopes 仅包含 src 目录:

export default {
  source: {
    moduleScopes: ['./src'],
  },
};

然后在 src/App.tsx 中导入 src 目录外部的 utils/a 模块:

import a from '../utils/a';

在编译时,会提示引用路径错误:

scopes-error

通过该选项配置 utils 目录,再进行编译,则不会出现错误提示。

export default {
  source: {
    moduleScopes: ['./src', './utils'],
  },
};

Array 类型

moduleScopes 的值为 Array 类型时,可以直接设置若干个代码路径,比如添加以下配置:

export default {
  source: {
    moduleScopes: ['./src', './shared', './utils'],
  },
};

Function 类型

moduleScopes 也支持通过函数的形式来进行修改,避免覆盖默认值:

export default {
  source: {
    moduleScopes: scopes => {
      scopes.push('./shared');
    },
  },
};

source.transformImport

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

它与 babel-plugin-import 的区别在于,source.transformImport 不与 Babel 耦合。Builder 会自动识别当前使用的编译工具是 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;
    }>;
  • 默认值:

当项目中安装了 Ant Design 组件库 <= 4.x 版本时,Builder 会自动添加以下默认配置:

const defaultAntdConfig = {
  libraryName: 'antd',
  libraryDirectory: isServer ? 'lib' : 'es',
  style: true,
};

当项目中安装了 Arco Design 组件库 时,Builder 会自动添加以下默认配置:

const defaultArcoConfig = [
  {
    libraryName: '@arco-design/web-react',
    libraryDirectory: isServer ? 'lib' : 'es',
    camelToDashComponentName: false,
    style: true,
  },
  {
    libraryName: '@arco-design/web-react/icon',
    libraryDirectory: isServer ? 'react-icon-cjs' : 'react-icon',
    camelToDashComponentName: false,
  },
];
TIP

当你添加了 antd@arco-design/web-react 相关的配置时,优先级会高于上述默认配置。

示例

当使用上述 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

用于指定需要按需加载的模块名称。当 Builder 遍历代码时,如果遇到了对应模块的 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<BuilderTarget, string>
  • 默认值: undefined

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

如果多个文件拥有相同的名称,但具有不同的文件后缀,Builder 会根据 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<BuilderTarget, 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'],
      },
    },
  },
};