close

Built-in rules

Introduction

Tip

Please refer to the Linter Type in this document for the type definition of linter.

[E1001] Duplicate packages

Rule details

  • The Duplicate Packages card displays the number of duplicate third-party packages in the project. Clicking the image allows you to view the specific details of the duplicate third-party packages. Note: The third-party packages referred to here are all bundled third-party packages.

  • Duplicate Package Warning Card

  • Clicking the icon to expand the duplicate package details allows you to see: the name, version, size, and reference files of the duplicate package.

    • Clicking the 「Show Relations」 on the far right can view the specific reference chain and the corresponding reference file code position of this third-party package.
    • Clicking the 「!(exclamation mark)」 icon on the far right can view the specific explanation of the rule for the duplicate third-party package.

Configuration

  • Configuration Example:
import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin';

export default {
  plugin: [
    new RsdoctorRspackPlugin({
      linter: {
        level: 'Error',
        extends: [],
        rules: {
          'duplicate-package': [
            'Error',
            {
              checkVersion: 'minor',
              ignore: ['chalk', '@babel/runtime'],
            },
          ],
        },
      },
    }),
  ],
};
Type
  • ignore: Configures the packages to be ignored.
  • checkVersion: Refers to the maximum version level to be checked, for example: if set to minor, then the duplicate package will no longer check the major level differences. Default is major.
interface Config {
  checkVersion: keyof typeof CheckVersion;
  ignore: string[];
}

enum CheckVersion {
  null = 0,
  prerelease = 0x1,
  prepatch = 0x10,
  patch = 0x100,
  preminor = 0x1000,
  minor = 0x10000,
  premajor = 0x100000,
  major = 0x1000000,
}

Duplicate package optimization problem

Please refer to the Duplicate Package Optimization Solution.

Clicking 「More」 can view the corresponding rule explanation.

[E1002] Cross chunks package

The cross chunks duplicate package rule can scan duplicate packages in different chunks. These duplicate packages may also lead to redundant code in the build artifacts, depending on the business logic and the size of the redundant code.

  • Display
    • Module refers to the module that is repeatedly packaged in multiple chunks.
    • Chunks are the build artifacts that are repeatedly packaged.

Solution

Please refer to [E1002] Cross Chunks Packages

[E1003] Loader performance optimization

This module allows you to visually see some warning information about our project's compilation, which can help us further optimize the project's compilation performance.

Solution

Please refer to [E1003] Loader Performance Optimization

Configuration type

  • ignore: Can include strings or regular expressions, used to specify the loaders to be ignored.
  • threshold: Represents the total time threshold for the loader, in milliseconds. If the loader's execution time exceeds this threshold, it may trigger warnings or errors. The default value is 5000 milliseconds.
  • extensions: Strings or regular expressions, used to specify the file extensions that need to be matched in rule checks. By default, it includes common file types such as js, css, jpg, jpeg, png, gif, webp, and svg.
interface Config {
  /**
   * loaders which should be ignore.
   */
  ignore: (string | RegExp)[];
  /**
   * threshold which the loader total costs.
   * @unit millisecond
   * @default 5000
   */
  threshold: number;
  /**
   * the file extensions which will be match in rule check.
   * @default ["js", "css", "jpg", "jpeg", "png", "gif", "webp", "svg"]
   */
  extensions: (string | RegExp)[];
}

[E1004] ECMA version check

This rule is used to detect incompatible advanced syntax. When scanning the rule, the configuration of browserslist is prioritized; if browserslist is not configured, manual detection is required, as shown below:

import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin';

export default {
  plugin: [
    new RsdoctorRspackPlugin({
      linter: {
        rules: {
          'ecma-version-check': [
            'Warn',
            {
              ecmaVersion: 2015,
              // targets: ["chrome >= 53"],
            },
          ],
        },
      },
    }),
  ],
};

Type definitions

type CheckSyntaxOptions = {
  /**
   * The target browser range of the project.
   * Its value is a standard browserslist array.
   */
  targets?: string[];
  /**
   * Used to exclude a portion of source files during detection.
   * You can pass in one or more regular expressions to match the paths of source files.
   */
  exclude?: CheckSyntaxExclude;
  /**
   * Used to exclude files by output path before detection.
   * You can pass in one or more regular expressions to match the paths of source files.
   */
  excludeOutput?: CheckSyntaxExclude;
  /**
   * The minimum ECMAScript syntax version that can be used in the build artifact.
   * The priority of `ecmaVersion` is higher than `targets`.
   */
  ecmaVersion?: EcmaVersion;
  /**
   * Used to ignore specified syntax error messages after detection.
   * You can pass in one or more error message types to ignore.
   */
  excludeErrorLogs?: SyntaxErrorKey[];
};

