Source Config

This section describes configs related to source code parsing and compilation in Modern.js Builder.

source.alias

  • Type: Record<string, string | string[]> | Function
  • Default: undefined

Create aliases to import or require certain modules, same as the resolve.alias config of webpack and Rspack.

TIP

For TypeScript projects, you only need to configure compilerOptions.paths in the tsconfig.json file. The Builder will automatically recognize it, so there is no need to configure the source.alias option separately. For more details, please refer to Path Aliases.

Object Type

The alias can be an Object, and the relative path will be automatically converted to absolute path.

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

With above configuration, if @common/Foo.tsx is import in the code, it will be mapped to the <project>/src/common/Foo.tsx path.

Function Type

The alias can be a function, it will accept the previous alias object, and you can modify it.

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

You can also return a new object as the final result in the function, which will replace the previous alias object.

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

Exact Matching

By default, source.alias will automatically match sub-paths, for example, with the following configuration:

import path from 'path';

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

It will match as follows:

import a from '@common'; // resolved to `./src/common`
import b from '@common/util'; // resolved to `./src/common/util`

You can add the $ symbol to enable exact matching, which will not automatically match sub-paths.

import path from 'path';

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

It will match as follows:

import a from '@common'; // resolved to `./src/common`
import b from '@common/util'; // remains as `@common/util`

Handling npm packages

You can use alias to resolve an npm package to a specific directory.

For example, if multiple versions of the react are installed in the project, you can alias react to the version installed in the root node_modules directory to avoid bundling multiple copies of the React code.

import path from 'path';

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

When using alias to handle npm packages, please be aware of whether different major versions of the package are being used in the project.

For example, if a module or npm dependency in your project uses the React 18 API, and you alias React to version 17, the module will not be able to reference the React 18 API, resulting in code exceptions.

source.aliasStrategy

  • Type: 'prefer-tsconfig' | 'prefer-alias'
  • Default: 'prefer-tsconfig'

source.aliasStrategy is used to control the priority between the paths option in tsconfig.json and the alias option in the bundler.

prefer-tsconfig

By default, source.aliasStrategy is set to 'prefer-tsconfig'. In this case, both the paths option in tsconfig.json and the alias option in the bundler will take effect, but the paths option in tsconfig has a higher priority.

For example, if the following configurations are set at the same time:

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

Since the tsconfig paths have a higher priority, the following will happen:

  • @common will use the value defined in tsconfig paths, pointing to ./src/common-1
  • @utils will use the value defined in source.alias, pointing to ./src/utils

prefer-alias

If the value of source.aliasStrategy is set to prefer-alias, the paths option in tsconfig.json will only be used to provide TypeScript type definitions and will not affect the bundling result. In this case, the bundler will only read the alias option as the path alias.

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

For example, if the following configurations are set at the same time:

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

Since the tsconfig paths are only used to provide types, only the @common alias will be effective, pointing to the ./src/common-2 directory.

In most cases, you don't need to use prefer-alias, but you can consider using it if you need to dynamically generate some alias configurations. For example, generating the alias option based on environment variables:

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)$/,
];

The source.include is used to specify additional JavaScript files that need to be compiled.

To avoid redundant compilation, by default, Rsbuild only compiles JavaScript files in the current directory and TypeScript and JSX files in all directories. It does not compile JavaScript files under node_modules.

Through the source.include config, you can specify directories or modules that need to be compiled by Rsbuild. The usage of source.include is consistent with Rule.include in Rspack, which supports passing in strings or regular expressions to match the module path.

For example:

import path from 'path';

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

Compile Npm Packages

A typical usage scenario is to compile npm packages under node_modules, because some third-party dependencies have ES6+ syntax, which may cause them to fail to run on low-version browsers. You can solve the problem by using this config to specify the dependencies that need to be compiled.

Take query-string as an example, you can add the following config:

import path from 'path';

export default {
  source: {
    include: [
      // Method 1:
      // First get the path of the module by require.resolve
      // Then pass path.dirname to point to the corresponding directory
      path.dirname(require.resolve('query-string')),
      // Method 2:
      // Match by regular expression
      // All paths containing `/node_modules/query-string/` will be matched
      /\/node_modules\/query-string\//,
    ],
  },
};

The above two methods match the absolute paths of files using "path prefixes" and "regular expressions" respectively. It is worth noting that all referenced modules in the project will be matched. Therefore, you should avoid using overly loose values for matching to prevent compilation performance issues or compilation errors.

Compile Sub Dependencies

When you compile an npm package via source.include, Builder will only compile the matching module by default, not the Sub Dependencies of the module.

Take query-string for example, it depends on the decode-uri-component package, which also has ES6+ code, so you need to add the decode-uri-component package to source.include as well.

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

Compile Libraries in Monorepo

When developing in Monorepo, if you need to refer to the source code of other libraries in Monorepo, you can add the corresponding library to source.include:

import path from 'path';

export default {
  source: {
    include: [
      // Method 1:
      // Compile all files in Monorepo's package directory
      path.resolve(__dirname, '../../packages'),

      // Method 2:
      // Compile the source code of a package in Monorepo's package directory
      // This way of writing matches the range more accurately and has less impact on the overall build performance.
      path.resolve(__dirname, '../../packages/xxx/src'),
    ],
  },
};

