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

# Build command for production extensions

> Create production-ready extension artifacts for Chrome, Edge, or Firefox with the Extension.js build command. Supports multi-browser and zip output.

Create production extension artifacts for one or more browser targets.

`build` compiles your extension in production mode and writes output to `dist/<browser>`.

For monorepo/submodule projects, see [Environment variables](/docs/features/environment-variables#how-it-works) for configuration-time env resolution (project root first, then workspace-root fallback).

## When to use `build`

* Preparing extension packages for Chrome Web Store, Edge Add-ons, or Firefox Add-ons.
* Running continuous integration (CI) jobs that produce repeatable production artifacts.
* Validating production bundle output and browser-target differences before submission.

## Build command capabilities

| Capability | What it gives you |
| - | - |
| Production compilation | Generate optimized extension artifacts per target |
| Multi-target output | Build multiple browser targets in one command |
| Packaging support | Create distribution zip artifacts with optional source bundle |
| CI-friendly behavior | Keep build outputs and naming predictable in automation |

## Usage

<CodeGroup>
  ```bash npm theme={null}
  extension build [project-path] [options]
  ```

  ```bash pnpm theme={null}
  extension build [project-path] [options]
  ```

  ```bash yarn theme={null}
  extension build [project-path] [options]
  ```

  ```bash bun theme={null}
  extension build [project-path] [options]
  ```

  ```bash deno theme={null}
  extension build [project-path] [options]
  ```
</CodeGroup>

## Build output

After running `build`, Extension.js generates optimized files for the selected browser targets. Output goes to `dist/` with one subfolder per target. Each folder contains bundled JavaScript, CSS, HTML, and required runtime assets.

<Note>
  For TypeScript projects, `build` also regenerates the `extension-env.d.ts`
  ambient type declarations (the same file [`dev`](/docs/commands/dev) writes),
  so a CI `tsc --noEmit` stays clean whether or not you ran `dev` first.
  JavaScript-only projects skip this step.
</Note>

**Example output structure:**

```plaintext theme={null}
dist/
├── chrome/
│   ├── manifest.json
│   ├── background/service_worker.js
│   ├── content_scripts/content-0.js
├── edge/
│   ├── manifest.json
│   ├── background/service_worker.js
│   ├── content_scripts/content-0.js
```

## Browser target matrix

| Target style | Examples | Notes |
| - | - | - |
| Named targets | `chromium`, `chrome`, `edge`, `firefox` | Build one specific browser target |
| Engine targets | `chromium-based`, `gecko-based`, `firefox-based` | Family-generic artifacts. See [below](#what-engine-targets-mean-for-build) |
| Multi-target | `chrome,firefox` | Comma-separated targets |

### What engine targets mean for `build`

`build` never launches a browser, so engine targets don't point at a binary here, but they still produce a distinct artifact, not a renamed copy of a named-target build:

* **Own output folder.** `--browser=chromium-based` writes to `dist/chromium-based`, the same folder `dev`, `preview`, and `start` use for that target, so a project developed against a custom Chromium binary builds to matching paths.
* **Own env resolution.** `.env.chromium-based` and `.env.chromium-based.production` win over the family's `.env.chromium`/`.env.chrome`/`.env.edge`, and bundled code sees `EXTENSION_BROWSER === "chromium-based"`, so code and config can branch on "generic Chromium" vs a specific store build.
* **Own manifest prefix.** `chromium-based:` keys in `manifest.json` resolve as the most-specific match for this target, on top of the family-wide `chromium:` keys. `chrome:` and `edge:` keys do not apply.

`gecko-based` works the same way relative to `firefox`. No browser binary is required, and `--chromium-binary`/`--gecko-binary` only matter for commands that launch a browser.

## Arguments and flags

| Flag | Alias | What it does | Default |
| - | - | - | - |
| `[path]` | - | Builds a local extension project. | `process.cwd()` |
| `--browser <browser>` | - | Browser/engine target (`chromium`, `chrome`, `edge`, `firefox`, `safari`, engine aliases, or comma-separated values). | `commands.build.browser`, else `chromium` |
| `--polyfill [boolean]` | - | Enables `browser.*` API compatibility polyfill for Chromium targets. | `false` |
| `--no-polyfill` | - | Disables the cross-browser polyfill. | polyfill disabled |
| `--zip [boolean]` | - | Creates a packaged zip artifact in any mode. | `false` |
| `--zip-source [boolean]` | - | Includes source files in zip output. | `false` |
| `--zip-filename <name>` | - | Names both the distribution and the source archive. The browser is always appended to the distribution name. | sanitized extension name, version and browser |
| `--silent [boolean]` | - | Suppresses build logs. | `false` |
| `--addon-lint [boolean]` | - | Checks Firefox production builds against addons.mozilla.org rules with addons-linter when it is installed. Findings print as warnings. See [Store check for Firefox builds](#store-check-for-firefox-builds). | `true` |
| `--no-addon-lint` | - | Skips the addons.mozilla.org check. | check enabled |
| `--minify [boolean]` | - | Minifies first-party code in production builds. See [Readable output for Opera Add-ons](#readable-output-for-opera-add-ons). | `true`, `false` for `opera` |
| `--no-minify` | - | Ships readable first-party code. | minified, except `opera` |
| `--mode <mode>` | - | Bundler mode override (`development`, `production`, or `none`). Also sets `NODE_ENV`. | `production` |
| `--extensions <list>` | - | Comma-separated companion extensions or store URLs. | unset |
| `--install [boolean]` | - | Internal flag. Install project dependencies when missing. Lifecycle scripts stay disabled unless `EXTENSION_ALLOW_INSTALL_SCRIPTS=true`. | installs when `node_modules` is missing |
| `--output <pretty\|json>` | - | Result format. `json` prints a schema-1 envelope on stdout. | `pretty` |
| `--debug` | - | Enable maintainer diagnostics. | disabled |

`--author` and `--author-mode` are hidden, deprecated aliases for `--debug`.

### Safari flags

These flags apply to `safari` and `webkit-based` targets only. Passing any of them with another target exits with an error, so a typo never no-ops silently.

| Flag | What it does | Default |
| - | - | - |
| `--open [boolean]` | Open the built Safari app after packaging. `build` never opens it unless you ask. | `false` |
| `--app-name <name>` | Override the Safari app name. | the manifest `name` |
| `--bundle-id <id>` | User-owned bundle identifier in reverse-DNS form. A malformed value fails before any build. | a generated `dev.extensionjs.*` id |
| `--development-team <id>` | Apple Developer team id to sign the Safari app with. Without it the build is ad-hoc signed, which Safari accepts and lists normally. | unset (ad-hoc signed) |
| `--macos-only [boolean]` | Generate a macOS-only Xcode project. Pass `false` for a universal macOS + iOS project. | `true` |
| `--force-regenerate` | Regenerate the Safari Xcode project even when up to date. | disabled |

Safari packaging runs a preflight before the build. On a non-macOS host, `build` warns and skips the Safari packaging step but still compiles the bundle. On macOS with a broken or missing Xcode, the failure is fatal.

## Shared global options

Also supports [Global flags](/docs/workflows/global-flags).

## Mode override

`--mode` overrides the bundler mode and `NODE_ENV` for the build. Accepts `development`, `production`, or `none`. Use it to mirror Vite/webpack workflows where you need a non-production bundle for staging or debugging.

<CodeGroup>
  ```bash npm theme={null}
  extension build ./my-extension --mode development
  ```

  ```bash pnpm theme={null}
  extension build ./my-extension --mode development
  ```

  ```bash yarn theme={null}
  extension build ./my-extension --mode development
  ```

  ```bash bun theme={null}
  extension build ./my-extension --mode development
  ```

  ```bash deno theme={null}
  extension build ./my-extension --mode development
  ```
</CodeGroup>

Invalid values exit with an error. The default remains `production`.

A development-mode build is shippable. It keeps the CSP and permissions you wrote in the manifest, injects no reload client, and its zip carries no source maps (the `.map` files stay in `dist/<browser>` for you). Only `extension dev` turns the dev instrumentation on. `--zip` packages the output in any mode, not only `production`.

## Zip behavior

| Option | Effect | Typical use |
| - | - | - |
| `--zip` | Creates a packaged artifact zip | Store submission/manual distribution |
| `--zip-filename` | Sets custom zip name | CI naming conventions |
| `--zip-source` | Adds source archive alongside artifacts | Compliance/review pipelines |

Each zip lands in `dist/`, beside the `dist/<browser>` folder rather than inside it, because `dist/<browser>` is what a store upload or a load-unpacked takes whole. Without `--zip-filename`, the name is the manifest `name` lowercased with every character outside `a-z0-9` and spaces removed, remaining spaces turned into dashes, then the manifest `version` and the browser. A manifest named `My Extension+` at version `1.0.0` packages as `dist/my-extension-1.0.0-chrome.zip`. Because the name is rewritten, read the emitted path from the build output instead of composing it from the manifest name. Under `--output json`, each archive is listed in `zip_artifacts` with its path and size.

`--zip-filename` names both archives, and the browser is always appended to the distribution archive, so two browsers built with the same name in separate runs never write over each other. Passing `--zip-filename=review-bundle.zip` with `--zip-source` on a Chrome build writes `dist/review-bundle-chrome.zip` and `dist/review-bundle-source.zip`. The source archive is the same for every browser, so its name carries none, and one build writes one source archive. A name that already ends in the browser is left alone. Building `--browser=edge,chrome --zip-filename=my-extension.zip` writes `dist/my-extension-edge.zip` and `dist/my-extension-chrome.zip`.

## Examples

### Building with zip output and custom filename

<Frame>
  <iframe className="w-full aspect-video rounded-xl" src="https://www.youtube-nocookie.com/embed/zgJBCat97Sc?rel=0" title="Extension.js: extension build for Edge and Chrome" loading="lazy" allow="encrypted-media; picture-in-picture; fullscreen" allowFullScreen />
</Frame>

<CodeGroup>
  ```bash npm theme={null}
  extension build ./my-extension --browser=edge,chrome --zip --zip-filename=my-extension.zip
  ```

  ```bash pnpm theme={null}
  extension build ./my-extension --browser=edge,chrome --zip --zip-filename=my-extension.zip
  ```

  ```bash yarn theme={null}
  extension build ./my-extension --browser=edge,chrome --zip --zip-filename=my-extension.zip
  ```

  ```bash bun theme={null}
  extension build ./my-extension --browser=edge,chrome --zip --zip-filename=my-extension.zip
  ```

  ```bash deno theme={null}
  extension build ./my-extension --browser=edge,chrome --zip --zip-filename=my-extension.zip
  ```
</CodeGroup>

In this example, the build targets Edge and Chrome and zips both outputs. The archives are saved as `dist/my-extension-edge.zip` and `dist/my-extension-chrome.zip`, because the browser is appended to every distribution archive, even when you build one target.

### Building with polyfill support

<CodeGroup>
  ```bash npm theme={null}
  extension build ./my-extension --browser=chrome,firefox --polyfill
  ```

  ```bash pnpm theme={null}
  extension build ./my-extension --browser=chrome,firefox --polyfill
  ```

  ```bash yarn theme={null}
  extension build ./my-extension --browser=chrome,firefox --polyfill
  ```

  ```bash bun theme={null}
  extension build ./my-extension --browser=chrome,firefox --polyfill
  ```

  ```bash deno theme={null}
  extension build ./my-extension --browser=chrome,firefox --polyfill
  ```
</CodeGroup>

In this example, the build targets Chrome and Firefox and includes polyfill support where relevant.

### Building source and artifact zip

<CodeGroup>
  ```bash npm theme={null}
  extension build ./my-extension --zip --zip-source
  ```

  ```bash pnpm theme={null}
  extension build ./my-extension --zip --zip-source
  ```

  ```bash yarn theme={null}
  extension build ./my-extension --zip --zip-source
  ```

  ```bash bun theme={null}
  extension build ./my-extension --zip --zip-source
  ```

  ```bash deno theme={null}
  extension build ./my-extension --zip --zip-source
  ```
</CodeGroup>

## What a successful build prints

After the asset summary, a successful build prints the output directory and its
size, then a link you can use to share the build for review:

```plaintext theme={null}
⏵⏵⏵ Extension built for production in dist/chrome (35.5 KB).
⏵⏵⏵ Send this build to someone for review: https://docs.extension.dev/share/unpublished-build-for-review
```

Each target prints its own pair of lines, so a multi-browser build repeats them
once per browser.

Builds that succeed with warnings print the warning details above those lines,
and the compile line reads `compiled with warnings` instead of `compiled in`.

Do not gate CI on this prose. It is written for people and it changes between
releases. Use the exit code, or `--output json` below, which is the supported
machine contract.

## Machine output with `--output json`

`--output json` prints one schema-1 envelope on stdout and routes the human build lines to stderr. Stdout stays parseable as a single JSON document.

* A successful run prints a `status: "built"` frame. Its `value` carries the built browsers, the resolved mode, and one summary per browser. Each summary records the output path, asset totals, warning text, and the Safari app identity when relevant. Each warning string opens with `E_CODE: ` when a code resolves, for example `E_CSS_DEAD_REF: ...`, the same convention `logs` and `uninstall` use.
* A failed build prints one `ok: false` frame with `status: "build-failed"` before the process exits `1`. A compile error carries `error.code: "E_COMPILE"`, and `error.details` lists each diagnostic behind it with its own code, file, and position, errors first, capped at 20 with `truncated: true` when cut. See [Compile diagnostics](/docs/contracts/result-envelope). A missing project folder carries `E_PROJECT_NOT_FOUND`, a folder with no manifest carries `E_MANIFEST_NOT_FOUND`, and a manifest that does not parse carries `E_MANIFEST_INVALID`. A config file that fails to load carries `E_CONFIG_LOAD`.
* When the path argument is a remote URL, the download progress lines go to stderr as well. A reply that is not a ZIP archive, a damaged one, or one with an entry outside its folder carries `E_REMOTE_ZIP_INVALID`, a timeout carries `E_REMOTE_FETCH_TIMEOUT`, and a refused connection or an HTTP error carries `E_REMOTE_DOWNLOAD`. An archive with an entry outside its folder is refused before anything is written, and its message names that entry.

Every build also writes `dist/extension-js/<browser>/build-summary.json`. Scripts that shell out to `extension build` can read structured warnings there. Guard against stale files by checking the file's modification time.

## Store check for Firefox builds

From 4.1.18, a production build for a Gecko target (`firefox`, `gecko-based`, and the Gecko forks) runs [addons-linter](https://github.com/mozilla/addons-linter) over `dist/<browser>` when that package is installed in your project. The linter is what addons.mozilla.org runs on a submission, so the check surfaces a rejection before you upload.

Every finding prints as a warning. The build never fails because of one, and the exit code stays `0`. A summary line comes first, then one line per finding with the linter's code, message, and location:

```plaintext theme={null}
Store check for addons.mozilla.org: addons-linter found 1 error and 2 warnings in dist/firefox
AMO <error|warning> <CODE>: <message> (<file>:<line>)
```

The output is capped at 20 findings, and the linter gets 10 seconds. Past 20, one line names how many more there are and points at `npx addons-linter dist/firefox` for the full report. A linter that runs past 10 seconds is dropped, and the build goes on without the check.

A finding in a chunk that bundles a dependency is attributed, because the file name alone can point at the wrong author. The line ends with `- this file is bundled dependency code (react, react-dom), not yours` when nothing of yours is in that chunk, or `- this file also bundles react, react-dom, so the finding may be theirs` when the chunk mixes both.

When addons-linter is not installed, the build prints one info line per project and skips the check:

```plaintext theme={null}
Skipped the addons.mozilla.org lint: addons-linter is not installed. Install it with: npm install -D addons-linter or pass --no-addon-lint to silence this.
```

The install command is phrased for the package manager that your project uses.

Turn the check off with `--no-addon-lint`, or with `commands.build.addonLint: false` in `extension.config.js`. Non-production modes skip it, because a dev artifact carries dev-only grants that the linter would flag for nothing. `extension start` skips it too, since `start` previews a build rather than shipping one.

## Readable output for Opera Add-ons

Opera's [acceptance criteria](https://help.opera.com/en/extensions/acceptance-criteria/) require code the reviewers can read: an extension whose own code is minified or obfuscated is rejected, while third-party libraries may stay minified. So `extension build --browser=opera` in production mode does not minify, and prints one info line:

```plaintext theme={null}
Opera Add-ons reviews readable source, so this build does not minify first-party code. Pass --minify to minify it anyway.
```

The build turns minification off for the whole bundle, dependencies included, because a bundled chunk can mix your code with theirs. Pass `--minify` to override it, or set `commands.build.minify` in `extension.config.js`. The same option works the other way for every other target: `--no-minify` keeps a Chrome or Firefox build readable when a reviewer asks for it.

## Best practices

* **Check build logs:** Review logs for warnings and missing assets after each build.
* **Optimize your manifest:** Keep `manifest.json` compatible with every target browser.
* **Name artifacts intentionally:** Use `--zip-filename` for stable CI artifact naming.
* **Validate target output:** Check each `dist/<browser>` folder before publishing. A later `dev` session overwrites that same folder with a dev-instrumented build that adds permissions such as `scripting`, `tabs`, `management`, and `storage`, unions the match patterns of your content scripts into `host_permissions`, and lists `hot/*` and `extension-js-control.json` as web-accessible resources for those same matches. A project with no content scripts gets neither of the last two. Run `build` again before you package or publish.

## Next steps

* Send the build to a reviewer behind a link by following [Share an unpublished build for review](https://docs.extension.dev/share/unpublished-build-for-review?utm_source=extension-js-org\&utm_medium=sponsor\&utm_campaign=docs-seam).
* Submit the artifacts to the browser stores by following [the extension.dev publish docs](https://docs.extension.dev/publish/overview?utm_source=extension-js-org\&utm_medium=sponsor\&utm_campaign=docs-seam).
* Get a shareable URL for a project on extension.dev with [`publish`](/docs/commands/publish).
* Run existing build output with [`preview`](/docs/commands/preview).
* Build and launch in one command with [`start`](/docs/commands/start).
* Configure shared defaults in [`extension.config.js`](/docs/features/extension-configuration).
* Review configuration env loading behavior in [Environment variables](/docs/features/environment-variables#how-it-works).
* Review supported targets in [Browsers available](/docs/browsers/browsers-available).


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