For more ECMA Version Check configuration options, please refer to ECMA Version Check Options

[E1005] Default import check

Typically, Rspack automatically supports different types of modules, but in some cases, compatibility operations may fail. For example, when using Default Import to import a cjs module, if the module does not have a compatible statement (such as exports.default), issues may arise.

Solution

Please refer to [E1005] Default Import Check

Configuration

  • ignore:Configure to ignore some imported files.
interface Config {
  /** Packages that need to be ignored */
  ignore: string[];
}

[E1006] Module Mixed Chunks

When a module is included in both initial chunks and async chunks, the same module code is bundled into multiple chunks, increasing output size and potentially affecting first-screen load and cache efficiency.

  • Initial chunks: Chunks loaded with the main entry (e.g. entry points, synchronous import in the main bundle).
  • Async chunks: Chunks loaded on demand via dynamic import() or similar.

Rule details

  • In the 「Module Mixed Chunks」 tab of Bundle Alerts, all modules that appear in both initial and async chunks are listed.
  • Each entry shows: module path, Initial Chunks list, and Async Chunks list, so you can locate duplicated modules.

Common causes

  • Same module referenced in two ways: The module is both synchronously imported in the main bundle or entry, and dynamically import()ed somewhere else, so the bundler emits it in both initial and async chunks.
  • A file is both an entry and an async chunk: For example, a utility module is configured as an entry and also import()ed in app code, so it appears in the entry’s initial chunk and in a dynamically loaded async chunk.
  • splitChunks overlapping with entry: A path is split into an async chunk via splitChunks / chunkSplit, but that path is also an entry or a main-bundle dependency, leading to mixed chunk types.

Solutions and recommendations

  1. Use a single import style
    Prefer one way to reference a module: either all synchronous imports (in initial) or all dynamic import() (in async). Avoid having the same file both synchronously imported in the main bundle and dynamically imported elsewhere.

  2. Review entry vs dynamic loading
    If a file is both an entry and part of an async chunk, either remove one of those usages or treat the file as a shared dependency and extract it into a single shared chunk via build config, so both initial and async chunks reference it instead of duplicating it.

  3. Adjust splitChunks / chunkSplit
    Check rules for that module path in optimization.splitChunks (Rspack/Webpack) or performance.chunkSplit (Rsbuild), and avoid the same module being split into both initial and async chunks. Use chunks: 'async' or chunks: 'initial' where appropriate, or control which chunk type it goes into via cacheGroups and test / chunks.

  4. Trace dependencies
    From the reported module path and chunk list, search the codebase for references to that module, distinguish sync vs dynamic imports, then converge to a single chunk type or extract a common chunk as above.

Configuration

  • ignore: Module path patterns to ignore (string match: if the module path contains any of these strings, it is ignored).
interface Config {
  /** Module path fragments to ignore */
  ignore: string[];
}

Configuration example:

import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin';

export default {
  plugin: [
    new RsdoctorRspackPlugin({
      linter: {
        rules: {
          'module-mixed-chunks': ['Warn', { ignore: ['node_modules/'] }],
        },
      },
    }),
  ],
};

[E1007] Tree Shaking Side Effects Only

Rule key: tree-shaking-side-effects-only

This rule detects modules that are pulled in and bundled solely due to side effects. This is often caused by unintended tree-shaking failures (e.g. missing or incorrect "sideEffects" field in package.json, or non-tree-shakeable import patterns), resulting in the entire module being bundled even though none of its exports are used.

Common causes

  • The package's package.json is missing "sideEffects": false (or incorrectly set to true), preventing the bundler from pruning unused exports.
  • An import statement like import 'some-module' or import './styles.css' is being treated as a side-effect-only import, but the intended use was to consume exports.
  • Barrel files (index files that re-export many things) cause the whole module to be kept alive when only a side-effect import is present.

Solutions

  1. Audit import statements: Make sure you are actually importing and using named exports from this module. Replace bare side-effect imports with explicit named imports when you intend to use the module's exports.
  2. Set "sideEffects" correctly: In the module's package.json, set "sideEffects": false if the module has no global side effects, so the bundler can safely tree-shake unused exports.
  3. Avoid unintended side-effect imports: Remove or convert import 'module' patterns to explicit import { foo } from 'module' patterns where the exports are needed.

Configuration

  • ignore: Module path patterns to ignore (string match: if the module path contains any of these strings, it is ignored).
  • include: Module path patterns to include when the module is under node_modules (by default, modules in node_modules are skipped).
interface Config {
  /** Module path fragments to ignore */
  ignore: string[];
  /**
   * Module path patterns to include when the module is under node_modules.
   * Example: ['react', '@babel/runtime']
   */
  include: string[];
}

