For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt, and this page is available as Markdown at /api/plugin-api/normal-module-hooks.md.
close

NormalModule hooks

NormalModule represents a module Rspack resolved from the module graph and builds by running loaders over its source. It provides hooks for intervening in how an individual module is built.

You can obtain these hooks as follows:

rspack.config.mjs
export default {
  plugins: [
    {
      apply(compiler) {
        const { NormalModule } = compiler.rspack;
        compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
          const hooks = NormalModule.getCompilationHooks(compilation);
          // ...
        });
      },
    },
  ],
};

beforeLoaders

SyncHook<[LoaderItem[], NormalModule]>

Called right before the loaders of a module run, with the loader list Rspack resolved from module.rules and the module they belong to. Taps can add, remove, reorder, or reconfigure entries, and the module is then built with the resulting list.

Each entry has the following shape:

  • loader: absolute path of the loader, without the options query.
  • options: the options object the loader was configured with, the raw query string when it was configured with a query, or undefined when it has no options.
  • ident: the key options is registered under, or null when the options came from an inline query.
  • type: module type of the loader itself ('module', 'commonjs', or undefined), derived from its file extension and the type field of the closest package.json.

To change how a loader is configured, replace its options rather than mutating the object in place. As in webpack, one options object is shared by every module the rule matched, so mutating it affects all of them; Rspack additionally cannot tell that a shared object changed, so the loader keeps the cache key it was resolved with and Rule.use[].cache may serve output produced with the previous options.

The following example runs an extra loader over every module under src/generated:

const loader = require.resolve('./my-loader.cjs');

hooks.beforeLoaders.tap('MyPlugin', (loaders, module) => {
  if (module.userRequest.includes('/src/generated/')) {
    loaders.push({
      loader,
      options: { mode: 'strict' },
      ident: null,
      type: null,
    });
  }
});

Loaders execute from right to left, so pushing appends a loader that runs first. Adding a loader that was not part of the configuration makes Rspack resolve it the same way loaders coming from module.rules are resolved, which means resolveLoader and inline builtin: loaders work here as well. type is always derived during that resolution, for a rewritten entry as much as for an added one, so a value a tap writes to it is ignored. webpack instead honours it, and uses it to pick between import() and require() when loading the loader; in Rspack, change the loader's file extension or the type field of its package.json to change how it is loaded.

Differences with webpack

webpack passes a third loaderContext argument to beforeLoaders. Rspack does not, because a module's loader context is only created once the loader runner starts, which is after this hook has run.