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

# Extension config (extension.config.js)

> Set browser defaults, command behavior, and Rspack bundler options in one config file. Applies to dev, start, preview, and build commands.

Use one configuration file to set browser defaults, command behavior, and bundler customization.

Share browser defaults, command options, and build settings across your team. Stop repeating CLI flags. Extension.js reads `extension.config.js` (or `.mjs` / `.cjs`) from your project root. It applies settings to every command and the bundler.

## How it works

Add `extension.config.js` at your project root (same level as `package.json` in typical setups).

Supported file names:

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

Top-level keys:

| Key | Description |
| - | - |
| `browser` | Browser-specific defaults keyed by browser target. |
| `commands` | Per-command defaults (`dev`, `start`, `preview`, `build`). |
| `extensions` | Companion extensions applied across commands (load-only). |
| `transpilePackages` | Packages to compile before bundling (useful for monorepo/workspace setups). |
| `perfBudgets` | Per-category asset size budgets in bytes, overridable per browser and per command. |
| `define` | Compile-time constants inlined into every bundle, overridable per browser and per command. |
| `folders` | Where the special folders live, or `false` to turn one off, overridable per browser and per command. |
| `config` | Hook to extend/override the generated Rspack config. Receives the target browser, mode, and command. |
| `configResolved` | Hook that runs once the Rspack config is final, with every loader rule attached. |

### Type-safe configuration

Extension.js exports the `FileConfig` type from the `extension` package so editors can autocomplete and type-check your config. Annotate the export with a JSDoc `@type` tag, which works in `extension.config.js`, `.mjs`, and `.cjs` without a build step:

```js theme={null}
/** @type {import('extension').FileConfig} */
export default {
  browser: {
    chrome: { profile: "path/to/profile" },
  },
};
```

### Environment loading for configuration files

`extension.config.*` runs in Node and should read values from `process.env.*`.

* Extension.js preloads env files before evaluating `extension.config.*`: `.env.defaults`, `.env`, `.env.local`, and `.env.development`, weakest to strongest. Every file that exists loads, and a variable your shell exported always wins.
* It first checks the project folder.
* In monorepos, if Extension.js finds no project-local `.env*` file, it falls back to the nearest workspace root. The workspace root is the folder containing `pnpm-workspace.yaml`.
* Prefer built-in env preload over importing `dotenv` in your configuration file.

### Browser configuration

Need different browser defaults per target? Use `browser`:

```js theme={null}
export default {
  browser: {
    chrome: {
      profile: "path/to/profile",
      preferences: { darkMode: true },
      browserFlags: ["--disable-web-security"],
      excludeBrowserFlags: ["--mute-audio"],
    },
    "chromium-based": {
      chromiumBinary: "/path/to/brave-or-other-chromium-binary",
    },
    firefox: {
      geckoBinary: "/path/to/firefox",
    },
  },
};
```

Supported browser keys include: `chrome`, `edge`, `firefox`, `chromium`, `chromium-based`, `gecko-based`, `firefox-based`.

Common browser fields:

* `profile`, `persistProfile`
* `preferences`
* `browserFlags`, `excludeBrowserFlags`
* `chromiumBinary`, `geckoBinary`
* `extensions` (companion load-only extensions)

#### Browser target capabilities

| Key | What it does |
| - | - |
| `profile` | Sets the browser profile path (or profile behavior) used when launching. |
| `persistProfile` | Reuses profile data between runs. |
| `keepProfileChanges` | Keeps the managed profile and its changes across runs. |
| `copyFromProfile` | Seeds the managed profile as a copy of an existing profile directory. |
| `preferences` | Applies browser preference overrides. |
| `browserFlags` | Adds launch flags for the browser process. |
| `excludeBrowserFlags` | Removes default launch flags you do not want. |
| `chromiumBinary` | Uses a custom Chromium-family binary path. |
| `geckoBinary` | Uses a custom Gecko/Firefox binary path. |
| `extensions` | Loads companion extensions together with the main extension. |
| `transpilePackages` | Packages to compile for this browser. Outranks the top-level list. |
| `perfBudgets` | Asset budgets for this browser. Outranks the top-level budgets. |
| `define` | Constants for this browser. Adds to the top-level map and overrides the keys it repeats. |
| `folders` | Special folder locations for this browser. Replaces the top-level `folders` object. |

