> ## Documentation Index
> Fetch the complete documentation index at: https://extension.js.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Rspack configuration in extension.config.js

> Customize Rspack bundling with loaders, plugins, and aliases through extension.config.js without forking the default Extension.js build setup.

Customize bundling behavior without leaving the Extension.js workflow.

Solve custom bundler needs without forking the default setup. Add loaders (file-type transforms), plugins (build-time extensions), and aliases (import shortcuts) directly. Extension.js builds with [Rspack](https://rspack.dev), a Rust-based JavaScript bundler (`@rspack/core`), and you can extend the generated configuration directly.

## How it works

Create one of these files at project root:

* `extension.config.js`
* `extension.config.mjs`
* `extension.config.cjs`

Use the `config` key to patch the generated bundler configuration.

### `config` capabilities

| Capability | What it does |
| - | - |
| Function hook (`config: (config) => config`) | Gives full control to read and modify generated Rspack config before build. |
| Object merge (`config: { ... }`) | Merges additional config into the generated base config. |
| Rules (`config.module.rules`) | Adds or changes loader rules for file types. |
| Plugins (`config.plugins`) | Adds compile-time plugins and transformations. |
| Resolve (`config.resolve`) | Adds aliases and module resolution behavior. |

### Option 1: function hook (recommended)

```js theme={null}
export default {
  config: (config) => {
    config.module.rules.push({
      test: /\.mdx$/,
      use: ["babel-loader", "@mdx-js/loader"],
    });
    return config;
  },
};
```

### Option 2: object merge

`config` can also be an object. Extension.js merges it into the base configuration.

```js theme={null}
export default {
  config: {
    resolve: {
      alias: {
        "@ui": "/absolute/path/to/ui",
      },
    },
  },
};
```

## Rspack-first, webpack-compatible

Extension.js is Rspack-native, but you can still use much of the webpack ecosystem.

* The config type extends `@rspack/core` `Configuration`.
* Many webpack loaders/plugins work through compatibility layers.
* Some webpack internals/plugins are not 1:1 compatible with Rspack.

Example using a Rspack-native plugin:

```js theme={null}
import { DefinePlugin } from "@rspack/core";

export default {
  config: (config) => {
    config.plugins = [
      ...config.plugins,
      new DefinePlugin({
        __FEATURE_FLAG__: JSON.stringify("enabled"),
      }),
    ];
    return config;
  },
};
```

## Share a module between entries

Extension.js splits HTML pages only. The default `optimization.splitChunks` gives pages two stable siblings: `shared/framework.js` for the UI framework runtime, and `shared/commons.js` for anything two or more pages import. The framework group covers React, Preact, Vue, Svelte and Lit. Neither name carries a hash, so a manifest entry or a public asset can keep pointing at it.

Every other surface keeps exactly one file. The manifest names a single script for a background worker, a content script or an injected script, and those surfaces never load a sibling.

That is why sharing a module between a page and a content script still needs `import()`. Rspack emits one chunk for it, and every caller fetches that chunk on demand from the extension origin. A cache group gives the chunk a stable name:

```js theme={null}
export default {
  config: (config) => {
    config.optimization.splitChunks = {
      chunks: "async",
      cacheGroups: {
        shared: {
          name: "shared",
          test: /[\\/]src[\\/]shared[\\/]/,
          minChunks: 1,
          enforce: true,
        },
      },
    };
    return config;
  },
};
```

With this in place, `await import("../shared/big")` from the popup and from the options page both resolve to one emitted `shared.js`. Keep `chunks` at `"async"`. A sync split moves code out of an entry file. The emitted HTML and the manifest reference only that entry file, so the page never loads the split chunk. The service worker cannot use dynamic `import()` and keeps its own copy of the module. A content script can share the chunk too, but it must expose the file through `web_accessible_resources` and import it through `chrome.runtime.getURL`. See [Lazy loading](/docs/implementation-guide/lazy-loading) for both rules.

## When to use this

* Add custom loaders/rules for project-specific file types.
* Add plugins for compile-time transforms and diagnostics.
* Override resolve aliases and module behavior not exposed by first-class Extension.js options.

## Best practices

* **Prefer first-class options first**: Use `browser` / `commands` configuration keys before low-level bundler overrides.
* **Patch minimally**: Change only the pieces you need, then return the configuration.
* **Keep plugins Rspack-aware**: Prefer Rspack-native plugins when available.
* **Verify on all targets**: Test `dev`, `start`, `preview`, and `build` for your browser matrix after configuration changes.

## Next steps

* Learn more about [extension config (`extension.config.js`)](/docs/features/extension-configuration).
* Learn more about [Multi-platform builds](/docs/features/multi-platform-builds).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.