Skip to main content
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:

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:

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:
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

Commands configuration

Use commands to define defaults per command:
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)

build command capabilities

dev command capabilities

Logging capabilities

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:
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 or to turn one off. Paths resolve from the project root:
  • 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:
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:
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:
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:
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

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