### Commands configuration

Use `commands` to define defaults per command:

```js theme={null}
export default {
  transpilePackages: ["@workspace/ui"],
  commands: {
    dev: {
      browser: "chrome",
      polyfill: true,
      logLevel: "info",
      persistProfile: true,
      browserFlags: ["--headless=new"],
      extensions: { dir: "./extensions" },
    },
    start: {
      browser: "firefox",
    },
    preview: {
      browser: "edge",
      noBrowser: false,
    },
    build: {
      browser: "chrome",
      zip: true,
      zipSource: true,
      zipFilename: "my-extension.zip",
      transpilePackages: ["@workspace/ui", "@workspace/icons"],
    },
  },
};
```

Notes:

* `extensions`, `transpilePackages`, and `perfBudgets` layer from weakest to strongest on every command: top-level, then `browser.<vendor>`, then `commands.<cmd>`, then a CLI flag.
* `define` merges key by key across the same layers: top-level, then `browser.<vendor>`, then `commands.<cmd>` on `dev`, `start`, and `build`.
* `folders` never merges key by key. `browser.<vendor>.folders` replaces the top-level object, and `commands.<cmd>.folders` on `dev`, `start`, and `build` replaces both.
* `commands.build.browser`, `commands.start.browser`, and `commands.preview.browser` pick the target for those commands. A `--browser` flag still wins.
* The `start` command runs `build` then `preview` internally. Extension.js applies settings from `commands.start`, including browser-launch options like `profile`, `browserFlags`, and `startingUrl`. You can also put build-specific settings in `commands.build`.

#### Command capabilities (shared)

| Key | Applies to | What it does |
| - | - | - |
| `browser` | `dev`, `start`, `preview`, `build` | Sets browser or browser-family target. A `--browser` flag wins. |
| `profile` | `dev`, `start`, `preview` | Sets browser profile path/behavior. |
| `persistProfile` | `dev`, `start`, `preview` | Reuses browser profile data between runs. |
| `chromiumBinary` | `dev`, `start`, `preview` | Uses a custom Chromium-family binary. |
| `geckoBinary` | `dev`, `start`, `preview` | Uses a custom Gecko-family binary. |
| `polyfill` | `dev`, `start`, `build` | Enables compatibility polyfill behavior where relevant. |
| `preferences` | `dev`, `start`, `preview` | Applies browser preference overrides. |
| `browserFlags` | `dev`, `start`, `preview` | Adds launch flags for the browser process. |
| `excludeBrowserFlags` | `dev`, `start`, `preview` | Removes default launch flags you do not want. |
| `startingUrl` | `dev`, `start`, `preview` | Opens a specific URL on browser launch. |
| `port` | `dev`, `start`, `preview` | Sets runner/DevTools port. |
| `host` | `dev`, `start`, `preview` | Sets dev server host. Use `0.0.0.0` for Docker/dev containers. |
| `publicHost` | `dev`, `start`, `preview` | Sets the connectable host the browser dials for HMR and the reload bridge. `start` and `preview` accept it for parity and ignore it. |
| `allowedHosts` | `dev` | Extra `Host` names the dev server answers, as an array or a comma string. A leading dot allows every subdomain. |
| `noBrowser` | `dev`, `start`, `preview` | Disables browser launch (`commands.<cmd>.noBrowser`). |
| `extensions` | `dev`, `start`, `preview`, `build` | Loads companion extensions (or extension list). |
| `folders` | `dev`, `start`, `build` | Special folder locations for this command. Replaces the top-level and per-browser `folders`. |
| `install` | `dev`, `start`, `build` | Installs missing dependencies before running. |
| `transpilePackages` | `dev`, `start`, `preview`, `build` | Transpiles listed workspace/external packages. |

#### `build` command capabilities

| Key | What it does |
| - | - |
| `zip` | Generates a packaged zip artifact. |
| `zipSource` | Adds source bundle/archive output for review/compliance workflows. |
| `zipFilename` | Names both archives, like `--zip-filename`. The browser is always appended to the distribution archive, and the source archive is `<name>-source.zip` with no browser. |
| `silent` | Reduces build log output. |
| `perfBudgets` | Overrides the top-level per-category asset budgets for builds. |

#### `dev` command capabilities

