extension.config.js (or .mjs / .cjs) from your project root. It applies settings to every command and the bundler.
How it works
Addextension.config.js at your project root (same level as package.json in typical setups).
Supported file names:
extension.config.jsextension.config.mjsextension.config.cjs
Type-safe configuration
Extension.js exports theFileConfig 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:
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 containingpnpm-workspace.yaml. - Prefer built-in env preload over importing
dotenvin your configuration file.
Browser configuration
Need different browser defaults per target? Usebrowser:
chrome, edge, firefox, chromium, chromium-based, gecko-based, firefox-based.
Common browser fields:
profile,persistProfilepreferencesbrowserFlags,excludeBrowserFlagschromiumBinary,geckoBinaryextensions(companion load-only extensions)
Browser target capabilities
Commands configuration
Usecommands to define defaults per command:
extensions,transpilePackages, andperfBudgetslayer from weakest to strongest on every command: top-level, thenbrowser.<vendor>, thencommands.<cmd>, then a CLI flag.definemerges key by key across the same layers: top-level, thenbrowser.<vendor>, thencommands.<cmd>ondev,start, andbuild.foldersnever merges key by key.browser.<vendor>.foldersreplaces the top-level object, andcommands.<cmd>.foldersondev,start, andbuildreplaces both.commands.build.browser,commands.start.browser, andcommands.preview.browserpick the target for those commands. A--browserflag still wins.- The
startcommand runsbuildthenpreviewinternally. Extension.js applies settings fromcommands.start, including browser-launch options likeprofile,browserFlags, andstartingUrl. You can also put build-specific settings incommands.build.
Command capabilities (shared)
build command capabilities
dev command capabilities
Logging capabilities
Compile-time constants
Usedefine to inline constants into every bundle. Extension.js serializes each value as JSON, so strings, numbers, booleans, and plain objects all work:
__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
Usefolders to move a special folder or to turn one off. Paths resolve from the project root:
- A moved
scriptsorpagesfolder must keep its name. Extension.js does not read a path such assrc/injected. falseturns the folder off. Extension.js stops compiling its files as entrypoints, and stops copying apublicfolder.- A script in a turned-off
scriptsfolder that your code names, for example inchrome.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
publicfolder ships, and a root path such as/icon.pngno 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 ofpublic/.
- A script in a turned-off
- A moved folder behaves exactly like the default one. Scripts in a moved
scriptsfolder get the content script wrapper and reload in place duringdev, pages in a movedpagesfolder build to the samepages/output, and a page, a stylesheet, or the manifest reaches a file in a movedpublicfolder by the same root path, such as/logo.png. - When
publicnames a path, Extension.js copies from that folder only. browser.<vendor>.foldersreplaces the top-level object as a whole, andcommands.<cmd>.foldersreplaces both. Neither merges key by key.
Rspack configuration
Need advanced bundler customization? Useconfig 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:
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:
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 carriesbrowser (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:
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:
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
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/transpilePackagesat root; override only where needed. - Prefer
chromiumBinary/geckoBinarynames: They align with current command and type surface. - Keep
confighook minimal: Add only what first-class Extension.js options do not cover.
Next steps
- Learn more about Browsers available.
- Learn more about Rspack configuration.
- Tune launch behavior with Browser flags and Browser preferences.
- Manage env values with Environment variables.