Compile CommonJS Module

Babel cannot compile CommonJS modules by default, and if you compile a CommonJS module, you may get a runtime error message exports is not defined.

When you need to compile a CommonJS module using source.include, you can set Babel's sourceType configuration to unambiguous.

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

Setting sourceType to unambiguous may have some other effects, please refer to Babel official documentation.

If you match a module that is symlinked to the current project, then you need to match the real path of the module, not the symlinked path.

For example, if you symlink the packages/foo path in Monorepo to the node_modules/foo path of the current project, you need to match the packages/foo path, not the node_modules/foo path.

This behavior can be controlled via webpack's resolve.symlinks config.

Precautions

Note that source.include should not be used to compile the entire node_modules directory. For example, the following usage is wrong:

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

If you compile the entire node_modules, not only will the build time be greatly increased, but also unexpected errors may occur. Because most of the npm packages in node_modules are already compiled, there is usually no need for a second compilation. In addition, exceptions may occur after npm packages such as core-js are compiled.

source.exclude

  • Type: Array<string | RegExp>
  • Default: []

Specifies JavaScript/TypeScript files that do not need to be compiled. The usage is consistent with Rule.exclude in webpack, which supports passing in strings or regular expressions to match the module path.

For example:

import path from 'path';

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

source.define

  • Type: Record<string, unknown>
  • Default: {}

Replaces variables in your code with other values or expressions at compile time. This can be useful for allowing different behavior between development builds and production builds.

Each key passed into options is an identifier or multiple identifiers joined with ..

  • If the value is a string it will be used as a code fragment.
  • If the value isn't a string, it will be stringified (including functions).
  • If the value is an object all keys are defined the same way.
  • If you prefix typeof to the key, it's only defined for typeof calls.

For more information please visit webpack - DefinePlugin.

TIP

When using Rspack as the bundler, the supported types can be found in Rspack.builtins.define.

Example

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 },
    },
  },
};

Expressions will be replaced with the corresponding code fragments:

const foo = TWO;

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

source.globalVars

  • Type: Record<string, JSONValue> | Function
  • Default:
const defaultGlobalVars = {
  // The environment variable `process.env.NODE_ENV` will be added by default,
  // so you don't need to set it in manually.
  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
};

Used to define global variables. It can replace expressions like process.env.FOO in your code after compile. Such as:

console.log(process.env.NODE_ENV);

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

Example

In the following example, the ENABLE_VCONSOLE and APP_CONTEXT are injected into the code:

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

You can use them directly in your code:

if (ENABLE_VCONSOLE) {
  // do something
}

console.log(APP_CONTEXT);

Function Usage

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

You can set source.globalVars to a function to dynamically setting some environment variables.

For example, dynamically set according to the build target:

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

Difference with define

You can take source.globalVars as the syntax sugar of source.define, the only difference is that source.globalVars will automatically stringify the value, which makes it easier to set the value of global variables. The values of globalVars should be JSON-safe to ensure it can be serialized.

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),
      },
    },
  },
};

Precautions

source.globalVars injects environment variables through string replacement, so it cannot take effect on dynamic syntaxes such as destructuring.

When using destructuring assignment, Builder will not be able to determine whether the variable NODE_ENV is associated with the expression process.env.NODE_ENV to be replaced, so the following usage is invalid:

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

source.moduleScopes

  • Type: Array<string | Regexp> | Function
  • Default: undefined
  • Bundler: only support webpack

Restrict importing paths. After configuring this option, all source files can only import code from the specific paths, and import code from other paths is not allowed.

Example

First, we configure moduleScopes to only include the src directory:

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

Then we import the utils/a module outside the src directory in src/App.tsx:

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

After compiling, there will be a reference path error:

scopes-error

If we configure the utils directory in moduleScopes, the error will disappear.

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

Array Type

You can directly set several paths like this:

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

Function Type

moduleScopes also supports setting as a function, which can be modified instead of overriding the default value:

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

source.transformImport

Used to import the code and style of the component library on demand, which is equivalent to babel-plugin-import.

The difference between it and babel-plugin-import is that source.transformImport is not coupled with Babel. Builder will automatically identify whether the currently used tools is Babel, SWC or Rspack, and apply the corresponding on-demand import configuration.

  • Type:
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;
    }>;
  • Default:

When the Ant Design component library <= 4.x version is installed in the project, Builder will automatically add the following default configurations:

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

When the Arco Design component library is installed in the project, Builder will automatically add the following default configurations:

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

When you add configurations for antd or @arco-design/web-react, the priority will be higher than the default configurations mentioned above.

Example

When using the above antd default configuration:

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

The source code is as follows:

import { Button } from 'antd';

It will be transformed into:

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

Disable Default Config

You can manually set transformImport: false to disable the default config.

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

For example, if you use externals to avoid bundling antd, because transformImport will convert the imported path of antd by default, the matching path changes and externals cannot take effect. At this time, you can disable transformImport to avoid this problem.