| Key | What it does |
| - | - |
| `noOpen` | Runs dev server without auto-opening browser. |
| `hashContentScripts` | Set `false` to disable content-hashed content-script filenames in dev (on by default). |
| `perfBudgets` | Overrides the top-level per-category asset budgets for dev. |

#### Logging capabilities

| Key | Applies to | What it does |
| - | - | - |
| `logLevel` | `dev`, `start`, `preview` | Sets minimum log level (`off` to `all`). |
| `logContexts` | `dev`, `start`, `preview` | Filters logs by context (`background`, `content`, etc.). |
| `logFormat` | `dev`, `start`, `preview` | Sets output format (`pretty`, `json`, `ndjson`). |
| `logTimestamps` | `dev`, `start`, `preview` | Toggles timestamps on log lines. |
| `logColor` | `dev`, `start`, `preview` | Toggles color in log output. |
| `logUrl` | `dev`, `start`, `preview` | Filters logs by URL pattern. |
| `logTab` | `dev`, `start`, `preview` | Filters logs by tab ID. |

### Compile-time constants

Use `define` to inline constants into every bundle. Extension.js serializes each value as JSON, so strings, numbers, booleans, and plain objects all work:

```js theme={null}
export default {
  define: {
    __APP_VERSION__: "1.4.0",
    __IS_FIREFOX__: false,
  },
  browser: {
    firefox: { define: { __IS_FIREFOX__: true } },
  },
};
```

Your code reads `__APP_VERSION__` as a bare identifier, and the build replaces it with the value. In a TypeScript project, each key also gets an ambient declaration in the generated `extension-env.d.ts`, so `__APP_VERSION__` type-checks as a `string`.

### Special folder locations

Use `folders` to move a [special folder](/docs/features/special-folders) or to turn one off. Paths resolve from the project root:

```js theme={null}
export default {
  folders: {
    scripts: "src/scripts",
    public: "src/assets",
    pages: false,
  },
};
```

| Key | Default | Accepts |
| - | - | - |
| `scripts` | `scripts/` | A path to a folder named `scripts`, or `false` |
| `pages` | `pages/` | A path to a folder named `pages`, or `false` |
| `public` | `public/` | A path to any folder, or `false` |

* A moved `scripts` or `pages` folder must keep its name. Extension.js does not read a path such as `src/injected`.
* `false` turns the folder off. Extension.js stops compiling its files as entrypoints, and stops copying a `public` folder.
  * A script in a turned-off `scripts` folder that your code names, for example in `chrome.scripting.executeScript({files})`, still ships. Extension.js compiles it like a script in any other folder, without the content script wrapper.
  * Nothing inside a turned-off `public` folder ships, and a root path such as `/icon.png` no longer reaches into it. If the manifest still names a file there, the build fails and lists each file. If a page or a stylesheet references one, the build prints a warning for each reference. Turn the folder back on, or move the files out of `public/`.
* A moved folder behaves exactly like the default one. Scripts in a moved `scripts` folder get the content script wrapper and reload in place during `dev`, pages in a moved `pages` folder build to the same `pages/` output, and a page, a stylesheet, or the manifest reaches a file in a moved `public` folder by the same root path, such as `/logo.png`.
* When `public` names a path, Extension.js copies from that folder only.
* `browser.<vendor>.folders` replaces the top-level object as a whole, and `commands.<cmd>.folders` replaces both. Neither merges key by key.

### Rspack configuration

Need advanced bundler customization? Use `config` to patch the generated Rspack configuration. This sample needs the release after 4.1.31: until then `config.module.rules` is undefined inside `config`, and `configResolved` is the hook that sees the rules:

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

`config` may also be an object, which Extension.js merges on top of the generated config.

`config` runs before Extension.js attaches its loader rules. To read or change the final configuration, use `configResolved`. It runs once per `dev`, `build`, or `start` run, right before the first compile, and receives the Rspack configuration with every loader rule attached:

```js theme={null}
export default {
  configResolved: (config) => {
    config.module.rules.push({
      test: /\.graphql$/,
      type: "asset/source",
    });
    return config;
  },
};
```

The hook can change `module`, `resolve`, `resolveLoader`, `node`, `optimization.minimize`, `optimization.minimizer`, and the `output` options that Rspack reads when the build starts, such as file names and `environment`. Change them in place, or return a new configuration object. The hook may be async, and a hook that returns nothing keeps the configuration as it is.