Configuration example:

import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin';

export default {
  plugins: [
    new RsdoctorRspackPlugin({
      linter: {
        rules: {
          'tree-shaking-side-effects-only': [
            'Warn',
            {
              ignore: ['src/polyfills'],
              include: ['some-lib'],
            },
          ],
        },
      },
    }),
  ],
};

[E1008] CJS Require Cannot Tree-Shake

Rule key: cjs-require

This rule warns when code uses a bare require() call (e.g. const mod = require('module')) to import an entire module without statically accessing its exports. This pattern prevents tree-shaking because the bundler cannot statically determine which exports are actually used, resulting in the entire module being bundled and increasing output size.

Common causes

  • Using const mod = require('module') instead of const { foo } = require('module').
  • Dynamically composing the require argument (e.g. require('module/' + name)), which the bundler cannot statically analyze.
  • Mixing require() inside ESM files, preventing the bundler from tree-shaking the module.

Solutions

  1. Switch to destructured require: Replace const mod = require('module') with const { foo, bar } = require('module') so the bundler can statically analyze which exports are used.
  2. Migrate to ESM: Prefer static import { foo } from 'module' syntax to take full advantage of bundler tree-shaking.
  3. Avoid dynamic require: If dynamic loading is needed, replace dynamic require() with dynamic import().

Configuration

  • ignore: Module path patterns to ignore (substring match applied to both issuer and required module paths).
interface Config {
  /** Module path fragments to ignore */
  ignore: string[];
}

Configuration example:

import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin';

export default {
  plugins: [
    new RsdoctorRspackPlugin({
      linter: {
        rules: {
          'cjs-require': [
            'Warn',
            { ignore: ['legacy-lib', 'node_modules/@internal/'] },
          ],
        },
      },
    }),
  ],
};

[E1009] ESM Import Resolved to CJS

Rule key: esm-resolved-to-cjs

This rule warns when a package provides both ESM and CJS formats (via the module field or exports["."]["import"] in package.json), but the bundler resolves an ESM import statement to the CJS entry instead. This prevents tree-shaking and leads to larger bundle sizes.

Common causes

  • The exports field in package.json is misconfigured, so the "import" condition does not map to the correct ESM entry.
  • An older bundler version that does not support exports conditional exports falls back to the CJS entry.
  • The package's module field or exports["import"] path is incorrect or points to a non-existent file.
  • The consumer's build config does not have mainFields: ['module', ...] or does not correctly handle exports conditions.

Solutions

  1. Check the package's exports config: Ensure the target package's exports field correctly defines the "import" condition pointing to a valid ESM entry file.
  2. Upgrade the bundler or plugins: Make sure you are using a bundler version that supports exports conditional exports (e.g. Rspack, Webpack 5+).
  3. Report to the package author: If the problem is a misconfigured package.json in a third-party package, file an issue with the package author.
  4. Temporarily ignore: If the package cannot be fixed for now, use the ignore option to skip the check for that package.

Configuration

  • ignore: Package name patterns to ignore (substring match against the import request string).
interface Config {
  /**
   * Package name patterns to ignore (substring match against the import request string).
   * Example: ['my-legacy-pkg', '@internal/']
   * @default []
   */
  ignore: string[];
}

Configuration example:

import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin';

export default {
  plugins: [
    new RsdoctorRspackPlugin({
      linter: {
        rules: {
          'esm-resolved-to-cjs': [
            'Warn',
            { ignore: ['my-legacy-pkg', '@internal/'] },
          ],
        },
      },
    }),
  ],
};

Linter type

  • The type definition for the linter field is as follows:
/** Linter options */
interface Options {
  rules?: RulesMap;
  level?: SeverityString;
  extends?: ExtendRuleData[];
}

/**
 * Linting level
 *   - `'Warn'` runs only rules categorized as `'Warn'`
 *   - `'Error'` runs all rules
 */
type SeverityString = 'Warn' | 'Error';

/** Rule level */
type SeverityInput = SeverityString | 'off' | 'on';

/** Rule configuration */
type RulesMap = Record<string, RuleConfigItem>;

/** Single rule configuration */
type RuleConfigItem =
  // Only error level, this level has higher priority than the rule's own configuration
  | SeverityInput
  // In the case of an array, the first item is the error level, and the second item is the rule configuration
  | [SeverityInput, unknown];

If you want to disable a rule, you can set SeverityInput to off, as shown in the following example:

import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin';

export default {
  plugin: [
    new RsdoctorRspackPlugin({
      linter: {
        level: 'Error',
        extends: [],
        rules: {
          'duplicate-package': 'off',
        },
      },
    }),
  ],
};