Configuration

libraryName

  • Type: string

The original import path that needs to be transformed.

libraryDirectory

  • Type: string
  • Default: 'lib'

Used to splice the transformed path, the splicing rule is ${libraryName}/${libraryDirectory}/${member}, where member is the imported member.

Example:

import { Button } from 'foo';

Out:

import Button from 'foo/lib/button';

style

  • Type: boolean
  • Default: undefined

Determines whether to import related styles. If it is true, the path ${libraryName}/${libraryDirectory}/${member}/style will be imported. If it is false or undefined, the style will not be imported.

When it is set to true:

import { Button } from 'foo';

Out:

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

styleLibraryDirectory

  • Type: string
  • Default: undefined

This configuration is used to splice the import path when importing styles. If this configuration is specified, the style configuration option will be ignored. The spliced import path is ${libraryName}/${styleLibraryDirectory}/${member}.

When it is set to styles:

import { Button } from 'foo';

Out:

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

camelToDashComponentName

  • Type: boolean
  • Default: true

Whether to convert camelCase imports to kebab-case.

Example:

import { ButtonGroup } from 'foo';

Out:

// set to true:
import ButtonGroup from 'foo/button-group';
// set to false:
import ButtonGroup from 'foo/ButtonGroup';

transformToDefaultImport

  • Type: boolean
  • Default: true

Whether to convert import statements to default imports.

Example:

import { Button } from 'foo';

Out:

// set to true:
import Button from 'foo/button';
// set to false:
import { Button } from 'foo/button';

customName

  • Type: ((member: string) => string | undefined) | string
  • Default: undefined
Note
  • Function-type configurations can only be used in Webpack builds.
  • Template-type configurations can only be used in Rspack builds or Webpack builds that use SWC.

Customize the imported path after conversion. The input is the imported member. For example, configure it as (member) => `my-lib/${member}` , which will convert import { foo } from 'bar' to import foo from 'my-lib/foo'.

When using Rspack to build, function configurations cannot be used, but you can use handlebars template strings. For the above function configuration, you can use the following template instead of my-lib/{{ member }}, or use some built-in helper methods, such as my-lib/{{ kebabCase member }} to convert it to kebab-case format. In addition to kebabCase, there are also camelCase, snakeCase, upperCase, and lowerCase that can be used.

customStyleName

  • Type: ((member: string) => string | undefined) | string
  • Default: undefined
Note
  • Function-type configurations can only be used in Webpack builds.
  • Template-type configurations can only be used in Rspack builds or Webpack builds that use SWC.

Customize the imported style path after conversion. The input is the imported member. For example, configure it as (member) => `my-lib/${member}` , which will convert import { foo } from 'bar' to import foo from 'my-lib/foo'.

When using Rspack to build, function configurations cannot be used, but you can use handlebars template strings. For the above function configuration, you can use the following template instead of my-lib/{{ member }}, or use some built-in helper methods, such as my-lib/{{ kebabCase member }} to convert it to kebab-case format. In addition to kebabCase, there are also camelCase, snakeCase, upperCase, and lowerCase that can be used.

source.preEntry

  • Type: string | string[]
  • Default: undefined

Add a script before the entry file of each page. This script will be executed before the page code. It can be used to execute global logics, such as injecting polyfills, setting global styles, etc.

Add a single script

First create a src/polyfill.ts file:

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

Then configure src/polyfill.ts to source.preEntry:

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

Re-run the compilation and visit any page, you can see that the code in src/polyfill.ts has been executed, and the I am a polyfill is logged in the console.

Add global style

You can also configure the global style through source.preEntry, this CSS code will be loaded earlier than the page code, such as introducing a normalize.css file:

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

Add multiple scripts

You can add multiple scripts by setting preEntry to an array, and they will be executed in array order:

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

source.resolveExtensionPrefix

  • Type: string | Record<BuilderTarget, string>
  • Default: undefined

Add a prefix to resolve.extensions.

If multiple files share the same name but have different extensions, Builder will resolve the one with the extension listed first in the array and skip the rest.

Example

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

With the configuration above, the extensions array will become:

// before
const extensions = ['.js', '.ts', ...];

// after
const extensions = ['.web.js', '.js', '.web.ts' , '.ts', ...];

When import './foo' in the code, the foo.web.js file will be resolved first, then the foo.js file.

Set according to Targets

When you build multiple targets at the same time, you can set different extension prefix for different targets. At this point, you need to set resolveExtensionPrefix to an object whose key is the corresponding build target.

For example to set different extension prefix for web and node:

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

When import './foo' in the code, the foo.node.js file will be resolved for node target, and the foo.web.js file will be resolved for web target.

source.resolveMainFields

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

type ResolveMainFields = Fields | Record<BuilderTarget, Fields>;
  • Default: undefined

This config will determine which field of package.json you use to import the npm module. Same as the resolve.mainFields config of webpack.

Example

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

Set according to Targets

When you build multiple targets at the same time, you can set different mainFields for different targets. At this point, you need to set resolveMainFields to an object whose key is the corresponding build target.

For example to set different mainFields for web and node:

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