Rspack has already consumed every other key by then. That covers keys such as `entry`, `plugins`, `context`, `mode`, `target`, `devtool`, `externals`, `experiments`, and `performance`, every `optimization` key other than `minimize` and `minimizer`, and these `output` keys: `path`, `module`, `library`, `enabledLibraryTypes`, `chunkFormat`, `chunkLoading`, `enabledChunkLoadingTypes`, `wasmLoading`, `enabledWasmLoadingTypes`, `workerChunkLoading`, `workerWasmLoading`, `workerPublicPath`, `pathinfo`, `sourceMapFilename`, `devtoolModuleFilenameTemplate`, `devtoolFallbackModuleFilenameTemplate`, `devtoolNamespace`, and `bundlerInfo`. Extension.js undoes a change to any of them, including one made deep inside a value, and prints one warning that names each key by its path. Set those in `config`.

#### The hook context

Both hooks receive a second argument that names the run. It carries `browser` (the target, as `--browser` names it), `mode` (`development`, `production`, or `none`), and `command` (`dev`, `build`, `start`, or `preview`). Use it to change the bundler for one browser, without reading `process.argv`. This example keeps the Firefox store build readable for review:

```js theme={null}
export default {
  config: (config, { browser, command }) => {
    if (browser === "firefox" && command === "build") {
      config.optimization.minimize = false;
    }
    return config;
  },
};
```

The `extension` package exports the `ConfigHookContext` type for the argument. A hook that takes one argument keeps working.

#### Keep a set of locales for one browser

A store may accept fewer languages than your `_locales` folder holds. Extension.js copies the whole folder for every browser. To ship a subset for one browser, push a plugin from `config` that deletes the other locale assets:

```js theme={null}
const KEEP = { edge: ["en", "de"] };

export default {
  config: (config, { browser }) => {
    const keep = KEEP[browser];
    if (!keep) return config;
    config.plugins.push({
      apply(compiler) {
        compiler.hooks.thisCompilation.tap("keep-locales", (compilation) => {
          compilation.hooks.processAssets.tap(
            {
              name: "keep-locales",
              stage: compiler.rspack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE,
            },
            () => {
              for (const name of Object.keys(compilation.assets)) {
                const match = /^_locales\/([^/]+)\//.exec(name);
                if (match && !keep.includes(match[1])) {
                  compilation.deleteAsset(name);
                }
              }
            },
          );
        });
      },
    });
    return config;
  },
};
```

With this file, `extension build --browser=edge` ships `_locales/en` and `_locales/de` only. Every other browser keeps the whole folder. The filter must keep the folder that `default_locale` names. When it removes that folder, the build fails and the error names the locale.

## Full sample

```js theme={null}
export default {
  browser: {
    chrome: { browser: "chrome", profile: "default" },
    firefox: { browser: "firefox", persistProfile: true },
    "chromium-based": {
      browser: "chromium-based",
      chromiumBinary:
        "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
    },
  },
  commands: {
    dev: {
      browser: "chrome",
      polyfill: true,
      host: "0.0.0.0",
      extensions: { dir: "./extensions" },
    },
    start: { browser: "edge" },
    preview: { browser: "firefox" },
    build: {
      zipFilename: "extension.zip",
      zip: true,
      zipSource: true,
    },
  },
  extensions: { dir: "./extensions" },
  transpilePackages: ["@workspace/ui"],
  config: (config) => config,
};
```

## Best practices

* **Keep browser-specific values in `browser`**: Keep command definitions focused on workflow, not browser internals.
* **Use top-level defaults intentionally**: Put shared `extensions` / `transpilePackages` at root; override only where needed.
* **Prefer `chromiumBinary`/`geckoBinary` names**: They align with current command and type surface.
* **Keep `config` hook minimal**: Add only what first-class Extension.js options do not cover.

## Next steps

* Learn more about [Browsers available](/docs/browsers/browsers-available).
* Learn more about [Rspack configuration](/docs/features/rspack-configuration).
* Tune launch behavior with [Browser flags](/docs/browsers/browser-flags) and [Browser preferences](/docs/browsers/browser-preferences).
* Manage env values with [Environment variables](/docs/features/environment-variables).


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