# Feedback If you encounter incorrect, outdated, or confusing documentation on any page, submit feedback: POST https://extension.js.org/feedback ```json { "path": "/current-page-path", "feedback": "Description of the issue" } ``` Only submit feedback when you have something specific and actionable to report. # Announcing Extension.js 2.0.0-alpha Source: https://extension.js.org/blog/announcing-2-0-0-alpha Extension.js 2.0.0-alpha introduces 30+ starter templates, browser-specific manifest fields, and SWC-powered compilation for faster cross-browser builds. Extension.js 2.0.0-alpha banner Extension.js is all about making cross-browser extension development as easy as possible. Since being [featured on HackerNews](https://hnrankings.info/40210627/), we've made a lot of internal improvements to make the package smaller, faster, and more developer-friendly. To me, "very easy" means very low cognitive effort. It means something is familiar. Developers are familiar with copy/paste, and as a developer myself, it feels very easy if I can copy/paste code and have an instant response of what the code means without configuration. Even if the code is not understandable at first, a fast visual response is a clear indicator of how things are **expected** to work. That's the expectation of Extension.js v2: something very easy to work with. **Version 2.0.0-alpha** * 30+ templates for popular JavaScript frameworks, CSS pre-processors, and extension APIs. [View on GitHub](https://github.com/extension-js/examples/tree/main/examples). * Support for adding browser-specific fields in manifest.json that apply only to the target browser, like `{chrome:service_worker: "sw.js"}` * `extension.config.js` - For advanced users needing access to the compilation process. [See configuration docs](/docs/features/extension-configuration). * Faster compilation thanks to [SWC](https://swc.rs/). * A revamped documentation site (this one!). * A new templates site (coming soon). The alpha release is focused on bug fixes and testing all existing features while we finish the documentation and templates websites. Keep an eye on this blog or follow us on X for future updates. For the next release, we are going to focus on making all the existing features stable enough to document and test. Thanks for reading. Talk soon. Cezar Augusto
Creator and Lead Developer, Extension.js
# Announcing Extension.js 2.0.0-beta Source: https://extension.js.org/blog/announcing-2-0-0-beta Extension.js 2.0.0-beta stabilizes alpha features and resolves browser compatibility issues across Firefox, Edge, and Chrome for reliable cross-browser builds. Extension.js 2.0.0-beta banner Extension.js is now officially in beta. This beta stabilizes the features introduced in the alpha release and fixes all known issues with browser compatibility. **Version 2.0.0-beta** * Fixed Sass Modules not working in the content script. * Add support for Svelte templates. * Add preview command. * Add universal support for content\_scripts (via hard-reload and hot-reload). * Fix all known browser issues for Firefox, Edge, and Chrome. * Updated documentation (this one!) The beta release is focused on making all existing features stable enough to document and test. New features are locked until the next stable release. The documentation is now up to date. This is the last beta release before the release candidate. For the next release, we are going to focus on shaping Extension.js into a faster and smaller bundle that can work both locally and remotely (via `pnpm dlx`, `yarn dlx`, and of course `npx`). Thanks for reading. Talk soon. Cezar Augusto
Creator and Lead Developer, Extension.js
# Announcing Extension.js 3.0.0 Source: https://extension.js.org/blog/announcing-3-0-0 Extension.js 3.0.0 ships faster Rspack-powered builds, Firefox Manifest V3 support, and first-class TypeScript for cross-browser extension development. Extension.js 3.0.0 is now available. This release is focused on one thing: helping teams ship production browser extensions with less setup, clearer workflows, and better defaults across Chrome, Edge, and Firefox. ## What ships in v3 **Version 3.0.0** * Stronger cross-browser workflows across Chrome, Edge, and Firefox with one command surface. * Expanded template experience for modern stacks including TypeScript, React, Vue, Svelte, and Preact. * Improved development update behavior with clearer HMR, hard-reload, and restart-required boundaries. * Better production confidence workflows across troubleshooting, security checks, performance checks, CI templates, and Playwright E2E. * More predictable project configuration through `extension.config.js` and browser-aware build output. * A cleaner documentation and homepage experience designed for faster onboarding and lower cognitive load. ## The day-one flow is simpler Use one path from project creation to production output: 1. `create` to scaffold. 2. `dev` to iterate quickly. 3. `build` to generate production artifacts. 4. `start` or `preview` to validate production behavior. ```bash theme={null} npx extension@latest create my-extension --template=new-typescript cd my-extension pnpm dev pnpm build ``` ## Better for teams, not just demos v3 is designed for real teams maintaining real extension codebases: * Typed templates and framework-first baselines reduce setup drift. * Browser-targeted builds and flags make compatibility checks explicit. * Workflow docs now map directly to release confidence tasks. * Security and performance guidance is integrated into standard delivery flow. ## Who this release is for * Teams migrating from custom extension build scripts. * Web developers moving from app development to extension development. * Teams standardizing extension DX across multiple repos and contributors. ## Upgrade notes If you are already using Extension.js: * update the package to latest, * prefer canonical template slugs (`new-react`, `new-typescript`, `new-vue`, etc.), * keep your command flow aligned with `create` -> `dev` -> `build`. ## Thanks Thanks to everyone building with Extension.js, reporting issues, and improving docs and templates with feedback. v3 is a major step forward, and we are just getting started. Cezar Augusto
Creator and Lead Developer, Extension.js
# Extension.js blog and release announcements Source: https://extension.js.org/blog/index Stay up to date with Extension.js release notes, feature announcements, and ecosystem updates for cross-browser extension development. Follow the latest news and updates from the team at Extension.js! Here, you will find information about new releases, features, and more. For the latest updates, we are also available on [Twitter](https://twitter.com/extension_js) and [Discord](https://discord.gg/extensionjs). ## [Announcing v3.0.0](/blog/announcing-3-0-0) > February 18, 2026 ## [Announcing v2.0.0-beta](/blog/announcing-2-0-0-beta) > October 18, 2024 ## [Announcing v2.0.0-alpha](/blog/announcing-2-0-0-alpha) > September 9, 2024 # AI access via MCP and llms.txt Source: https://extension.js.org/docs/ai-access Connect Extension.js documentation to your AI tools via the hosted MCP server, or pull structured summaries from the llms.txt endpoint. You can access Extension.js documentation through two standard AI-accessible surfaces. This lets your editor and assistants answer questions grounded in the real docs instead of stale training data. ## Hosted MCP (Model Context Protocol) server The docs site exposes a [Model Context Protocol](https://modelcontextprotocol.io) server at: ``` https://extensionjs.mintlify.app/mcp ``` Point any MCP-compatible client at this URL and it gains tools to search the docs, fetch specific pages, and answer questions from current content. ### One-click install Each page has a contextual menu (top-right of the page) with install buttons for common clients — **Claude**, **Cursor**, **VS Code**, and **ChatGPT**. Use those for the shortest path. ### Manual install ```json Claude Desktop (claude_desktop_config.json) theme={null} { "mcpServers": { "extensionjs": { "url": "https://extensionjs.mintlify.app/mcp" } } } ``` ```json Cursor (.cursor/mcp.json) theme={null} { "mcpServers": { "extensionjs": { "url": "https://extensionjs.mintlify.app/mcp" } } } ``` ```jsonc VS Code (settings.json) theme={null} "mcp.servers": { "extensionjs": { "url": "https://extensionjs.mintlify.app/mcp" } } ``` ```bash Claude Code theme={null} claude mcp add --transport http extensionjs https://extensionjs.mintlify.app/mcp ``` After installing, ask your assistant something like *"How do I configure browser-specific manifest fields in Extension.js?"* — it consults the MCP tools and cites docs pages. ## llms.txt The documentation provides a static, machine-friendly index at: * [`/llms.txt`](https://extensionjs.mintlify.app/llms.txt) — short index of all pages * [`/llms-full.txt`](https://extensionjs.mintlify.app/llms-full.txt) — full-content bundle for ingestion Use these when you want to feed the docs directly into a retrieval pipeline or summarize them in a custom agent. Extension.js regenerates them on every docs deploy. ## Per-page AI actions The page-level contextual menu also offers: * **Copy page** — copies the current page as Markdown for pasting into chats * **View as Markdown** — opens the raw Markdown source in a new tab * **Ask ChatGPT / Claude / Perplexity** — opens the respective assistant pre-loaded with the page as context ## Best practices * Prefer the MCP server when you want the assistant to reason across multiple pages and keep answers current. * Prefer `llms-full.txt` for offline tooling, evaluations, or custom RAG (retrieval-augmented generation) pipelines. * Use the per-page actions for quick spot questions about the page you are already reading. # Browser launch flags for debugging and automation Source: https://extension.js.org/docs/browsers/browser-flags Control Chrome, Firefox, and Edge launch behavior with browser flags for debugging, automation, and runtime experiments in Extension.js. Control browser launch behavior for debugging, automation, and runtime experiments. Tune browser launch behavior without changing extension source code. Extension.js merges browser flags from your `extension.config.*` and applies them in `dev`, `preview`, and `start` flows. ## Template example ### `new-browser-flags` new-browser-flags template screenshot See browser flags in action with a new-tab extension that configures launch behavior. ```bash npm theme={null} npx extension@latest create my-extension --template=new-browser-flags ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=new-browser-flags ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=new-browser-flags ``` Repository: [extension-js/examples/new-browser-flags](https://github.com/extension-js/examples/tree/main/examples/new-browser-flags) ## How it works Define flags in configuration: * `browser..browserFlags` * `commands.dev|start|preview.browserFlags` * optional `excludeBrowserFlags` to remove defaults or user flags (runner-dependent behavior) Merge precedence is browser defaults -> command defaults -> CLI-selected command context. ## Flag capabilities | Config key | What it does | | -------------------------------------- | -------------------------------------------------------- | | `browser..browserFlags` | Sets default launch flags for a specific browser target. | | `commands.dev.browserFlags` | Adds or overrides flags for `dev` runs. | | `commands.start.browserFlags` | Adds or overrides flags for `start` runs. | | `commands.preview.browserFlags` | Adds or overrides flags for `preview` runs. | | `browser..excludeBrowserFlags` | Removes matching default or user flags for a target. | | `commands..excludeBrowserFlags` | Removes flags in a command-specific context. | ### Example configuration ```js theme={null} export default { browser: { chrome: { browserFlags: ['--disable-web-security', '--auto-open-devtools-for-tabs'], excludeBrowserFlags: ['--mute-audio'] }, firefox: { browserFlags: ['--devtools', '--new-instance'], excludeBrowserFlags: ['--devtools'] } } } ``` ## Chromium vs Firefox behavior * **Chromium family (`chrome`, `edge`, `chromium`, `chromium-based`)** * Starts from an internal default flag set, then appends your `browserFlags`. * `excludeBrowserFlags` removes matching default flags (exact-match filtering). * Extension.js manages `--load-extension=...` and filters it out from user-provided flags. * **Firefox/Gecko family (`firefox`, `gecko-based` / `firefox-based`)** * Uses user-provided `browserFlags` (no large default flag bundle like Chromium). * `excludeBrowserFlags` removes user flags by prefix matching. ## Supported targets and references | Browser | Usage | More Information | | -------------- | ---------------------------------------- | -------------------------------------------------------------------------------------- | | Chrome | `extension dev --browser=chrome` | [Chrome Flags](https://peter.sh/experiments/chromium-command-line-switches/) | | Edge | `extension dev --browser=edge` | [Edge Flags](https://docs.microsoft.com/en-us/deployedge/microsoft-edge-policies) | | Firefox | `extension dev --browser=firefox` | [Firefox Flags](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options) | | Chromium-based | `extension dev --browser=chromium-based` | [Chromium Flags](https://peter.sh/experiments/chromium-command-line-switches/) | | Gecko-based | `extension dev --browser=gecko-based` | Firefox-based browsers share the same flags as Firefox. | Use binary flags when needed for engine-based targets: * `--chromium-binary=...` * `--gecko-binary=...` ## Best practices * **Add only necessary flags**: Minimize long flag lists to reduce flaky or non-portable setups. * **Prefer `excludeBrowserFlags` over replacing defaults**: Remove only what conflicts with your workflow. * **Do not pass `--load-extension` manually**: Extension.js manages extension loading flags internally. * **Validate per browser family**: A flag working in Chromium may be invalid or ignored in Firefox. ## Next steps * Learn more about [browser preferences](/docs/browsers/browser-preferences). * Learn more about [browser profile](/docs/browsers/browser-profile). # Firefox and Gecko runtime preferences setup Source: https://extension.js.org/docs/browsers/browser-preferences Set Firefox and Gecko runtime preferences for development without editing extension source. Configure homepage, devtools, and notification defaults. Tune browser runtime behavior for development without changing your extension source. Get repeatable browser behavior during development (for example, homepage defaults, devtools settings, or notification behavior) without editing extension code. Extension.js reads `preferences` from `extension.config.*` and applies them in browser launch flows where supported. ## How it works Configure preferences in `extension.config.js` (or `.mjs` / `.cjs`) under: * `browser..preferences` * `commands.dev|start|preview.preferences` Command-level values can override browser defaults. ## Preference capabilities | Config key | What it does | | ------------------------------ | ------------------------------------------------------- | | `browser..preferences` | Sets default preferences for a specific browser target. | | `commands.dev.preferences` | Sets or overrides preferences for `dev` runs. | | `commands.start.preferences` | Sets or overrides preferences for `start` runs. | | `commands.preview.preferences` | Sets or overrides preferences for `preview` runs. | ## Firefox and Gecko-based behavior ### Example configuration ```js theme={null} export default { browser: { firefox: { preferences: { 'browser.startup.homepage': 'https://developer.mozilla.org', 'devtools.theme': 'dark', 'dom.webnotifications.enabled': false } } } } ``` In Firefox/Gecko flows, Extension.js writes a `user.js` file into the active profile (managed or explicit profile) and merges: * internal baseline preferences required for dev/runtime behavior * your custom `preferences` values (your values win on key conflicts) If you enable system profile mode (`EXTENSION_USE_SYSTEM_PROFILE=true`), Extension.js does not write a managed profile file. ## Chromium-family behavior Chromium-family launches (`chrome`, `edge`, `chromium`, `chromium-based`) primarily use **browser flags**. `preferences` can still exist in merged config objects, but flags and profile args control Chromium launch behavior, not a Firefox-style `user.js` preference file. For Chromium customization, prefer: * `browserFlags` * `excludeBrowserFlags` * `profile` / `persistProfile` ## Dark mode defaults Extension.js injects dark-mode defaults unless you already define those keys: * Chromium family: dark-mode launch flags * Firefox/Gecko family: dark-mode preference keys (for UI + content color scheme) Your explicit `preferences`/flags override these defaults. ## Interface example ```js theme={null} export default { commands: { dev: { browser: 'firefox', preferences: { 'devtools.theme': 'dark' } } } } ``` ### Example with custom profile ```js theme={null} export default { browser: { firefox: { profile: 'path/to/custom-profile', preferences: { 'browser.startup.homepage': 'https://example.com' } } } } ``` ## More detailed preference references For a comprehensive list of available Firefox preferences, explore the [Firefox source code](https://searchfox.org/mozilla-central/source/). Mozilla defines many defaults in `all.js` or `firefox.js`. ## Best practices * **Prefer browser-scoped preferences**: Keep Firefox/Gecko preference keys under browser-targeted config blocks. * **Use command overrides for temporary experiments**: Put short-lived preference tweaks in `commands.dev`. * **Keep profiles isolated**: Use separate profiles for reproducible debugging. * **Use flags for Chromium tuning**: Treat flags as the primary configuration surface on Chromium-family targets. ## Next steps * Learn more about [browser flags](/docs/browsers/browser-flags). * Learn more about [browser profile](/docs/browsers/browser-profile). # Browser profile management for isolated dev runs Source: https://extension.js.org/docs/browsers/browser-profile Manage browser state isolation during extension development with managed, persistent, or custom profile paths for Chrome and Firefox. Control browser state isolation during development with managed, persistent, or custom profiles. Keep browser sessions isolated or persistent based on your workflow. Extension.js launches browsers with profile-aware defaults so you can choose clean runs, reusable state, or an explicit local profile path. ## How it works Extension.js selects the profile mode in this order: 1. Explicit `profile` path (if provided) 2. Managed profile mode (default) * ephemeral by default * persistent when `persistProfile: true` 3. System profile mode when `EXTENSION_USE_SYSTEM_PROFILE=true` ## Profile capabilities | Config / option | What it does | | ----------------------------------- | --------------------------------------------------------------- | | `browser..profile` | Uses an explicit profile folder for that browser target. | | `commands.dev.profile` | Uses an explicit profile only for `dev`. | | `commands.start.profile` | Uses an explicit profile only for `start`. | | `commands.preview.profile` | Uses an explicit profile only for `preview`. | | `browser..persistProfile` | Reuses managed profile state between runs for a target. | | `commands..persistProfile` | Reuses managed profile state in a command-specific context. | | `--profile=/abs/path` | CLI override for explicit profile path. | | `EXTENSION_USE_SYSTEM_PROFILE=true` | Uses the OS/browser system profile instead of managed profiles. | ## Profile modes | Mode | How to enable | Typical use | | ----------------------- | ----------------------------------------------- | --------------------------------------------- | | Managed ephemeral | default | clean runs with isolated state | | Managed persistent | `persistProfile: true` | iterative debugging with stable browser state | | Explicit custom profile | `profile: "/abs/path"` or `--profile=/abs/path` | reuse an existing profile | | System profile | `EXTENSION_USE_SYSTEM_PROFILE=true` | launch with OS/browser default profile | Extension.js creates managed profiles under: * `dist/extension-js/profiles/-profile/<...>` Persistent mode uses: * `dist/extension-js/profiles/-profile/dev` ## Configure in `extension.config.*` ```js theme={null} export default { browser: { chrome: { // Use your own profile folder: profile: 'path/to/custom/profile' }, firefox: { // Keep a stable managed profile for repeated sessions: persistProfile: true } } } ``` You can also scope profile defaults by command: ```js theme={null} export default { commands: { dev: { persistProfile: true } } } ``` ## CLI usage Use an explicit profile path directly: ```bash theme={null} extension dev --browser=chrome --profile=/path/to/custom/profile ``` Works similarly with `start` and `preview`. ## Lifecycle notes * Extension.js creates ephemeral managed profiles for each run. * Extension.js reuses the persistent managed profile (`dev`) across runs. * Extension.js removes old managed profile folders automatically (controlled by `EXTENSION_TMP_PROFILE_MAX_AGE_HOURS`). ## Best practices * **Use managed ephemeral profiles for baseline testing**: Reduces hidden state and flaky reproductions. * **Use `persistProfile` for long-lived debug sessions**: Keep auth/session/devtools state between runs. * **Keep custom profiles per browser family**: Avoid cross-browser contamination. * **Use system profile mode intentionally**: Useful for reproduction, but less isolated than managed profiles. ## Next steps * Learn more about [browser preferences](/docs/browsers/browser-preferences). * Learn more about [browser flags](/docs/browsers/browser-flags). # Supported browsers for Extension.js development Source: https://extension.js.org/docs/browsers/browsers-available See which browsers Extension.js supports. Run and test extensions across Chrome, Edge, Firefox, and custom binaries from a single CLI workflow. Run and test your extension across major browsers from one CLI workflow. Validate one extension across Chrome, Edge, Firefox, and custom browser binaries from a single CLI. ## Choose the right target | Target | Use when | Example | | ---------------- | --------------------------------------------------- | --------------------------------------------------------------------------- | | `chromium` | Fast default local development | `extension dev --browser=chromium` | | `chrome` | Validating Chrome-specific behavior | `extension dev --browser=chrome` | | `edge` | Validating Edge distribution behavior | `extension dev --browser=edge` | | `firefox` | Validating Gecko compatibility and APIs | `extension dev --browser=firefox` | | `chrome,firefox` | Release checks across both major engines | `extension build --browser=chrome,firefox` | | `chromium-based` | Running custom Chromium binaries (Brave, Arc, etc.) | `extension dev --browser=chromium-based --chromium-binary=/path/to/browser` | | `gecko-based` | Running custom Firefox-family binaries | `extension dev --browser=gecko-based --gecko-binary=/path/to/browser` | ## How it works Use `--browser` to choose a target in `dev`, `start`, `preview`, and `build`. If you do not specify a browser, the CLI defaults to `chromium`. ## Supported browsers Named browser targets: | Browser | Usage | | ------------ | -------------------------------------- | | **Chrome** | `npx extension dev --browser=chrome` | | **Edge** | `npx extension dev --browser=edge` | | **Firefox** | `npx extension dev --browser=firefox` | | **Chromium** | `npx extension dev --browser=chromium` | Engine-based targets (custom binary required): | Engine target | Usage | | --------------------------------- | ------------------------------------------------------------------------------- | | **Chromium-based** | `npx extension dev --browser=chromium-based --chromium-binary=/path/to/browser` | | **Gecko-based** (`firefox-based`) | `npx extension dev --browser=gecko-based --gecko-binary=/path/to/browser` | Extension.js treats `firefox-based` as a Gecko engine target internally. ## Safari and other WebKit targets **Fully supported CLI targets** are the Chromium family (`chromium`, `chrome`, `edge`, and `chromium-based` with a custom binary) and Firefox (`firefox`, `gecko-based` / `firefox-based`). Extension.js documents and tests those `--browser` values end-to-end. **Safari** is not a supported `--browser` value in the CLI today. You can still author extensions with shared web code and add Safari-specific manifest or build steps outside this workflow. Do not assume that npm package keywords imply a managed Safari runner. ## Multi-browser selection You can run multiple named browsers in one command: ```bash theme={null} npx extension dev --browser=chrome,firefox ``` Use comma-separated values to run multiple named targets in sequence (for example `--browser=chrome,edge,firefox`). ## Constraints and behavior * `chromium-based` requires `--chromium-binary`. * `gecko-based` / `firefox-based` require `--gecko-binary`. * Engine-based targets route to the same Chromium/Firefox runners with engine-aware behavior. ## Best practices * **Use named browsers for daily iteration**: `chrome`, `edge`, and `firefox` are the fastest path for regular testing. * **Use engine-based mode intentionally**: Prefer `chromium-based` / `gecko-based` only when validating custom binaries. * **Keep profiles isolated per browser**: Reduce cross-browser state leakage while debugging. * **Pair with browser-specific fields**: Use browser-prefixed manifest keys for true behavior differences. ## Next steps * [Customize browser flags](/docs/browsers/browser-flags). * [Customize browser preferences](/docs/browsers/browser-preferences). * [Run other browsers from custom binaries](/docs/browsers/running-other-browsers). # Browser targeting guide Source: https://extension.js.org/docs/browsers/index Choose and configure browser targets for Extension.js development. Covers Chrome, Firefox, and Edge targeting, profiles, flags, and custom binaries. Run one extension codebase across Chrome, Edge, Firefox, and custom binaries with explicit browser targeting workflows. ## What to read first | Need | Read this | | ------------------------------ | --------------------------------------------------------------- | | Choose browser targets quickly | [Browsers available](/docs/browsers/browsers-available) | | Customize launch behavior | [Browser flags](/docs/browsers/browser-flags) | | Configure Firefox preferences | [Browser preferences](/docs/browsers/browser-preferences) | | Control profile isolation | [Browser profile](/docs/browsers/browser-profile) | | Run Brave or custom binaries | [Running other browsers](/docs/browsers/running-other-browsers) | ## Practical target strategy 1. Use named targets (`chrome`, `edge`, `firefox`) for daily checks. 2. Use comma-separated targets for release validation. 3. Use engine targets only when you need custom binaries. 4. Keep browser differences in browser-prefixed manifest fields. ## Next steps * Learn manifest filtering in [Cross-browser compatibility](/docs/features/cross-browser-compatibility). * Configure browser-specific keys in [Browser-specific manifest fields](/docs/features/browser-specific-fields). ## Video walkthrough # Running other browsers from binary path Source: https://extension.js.org/docs/browsers/running-other-browsers Test extensions in Brave, Vivaldi, Waterfox, or other Chromium and Gecko browsers by providing an explicit binary path to Extension.js. Run Chromium- and Gecko-family browsers beyond the default named targets by providing explicit binary paths. Test custom browser binaries (for example, Brave, Vivaldi, or Waterfox) from the same Extension.js workflow using binary flags and `extension.config.*` in `dev`, `start`, and `preview`. ## How it works Use one of these flags: * `--chromium-binary ` * `--gecko-binary ` (alias: `--firefox-binary `) These binary flags override named browser selection at runner level. ## Binary capabilities | Option / key | What it does | | --------------------------------- | ------------------------------------------------- | | `--chromium-binary ` | Launches a custom Chromium-family browser binary. | | `--gecko-binary ` | Launches a custom Gecko-family browser binary. | | `--firefox-binary ` | Alias of `--gecko-binary`. | | `browser..chromiumBinary` | Sets default custom Chromium binary in config. | | `browser..geckoBinary` | Sets default custom Gecko binary in config. | | `commands..chromiumBinary` | Sets command-specific custom Chromium binary. | | `commands..geckoBinary` | Sets command-specific custom Gecko binary. | ### CLI examples ```bash theme={null} extension dev --browser=chromium-based --chromium-binary="/path/to/brave" ``` ```bash theme={null} extension dev --browser=firefox --gecko-binary="/path/to/firefox-developer-edition" ``` You can also use them with `start` and `preview`. ## Configure in `extension.config.*` ```js theme={null} export default { browser: { 'chromium-based': { chromiumBinary: '/path/to/custom-chromium-browser' }, 'gecko-based': { geckoBinary: '/path/to/custom-gecko-browser' } } } ``` You can also place binary paths in command blocks: ```js theme={null} export default { commands: { dev: { chromiumBinary: '/path/to/custom-chromium-browser' }, preview: { geckoBinary: '/path/to/custom-gecko-browser' } } } ``` ## Target mapping behavior Binary hints map to engine targets: * `chromiumBinary` -> `chromium-based` * `geckoBinary` / `firefoxBinary` -> `gecko-based` If you provide both, Extension.js applies Chromium binary resolution first. ## Available browsers Common browsers you can run with binary flags: | **Browser Name** | **Type** | **CLI Flag** | **Official Website** | | ----------------------------- | ---------------------- | ------------------- | --------------------------------------------------------- | | **Brave** | Chromium-based browser | `--chromium-binary` | [brave.com](https://brave.com) | | **Opera** | Chromium-based browser | `--chromium-binary` | [opera.com](https://www.opera.com) | | **Vivaldi** | Chromium-based browser | `--chromium-binary` | [vivaldi.com](https://vivaldi.com) | | **Waterfox** | Gecko-based browser | `--gecko-binary` | [waterfox.net](https://www.waterfox.net) | | **Firefox Developer Edition** | Gecko-based browser | `--gecko-binary` | [firefox.com](https://www.mozilla.org/firefox/developer/) | ## Important constraints * `chromium-based` requires a valid `chromiumBinary` path. * `gecko-based` / `firefox-based` require a valid `geckoBinary` path. * Invalid paths fail fast with a clear CLI/runtime error. * `build` does not accept binary flags; binary-based launching applies to `dev`, `start`, and `preview`. ## Best practices * **Pair binaries with explicit browser target**: Use `--browser=chromium-based` or `--browser=gecko-based` for predictable intent. * **Use absolute paths**: Avoid shell-dependent path resolution issues. * **Version-pin in CI runners**: Keep browser binary paths deterministic for automated checks. * **Combine with profile/flags carefully**: Reuse the same profile and flag strategy used for named browser targets. ## Next steps * Learn more about [browser preferences](/docs/browsers/browser-preferences). * Learn more about [browser profile](/docs/browsers/browser-profile). # Build command for production extension artifacts Source: https://extension.js.org/docs/commands/build Create production-ready extension artifacts for Chrome, Edge, or Firefox with the Extension.js build command. Supports multi-browser output and zip packaging. Create production extension artifacts for one or more browser targets. `build` compiles your extension in production mode and writes output to `dist/`. For monorepo/submodule projects, see [Environment variables](/docs/features/environment-variables#how-it-works) for config-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 deterministic release 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 ```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] ``` ## Build output After running `build`, Extension.js generates optimized files for the selected browser targets and writes output to `dist/` with one subfolder per target. Each folder contains bundled JavaScript, CSS, HTML, and required runtime assets. **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` | Useful for engine-family workflows | | Multi-target | `chrome,firefox` | Comma-separated targets | ## Arguments and flags | Flag | Alias | What it does | Default | | ------------------------ | --------------- | ---------------------------------------------------------------------------------------------------- | ------------------------ | | `[path]` | - | Builds a local extension project. | `process.cwd()` | | `--browser ` | `-b` | Browser target (`chromium`, `chrome`, `edge`, `firefox`, engine aliases, or comma-separated values). | `chromium` | | `--polyfill [boolean]` | - | Enables `browser.*` API compatibility polyfill for Chromium targets. | `false` | | `--zip [boolean]` | - | Creates a packaged zip artifact. | `false` | | `--zip-source [boolean]` | - | Includes source files in zip output. | `false` | | `--zip-filename ` | - | Sets custom zip filename. | extension name + version | | `--silent [boolean]` | - | Suppresses build logs. | `false` | | `--extensions ` | - | Comma-separated companion extensions or store URLs. | unset | | `--install [boolean]` | - | Install project dependencies when missing. | command behavior default | | `--author` | `--author-mode` | Enable maintainer diagnostics. | disabled | ## Shared global options Also supports [global flags](/docs/workflows/global-flags). ## 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 | ## Examples ### Building with zip output and custom filename ```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 ``` In this example, the build targets Edge and Chrome, zips the output, and saves it as `my-extension.zip`. ### Building with polyfill support ```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 ``` In this example, the build targets Chrome and Firefox and includes polyfill support where relevant. ### Building source and artifact zip ```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 ``` ## 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/` folder before publishing. ## Next steps * 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 config env loading behavior in [Environment variables](/docs/features/environment-variables#how-it-works). * Review supported targets in [browsers available](/docs/browsers/browsers-available). # Create command to scaffold extension projects Source: https://extension.js.org/docs/commands/create Scaffold a new browser extension project from an official template with one CLI command. Choose React, Vue, Svelte, TypeScript, or vanilla JS. `create` scaffolds files, config, and starter scripts for the selected template and optionally installs dependencies. ## When to use `create` * Start a new extension from scratch. * Spin up multiple proof-of-concept ideas quickly. * Standardize onboarding for teammates with consistent template defaults. ## Create command capabilities | Capability | What it gives you | | -------------------- | ---------------------------------------------------------- | | Template scaffolding | Start from official templates with ready project structure | | Dependency bootstrap | Install required packages automatically after scaffold | | Path flexibility | Create by project name or explicit folder path | | Fast onboarding | Move from empty folder to runnable extension quickly | ## Usage ```bash npm theme={null} npx extension@latest create [options] ``` ```bash pnpm theme={null} pnpx extension@latest create [options] ``` ```bash yarn theme={null} yarn dlx extension@latest create [options] ``` ## Arguments and flags | Flag | Alias | What it does | Default | | --------------------- | ----- | ---------------------------------------- | ------------ | | `[path or name]` | - | Project folder/name to create. | required | | `--template ` | `-t` | Selects template slug. | `javascript` | | `--install [boolean]` | - | Installs dependencies after scaffolding. | `true` | When you want the default JavaScript starter, omit `--template` entirely. Add `--template=` only for another stack from the [official examples](https://github.com/extension-js/examples/tree/main/examples). The slug `init` is an alias for the same default starter. Only use a literal slug named `default` if that folder actually exists in the examples repository. ## Shared global options Also supports [global flags](/docs/workflows/global-flags). ## Example commands ```bash npm theme={null} npx extension@latest create my-extension --template=new-react ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=new-react ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=new-react ``` ```bash npm theme={null} npx extension@latest create my-extension --install=false ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --install=false ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --install=false ``` ## Available templates For the full, continuously updated list of templates, browse the [examples repository](https://github.com/extension-js/examples/tree/main/examples). Minimal starter. Use when you want a clean baseline. Typed starter with `tsconfig.json` preconfigured. React UI wired for content scripts and popup views. Vue UI with single-file component (SFC) support baked in. ## Best practices * Start from a template that matches your UI/runtime needs to reduce setup drift. * Keep the first run small, then add extra tooling after verifying baseline command flow. # Dev command for watch mode and live reload Source: https://extension.js.org/docs/commands/dev Develop browser extensions with watch mode, hot module replacement, automatic browser launch, and context-aware reload via the Extension.js dev command. Use `dev` for day-to-day browser extension development with watch mode, browser launch, and context-aware update behavior. `dev` runs the development pipeline and watches your project files. It applies update strategies based on what changed: hot module replacement (HMR), hard reload, or restart-required diagnostics. ## When to use `dev` * Building features and validating changes in real time. * Debugging extension behavior in one or more browser targets. * Running source-inspection workflows with `--source`. Use `build` for production artifacts, `start` for production build + launch, and `preview` to run existing build output only. If your extension lives inside a monorepo/submodule, review how `extension.config.*` loads env files (including workspace-root fallback): [Environment variables](/docs/features/environment-variables#how-it-works). ## Dev command capabilities | Capability | What it gives you | | ---------------------- | ----------------------------------------------------- | | Watch mode iteration | Tight edit -> rebuild -> validate loop while coding | | Browser-target control | Explicit cross-browser validation per command | | Profile-aware runs | Reliable fresh or persisted profiles by workflow need | | Source inspection mode | Structured output and diagnostics with source flags | ## Usage ```bash npm theme={null} extension dev [path-or-url] [options] ``` ```bash pnpm theme={null} extension dev [path-or-url] [options] ``` ```bash yarn theme={null} extension dev [path-or-url] [options] ``` If you omit the path, Extension.js uses `process.cwd()`. You can also pass a **GitHub tree URL** (for example, `https://github.com/user/repo/tree/main/path`). Extension.js downloads the repository and runs dev mode on the local copy. ## Arguments and flags | Flag | Alias | What it does | Default | | ------------------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------- | ------------------------ | | `[path or url]` | - | Extension path or remote URL. | `process.cwd()` | | `--browser ` | `-b` | Browser/engine target (`chromium`, `chrome`, `edge`, `firefox`, engine aliases, or comma-separated values). | `chromium` | | `--profile ` | - | Browser profile path or boolean profile mode. | fresh profile | | `--chromium-binary ` | - | [Custom Chromium-family binary path](/docs/browsers/running-other-browsers). | system default | | `--gecko-binary ` | `--firefox-binary` | [Custom Gecko-family binary path](/docs/browsers/running-other-browsers). | system default | | `--polyfill [boolean]` | - | Enable `browser.*` API compatibility polyfill for Chromium targets. | `false` | | `--starting-url ` | - | Starting URL in launched browser. | unset | | `--port ` | - | Dev server port. Use `0` for OS-assigned port. | `8080` | | `--host ` | - | Host to bind the dev server to. Use `0.0.0.0` for Docker/devcontainers. | `127.0.0.1` | | `--no-open` | - | Do not automatically open browser. | browser opens by default | | `--no-browser` | - | Do not launch browser. | browser launch enabled | | `--wait [boolean]` | - | Wait for `dist/extension-js//ready.json` and exit. | disabled | | `--wait-timeout ` | - | Timeout for `--wait` mode. | `60000` | | `--wait-format ` | - | Output format for wait results (`json` is machine-readable). | `pretty` | | `--extensions ` | - | Comma-separated companion extensions or store URLs. | unset | | `--install [boolean]` | - | Install project dependencies when missing. | command behavior default | | `--author` | `--author-mode` | Enable maintainer diagnostics. | disabled | ## Automation metadata (recommended for scripts/agents) When `dev` runs, Extension.js emits machine-readable metadata under: * `dist/extension-js//ready.json` * `dist/extension-js//events.ndjson` For automation (Playwright, continuous integration (CI), AI agents), prefer these files over terminal log parsing. Treat `ready.json` as the readiness contract: * `status: "starting"` while booting * `status: "ready"` when runtime is ready * `status: "error"` for startup/compile failures * `runId` uniquely identifies a runtime session * `startedAt` marks the runtime session start timestamp ### `--no-browser` and readiness synchronization `--no-browser` only disables browser launch. It does not block external runners until the compile finishes. For Playwright/CI/AI workflows: 1. run `extension dev --no-browser` as a long-lived process 2. run `extension dev --wait --browser=` as the readiness gate 3. launch external browser automation only after `status: "ready"` `--wait` is designed for a second process (or CI step) and exits non-zero on `error`/timeout. When `--wait` sees a stale `ready.json` from a dead process (`pid` no longer alive), it keeps waiting for a live producer. If you pass both `--wait` and `--no-browser` in the same command invocation, `--wait` takes precedence and the command runs in wait-only mode. ## Source inspection workflow Use source options only with `dev`. Source inspection opens a target URL in the browser and prints the full, live HTML of the page **after** your content scripts run. This helps you verify what your extension actually changes on a page. * `--source [url]` to inspect a page (defaults to `--starting-url` or `https://example.com`) * `--watch-source` / `--no-watch-source` to re-print HTML on each rebuild `start` and `preview` do not support source-inspection flags. ### Source flags | Flag | What it does | Default behavior | | ----------------------------------------------- | ------------------------------------------ | ---------------------------------- | | `--source [url]` | Enable source inspection flow. | disabled | | `--watch-source [boolean]` | Re-print source output on watched updates. | `true` when `--source` is enabled | | `--source-format ` | Output format for source inspection. | falls back to log format or `json` | | `--source-summary [boolean]` | Emit compact summary output. | disabled | | `--source-meta [boolean]` | Include page metadata. | enabled when source is on | | `--source-probe ` | Comma-separated CSS selectors to probe. | unset | | `--source-tree ` | Include compact extension tree details. | unset | | `--source-console [boolean]` | Include console summary. | disabled | | `--source-dom [boolean]` | Include DOM snapshots/diffs. | enabled when watch-source is on | | `--source-max-bytes ` | Cap output payload size. | unset | | `--source-redact ` | Redaction strategy for source output. | format-dependent | | `--source-include-shadow ` | Control Shadow DOM inclusion. | `open-only` when source is on | | `--source-diff [boolean]` | Include diff metadata on watch updates. | enabled when watch-source is on | ## Logging flags | Flag | What it does | Default | | ---------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------ | | `--logs ` | Minimum log level. | `off` | | `--log-context ` | Context filter (`background`, `content`, `page`, `sidebar`, `popup`, `options`, `devtools`). | `all` | | `--log-format ` | Logger output format. | `pretty` | | `--no-log-timestamps` | Disable timestamps in pretty mode. | timestamps enabled | | `--no-log-color` | Disable color in pretty mode. | color enabled | | `--log-url ` | Filter log events by URL substring/regex. | unset | | `--log-tab ` | Filter log events by tab ID. | unset | ## Shared global options Also supports [global flags](/docs/workflows/global-flags). ## Examples ### Running a local extension ```bash npm theme={null} extension dev ./my-extension ``` ```bash pnpm theme={null} extension dev ./my-extension ``` ```bash yarn theme={null} extension dev ./my-extension ``` ### Running a remote extension from GitHub Pass a GitHub tree URL as the argument to develop a remote extension locally: ```bash npm theme={null} extension dev https://github.com/nicedoc/browserless/tree/main/packages/screencast ``` ```bash pnpm theme={null} extension dev https://github.com/nicedoc/browserless/tree/main/packages/screencast ``` ```bash yarn theme={null} extension dev https://github.com/nicedoc/browserless/tree/main/packages/screencast ``` ### Inspecting page HTML with source mode ```bash npm theme={null} extension dev ./my-extension --source https://example.com ``` ```bash pnpm theme={null} extension dev ./my-extension --source https://example.com ``` ```bash yarn theme={null} extension dev ./my-extension --source https://example.com ``` ### Running in Firefox ```bash npm theme={null} extension dev ./my-extension --browser firefox ``` ```bash pnpm theme={null} extension dev ./my-extension --browser firefox ``` ```bash yarn theme={null} extension dev ./my-extension --browser firefox ``` ### Running in multiple browsers in sequence ```bash npm theme={null} extension dev ./my-extension --browser=chrome,firefox ``` ```bash pnpm theme={null} extension dev ./my-extension --browser=chrome,firefox ``` ```bash yarn theme={null} extension dev ./my-extension --browser=chrome,firefox ``` ### Running inside Docker or a devcontainer When you run inside Docker, devcontainers, or GitHub Codespaces, bind the dev server to `0.0.0.0` so the host machine can reach it: ```bash npm theme={null} extension dev ./my-extension --host 0.0.0.0 ``` ```bash pnpm theme={null} extension dev ./my-extension --host 0.0.0.0 ``` ```bash yarn theme={null} extension dev ./my-extension --host 0.0.0.0 ``` Combine with `--port 0` to let the OS choose an available port automatically: ```bash npm theme={null} extension dev ./my-extension --host 0.0.0.0 --port 0 ``` ```bash pnpm theme={null} extension dev ./my-extension --host 0.0.0.0 --port 0 ``` ```bash yarn theme={null} extension dev ./my-extension --host 0.0.0.0 --port 0 ``` ### Running in Brave as a custom binary ```bash npm theme={null} extension dev ./my-extension --chromium-binary /path/to/brave ``` ```bash pnpm theme={null} extension dev ./my-extension --chromium-binary /path/to/brave ``` ```bash yarn theme={null} extension dev ./my-extension --chromium-binary /path/to/brave ``` ## Best practices * **Browser compatibility:** Test your extension in different browsers to ensure compatibility with your target browsers. * **Polyfilling:** If Firefox or a Gecko-based browser is also a target, use `--polyfill`. This flag enables `browser.*` API compatibility in Chromium-based browsers. * **Source inspection workflows:** Use `dev` (not `start`/`preview`) when you need source inspection options. * **Automation reliability:** Treat `dev` as the watch-mode companion (`--no-browser` + `dev --wait`). Treat `start` as the production companion (`--no-browser` + `start --wait`). Use `--wait-format=json` for scripts and CI automation. ## Next steps * Build production artifacts with [`build`](/docs/commands/build). * Validate production launch flow with [`start`](/docs/commands/start). * Review browser targeting with [browser-specific manifest fields](/docs/features/browser-specific-fields). * Configure shared defaults in [`extension.config.js`](/docs/features/extension-configuration). * Review config env loading behavior in [Environment variables](/docs/features/environment-variables#how-it-works). # Extension.js command guide Source: https://extension.js.org/docs/commands/index Reference for every Extension.js CLI command including dev, build, start, preview, create, install, and uninstall with usage examples. Choose the right command for the current phase of extension development, from first scaffold to production validation. ## Command chooser | Goal | Command | Example | | ---------------------------------- | ----------- | --------------------------------------------------------------- | | Scaffold a new project | `create` | `npx extension@latest create my-extension --template=new-react` | | Develop with watch mode | `dev` | `extension dev --browser=firefox` | | Build production artifacts | `build` | `extension build --browser=chrome,firefox --zip` | | Build and launch production output | `start` | `extension start --browser=edge` | | Launch existing build output only | `preview` | `extension preview --browser=chrome` | | Install a managed browser runtime | `install` | `extension install chrome` | | Remove a managed browser runtime | `uninstall` | `extension uninstall chrome` | ## Typical developer flow 1. `create` to bootstrap. 2. `dev` for iterative coding and reload cycles. 3. `build` for release artifacts. 4. `preview` or `start` for production-like validation. 5. `install` or `uninstall` when you need to manage Extension.js browser runtimes explicitly. ## Command references * [Create](/docs/commands/create) * [Dev](/docs/commands/dev) * [Build](/docs/commands/build) * [Start](/docs/commands/start) * [Preview](/docs/commands/preview) * [Install](/docs/commands/install) * [Uninstall](/docs/commands/uninstall) ## Next steps * Apply cross-command controls in [Global flags](/docs/workflows/global-flags). * Keep defaults centralized in [Extension config](/docs/features/extension-configuration). ## Video walkthrough # Install command for managed browser runtimes Source: https://extension.js.org/docs/commands/install Add a managed browser runtime to the Extension.js cache for deterministic builds. Supports Chrome for Testing, Chromium, Firefox, and Edge. Use `install` to add a managed browser runtime into the Extension.js cache. This is most useful when you want a consistent browser binary for `dev`, `build`, `start`, or `preview`. It supports Chrome for Testing, Chromium, Firefox, and Edge. ## When to use `install` * You need a deterministic browser binary for CI, automation, or team-consistent local runs. * You want Chrome for Testing instead of relying on whatever system Chrome is installed. * You are setting up cross-browser testing with managed Firefox or Edge runtimes. ## Canonical usage For a single browser, use the positional form: ```bash npm theme={null} extension install ``` ```bash pnpm theme={null} extension install ``` ```bash yarn theme={null} extension install ``` Use `--browser` only when you need multiple targets, browser families, or `all`. ## Install command capabilities | Capability | What it gives you | | --------------------- | -------------------------------------------------------------------------- | | Managed browser cache | Stable install location under the Extension.js browser cache | | Deterministic runtime | Consistent binaries for repeatable local runs and automation | | Cross-browser setup | One command flow for Chrome, Chromium, Edge, and Firefox | | Path discovery | `--where` reveals the resolved cache root or browser-specific install path | ## Usage ```bash npm theme={null} extension install [browser-name] [options] ``` ```bash pnpm theme={null} extension install [browser-name] [options] ``` ```bash yarn theme={null} extension install [browser-name] [options] ``` ## Arguments and flags | Flag / argument | What it does | Default | | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------- | | `[browser-name]` | Install a single browser target such as `chrome`, `chromium`, `edge`, or `firefox` | `chromium` | | `--browser ` | Override the positional browser name and support multi-target installs | unset | | `--where` | Print the resolved managed cache root, or browser-specific install path | disabled | ## Examples ### Install Chrome for Testing ```bash theme={null} extension install chrome ``` ### Install multiple targets in one command ```bash theme={null} extension install --browser chrome,firefox ``` ### Show the managed install path for Chrome ```bash theme={null} extension install chrome --where ``` ## Cache locations By default, Extension.js stores managed browsers in a stable per-user cache: * macOS: `~/Library/Caches/extension.js/browsers` * Linux: `~/.cache/extension.js/browsers` or `$XDG_CACHE_HOME/extension.js/browsers` * Windows: `%LOCALAPPDATA%\extension.js\browsers` You can override the cache root with `EXT_BROWSERS_CACHE_DIR`. ## Best practices * **Use `install` in CI** to pin a deterministic browser binary instead of relying on whatever the runner provides. * **Prefer `chrome`** over `chromium` for Chrome for Testing — it matches stable Chrome behavior more closely. * **Use `--where`** to verify cache paths before scripting automation around managed browsers. * `install` only manages browsers inside the Extension.js cache. It does not modify system browser installs. ## Behavior notes * `chrome` installs Chrome for Testing rather than relying on the system Google Chrome app. * `edge` may require a privileged interactive session on Linux. ## Next steps * Remove managed browsers with [`uninstall`](/docs/commands/uninstall). * Use managed browsers with [`dev`](/docs/commands/dev) and [`start`](/docs/commands/start). * Learn about [running other browsers](/docs/browsers/running-other-browsers) with custom binary paths. # Preview command to launch built extensions Source: https://extension.js.org/docs/commands/preview Launch an already-built extension for production-like manual testing without recompiling. Load unpacked output and run the browser launcher flow. Launch an already-built extension output for production-like manual testing. `preview` does not compile your project. It loads an existing unpacked extension root and runs the browser launcher flow. ## When to use `preview` * Running existing build output without rebuilding. * Comparing packaged behavior across browser targets quickly. * Debugging runtime issues tied to production artifacts rather than dev/watch mode. ## Preview command capabilities | Capability | What it gives you | | ----------------------- | ------------------------------------------------------ | | Build-output validation | Test real production artifacts without rebuilding | | Browser-target checks | Run compiled output against selected browser targets | | Runner control | Launch or skip browser runner based on workflow needs | | Fast manual QA | Verify packaging-ready behavior quickly before release | > `preview` is run-only. It prefers `dist/` when that output exists, but it can also run from another unpacked extension folder if that folder already contains a `manifest.json`. ## Usage ```bash npm theme={null} extension preview [project-path] [options] ``` ```bash pnpm theme={null} extension preview [project-path] [options] ``` ```bash yarn theme={null} extension preview [project-path] [options] ``` If you omit the path, Extension.js uses `process.cwd()`. ## How `preview` chooses what to run `preview` checks these locations in order: 1. `dist/` for the selected browser target 2. the provided project path or current working folder 3. `--output-path` when you explicitly point to an unpacked extension root What matters is not whether a build happened in the same command. What matters is whether the chosen folder already contains an unpacked extension with a `manifest.json`. ## Arguments and flags | Flag | Alias | What it does | Default | | --------------------------- | ------------------ | ---------------------------------------------------------------------------- | ---------------------- | | `[path]` | - | Preview built extension from a project path. | `process.cwd()` | | `--browser ` | `-b` | Browser/engine target (`chromium`, `chrome`, `edge`, `firefox`, etc.). | `chromium` | | `--profile ` | - | Browser profile path or boolean profile mode. | fresh profile | | `--chromium-binary ` | - | [Custom Chromium-family binary path](/docs/browsers/running-other-browsers). | system default | | `--gecko-binary ` | `--firefox-binary` | [Custom Gecko-family binary path](/docs/browsers/running-other-browsers). | system default | | `--starting-url ` | - | Starting URL in launched browser. | unset | | `--no-browser` | - | Skip browser launch. | browser launch enabled | | `--port ` | - | Runner/devtools port when runner is enabled. Use `0` for OS-assigned port. | `8080` | | `--host ` | - | Host to bind the dev server to. Use `0.0.0.0` for Docker/devcontainers. | `127.0.0.1` | | `--extensions ` | - | Comma-separated companion extensions or store URLs. | unset | | `--author` | `--author-mode` | Enable maintainer diagnostics. | disabled | ## Automation metadata `preview` writes readiness metadata to: * `dist/extension-js//ready.json` For `--no-browser` flows, this provides deterministic command state: * `starting` while command initializes * `ready` when run-only validation is complete * `error` when required output is missing or startup fails * `runId` and `startedAt` for session correlation in scripts/agents `preview` does not provide a `--wait` gate flag. For `preview` automation, consume `ready.json` directly. ## Logging flags | Flag | What it does | Default | | ---------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------ | | `--logs ` | Minimum log level. | `off` | | `--log-context ` | Context filter (`background`, `content`, `page`, `sidebar`, `popup`, `options`, `devtools`). | `all` | | `--log-format ` | Logger output format. | `pretty` | | `--no-log-timestamps` | Disable timestamps in pretty mode. | timestamps enabled | | `--no-log-color` | Disable color in pretty mode. | color enabled | | `--log-url ` | Filter log events by URL substring/regex. | unset | | `--log-tab ` | Filter log events by tab ID. | unset | ## Source inspection flags (not supported in `preview`) `preview` accepts these flags in parsing, but exits with an error if you use them. Use `dev --source ...` instead. | Flag | Support in `preview` | | ----------------------------------------------- | ------------------------------------------- | | `--source [url]` | not supported (command exits with guidance) | | `--watch-source [boolean]` | not supported (command exits with guidance) | | `--source-format ` | not supported (command exits with guidance) | | `--source-summary [boolean]` | not supported (command exits with guidance) | | `--source-meta [boolean]` | not supported (command exits with guidance) | | `--source-probe ` | not supported (command exits with guidance) | | `--source-tree ` | not supported (command exits with guidance) | | `--source-console [boolean]` | not supported (command exits with guidance) | | `--source-dom [boolean]` | not supported (command exits with guidance) | | `--source-max-bytes ` | not supported (command exits with guidance) | | `--source-redact ` | not supported (command exits with guidance) | | `--source-include-shadow ` | not supported (command exits with guidance) | | `--source-diff [boolean]` | not supported (command exits with guidance) | ## Shared global options Also supports [global flags](/docs/workflows/global-flags). ## Examples ### Previewing a local extension ```bash npm theme={null} extension preview ./my-extension ``` ```bash pnpm theme={null} extension preview ./my-extension ``` ```bash yarn theme={null} extension preview ./my-extension ``` ### Previewing in Edge and Chrome ```bash npm theme={null} extension preview ./my-extension --browser=edge,chrome ``` ```bash pnpm theme={null} extension preview ./my-extension --browser=edge,chrome ``` ```bash yarn theme={null} extension preview ./my-extension --browser=edge,chrome ``` ### Preview without launching browser ```bash npm theme={null} extension preview ./my-extension --no-browser ``` ```bash pnpm theme={null} extension preview ./my-extension --no-browser ``` ```bash yarn theme={null} extension preview ./my-extension --no-browser ``` ## Behavior notes * `preview` is run-only and never compiles the project. * `preview` prefers existing build output (`dist/`) but can fall back to another unpacked extension root. * `preview` does not run watch mode or hot module replacement (HMR). * Source inspection flags are not supported in `preview`; use `dev` for that workflow. * For scripts/agents, rely on `ready.json` and avoid parsing terminal output. ## Best practices * Run `build` before `preview` when testing a fresh production artifact. * Pass `--output-path` when your unpacked extension lives outside the default project output. * Use `--browser` to verify behavior across targets before packaging. ## Next steps * Build and launch in one step with [`start`](/docs/commands/start). * Generate production artifacts with [`build`](/docs/commands/build). * Configure shared defaults in [`extension.config.js`](/docs/features/extension-configuration). * Review config env loading behavior in [Environment variables](/docs/features/environment-variables#how-it-works). # Start command for build-and-launch workflow Source: https://extension.js.org/docs/commands/start Run a production build and immediately launch the extension in the browser with one command. Combines build and preview into a single step. Use `start` when you want a production build and immediate browser launch in one command. The `start` command runs a production build first, then launches the built extension using the same flow as the `preview` command. ## When to use `start` * Manually validating production behavior right after compilation. * Reproducing runtime differences between watch mode and production output. * Running a production-like check locally without a separate `build` then `preview` step. ## Start command capabilities | Capability | What it gives you | | -------------------------- | --------------------------------------------------------- | | Build + launch workflow | Run production compile and browser launch in one step | | Target selection | Start directly in selected browser or engine target | | Runner control | Skip browser launch when you only need build verification | | Production-like validation | Check real compiled output instead of watch-mode state | ## How it differs from other commands * `dev`: development server + hot module replacement (HMR)/watch loop * `build`: production build only * `preview`: launch an existing built extension without building * `start`: `build` + `preview` in sequence ## Usage ```bash npm theme={null} extension start [path-or-url] [options] ``` ```bash pnpm theme={null} extension start [path-or-url] [options] ``` ```bash yarn theme={null} extension start [path-or-url] [options] ``` If you omit the path, the command uses the current working folder. ## Arguments and flags | Flag | Alias | What it does | Default | | ------------------------------ | ------------------ | -------------------------------------------------------------------------- | ------------------------ | | `[path or url]` | - | Extension path or remote URL. | `process.cwd()` | | `--browser ` | - | Browser/engine target. | `chromium` | | `--profile ` | - | Browser profile path or boolean profile mode. | fresh profile | | `--chromium-binary ` | - | Custom Chromium-family binary path. | system default | | `--gecko-binary ` | `--firefox-binary` | Custom Gecko-family binary path. | system default | | `--polyfill [boolean]` | - | Enable `browser.*` API compatibility polyfill for Chromium targets. | `true` | | `--starting-url ` | - | Starting URL in launched browser. | unset | | `--no-browser` | - | Build but skip browser launch. | browser launch enabled | | `--wait [boolean]` | - | Wait for `dist/extension-js//ready.json` and exit. | disabled | | `--wait-timeout ` | - | Timeout for `--wait` mode. | `60000` | | `--wait-format ` | - | Output format for wait results (`json` is machine-readable). | `pretty` | | `--port ` | - | Runner/devtools port when runner is enabled. Use `0` for OS-assigned port. | `8080` | | `--host ` | - | Host to bind the dev server to. Use `0.0.0.0` for Docker/devcontainers. | `127.0.0.1` | | `--extensions ` | - | Comma-separated companion extensions or store URLs. | unset | | `--install [boolean]` | - | Install project dependencies when missing. | command behavior default | | `--author` | `--author-mode` | Enable maintainer diagnostics. | disabled | ## Automation metadata `start` writes readiness metadata to: * `dist/extension-js//ready.json` This is useful for automation when using `--no-browser`: * wait for `status: "ready"` before launching external runners * handle `status: "error"` as a deterministic failure signal * use `runId` and `startedAt` to correlate a specific runtime session ### `--no-browser` and readiness synchronization `--no-browser` only disables browser launch. It does not block external runners until the production build finishes. For production-oriented Playwright, continuous integration (CI), and AI workflows: 1. run `extension start --no-browser` as the producer process 2. run `extension start --wait --browser=` as the readiness gate 3. launch external browser automation only after `status: "ready"` `--wait` exits non-zero on `error`/timeout and ignores stale contracts from dead processes (`pid` no longer alive). If you pass both `--wait` and `--no-browser` in the same command invocation, `--wait` takes precedence and the command runs in wait-only mode. ## Logging flags | Flag | What it does | Default | | ---------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------ | | `--logs ` | Minimum log level. | `off` | | `--log-context ` | Context filter (`background`, `content`, `page`, `sidebar`, `popup`, `options`, `devtools`). | `all` | | `--log-format ` | Logger output format. | `pretty` | | `--no-log-timestamps` | Disable timestamps in pretty mode. | timestamps enabled | | `--no-log-color` | Disable color in pretty mode. | color enabled | | `--log-url ` | Filter log events by URL substring/regex. | unset | | `--log-tab ` | Filter log events by tab ID. | unset | ## Source inspection flags (not supported in `start`) `start` accepts these flags in parsing, but exits with an error if you use them. Use `dev --source ...` instead. | Flag | Support in `start` | | ----------------------------------------------- | ------------------------------------------- | | `--source [url]` | not supported (command exits with guidance) | | `--watch-source [boolean]` | not supported (command exits with guidance) | | `--source-format ` | not supported (command exits with guidance) | | `--source-summary [boolean]` | not supported (command exits with guidance) | | `--source-meta [boolean]` | not supported (command exits with guidance) | | `--source-probe ` | not supported (command exits with guidance) | | `--source-tree ` | not supported (command exits with guidance) | | `--source-console [boolean]` | not supported (command exits with guidance) | | `--source-dom [boolean]` | not supported (command exits with guidance) | | `--source-max-bytes ` | not supported (command exits with guidance) | | `--source-redact ` | not supported (command exits with guidance) | | `--source-include-shadow ` | not supported (command exits with guidance) | | `--source-diff [boolean]` | not supported (command exits with guidance) | ## Shared global options Also supports [global flags](/docs/workflows/global-flags). ## Examples ### Start with default browser ```bash npm theme={null} extension start ``` ```bash pnpm theme={null} extension start ``` ```bash yarn theme={null} extension start ``` ### Start in Firefox ```bash npm theme={null} extension start --browser firefox ``` ```bash pnpm theme={null} extension start --browser firefox ``` ```bash yarn theme={null} extension start --browser firefox ``` ### Build and skip browser launch ```bash npm theme={null} extension start --no-browser ``` ```bash pnpm theme={null} extension start --no-browser ``` ```bash yarn theme={null} extension start --no-browser ``` ## Behavior notes * `start` does not run a dev server and does not provide hot module replacement (HMR) or watch mode. * `start` is production-mode oriented; use `dev` for iterative local development. * `start` does not support source inspection options. Use `dev --source ...` for that workflow. * For machine consumers, parse `dist/extension-js//ready.json` instead of terminal text. ## Next steps * Iterate quickly with [`dev`](/docs/commands/dev). * Launch existing build output with [`preview`](/docs/commands/preview). # Uninstall command to remove managed browsers Source: https://extension.js.org/docs/commands/uninstall Remove managed browser runtimes from the Extension.js cache. Only affects browsers installed by Extension.js, not your system browsers. Use `uninstall` to remove managed browser runtimes from the Extension.js cache. This only removes browsers that Extension.js installed in its managed cache root. It does not touch system Chrome, system Edge, or any browser installed outside that cache. ## When to use `uninstall` * You want to reclaim disk space from managed browsers you no longer need. * You are resetting a managed browser install to force a fresh download on next `install`. * You are cleaning up CI caches or switching browser targets. ## Canonical usage For a single browser, use the positional form: ```bash npm theme={null} extension uninstall ``` ```bash pnpm theme={null} extension uninstall ``` ```bash yarn theme={null} extension uninstall ``` Use `--all` to remove every managed browser target. ## Usage ```bash npm theme={null} extension uninstall [browser-name] [options] ``` ```bash pnpm theme={null} extension uninstall [browser-name] [options] ``` ```bash yarn theme={null} extension uninstall [browser-name] [options] ``` ## Arguments and flags | Flag / argument | What it does | Default | | ---------------- | ---------------------------------------------------------------------------------- | -------- | | `[browser-name]` | Remove a single managed browser such as `chrome`, `chromium`, `edge`, or `firefox` | unset | | `--all` | Remove all managed browser runtimes from the Extension.js cache | disabled | | `--where` | Print the resolved cache root, or browser-specific managed paths | disabled | ## Examples ### Remove managed Chrome for Testing ```bash theme={null} extension uninstall chrome ``` ### Remove all managed browsers ```bash theme={null} extension uninstall --all ``` ### Show the managed uninstall path for Firefox ```bash theme={null} extension uninstall firefox --where ``` ## Best practices * **Use `--all` in CI teardown** to clean up managed browsers after test runs. * **Use `--where` first** to confirm what will be removed before scripting bulk uninstalls. * `uninstall` is safe for system browsers — it only removes Extension.js-managed cache folders. ## Behavior notes * If you set `EXT_BROWSERS_CACHE_DIR`, uninstall uses that custom cache root. ## Next steps * Reinstall managed browsers with [`install`](/docs/commands/install). * Review browser targeting with [`dev`](/docs/commands/dev) and [`start`](/docs/commands/start). * Learn about [running other browsers](/docs/browsers/running-other-browsers) with custom binary paths. # Browser-specific manifest fields Source: https://extension.js.org/docs/features/browser-specific-fields Define Chrome, Firefox, and Edge manifest values inline with browser prefixes. Extension.js emits only the fields matching each target at build time. Avoid maintaining separate manifest files for every browser. Extension.js lets you define browser-specific values inline with prefixes, then emits only the fields that match the active target at compile time. ## Why this matters Browsers still differ in key manifest areas, like background configuration and vendor metadata. Prefixed fields let you keep one source `manifest.json` while producing browser-correct output for Chromium and Firefox families. ## How it works Extension.js scans manifest keys and resolves prefixed entries for the selected browser: * Chromium family targets (`chrome`, `edge`, `chromium-based`) resolve: `chromium:`, `chrome:`, `edge:` * Gecko family targets (`firefox`, `gecko-based`) resolve: `firefox:`, `gecko:` When a prefixed key matches the active target, Extension.js rewrites it to the unprefixed key in the emitted manifest. ### For Chromium-based browsers (Chrome, Edge, ...) ```json theme={null} { "chromium:background": { "service_worker": "sw.js" } } ``` ### For Firefox ```json theme={null} { "firefox:background": { "scripts": ["sw.js"] } } ``` You can also target vendor-specific variants inside the same browser family: ```json theme={null} { "chrome:action": { "default_title": "Chrome variant" }, "edge:action": { "default_title": "Edge variant" } } ``` This makes `service_worker` available only for Chromium-family outputs while keeping `background.scripts` for Firefox outputs. Supported prefix map: | Prefix | Included for target browser | | ----------- | ----------------------------------------- | | `chromium:` | `chrome`, `edge`, `chromium-based` | | `chrome:` | `chrome`, `chromium-based` | | `edge:` | `edge`, `chromium-based` | | `firefox:` | `firefox`, `gecko-based`, `firefox-based` | | `gecko:` | `firefox`, `gecko-based`, `firefox-based` | This works for any manifest field at any level, including `permissions`, `content_scripts`, and `background`. ## Best practices * **Keep shared defaults unprefixed**: Put common fields in regular manifest keys, then prefix only browser-specific differences. * **Prefix only when behavior diverges**: Use browser prefixes when runtime requirements differ. * **Build per target in CI (continuous integration)**: Generate and verify each browser output (`dist/`) to catch compatibility regressions early. * **Validate with MDN**: Use [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions) to confirm support before adding browser-only settings. ## Next steps * Learn more about the [browsers available](/docs/browsers/browsers-available). * Learn more about [cross-browser compatibility](/docs/features/cross-browser-compatibility). # Cross-browser compatibility for Chrome and Firefox Source: https://extension.js.org/docs/features/cross-browser-compatibility Ship one extension to Chrome, Edge, and Firefox from a single project. Extension.js handles browser-specific build output for Chromium and Gecko targets. Extension.js handles browser-specific setup during development and build, so you can produce runtime-correct outputs for Chromium and Gecko targets from one project. Use one `manifest.json`, target specific browsers or custom binaries, and enable polyfills when needed for `browser.*` API compatibility in Chromium-family targets. ## Template example ### `action` action template screenshot Try cross-browser compatibility with an action popup that runs in Chrome, Firefox, and Edge. ```bash npm theme={null} npx extension@latest create my-extension --template=action ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=action ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=action ``` Repository: [extension-js/examples/action](https://github.com/extension-js/examples/tree/main/examples/action) ## How it works ### 1) Choose a browser target Choose where you want to run your extension. Use `--browser` for common targets, or pass a custom browser binary. ```bash npm theme={null} extension dev --browser firefox ``` ```bash pnpm theme={null} extension dev --browser firefox ``` ```bash yarn theme={null} extension dev --browser firefox ``` ```bash npm theme={null} extension dev --chromium-binary /Applications/Brave\\ Browser.app/Contents/MacOS/Brave\\ Browser ``` ```bash pnpm theme={null} extension dev --chromium-binary /Applications/Brave\\ Browser.app/Contents/MacOS/Brave\\ Browser ``` ```bash yarn theme={null} extension dev --chromium-binary /Applications/Brave\\ Browser.app/Contents/MacOS/Brave\\ Browser ``` Use a binary path that matches your OS: * **macOS**: `/Applications/Brave Browser.app/Contents/MacOS/Brave Browser` * **Linux**: `/usr/bin/brave-browser` (or another Chromium-based browser binary) * **Windows**: `"C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"` When you pass a binary, Extension.js maps it to a browser engine target: * `chromium-based` for `--chromium-binary` * `gecko-based` for `--gecko-binary` ### 2) Compile with browser-specific manifest filtering During compilation, Extension.js includes only the manifest fields for your selected browser. For example: ```json theme={null} { "chromium:background": { "service_worker": "sw.js" }, "firefox:background": { "scripts": ["sw.js"] } } ``` For Chromium-family targets, Extension.js uses prefixes like `chromium:`, `chrome:`, and `edge:`.\ For Firefox-family targets, Extension.js uses `firefox:` and `gecko:`. > Note: Browser-prefixed manifest fields are covered in detail in [Browser-specific manifest fields](/docs/features/browser-specific-fields). ### 3) Output per browser target Extension.js writes each target to its own build folder: * `dist/chrome` * `dist/edge` * `dist/firefox` * `dist/chromium-based` (custom Chromium engines) * `dist/gecko-based` (custom Gecko engines) This keeps your builds organized and easier to ship in CI. ### 4) Optional `browser.*` polyfill for Chromium targets If your code uses `browser.*`, enable `--polyfill` for Chromium-family targets: ```bash npm theme={null} extension build --browser chrome --polyfill ``` ```bash pnpm theme={null} extension build --browser chrome --polyfill ``` ```bash yarn theme={null} extension build --browser chrome --polyfill ``` When enabled, Extension.js uses `webextension-polyfill` for non-Firefox targets.\ Extension.js skips this step for Firefox because Firefox already supports `browser.*` natively. ## Best practices * **Keep one codebase**: Put browser differences in prefixed manifest fields when possible. * **Build one target at a time**: Generate dedicated artifacts (`dist/`) for each browser in CI (continuous integration). * **Use `--polyfill` when needed**: Enable it only when your code depends on `browser.*` in Chromium-family targets. * **Check API support early**: Use [MDN WebExtensions docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions) before relying on browser-specific APIs. ## Next steps * Learn more about the [browsers available](/docs/browsers/browsers-available). * Learn more about [browser-specific manifest fields](/docs/features/browser-specific-fields). # Environment variables for cross-browser configs Source: https://extension.js.org/docs/features/environment-variables Manage API hosts, keys, and browser-specific values across environments. Extension.js loads env files for compiled bundles and for the config layer. Use one extension codebase across browsers and environments without hardcoding values. Manage API hosts, keys, and browser-specific values cleanly across environments. Extension.js uses **two different** environment loading paths. One handles **compiled extension bundles** (browser/mode aware). The other runs while loading **`extension.config.*`** in Node. Both paths matter, depending on where you read your variables. ## Template examples ### `new-env` new-env template screenshot See environment variables in action with a new-tab extension that reads `EXTENSION_PUBLIC_*` values. ```bash npm theme={null} npx extension@latest create my-extension --template=new-env ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=new-env ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=new-env ``` Repository: [extension-js/examples/new-env](https://github.com/extension-js/examples/tree/main/examples/new-env) ### `content-env` content-env template screenshot Use environment variables inside content scripts injected into web pages. ```bash npm theme={null} npx extension@latest create my-extension --template=content-env ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=content-env ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=content-env ``` Repository: [extension-js/examples/content-env](https://github.com/extension-js/examples/tree/main/examples/content-env) ## How it works ### Extension bundles (compile time) When building your extension, the compiler picks **one** env file from your extension package folder by first match in this order: 1. `.env.[browser].[mode]` (for example `.env.chrome.development`) 2. `.env.[browser]` 3. `.env.[mode]` 4. `.env.local` 5. `.env` 6. `.env.example` Extension.js **always merges** `.env.defaults` first when present, then the selected file’s variables, then **system** `process.env` (highest precedence for overlapping keys). > Note: Selection is a **single** env file from the list above (plus `.env.defaults`), not a full cascade through every file. If no matching file exists next to the project, Extension.js repeats the same search from the **nearest workspace root**. The workspace root is the folder containing `pnpm-workspace.yaml`, found by walking up from the project. This lets monorepo extensions share root-level env files for **bundle** injection. **Monorepo constraint:** Workspace fallback only runs when a `pnpm-workspace.yaml` marker exists on an ancestor. Pure npm or Yarn workspaces that rely only on `package.json` `"workspaces"` do **not** get this automatic root lookup. Keep env files beside the extension package, or add a `pnpm-workspace.yaml` at the repo root. ### `extension.config.*` (Node, before your config runs) `extension.config.js` / `.mjs` / `.cjs` runs in Node. Before Extension.js evaluates the file, it preloads a **small** set of files into `process.env` so the config can read them: 1. `.env.defaults` (merged when present) 2. Then the **first** file that exists among: `.env.development`, `.env.local`, `.env` Extension.js does **not** use browser-scoped files such as `.env.chrome` or `.env.chrome.development` for this preload step. Instead, use plain `process.env` set by your shell or CI (continuous integration) pipeline. You can also rely on bundle-time env (described above) for injecting `EXTENSION_PUBLIC_*` values into extension code. **Monorepo:** If none of those files exist in the extension package folder, the same preload runs from the nearest folder containing `pnpm-workspace.yaml` (same constraint as above). This split exists because config loading is browser-agnostic at file-read time, while the bundler knows the active browser and mode. ## Built-in environment variables Extension.js injects built-in variables at compile time, so browser and mode are always available in your extension code. | Variable Name | Description | | -------------------------- | ----------------------------------------------------------------------------------------- | | `EXTENSION_PUBLIC_BROWSER` | The current browser target for your extension (for example, `chrome`, `firefox`, `edge`). | | `EXTENSION_PUBLIC_MODE` | The mode in which your extension is running, such as `development` or `production`. | | `EXTENSION_BROWSER` | Browser target (non-legacy alias). | | `EXTENSION_MODE` | Build mode (non-legacy alias). | | `BROWSER` | Short browser alias. | | `MODE` | Short mode alias. | | `NODE_ENV` | Node environment aligned to compiler mode (`development` / `production`). | All built-ins above are available through both `process.env.*` and `import.meta.env.*`. ## Environment variable inventory ### Public/runtime variables (user-defined) | Variable pattern | Purpose | Available in JS runtime | Notes | | -------------------- | -------------------------------------------- | --------------------------------------- | ------------------------ | | `EXTENSION_PUBLIC_*` | Expose user-defined values to extension code | Yes (`process.env` + `import.meta.env`) | Safe-to-ship values only | ### Static placeholder variables | Variable pattern | Purpose | Available in JS runtime | Notes | | -------------------------------------------------- | --------------------------------------------------- | ----------------------- | --------------------------------------- | | `$EXTENSION_*` tokens in emitted `.html` / `.json` | Build-time placeholder replacement in static assets | Not as JS vars | Avoid using secrets in static templates | ### Built-in/alias variables | Variable | Type | Notes | | -------------------------- | -------------- | ------------------ | | `EXTENSION_PUBLIC_BROWSER` | built-in | Browser target | | `EXTENSION_PUBLIC_MODE` | built-in | Build mode | | `EXTENSION_BROWSER` | built-in alias | Browser target | | `EXTENSION_MODE` | built-in alias | Build mode | | `BROWSER` | built-in alias | Short browser name | | `MODE` | built-in alias | Short mode name | | `NODE_ENV` | built-in | Compiler mode | ### CLI and dev-server operational variables | Variable | Purpose | Typical usage | | -------------------------- | -------------------------------------------- | ------------------------------------- | | `EXTENSION_AUTO_EXIT_MS` | Auto-exit dev process after N ms | CI hard-stop control | | `EXTENSION_FORCE_KILL_MS` | Force-kill timeout fallback | CI cleanup resilience | | `EXTENSION_VERBOSE` | Verbose diagnostics in selected flows | Debugging CLI behavior | | `EXTENSION_AUTHOR_MODE` | Maintainer/author diagnostics mode | Internal diagnostics and tooling | | `EXTENSION_CLI_NO_BROWSER` | Disable browser launch from CLI (`1` to set) | Equivalent to `--no-browser` flag | | `EXTENSION_DEV_NO_BROWSER` | Disable browser launch in dev server | Monorepo watch without browser spawns | | `EXTENSION_DEV_DRY_RUN` | Skip dev server startup (return early) | Smoke-testing CLI wiring | | `EXT_BROWSERS_CACHE_DIR` | Override managed browser cache folder | Custom CI cache paths | ### Telemetry control variables | Variable | Purpose | Default | | --------------------------------- | ----------------------------------------------- | ------- | | `EXTENSION_TELEMETRY_DISABLED` | Disable telemetry entirely (`1` to set) | unset | | `EXTENSION_TELEMETRY` | Back-compat disable (`0` to disable) | unset | | `EXTENSION_TELEMETRY_SAMPLE_RATE` | Sampling rate for `command_executed` (0.0–1.0) | `0.2` | | `EXTENSION_TELEMETRY_MAX_EVENTS` | Maximum events emitted per CLI process | `3` | | `EXTENSION_TELEMETRY_DEBOUNCE_MS` | Dedup window for identical event tuples (ms) | `60000` | | `EXTENSION_TELEMETRY_DEBUG` | Print telemetry payloads to stderr (`1` to set) | unset | See [Telemetry and privacy](/docs/features/telemetry-and-privacy) for the full opt-out contract. ### Browser transport tuning variables These variables override internal Chrome DevTools Protocol (CDP) and Remote Debugging Protocol (RDP) timeouts. Useful for slow CI (continuous integration) environments, Docker containers, or debugging flaky browser connections. | Variable | Purpose | Default | | ------------------------------------- | --------------------------------------- | ------- | | `EXTENSION_CDP_COMMAND_TIMEOUT_MS` | CDP sendCommand timeout (ms) | `12000` | | `EXTENSION_CDP_HTTP_TIMEOUT_MS` | CDP HTTP `/json` discovery timeout (ms) | `1200` | | `EXTENSION_CDP_HEARTBEAT_INTERVAL_MS` | CDP WebSocket heartbeat interval (ms) | `30000` | | `EXTENSION_RDP_EVAL_TIMEOUT_MS` | Firefox RDP evaluation timeout (ms) | `8000` | | `EXTENSION_RDP_MAX_RETRIES` | Firefox RDP connect retry count | `150` | | `EXTENSION_RDP_RETRY_INTERVAL_MS` | Firefox RDP connect retry interval (ms) | `1000` | ## Browser-specific environment files The rules below apply to **compile-time / bundle** env selection (see [Extension bundles](#extension-bundles-compile-time) above). They do **not** apply to the narrow `extension.config.*` preload in Node. Need different values per browser? Extension.js supports browser-scoped env files such as `.env.chrome` and `.env.firefox`. You can also combine browser and mode for a single build variant: * `.env.chrome.development`: Extension.js applies this only when running the extension in Chrome during development mode. * `.env.firefox.production`: Extension.js applies this only when building the extension for Firefox in production mode. Priority order is: * `.env.[browser].[mode]` * `.env.[browser]` * `.env.[mode]` * `.env.local` * `.env` * `.env.example` ### Example files ```ini theme={null} # .env.chrome.development EXTENSION_PUBLIC_API_URL=https://api-dev.chrome.com ``` ```ini theme={null} # .env.firefox.production EXTENSION_PUBLIC_API_URL=https://api.firefox.com ``` ## Custom environment variables You can define custom variables in env files at project root.\ Extension.js only injects variables prefixed with `EXTENSION_PUBLIC_` into JavaScript bundles (`process.env` / `import.meta.env`). ```ini theme={null} # .env EXTENSION_PUBLIC_API_KEY=your_api_key_here EXTENSION_PUBLIC_SITE_URL=https://example.com PRIVATE_KEY=abc123 # Not injected into JS bundles ``` **Important:** Extension.js does not inject variables without `EXTENSION_PUBLIC_` into JS bundles.\ However, placeholders in emitted `.json`/`.html` files can resolve `$EXTENSION_*` tokens, so avoid referencing secrets in static asset templates. ## Using environment variables You can use environment variables in `manifest.json`, locales, HTML, and JavaScript/TypeScript files. ### 1. In `manifest.json` `manifest.json` does not natively support environment variables, but Extension.js replaces supported placeholders during build. For example: ```json theme={null} { "name": "My Extension", "version": "1.0", "description": "This extension is connected to $EXTENSION_PUBLIC_API_KEY", "background": { "service_worker": "service_worker.js" } } ``` During compilation, Extension.js replaces `$EXTENSION_PUBLIC_API_KEY` with the resolved env value. ### 2. In locale files You can also use placeholders in locale files when values should change by environment. For example: ```json theme={null} { "appName": { "message": "My Extension - $EXTENSION_PUBLIC_SITE_URL" }, "appDescription": { "message": "Connected to API at $EXTENSION_PUBLIC_API_KEY" } } ``` When Extension.js emits assets, it replaces placeholders such as `$EXTENSION_PUBLIC_SITE_URL` with resolved values. ### 3. In HTML files You can also use placeholders in static HTML files (for example, under `pages/`): ```html theme={null} My Extension

Welcome to My Extension

API Key: $EXTENSION_PUBLIC_API_KEY

``` During compilation, Extension.js replaces `$EXTENSION_PUBLIC_API_KEY` in the output HTML. ### 4. In JSX components In React/JSX/TS files, read env values with `process.env`: ```jsx theme={null} const ApiInfo = () => { const apiUrl = process.env.EXTENSION_PUBLIC_SITE_URL const apiKey = process.env.EXTENSION_PUBLIC_API_KEY return (

API Information

URL: {apiUrl}

Key: {apiKey}

) } export default ApiInfo ``` Extension.js inlines these values at compile time, and they can vary by browser/mode. ## `import.meta` support For ECMAScript Module (ESM) workflows, Extension.js also supports `import.meta.env`: ```js title="service_worker.mjs" theme={null} const apiUrl = import.meta.env.EXTENSION_PUBLIC_API_URL console.log(`API URL for the current environment: ${apiUrl}`) ``` `import.meta.env` and `process.env` have parity for injected env keys. ## Best practices * **Expose only what must ship:** Prefix only client-safe keys with `EXTENSION_PUBLIC_`. * **Use `.env.defaults` for shared defaults:** Keep predictable team defaults while allowing local/system overrides. * **Keep secrets out of static placeholders:** Avoid putting secret `$EXTENSION_*` tokens in HTML/JSON templates. * **Version control hygiene:** Commit `.env.example` and ignore real env files (`.env`, `.env.local`, browser/mode variants). ## Next steps * Review browser targeting in [browsers available](/docs/browsers/browsers-available). * Configure shared defaults in [`extension.config.js`](/docs/features/extension-configuration). # Extension config (extension.config.js) Source: https://extension.js.org/docs/features/extension-configuration Set browser defaults, command behavior, and Rspack bundler options in one config file. Applies to dev, start, preview, and build commands. Use one config file to set browser defaults, command behavior, and bundler customization. Share browser defaults, command options, and build settings across your team without repeating CLI flags. Extension.js reads `extension.config.js` (or `.mjs` / `.cjs`) from your project root. It applies settings to `dev`, `start`, `preview`, `build`, and bundler config. ## 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` | Default package transpile allowlist (monorepo/workspace support). | | `config` | Hook to extend/override the generated Rspack config. | ### Environment loading for config files `extension.config.*` runs in Node and should read values from `process.env.*`. * Extension.js preloads env files before evaluating `extension.config.*`. * 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 folder containing `pnpm-workspace.yaml`) and applies the same order there. * Prefer built-in env preload over importing `dotenv` in your config 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. | | `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. | ### 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', source: 'https://example.com' }, preview: { browser: 'edge', noBrowser: false }, build: { browser: 'chrome', zip: true, zipSource: true, zipFilename: 'my-extension.zip', transpilePackages: ['@workspace/ui', '@workspace/icons'] } } } ``` Notes: * Extension.js merges top-level `extensions` and `transpilePackages` into command defaults. * Per-command values override top-level values. * The `start` command runs `build` then `preview` internally. Extension.js applies settings from `commands.start`. You can also put build-specific settings in `commands.build`. For finer control over browser-launch settings, use `commands.preview`. #### Command capabilities (shared) | Key | Applies to | What it does | | ------------------- | ---------------------------------- | ------------------------------------------------------- | | `browser` | `dev`, `start`, `preview`, `build` | Sets browser or browser-family target. | | `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. | | `startingUrl` | `dev`, `start`, `preview` | Opens a specific URL on browser launch. | | `port` | `dev`, `start`, `preview` | Sets runner/devtools port. | | `noBrowser` | `dev`, `start`, `preview` | Disables browser launch. | | `extensions` | `dev`, `start`, `preview`, `build` | Loads companion extensions (or extension list). | | `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` | Sets a custom zip output filename. | | `silent` | Reduces build log output. | #### `dev` command capabilities | Key | What it does | | --------------------- | -------------------------------------------------------- | | `noOpen` | Runs dev server without auto-opening browser. | | `source` | Enables source-inspection flow from a remote source URL. | | `watchSource` | Re-runs source inspection when watched updates occur. | | `sourceFormat` | Sets source output format (`pretty`, `json`, `ndjson`). | | `sourceSummary` | Emits compact source summary output. | | `sourceMeta` | Includes page metadata in source output. | | `sourceProbe` | Probes source output with specific CSS selectors. | | `sourceTree` | Controls compact extension tree output. | | `sourceConsole` | Includes console summary in source output. | | `sourceDom` | Includes DOM snapshots/diffs in source output. | | `sourceMaxBytes` | Caps source output payload size. | | `sourceRedact` | Controls source redaction strategy. | | `sourceIncludeShadow` | Controls Shadow DOM inclusion strategy. | | `sourceDiff` | Includes diff metadata on source watch updates. | #### Logging capabilities | Key | Applies to | What it does | | ------------ | ------------------------- | -------------------------------------------------------- | | `logs` | `dev`, `start`, `preview` | Sets minimum log level (`off` to `all`). | | `logContext` | `dev`, `start`, `preview` | Filters logs by context (`background`, `content`, etc.). | | `logFormat` | `dev`, `start`, `preview` | Sets output format (`pretty`, `json`, `ndjson`). | | `logUrl` | `dev`, `start`, `preview` | Filters logs by URL pattern. | | `logTab` | `dev`, `start`, `preview` | Filters logs by tab ID. | ### Rspack configuration Need advanced bundler customization? Use `config` to patch the generated rspack config: ```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. ## 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, 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 cannot be expressed through first-class Extension.js options. ## Next steps * Learn more about [browsers available](/docs/browsers/browsers-available). * Learn more about [Rspack configuration](/docs/features/rspack-configuration). # Extension.js features overview Source: https://extension.js.org/docs/features/index Explore core Extension.js capabilities including cross-browser builds, config files, HMR reload behavior, path resolution, and telemetry controls. Understand the core platform capabilities that make Extension.js practical for real-world browser extension development. ## Feature chooser | Goal | Start here | | -------------------------------------------- | ----------------------------------------------------------------------------- | | Ship one codebase to multiple browsers | [Cross-browser compatibility](/docs/features/cross-browser-compatibility) | | Handle browser-specific manifest differences | [Browser-specific manifest fields](/docs/features/browser-specific-fields) | | Keep non-manifest assets organized | [Special folders](/docs/features/special-folders) | | Manage environment-specific behavior | [Environment variables](/docs/features/environment-variables) | | Centralize build/runtime defaults | [Extension configuration](/docs/features/extension-configuration) | | Extend bundler behavior safely | [Rspack configuration](/docs/features/rspack-configuration) | | Understand update behavior in dev mode | [Page reload and hot module replacement (HMR)](/docs/features/reload-and-hmr) | | Avoid path drift between source and output | [Path resolution](/docs/features/path-resolution) | | Build and package for target matrices | [Multi-platform builds](/docs/features/multi-platform-builds) | | Review the telemetry privacy contract | [Telemetry and privacy](/docs/features/telemetry-and-privacy) | ## Recommended progression 1. Validate browser output guarantees (`cross-browser-compatibility`). 2. Lock project defaults (`extension-configuration`). 3. Add advanced behavior (`rspack-configuration`, `environment-variables`). 4. Optimize release flows (`multi-platform-builds`). ## Video walkthrough ## Next steps * Apply these capabilities in [Workflows](/docs/workflows/index). * Choose the right execution command in [Commands reference](/docs/commands/index). * Review the privacy contract in [Telemetry and privacy](/docs/features/telemetry-and-privacy). # Multi-platform builds for Chrome, Edge, and Firefox Source: https://extension.js.org/docs/features/multi-platform-builds Build and package the same extension for Chrome, Edge, and Firefox without separate build scripts. Output artifacts to dist and generate zip packages. Build and package the same extension for multiple browsers with predictable outputs. Ship the same extension to Chrome, Edge, and Firefox without maintaining separate build scripts. Extension.js can run production builds per target, write artifacts to `dist/`, and optionally generate zip packages for distribution. ## How it works Run a production build: ```bash npm theme={null} npx extension build ``` ```bash pnpm theme={null} pnpx extension build ``` ```bash yarn theme={null} yarn dlx extension build ``` The default browser target is `chromium` unless you override it. ## Browser selection You can target a specific browser/engine: ```bash npm theme={null} npx extension build --browser=chrome ``` ```bash pnpm theme={null} pnpx extension build --browser=chrome ``` ```bash yarn theme={null} yarn dlx extension build --browser=chrome ``` ```bash npm theme={null} npx extension build --browser=firefox ``` ```bash pnpm theme={null} pnpx extension build --browser=firefox ``` ```bash yarn theme={null} yarn dlx extension build --browser=firefox ``` Supported values include: * `chrome` * `edge` * `firefox` * `chromium` * `chromium-based` * `gecko-based` / `firefox-based` (aliases) You can also run a build matrix in one command: ```bash npm theme={null} npx extension build --browser=chrome,edge,firefox ``` ```bash pnpm theme={null} pnpx extension build --browser=chrome,edge,firefox ``` ```bash yarn theme={null} yarn dlx extension build --browser=chrome,edge,firefox ``` This builds sequentially for `chrome`, `edge`, and `firefox`. ## Output layout Each target writes to its own folder: * `dist/chrome` * `dist/edge` * `dist/firefox` * `dist/chromium` * `dist/chromium-based` * `dist/gecko-based` ### Build capabilities | Option | What it does | | ----------------------- | ------------------------------------------------------------------------------------ | | `--browser=` | Builds for a specific browser or engine family. | | `--zip` | Creates a distribution zip per target output. | | `--zip-filename=` | Overrides the default zip file name. | | `--zip-source` | Creates a source archive alongside output. | | `--polyfill` | Enables compatibility behavior for `browser.*` API usage in Chromium-family targets. | ## Generating a zip file Generate a distribution zip from each target output with `--zip`: ```bash npm theme={null} npx extension build --zip ``` ```bash pnpm theme={null} pnpx extension build --zip ``` ```bash yarn theme={null} yarn dlx extension build --zip ``` By default, the zip name uses sanitized manifest `name` + `version`, for example: * `my-extension-1.0.0.zip` Customize filename: ```bash npm theme={null} npx extension build --zip --zip-filename=my-release ``` ```bash pnpm theme={null} pnpx extension build --zip --zip-filename=my-release ``` ```bash yarn theme={null} yarn dlx extension build --zip --zip-filename=my-release ``` This creates `my-release.zip` inside the target `dist/` folder. ## Include source archive Use `--zip-source` to generate a source archive alongside distribution output. `--zip-source` produces: * `dist/--source.zip` ## Polyfill support If your code relies on Gecko-style `browser.*` APIs and you need Chromium compatibility, enable `--polyfill`: ```bash npm theme={null} npx extension build --polyfill ``` ```bash pnpm theme={null} pnpx extension build --polyfill ``` ```bash yarn theme={null} yarn dlx extension build --polyfill ``` ## Best practices * **Build per target in CI (continuous integration)**: Treat each browser output as an independent artifact. * **Use a browser matrix command for parity checks**: Catch target-specific issues early in one pipeline step. * **Package intentionally**: Use `--zip` for store uploads and `--zip-source` for traceable source artifacts. * **Keep target config explicit**: Use `extension.config.*` command/browser defaults for reproducible builds. ## Next steps * Learn more about the [browsers available](/docs/browsers/browsers-available). * Explore [path resolution](/docs/features/path-resolution) for asset/output mapping details. # Predictable path resolution Source: https://extension.js.org/docs/features/path-resolution Write paths naturally and let Extension.js normalize them to runtime-safe output. Handles manifest fields, public assets, and extension API references. Write paths the way you naturally author them, and let Extension.js normalize them to runtime-safe output paths. Keep runtime paths working when files move, entries are renamed, or browser targets change. Extension.js rewrites supported paths across manifest fields and extension API usage so compiled assets resolve correctly inside `dist/`. ## How it works Path resolution runs in two places: * **Manifest compilation pipeline**: resolves manifest-declared paths to emitted assets. * **Resolve plugin for JS/TS**: rewrites static path literals passed to supported extension APIs (for example `runtime.getURL`, `tabs.update`, `scripting.*`, `action.*`, `sidePanel.*`). ## Supported input styles Extension.js normalizes common path inputs as follows: | Input style | Example | Normalized output | | -------------------------------- | -------------------------------------------------- | -------------------------------------------------------------------- | | Public root (absolute) | `/public/icons/icon.png` | `icons/icon.png` | | Public root (relative) | `public/icons/icon.png`, `./public/icons/icon.png` | `icons/icon.png` | | Root absolute | `/foo/bar.js` | `foo/bar.js` | | Root special folder | `/pages/popup.njk` | `pages/popup.html` | | Special folders | `pages/popup.html`, `scripts/content.tsx` | `pages/popup.html`, `scripts/content.js` | | Parent-relative from author file | `../scripts/content.ts` | `scripts/content.js` (when resolving to project root special folder) | Extension mapping for `pages/` and `scripts/` includes: * `.ts`, `.tsx`, `.js`, `.jsx` -> `.js` * `.njk`, `.nunjucks`, `.html` -> `.html` * `.scss`, `.sass`, `.less`, `.css` -> `.css` ## Cases intentionally not rewritten To avoid false positives, Extension.js intentionally skips these inputs: * `http://` and `https://` URLs * `data:` URLs * `chrome://` URLs * `moz-extension://` URLs * Glob patterns (`*`, `?`, `{}`, `[]`) * Non-static/dynamic expressions that cannot be safely resolved at build time ## Manifest output examples Assuming target browser `chrome`: | Manifest field | Input Path | Output Path | | ------------------------------ | -------------------------- | ------------------------------- | | `action.default_popup` | `/src/popup.html` | `action/index.html` | | `background.page` | `/src/background.html` | `background/index.html` | | `background.service_worker` | `/src/background.ts` | `background/service_worker.js` | | `browser_action.default_popup` | `/src/popup.html` | `action/index.html` | | `chrome_url_overrides.newtab` | `/src/newtab.html` | `newtab/index.html` | | `content_scripts.js` | `/src/content_script.ts` | `content_scripts/content-0.js` | | `content_scripts.css` | `/src/content_style.css` | `content_scripts/content-0.css` | | `devtools_page` | `/src/devtools.html` | `devtools/index.html` | | `options_ui.page` | `/src/options.html` | `options/index.html` | | `sandbox.pages` | `/src/sandbox.html` | `sandbox/page-0.html` | | `side_panel.default_path` | `/src/sidepanel.html` | `sidebar/index.html` | | `sidebar_action.default_panel` | `/src/sidebar_panel.html` | `sidebar/index.html` | | `storage.managed_schema` | `/src/storage_schema.json` | `storage/managed_schema.json` | | `user_scripts.api_script` | `/src/user_script.ts` | `user_scripts/api_script.js` | Not every manifest field rewrites to a folder with the same source name. Extension.js bases the emitted path on the browser-facing extension contract, not on your original authoring path. Extension.js emits all compiled outputs under `dist/`. ## Referencing output files For browser APIs like `chrome.runtime.getURL()`, prefer stable, output-root paths: ```javascript theme={null} const iconUrl = chrome.runtime.getURL('/icons/icon.png') console.log(iconUrl) ``` Output: `chrome-extension:/icons/icon.png` ## Diagnostics and safety checks The resolver emits warnings when a referenced path cannot be resolved to a packaged asset (for example, nested `src/pages/...` or missing `public/` files after rewrite).\ Extension.js de-duplicates warnings per file transform pass to reduce noise. ## Important path rules * Leading `/` means extension output root, not filesystem root. * `public/...` and `/public/...` normalize to output-root assets. * `pages/` and `scripts/` are special folders with extension mapping rules. * Extension.js preserves absolute OS file paths instead of treating them as extension output paths. * Extension.js skips dynamic expressions when it cannot safely determine the final packaged path. ## Best practices * Keep `pages/`, `scripts/`, and `public/` at project root for predictable rewrites. * Use static literals for extension API paths when possible; dynamic expressions may be skipped. * Treat `/public/...` as output-root assets and avoid source-relative assumptions at runtime. * Validate warnings during `dev` as they usually indicate path mismatches before packaging. ## Next steps * Learn more about [special folders](/docs/features/special-folders). * Learn more about [web accessible resources](/docs/implementation-guide/web-accessible-resources). # Page reload and hot module replacement (HMR) Source: https://extension.js.org/docs/features/reload-and-hmr Understand how Extension.js picks the fastest safe update path during development, from HMR for styles to hard reloads and restart-required changes. Keep development fast by using the lightest update strategy for each file change. Need fast feedback after each file change? Extension.js chooses the safest and fastest update path automatically: * **HMR** when module updates are safe * **Hard extension reload** when runtime-critical assets change * **Restart-required errors/warnings** when entrypoint structure changes ## Reload prerequisites (DevTools setup dialog) Before evaluating reload behavior, confirm the same setup checks shown in the Extension.js DevTools **Confirm setup** dialog: | Setup step | Why it matters for reload/HMR | | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | Enable Developer mode in `chrome://extensions` | Lets extension runtime updates (including service worker changes) apply reliably during development. | | Allow local network access prompt for content scripts | Required for reliable content-script reload workflows when the browser asks for permission. | If either step is missing, reload behavior can appear inconsistent even when the build pipeline is healthy. ## Reload tiers ### 1) Hot module replacement (fastest path) Extension.js uses HMR when code can update without restarting the extension's background processes and event listeners. Common examples: * Scripts attached to extension HTML pages (accept updates through injected HMR wrappers) * Background script modules in non-service-worker flows * User script API script flows * CSS updates in content-script/runtime wrappers where supported ### 2) Hard extension reload Some file changes require reloading the extension runtime itself. Typical hard-reload triggers: * `manifest.json` * `_locales/**.json` * Service worker/background runtime-critical changes * Content script output changes (browser runner enforces extension reload for consistency) Extension.js applies browser-specific hard reload mechanisms internally (Chromium and Firefox flows differ under the hood) but unifies behavior at the CLI level. > **Why do content script filenames include hashes in dev?** Chrome aggressively caches `chrome-extension://` resources. A stable filename like `content-0.js` can serve stale code even after a full extension reload. Extension.js appends a short build hash (for example, `content-0.abcd1234.js`) in development. Each rebuild produces a fresh URL that bypasses the cache. Production builds use clean names. ### 3) Restart required (dev server) When extension entrypoint references change (instead of just module contents), Extension.js reports restart-required diagnostics so you do not continue with a stale graph. Typical restart-required scenarios: * Manifest script entrypoint list changes * HTML entrypoint script/style references change * `pages/` / `scripts/` file set changes in watch mode (especially removals) ## Behavior matrix | Change type | Default behavior | | --------------------------------------------------------------------- | ----------------------------------- | | JS/CSS module updates in existing entries | HMR where supported | | `manifest.json` / locales / service-worker-critical updates | Hard extension reload | | Entrypoint structure updates (manifest lists, HTML script/style refs) | Restart required | | `pages/` or `scripts/` file add/remove during watch | Warning/error with restart guidance | ## Reloading scripts and HTML outside the manifest `pages/` and `scripts/` follow the same reload strategy as manifest-declared assets. * Existing module updates can use hot-update paths. * Entrypoint set changes (add/remove or reference graph changes) may require restart. **Example:** ```plaintext theme={null} pages/ └── extra-page.html scripts/ └── extra-script.js ``` > Note: The `/pages` and `/scripts` folders are special folders recognized by Extension.js for hot-reloading. Extension.js treats each entry in these folders as a separate page or script that it can reload independently. ## Best practices * Keep entrypoint references stable during active dev sessions to maximize HMR. * Batch `manifest.json` and locale edits to avoid repeated hard extension reloads. * Use `pages/` and `scripts/` for off-manifest assets, then restart when file sets or entry wiring changes. * Treat restart-required diagnostics as intentional safety checks, not transient warnings. ## Next steps * Review off-manifest asset flows in [special folders](/docs/features/special-folders). * Understand structural update outcomes in [dev update behavior](/docs/workflows/dev-update-behavior). # Rspack configuration in extension.config.js Source: https://extension.js.org/docs/features/rspack-configuration 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 (loaders, plugins, aliases) without forking the default setup. Extension.js builds with [Rspack](https://rspack.dev), a Rust-based JavaScript bundler (`@rspack/core`), and lets you extend the generated config 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 generated bundler config. ### `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 config. ```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. * Config type is based on `@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 } } ``` ## 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` config keys before low-level bundler overrides. * **Patch minimally**: Change only the pieces you need, then return the config. * **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 config 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). # Special folders for pages, scripts, and assets Source: https://extension.js.org/docs/features/special-folders Use special folders for extra pages, injected scripts, static assets, and companion extensions that do not fit in manifest.json entrypoints. Use special folders when your extension needs entrypoints or assets that do not fit cleanly in `manifest.json`. Handle extra pages, runtime-injected scripts, static assets with exact paths, and companion extensions for local development without splitting your project structure. ## Template examples ### `special-folders-pages` special-folders-pages template screenshot See the `pages/` special folder in action with extra HTML entrypoints. ```bash npm theme={null} npx extension@latest create my-extension --template=special-folders-pages ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=special-folders-pages ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=special-folders-pages ``` Repository: [extension-js/examples/special-folders-pages](https://github.com/extension-js/examples/tree/main/examples/special-folders-pages) ### `special-folders-scripts` special-folders-scripts template screenshot See the `scripts/` special folder in action with standalone script entrypoints. ```bash npm theme={null} npx extension@latest create my-extension --template=special-folders-scripts ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=special-folders-scripts ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=special-folders-scripts ``` Repository: [extension-js/examples/special-folders-scripts](https://github.com/extension-js/examples/tree/main/examples/special-folders-scripts) ## Why this matters The manifest does not directly declare many extension files: iframe pages, scripts injected with `scripting.executeScript`, static vendor assets, and companion extensions used in development. Special folders make these first-class in the build pipeline. ## How it works Each special folder has a specific role: | Folder name | **Description** | | ------------- | ------------------------------------------------------------------------------------------------------------ | | `pages/` | Adds HTML pages to compilation as entrypoints, even when they are not listed in the manifest. | | `scripts/` | Adds script files to compilation as entrypoints, even when they are not referenced by manifest/HTML entries. | | `public/` | Copies static assets to the output root as-is (`public/**` -> `dist/**`) without bundling or transformation. | | `extensions/` | Provides a conventional location for load-only companion extensions in dev/preview/start workflows. | ## `pages/`: additional HTML entrypoints Use `pages/` for extra extension pages such as sandbox iframes, diagnostics pages, or internal tools. Extension.js treats each `.html` file in `pages/` as an entrypoint and compiles it like manifest-declared pages. For a sandboxed iframe example, see the [Chrome Sandbox Sample](https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/api-samples/sandbox/sandbox). ## `scripts/`: standalone script entrypoints Use `scripts/` for executable scripts that you load dynamically and that are not tied to an HTML page entry. Extension.js compiles files in `scripts/` as entrypoints using the same extension resolution pipeline as the rest of your project. ### Important contract When you use a `scripts/` entry as a content-script-like runtime entry, follow the same mount-style authoring contract as content scripts: * export a default function * perform setup inside that function * optionally return a synchronous cleanup This is especially important during development, where Extension.js remounts content-script-like entries safely instead of relying on blind page reloads. For dynamic injection examples, see the [Chrome Scripting Sample](https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/api-samples/scripting). ## `public/`: copy-only static assets Use `public/` when you need stable file paths and no bundling/transformation. Extension.js copies everything under `public/` to the output root 1:1. ### Important `public/` guard Do not place `manifest.json` at `public/manifest.json`. Extension.js prevents this to avoid overwriting the generated manifest during compilation. ## `extensions/`: companion extensions (load-only) When using companion extensions (for example, devtools helpers), Extension.js supports an `extensions/` folder as a load-only source in dev/preview/start flows. At a high level: * Scans subfolders under `extensions/` for unpacked extension roots (`manifest.json` present). * Extension.js loads companion extensions alongside your main extension. * This is for loading, not building those companion extensions into your main artifact. You can also load companion extensions via the `--extensions` CLI flag or the `extensions` key in `extension.config.js`: ```bash theme={null} # Load from a local folder extension dev --extensions ./path/to/companion # Load from Chrome Web Store or Firefox Add-ons extension dev --extensions "https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi" ``` Extension.js downloads, unpacks, and loads store URLs alongside your extension automatically. ## Development behavior (watch mode) In development watch mode, Extension.js monitors `pages/` and `scripts/` for file set changes: * Adding supported files triggers a warning (you can keep working). * Removing supported files triggers a compilation error and requires restarting the dev server. This protects the running compilation graph from stale or broken entrypoints. ## Best practices * **Keep shared runtime assets in `public/`**: Use it for files that must keep exact names and paths in output. * **Use `pages/` and `scripts/` for true entrypoints**: Keep off-manifest execution paths explicit. * **Restart dev server after entrypoint changes**: Especially after removing files under `pages/` or `scripts/`. * **Keep companion extensions isolated**: Treat `extensions/` as load-only dependencies for local workflows. * **Do not place `manifest.json` in `public/`**: Extension.js blocks `public/manifest.json` to protect the generated extension output. ## Next steps * Learn more about [page reload and hot module replacement (HMR)](/docs/features/reload-and-hmr). * Understand the mount contract in [Content scripts](/docs/implementation-guide/content-scripts). * Check out the different [Templates](../getting-started/templates) to bootstrap your next extension. # Telemetry and privacy controls in Extension.js Source: https://extension.js.org/docs/features/telemetry-and-privacy Review the Extension.js telemetry contract: what anonymous data is collected, what is never collected, and how to opt out of telemetry entirely. Extension.js collects a tiny amount of anonymous telemetry to understand which commands are used and which fail. Extension.js never collects source code, file paths, URLs, or project content. The privacy bar is strict by design: * Two events total: `command_executed` and `command_failed` * Three properties per event: `command`, `success`, `version` * Opt-out with an environment variable, a CLI flag, or a persistent consent command * Sampled and capped to stay well inside the PostHog free tier ## What Extension.js collects Per CLI run, at most one of: | Event | Sampled | Properties | | ------------------ | -------------------------------- | -------------------------------------- | | `command_executed` | 20 % (configurable, see below) | `command`, `success: true`, `version` | | `command_failed` | 100 % (failures are always sent) | `command`, `success: false`, `version` | Common context attached to every event: `os` (`darwin`/`linux`/`win32`), `arch`, `node_major`, `is_ci`. Nothing else. ## Volume controls Three independent controls limit how much data leaves the machine: * **Sampling** — Extension.js samples `command_executed` at 20 % by default. Override with `EXTENSION_TELEMETRY_SAMPLE_RATE` (0.0–1.0). Failures are never sampled. * **Per-run cap** — at most **3 events** per CLI process. Override with `EXTENSION_TELEMETRY_MAX_EVENTS`. * **Debounce** — the CLI drops duplicate `(event, command, success)` tuples within 60 s. Override with `EXTENSION_TELEMETRY_DEBOUNCE_MS`. ## What Extension.js never collects The CLI telemetry contract explicitly excludes: * Source code, manifest contents, HTML output, or `package.json` contents * Repo names, Git remotes, GitHub org/user names, branch names, commit SHAs, or preview URLs * Dependency lists, permission lists, or freeform project identifiers * Environment variable values, filesystem paths, or machine-local URLs * Stack traces, error messages, or free-text error names * IP addresses (the CLI sets `$ip` to `null` on every payload) ## Opt out Three ways to disable telemetry, listed in precedence order: ```bash theme={null} # 1. Environment variable (wins over everything else) EXTENSION_TELEMETRY_DISABLED=1 extension dev # preferred EXTENSION_TELEMETRY=0 extension dev # back-compat, also honored # 2. Per-run flag extension dev --no-telemetry # 3. Persistent consent file via the telemetry command extension telemetry disable extension telemetry enable extension telemetry status # show current state (default when no argument) ``` The consent file lives at `$XDG_CONFIG_HOME/extensionjs/telemetry/consent` (or the platform equivalent). ## Default behavior Telemetry is **opt-out**. On the first run where none of the overrides above apply, the CLI prints a one-line notice explaining how to disable telemetry. It also records an `enabled` consent marker so the notice does not repeat. CI environments follow the same rules — set `EXTENSION_TELEMETRY=0` in your CI environment to turn telemetry off across the board. ## Local audit log The CLI appends every event it considers sending (whether or not it actually ships) to `events.jsonl` next to the consent file. Inspect it any time. Delete it freely. ## Best practices * Use `EXTENSION_TELEMETRY_DISABLED=1` in CI when policy requires no telemetry. * Treat privacy regressions as product regressions. * Read the repository-level contract for the exact event list. ## Next steps * Review [Global flags](/docs/workflows/global-flags) for `--no-telemetry` and environment variable overrides. * Use [Build](/docs/commands/build) and [Dev](/docs/commands/dev) for release and automation workflows. * Read the repository-level [`TELEMETRY.md`](https://github.com/extension-js/extension.js/blob/main/TELEMETRY.md) contract for the exact event list. # Create your first browser extension step by step Source: https://extension.js.org/docs/getting-started/create-your-first-extension Build your first browser extension end-to-end with Extension.js. Create a GitHub search omnibox shortcut and learn the core development loop. You will build an omnibox (address bar) shortcut: type `gh` in the browser address bar, enter a query, and land on GitHub search results. Along the way you will wire a `manifest.json` and handle input in a background service worker. You will also practice the dev loop (`create` → `dev` → `build`) that you will reach for on every project after this one. ## What you will build | Capability | What you get | | ------------------------- | ------------------------------------------------------------- | | Omnibox keyword flow | Trigger extension behavior with `gh` from the browser URL bar | | Background event handling | Handle user input through a service worker | | Local dev loop | Run, load, and validate extension behavior quickly | | Progressive enhancement | Add live GitHub suggestions after baseline flow works | ## The plan Make GitHub search as fast as a native browser shortcut. The extension reserves the keyword `gh`; after you type `gh` and a query, it opens GitHub search results. > The interface you are creating here. ## Step 1 - create the extension Use the Extension.js `create` command to bootstrap a minimal extension named `github-search`. ```bash npm theme={null} npx extension@latest create github-search ``` ```bash pnpm theme={null} pnpx extension@latest create github-search ``` ```bash yarn theme={null} yarn dlx extension@latest create github-search ``` ## Step 2 - create the manifest file > Step 2 Demo Every extension starts with a manifest file. It defines metadata, permissions, and runtime files. Based on the [plan above](#the-plan), set the `gh` shortcut and add a service worker for user events. ```json theme={null} { "manifest_version": 3, "name": "GitHub Search", "version": "1.0", "omnibox": {"keyword": "gh"}, "background": { "service_worker": "service_worker.js" } } ``` * `omnibox.keyword`: When you type `gh`, the browser fires an event. * `background.service_worker`: Listens to the event you just triggered. ## Step 3 - create the background service worker In browser extensions, the background service worker handles browser events. For this example, add a script that listens to omnibox input and routes the query to GitHub search. Create `service_worker.js`: ```js theme={null} // When the user has accepted what is typed into the omnibox. chrome.omnibox.onInputEntered.addListener((text) => { // Convert any special character (spaces, &, ?, etc) // into a valid character for the URL format. const encodedSearchText = encodeURIComponent(text) const url = `https://github.com/search?q=${encodedSearchText}&type=issues` chrome.tabs.create({url}) }) ``` The script above opens a new tab with GitHub search results whenever you type something after "gh" in the URL bar. ## Step 4 - load your extension Your `package.json` file now looks like this: ```json theme={null} { "scripts": { "dev": "extension dev", "start": "extension start", "build": "extension build" }, "devDependencies": { "extension": "latest" } } ``` These scripts are the default Extension.js commands. Run the extension for the first time: ```bash npm theme={null} npm run dev ``` ```bash pnpm theme={null} pnpm run dev ``` ```bash yarn theme={null} yarn run dev ``` If you configured everything correctly, you should see the following: That's it! You created your first browser extension that searches on GitHub! ## Step 5 - make it better Improve the search experience by adding suggestions directly in the URL bar with an omnibox input listener. Update `service_worker.js` to fetch GitHub suggestions and display them while typing. ```js title="service_worker.js" theme={null} // Create a debounce function to avoid excessive // calls to the GitHub API while the user is still // typing the search query. function debounce(fn, delay) { let timeoutID return function (...args) { if (timeoutID) clearTimeout(timeoutID) timeoutID = setTimeout(() => fn(...args), delay) } } // When the user has changed what is typed into the omnibox. chrome.omnibox.onInputChanged.addListener( debounce(async (text, suggest) => { const response = await fetch( `https://api.github.com/search/issues?q=${text}` ) const data = await response.json() const suggestions = data.items.map((issue) => ({ content: issue.html_url, description: issue.title })) suggest(suggestions) }, 250) ) // When the user has accepted what is typed into the omnibox. chrome.omnibox.onInputEntered.addListener((text) => { // Convert any special character (spaces, &, ?, etc) // into a valid character for the URL format. const encodedSearchText = encodeURIComponent(text) const url = `https://github.com/search?q=${encodedSearchText}&type=issues` chrome.tabs.create({url}) }) ``` This code adds live GitHub suggestions directly in the URL bar. You now have a working GitHub search extension. Iterate on it and adapt it to your own workflow. ## Next steps * Create another extension with [templates](/docs/getting-started/templates). * Add automated checks with [Playwright E2E](/docs/workflows/playwright-e2e). * Review [Troubleshooting](/docs/workflows/troubleshooting), [Security checklist](/docs/workflows/security-checklist), and [Performance playbook](/docs/workflows/performance-playbook) as your extension grows. # Get started immediately with Extension.js samples Source: https://extension.js.org/docs/getting-started/immediately Build and run working browser extensions in minutes using Extension.js. Follow the fastest path from sample projects to a running dev environment. There are two common entrypoints: point `extension dev` at a local folder you cloned, or point it at a GitHub URL and let it fetch the sample for you. The sections below cover both paths. You will also learn what Extension.js treats as a project root and how it handles ambiguous inputs like a bare sample name. ## How `dev` resolves the project `extension dev` accepts an optional first argument. Resolution works like this: * **No argument:** uses the current working folder as the extension project. * **Local path:** treated as a path relative to the current working folder (for example `./my-extension` or `../samples/page-redder` after you clone a repo). * **`https://github.com/...` URL:** Extension.js fetches the repository (or tree path) into your working folder, then builds from the extracted folder. See [Project detection and inputs](https://github.com/extension-js/extension.js/blob/main/programs/develop/README.md#project-detection-and-inputs) in the develop package README. * **Other `http(s)` URLs:** treated as a ZIP archive, downloaded and extracted locally. A bare name like `sample.page-redder` is **not** a remote fetch — it is only a folder name under your current folder. Use a full GitHub URL, or clone first and point `dev` at the path. ## Run Chrome extension samples fast Use samples from the Chrome Extension Samples repository to validate your setup and learn the workflow quickly. ### Option A: full GitHub tree URL (recommended) 1. Open your terminal. 2. `cd` to the folder where cloned/extracted projects should appear (often an empty folder). 3. Run: ```bash npm theme={null} npx extension@latest dev https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/sample.page-redder ``` ```bash pnpm theme={null} pnpx extension@latest dev https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/sample.page-redder ``` ```bash yarn theme={null} yarn dlx extension@latest dev https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/sample.page-redder ``` Extension.js downloads the sample, then runs the dev workflow against it. ### Option B: clone first, then `dev` a local path ```bash theme={null} git clone https://github.com/GoogleChrome/chrome-extensions-samples.git cd chrome-extensions-samples/functional-samples/sample.page-redder npx extension@latest dev . ``` Browse more samples in [Chrome Extension Samples](https://github.com/GoogleChrome/chrome-extensions-samples). ## Run samples in Microsoft Edge Extension.js supports Microsoft Edge out of the box. ### Full URL with Edge ```bash npm theme={null} npx extension@latest dev https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/api-samples/topSites/magic8ball --browser=edge ``` ```bash pnpm theme={null} pnpx extension@latest dev https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/api-samples/topSites/magic8ball --browser=edge ``` ```bash yarn theme={null} yarn dlx extension@latest dev https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/api-samples/topSites/magic8ball --browser=edge ``` ### Clone first, then Edge ```bash theme={null} git clone https://github.com/GoogleChrome/chrome-extensions-samples.git cd chrome-extensions-samples/api-samples/topSites/magic8ball npx extension@latest dev . --browser=edge ``` This example follows the [magic8ball](https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/api-samples/topSites/magic8ball) sample. ## Run Mozilla extensions in Edge with polyfill You can run examples from MDN WebExtensions in Edge by passing the GitHub tree URL and enabling the polyfill. The polyfill maps `browser.*` API calls to `chrome.*` so Firefox-oriented extensions work in Chromium-based browsers. ```bash npm theme={null} npx extension@latest dev https://github.com/mdn/webextensions-examples/tree/main/apply-css --browser=edge --polyfill=true ``` ```bash pnpm theme={null} pnpx extension@latest dev https://github.com/mdn/webextensions-examples/tree/main/apply-css --browser=edge --polyfill=true ``` ```bash yarn theme={null} yarn dlx extension@latest dev https://github.com/mdn/webextensions-examples/tree/main/apply-css --browser=edge --polyfill=true ``` ### Clone first, then polyfill + Edge ```bash theme={null} git clone https://github.com/mdn/webextensions-examples.git cd webextensions-examples/apply-css npx extension@latest dev . --browser=edge --polyfill=true ``` This follows the [Apply CSS](https://github.com/mdn/webextensions-examples/tree/main/apply-css) example from MDN WebExtensions Examples. ## Create a minimal extension from scratch Use `extension create` with the `init` template for the lightest possible starting point — just a manifest and icons, no framework or UI surface. ### `init` init template screenshot ```bash npm theme={null} npx extension@latest create my-extension --template=init ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=init ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=init ``` Repository: [extension-js/examples/init](https://github.com/extension-js/examples/tree/main/examples/init) ## Quick tips * **Use TypeScript:** add a `tsconfig.json` file to your project root. * **Use React:** add `react` and `react-dom` to your `package.json`. * A `tsconfig.json` configured for React enables TypeScript + React authoring. * If you need to handle assets not declared in the manifest, learn more about [special folders](/docs/features/special-folders). ## Best practices * **Use the `extension` package** to build, run, and bundle your extension from one toolchain. * **Use `--browser`** to target a specific browser while developing. * **Use `--polyfill`** when adapting Mozilla-oriented examples for Chromium-based browsers. * **Prefer full GitHub URLs** for one-shot tries; **clone + local path** when you want a stable folder layout. ## Next steps * Continue with [create your first extension](/docs/getting-started/create-your-first-extension). * Explore starter [templates](/docs/getting-started/templates) for your preferred stack. * Review [dev command options](/docs/commands/dev) to target specific browsers and binaries. # Getting started with Extension.js Source: https://extension.js.org/docs/getting-started/index Ship your first browser extension quickly with Extension.js, then level up with official templates and workflow guides as your project grows. ## Who this path is for * **New to extension development:** [Create your first extension](/docs/getting-started/create-your-first-extension) * **Exploring quickly with existing samples:** [Get started immediately](/docs/getting-started/immediately) * **Choosing a starting stack:** [Templates](/docs/getting-started/templates) ## First successful outcome Use `extension create` or choose a template to start from. Run `extension dev` and load the build output in a browser target. Make one change and confirm the extension reloads. Run `extension build` for at least one browser. ## Recommended reading flow 1. [Get started immediately](/docs/getting-started/immediately) 2. [Create your first extension](/docs/getting-started/create-your-first-extension) 3. [Templates](/docs/getting-started/templates) 4. [Commands overview](/docs/commands/index) ## Next steps * Learn command choices in [Commands reference](/docs/commands/index). * Configure project defaults in [Extension config](/docs/features/extension-configuration). * Validate release readiness with [Workflows](/docs/workflows/index). ## Video walkthrough # Extension project templates for React, Vue, and more Source: https://extension.js.org/docs/getting-started/templates Generate browser extensions from official Extension.js templates for React, Vue, Svelte, TypeScript, and vanilla JS instead of building from scratch. Start faster by generating extensions from official templates instead of building folder structure and config from scratch. ## Template capabilities * **Ready project structure:** Start with a working manifest, scripts, and source layout. * **Stack-specific setup:** Choose templates by framework and runtime needs. * **Faster onboarding:** Move from idea to runnable extension in minutes. * **Consistent defaults:** Reduce setup drift across projects and team members. ## Quick usage ```bash npm theme={null} npx extension@latest create my-extension --template= ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template= ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template= ``` Replace `` with the template you want to start from. ## Template naming convention Template slugs (the short names you pass to `--template`) follow a `-` pattern. The prefix tells you **what kind of extension surface** you get: | Prefix | Extension surface | Good for | | ----------- | -------------------------------------- | ------------------------------------------------- | | `new-*` | New tab override page | Full-page UI (dashboards, tools, landing pages) | | `content-*` | Content script injected into web pages | Page augmentation (overlays, readers, highlights) | | `action-*` | Toolbar popup (action button) | Quick actions, status panels, mini-apps | | `sidebar-*` | Side panel | Persistent companion UI alongside web pages | | (no prefix) | Varies — check the template README | Starter baseline or specialized example | **Quick picks:** * **Just want to start?** Use `new` (JS) or `new-typescript` (TS) for a minimal baseline. * **Framework-first?** Choose `new-react`, `new-preact`, `new-vue`, or `new-svelte`. * **Content script?** Choose `content` (JS) or `content-react`, `content-vue`, etc. * **Match your team stack** to reduce setup drift. ## Available templates Highlighted starter options include TypeScript, JavaScript, Svelte, Vue, React, and Preact. Toolbar popup · `--template=action` Toolbar popup · `--template=action-chatgpt` Toolbar popup · `--template=action-locales` Content script · `--template=content` Content script · `--template=content-css-modules` Content script · `--template=content-custom-font` Content script · `--template=content-env` Content script · `--template=content-less` Content script · `--template=content-less-modules` Content script · `--template=content-main-world` Content script · `--template=content-multi-one-entry` Content script · `--template=content-multi-three-entries` Content script · `--template=content-preact` Content script · `--template=content-react` Content script · `--template=content-sass` Content script · `--template=content-sass-modules` Content script · `--template=content-svelte` Content script · `--template=content-typescript` Content script · `--template=content-vue` Default baseline · `--template=init` Baseline · `--template=javascript` Default baseline · `--template=new` New tab page · `--template=new-browser-flags` New tab page · `--template=new-config-eslint` New tab page · `--template=new-config-prettier` New tab page · `--template=new-config-stylelint` New tab page · `--template=new-crypto` New tab page · `--template=new-env` New tab page · `--template=new-less` New tab page · `--template=new-preact` New tab page · `--template=new-react` New tab page · `--template=new-react-router` New tab page · `--template=new-sass` New tab page · `--template=new-svelte` New tab page · `--template=new-typescript` New tab page · `--template=new-vue` E2E testing · `--template=playwright` Baseline · `--template=preact` Baseline · `--template=react` Side panel · `--template=sidebar` Side panel · `--template=sidebar-monorepo-turbopack` Side panel · `--template=sidebar-shadcn` Side panel · `--template=sidebar-transformers-js` Special folders · `--template=special-folders-pages` Special folders · `--template=special-folders-scripts` Baseline · `--template=svelte` Baseline · `--template=typescript` Baseline · `--template=vue` The grid above is a **curated subset** of popular stacks. Card links point to [templates.extension.dev](https://templates.extension.dev) when a live preview exists. The **authoritative** list of slugs is every folder name under [extension-js/examples](https://github.com/extension-js/examples/tree/main/examples). Those names map 1:1 to `--template=`. Beyond `new*` baselines, the repo includes **context-first** families like `action`, `content`, and `sidebar`. It also has specialized `new-*` and `special-folders-*` samples. Browse GitHub to find the template that matches your use case. ## How it works When you run `create --template=`, Extension.js fetches the selected template, generates the project files, and optionally installs dependencies. ## More templates Find official template sources in the examples repository: * [extension-js/examples](https://github.com/extension-js/examples/tree/main/examples) Template folder names map directly to `--template=`. ## Best practices * Use templates to bootstrap your project quickly and efficiently. * Start with a template that matches a technology stack you already know. * Choose the browser extension context that best fits your project requirements. ## Next steps * Learn how [browser runners](/docs/browsers/browsers-available) help you validate extension behavior. * Learn how Extension.js handles [environment variables](/docs/features/environment-variables). *** # Extension.js hall of fame and contributors Source: https://extension.js.org/docs/hall-of-fame Recognize the contributors who shaped Extension.js across the framework core, documentation site, examples, and developer experience tooling. Extension.js and `extension.js.org` are both open-source efforts shaped by contributors across the framework, the docs site, and the surrounding developer experience. Browser-extension tooling improves because people contribute in different ways: compiler/runtime work, documentation, examples, design, and feedback loops. This page recognizes both sides explicitly. ## Authors of [extension.js](https://github.com/extension-js/extension.js) These authors helped build and improve the framework itself, including the CLI, build/runtime behavior, browser support, tests, and developer workflow: Extension.js Core Contributors ## Authors of [extension.js.org](https://github.com/extension-js/docs) These authors helped improve the docs site, examples, information architecture, and learning experience around Extension.js: Extension.js Documentation Contributors ## Contribute Want to help shape the framework too? Read the [Extension.js contributing guide](https://github.com/extension-js/extension.js/blob/main/CONTRIBUTING.md) to get set up and start contributing. # Background scripts and MV3 service workers Source: https://extension.js.org/docs/implementation-guide/background Run extension-wide logic with MV2 background scripts or MV3 service workers. Extension.js compiles entries from manifest.json and applies reload behavior. Run extension-wide logic in background contexts with clear support for both Manifest V2 (MV2) background scripts and Manifest V3 (MV3) service workers. Extension.js reads background entries from `manifest.json` and compiles them into dedicated background outputs. It applies browser-specific reload behavior during development. ## Template examples ### `action` action template screenshot An action popup extension with a background service worker handling browser events. ```bash npm theme={null} npx extension@latest create my-extension --template=action ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=action ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=action ``` Repository: [extension-js/examples/action](https://github.com/extension-js/examples/tree/main/examples/action) ### `action-chatgpt` action-chatgpt template screenshot Action popup with a background service worker that integrates with an external API (ChatGPT). ```bash npm theme={null} npx extension@latest create my-extension --template=action-chatgpt ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=action-chatgpt ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=action-chatgpt ``` Repository: [extension-js/examples/action-chatgpt](https://github.com/extension-js/examples/tree/main/examples/action-chatgpt) ## Background capabilities | Capability | What it gives you | | ---------------------------- | -------------------------------------------------------------------- | | MV2/MV3 compatibility | Support `background.scripts` and `background.service_worker` entries | | Dedicated background outputs | Emit background runtime files per active manifest shape | | Reload-aware development | Apply hard reload/restart behavior for structural changes | | Worker mode control | Respect `background.type` module/classic runtime behavior | ## Background script support The following fields in `manifest.json` are used to declare background scripts: | Manifest field | File type expected | Dev behavior | | --------------------------- | ---------------------------- | -------------------------------------------- | | `background.service_worker` | `.js`, `.ts`, `.mjs`, `.tsx` | hard reload flow | | `background.scripts` | `.js`, `.ts`, `.mjs`, `.tsx` | hot module replacement (HMR)-compatible path | `background.type` can also affect runtime mode (`module` vs classic service worker behavior). ## Sample background script declaration Here is an example of how to declare a background script in `manifest.json`: ```json theme={null} { "manifest_version": 3, "name": "My Extension", "version": "1.0.0", "background": { "service_worker": "./scripts/background.ts" } } ``` ## Development behavior * Extension.js tracks service worker/source changes and can trigger hard extension reload. * Manifest changes affecting background entries can trigger restart-required diagnostics. * Browser launch plugins apply target-specific hard reload strategies (Chromium/Firefox). * Extension.js treats structural entrypoint changes more strictly than regular module edits. ## MV3 service worker lifecycle `background.service_worker` runs under the browser's service-worker lifecycle, not as a permanently alive process. That means: * in-memory state can disappear between events * long-running work should be designed around events, not process permanence * startup should stay small and predictable * important state should be restored from storage or recomputed safely Use `background.scripts` only for MV2-style compatibility cases. For MV3-first extensions, treat the service worker as the source of truth for privileged orchestration. ## Output behavior Common outputs include: * `background/service_worker.js` * `background/scripts.js` The exact output depends on which manifest field remains active after browser filtering. ## Module and classic notes * `background.type: "module"` uses module worker semantics. * Classic service worker mode uses `importScripts`-based chunk loading behavior. * Keep dynamic import usage conservative in background runtime-critical paths. ## Recommended architecture * Keep the background entry thin and move feature logic into shared modules. * Use the background context as the privileged coordinator for messaging, storage access, and browser API orchestration. * Restore durable state from storage instead of assuming a singleton process. * Use alarms, explicit event listeners, and small feature modules instead of one large startup path. ## Common mistakes * Treating the service worker like a permanently running server process. * Keeping critical state only in memory. * Doing expensive startup work on every event wakeup. * Putting too much feature logic in popup or content-script code when it actually needs privileged coordination. ## Best practices * Prefer `background.service_worker` for MV3-first extensions. * Keep background entry files small and delegate logic to shared modules. * Avoid expensive startup work in service workers; initialize lazily where safe. * Treat manifest background field edits as structural changes in dev flow. * Route privileged actions through validated message handlers. * Store durable settings and caches in browser storage, not only in module-level variables. ## Next steps * Understand update outcomes in [dev update behavior](/docs/workflows/dev-update-behavior). * Persist durable runtime state with [Storage](/docs/implementation-guide/storage). * Coordinate contexts with [Messaging](/docs/implementation-guide/messaging). * Learn more about [JavaScript in development](/docs/implementation-guide/javascript). * Continue with [content scripts](/docs/implementation-guide/content-scripts). # Content scripts with HMR and manifest entries Source: https://extension.js.org/docs/implementation-guide/content-scripts Build page-integrated features with content scripts. Extension.js compiles manifest entries, wraps them for HMR, and emits predictable outputs. Build page-integrated extension features with content scripts while keeping a reliable dev loop for JS and CSS updates. Extension.js compiles content script entries from `manifest.json` and wraps them for runtime mounting and hot module replacement (HMR) behavior. It emits predictable `content_scripts/*` outputs. ## Content script capabilities | Capability | What it gives you | | ------------------------------ | ---------------------------------------------------------------------------------------- | | Manifest-driven entries | Compile JS/CSS content script lists directly from manifest | | Dev remount flow | Update scripts/styles quickly through wrapper-driven behavior | | MAIN vs isolated world support | Use MAIN world (page JavaScript context) or isolated world (sandboxed extension context) | | Predictable output layout | Emit normalized `content_scripts/*` artifacts | ## Template examples ### `content` content template screenshot Minimal content script setup with vanilla JS. ```bash npm theme={null} npx extension@latest create my-extension --template=content ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=content ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=content ``` Repository: [extension-js/examples/content](https://github.com/extension-js/examples/tree/main/examples/content) ### `content-react` content-react template screenshot Inject a React-powered UI into web pages through content scripts. ```bash npm theme={null} npx extension@latest create my-extension --template=content-react ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=content-react ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=content-react ``` Repository: [extension-js/examples/content-react](https://github.com/extension-js/examples/tree/main/examples/content-react) ## Supported manifest fields | Manifest field | File type expected | | --------------------- | ------------------------------------ | | `content_scripts.js` | `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs` | | `content_scripts.css` | `.css`, `.scss`, `.sass`, `.less` | ## Sample content script declaration Here is an example of how to declare content scripts in `manifest.json`: ```json theme={null} { "manifest_version": 3, "name": "My Extension", "version": "1.0.0", "content_scripts": [ { "matches": [""], "js": ["./scripts/content-script.ts"], "css": ["./styles/content-style.css"] } ] } ``` ## Authoring contract For every content-script-like entry, Extension.js expects a mount-style default export: * The module should `export default` a synchronous function. * That function should perform setup work. * It may return a synchronous cleanup callback. * Extension.js does not support classes as the default export. This applies to: * files referenced by `manifest.json > content_scripts[*].js` * files placed under the project `scripts/` folder when they are used as script entrypoints ### Valid shapes ```ts theme={null} export default function main() { return () => {} } ``` ```ts theme={null} const main = () => {} export default main ``` ### Invalid shapes ```ts theme={null} export default class App {} ``` ```ts theme={null} export default {} ``` ### Async guidance Keep the default export synchronous even when the feature does async work internally. Start async work inside the function and return a synchronous cleanup. Why this matters: Extension.js remounts content scripts during development. Without a cleanup contract, you can easily duplicate UI, event listeners, observers, and timers. ## What happens on contract violations * No default export: Extension.js warns during development and skips mounting. * Default export is not a function: Extension.js warns during development and skips mounting. * Async default export: the module still runs, but Extension.js does not treat the returned `Promise` as cleanup. If your content script appears to compile but never mounts, check the default export first. ## Runtime wrapper behavior * Extension.js wraps content script modules with mount/runtime helpers. * Dev mode adds HMR accept/dispose behavior and remount flow. * CSS updates trigger remount events (`__EXTENSIONJS_CSS_UPDATE__`) in development. * Extension.js respects `run_at` timing from manifest values. ## Multi-entry content scripts You can declare multiple content script entries in a single manifest. Each entry compiles independently with its own match patterns, run timing, and world settings. ### `content-multi-one-entry` content-multi-one-entry template screenshot Multiple content scripts bundled under one `content_scripts` manifest entry. ```bash npm theme={null} npx extension@latest create my-extension --template=content-multi-one-entry ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=content-multi-one-entry ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=content-multi-one-entry ``` Repository: [extension-js/examples/content-multi-one-entry](https://github.com/extension-js/examples/tree/main/examples/content-multi-one-entry) ### `content-multi-three-entries` content-multi-three-entries template screenshot Three separate `content_scripts` manifest entries with independent match patterns. ```bash npm theme={null} npx extension@latest create my-extension --template=content-multi-three-entries ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=content-multi-three-entries ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=content-multi-three-entries ``` Repository: [extension-js/examples/content-multi-three-entries](https://github.com/extension-js/examples/tree/main/examples/content-multi-three-entries) ## `scripts/` folder behavior The `scripts/` folder is for script entrypoints that no HTML page entry declares. In practice, these entries follow the same mount-style runtime expectations as content scripts. That means `scripts/` is not just a generic folder for loose JavaScript files: * script entry files should still have a default export function when they are meant to mount behavior * Extension.js treats adding or removing supported files under `scripts/` as a structural change in watch mode * Extension.js may require a dev server restart when that entry set changes ## Output path Extension.js normalizes content script entries per manifest index: ```plaintext theme={null} content_scripts/ ├── content-0.js # production ├── content-0.abcd1234.js # development (hash-based cache busting) ├── content-0.css └── ... ``` In **development mode**, content script JS filenames include a short hash suffix (for example, `content-0.abcd1234.js`). This forces the browser to load a fresh `chrome-extension://` URL after each rebuild. Chrome aggressively caches extension resources. Stable filenames can cause stale scripts to persist even after a hard refresh. Production builds use clean `content-0.js` names. ## Main world notes ### `content-main-world` See MAIN world content scripts in action with a working example that injects UI directly into the page context: ```bash npm theme={null} npx extension@latest create my-extension --template=content-main-world ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=content-main-world ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=content-main-world ``` Repository: [extension-js/examples/content-main-world](https://github.com/extension-js/examples/tree/main/examples/content-main-world) * **`world: "MAIN"` is Chromium-only.** Firefox does not support the `world` field and ignores it — your script still runs in the isolated world. For cross-browser MAIN world behavior, use the `chromium:` manifest prefix to declare it only for Chromium targets. Provide an isolated-world fallback for Firefox. * MAIN-world scripts use bridge-based chunk loading/public-path handling in development. * Extension APIs (`chrome.runtime`, `chrome.storage`, etc.) are **not available** in the MAIN world — only page-context globals are accessible. * Treat MAIN world as an advanced path. Validate behavior on each target browser early. ### Isolated vs MAIN quick example ```json theme={null} { "content_scripts": [ { "matches": [""], "js": ["./scripts/isolated.ts"] }, { "matches": [""], "js": ["./scripts/main-world.ts"], "world": "MAIN" } ] } ``` Use isolated world by default. Use `MAIN` only when you need page-context access, and account for extension API/runtime constraints. ### Cross-browser MAIN world pattern Use [browser-specific prefixes](/docs/features/browser-specific-fields) to declare MAIN world only for Chromium and provide an isolated fallback for Firefox: ```json theme={null} { "content_scripts": [ { "matches": [""], "js": ["./scripts/isolated.ts"] }, { "matches": [""], "chromium:js": ["./scripts/main-world.ts"], "chromium:world": "MAIN" } ] } ``` Firefox skips the `chromium:` prefixed fields entirely, so only Chromium targets get the MAIN-world script. ## Matching and execution guidance The browser still controls where a content script runs. Extension.js bundles the file, but the manifest entry still defines the matching contract. * Keep `matches` as narrow as the feature allows. * Add `exclude_matches`, `all_frames`, or `match_about_blank` only when the feature actually requires those behaviors. * Treat `run_at` and `world` as part of the feature contract, not just implementation detail. * Re-test permission and host-permission scope when changing where a content script runs. ## Development behavior * Editing content script code usually updates through wrapper-driven HMR/remount flow. * CSS-only entries receive dev helper behavior so style updates can propagate. * If content script entrypoint lists change in manifest, Extension.js can require dev server restart. ## Best practices * Keep content script entry files small and delegate logic to shared modules. * Scope selectors/styles carefully to avoid host-page collisions. * Prefer explicit `run_at` and `world` values when behavior depends on timing/context. * Treat manifest content-script list changes as structural development changes. * Pass page-derived data through validated messaging instead of performing privileged work directly in the page boundary. * Default to isolated world and move to `MAIN` only when the page context is strictly required. ## Next steps * Understand update outcomes in [dev update behavior](/docs/workflows/dev-update-behavior). * Design cross-context communication with [Messaging](/docs/implementation-guide/messaging). * Review access scope in [Permissions and host permissions](/docs/implementation-guide/permissions-and-host-permissions). * Learn about [web-accessible resources](/docs/implementation-guide/web-accessible-resources). * Continue with [locales in development](/docs/implementation-guide/locales). # CSS, Sass, and Less in browser extensions Source: https://extension.js.org/docs/implementation-guide/css Style extension pages and content scripts with plain CSS, Sass, or Less. Extension.js routes styles by context and handles reload behavior automatically. Extension.js supports plain CSS plus Sass and Less preprocessors out of the box. Styles are routed differently for page contexts (popup, options, side panel) and content-script contexts. Page styles ship as linked assets, while content-script styles are injected into the host document so they apply to the user's tab. ## Template examples ### `content-sass` content-sass template screenshot Content script with Sass styling injected into web pages. ```bash npm theme={null} npx extension@latest create my-extension --template=content-sass ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=content-sass ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=content-sass ``` Repository: [extension-js/examples/content-sass](https://github.com/extension-js/examples/tree/main/examples/content-sass) ### `content-custom-font` content-custom-font template screenshot Content script demonstrating custom font loading through CSS. ```bash npm theme={null} npx extension@latest create my-extension --template=content-custom-font ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=content-custom-font ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=content-custom-font ``` Repository: [extension-js/examples/content-custom-font](https://github.com/extension-js/examples/tree/main/examples/content-custom-font) ## CSS capabilities | Capability | What it gives you | | --------------------- | ------------------------------------------------------------------------------- | | Multi-context styling | Use one authoring model for pages and content scripts | | Preprocessor support | Compile Sass/Less when dependencies are present | | Context-aware output | Emit page CSS and content-script CSS to correct targets | | Dev update flow | Apply style updates through hot module replacement (HMR)/remount when supported | ## Where CSS can be referenced * `manifest.json` (`content_scripts[].css`) * HTML files (``) * script imports (`import "./styles.css"`, including Sass/Less when enabled) ## CSS support Manifest CSS entries: | Manifest field | File type expected | | --------------------- | --------------------------------- | | `content_scripts.css` | `.css`, `.scss`, `.sass`, `.less` | ## Example: CSS in `manifest.json` ```json theme={null} { "manifest_version": 3, "name": "My Extension", "version": "1.0.0", "content_scripts": [ { "matches": [""], "css": ["./styles/content.css"], "js": ["./scripts/content.ts"] } ] } ``` ## Example: CSS in extension page scripts ```ts theme={null} import './styles/popup.scss' ``` ## Output behavior by context | Context | Output behavior | | ------------------ | ----------------------------------------------------------------------------- | | HTML/page contexts | Extension.js bundles CSS with page entries (`feature.css`) | | Content scripts | Extension.js emits CSS as `content_scripts/[name].[contenthash:8].css` assets | Extension.js splits contexts automatically based on which entrypoint imports the CSS. ## Development behavior * Content script CSS imports participate in the content-script HMR/remount flow. * CSS-only content script entries get a dev helper script so updates can propagate. * Page CSS follows normal page HMR pipeline behavior. * Structural manifest/content-script changes can still require full extension reload or restart. ## Modules and preprocessors * CSS modules work best in extension page contexts. * Extension.js enables Sass/Less support when you install the related dependencies. * Extension.js applies PostCSS on top of the style pipeline when you configure it or when it detects a PostCSS config. ## Best practices * Keep content-script styles intentionally scoped to reduce host-page collisions. * Prefer component-local module styles for extension page UIs. * Keep preprocessor and PostCSS configs explicit to avoid unintended changes to your build setup over time. * Validate CSS paths referenced by manifest fields in CI. ## Next steps * Understand update outcomes in [dev update behavior](/docs/workflows/dev-update-behavior). * Learn more about [CSS modules](/docs/languages-and-frameworks/css-modules). * Learn more about [PostCSS integration](/docs/integrations/postcss). # HTML entrypoints for extension pages Source: https://extension.js.org/docs/implementation-guide/html Build extension UIs with plain HTML entrypoints. Extension.js handles asset wiring, script and style bundling, and dev-time update behavior for pages. Extension.js processes HTML entrypoints from two places: manifest fields (popup, options, side panel, new tab, DevTools) and the special `pages/` folder for routes the manifest does not name directly. Each entry is walked for scripts, styles, and linked assets. You author plain HTML and Extension.js wires the asset graph automatically. ## Template examples ### `new` new template screenshot Minimal new-tab extension with an HTML page entrypoint. ```bash npm theme={null} npx extension@latest create my-extension --template=new ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=new ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=new ``` Repository: [extension-js/examples/new](https://github.com/extension-js/examples/tree/main/examples/new) ### `sidebar` sidebar template screenshot Side panel extension with an HTML page entry for persistent companion UI. ```bash npm theme={null} npx extension@latest create my-extension --template=sidebar ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=sidebar ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=sidebar ``` Repository: [extension-js/examples/sidebar](https://github.com/extension-js/examples/tree/main/examples/sidebar) ## HTML capabilities | Capability | What it gives you | | -------------------- | ------------------------------------------------------------ | | Entrypoint discovery | Compile HTML declared in manifest and `pages/` with one flow | | Asset wiring | Bundle referenced scripts and styles from HTML tags | | Public path handling | Keep intentional public-root assets stable | | Dev tracking | Recompile on HTML and referenced asset edits | ## Supported HTML entrypoints The following manifest fields use HTML files as entrypoints: | Manifest field | File type expected | | ---------------------------------- | ------------------ | | `action.default_popup` | `.html` | | `background.page` | `.html` | | `chrome_url_overrides.*` | `.html` | | `devtools_page` | `.html` | | `options_ui.page` / `options_page` | `.html` | | `page_action.default_popup` | `.html` | | `sandbox.pages` | `.html` | | `side_panel.default_path` | `.html` | | `sidebar_action.default_panel` | `.html` | You can also add standalone pages under `pages/` without expanding manifest entrypoint fields for every file. ## Sample HTML entry ```html theme={null} My Extension Page
``` ## How Extension.js processes HTML For each HTML entrypoint, Extension.js: 1. emits the HTML file into compilation 2. discovers local scripts/styles/static assets from tags 3. bundles scripts/styles as proper extension outputs 4. patches HTML to inject compiled bundle references 5. keeps public-root references (like `/icon.png`) as public assets ## Development behavior * Extension.js tracks HTML and referenced assets in file dependencies, so edits trigger recompilation. * Script changes use hot module replacement (HMR) where supported in the injected script flow. * If the HTML script/style entry list changes (add/remove entries), Extension.js can require a dev server restart. * Changing manifest HTML entrypoints can also trigger restart-required diagnostics. ## Paths and environment variables * Extension.js resolves relative paths from the HTML file location. * Extension.js treats leading `/` paths as extension public-root paths. * Extension.js replaces `$EXTENSION_PUBLIC_*` and related placeholders in emitted HTML during compilation. ## Using extra pages Place extra files in `pages/`: ```plaintext theme={null} pages/ └── extra-page.html ``` ## Common extension surfaces Most user-facing extension surfaces that render UI are just HTML entrypoints with different manifest fields and runtime expectations. Use the surface that matches the user interaction model, then keep the HTML page itself thin. | Surface | Manifest field | Good fit | | ---------------- | ----------------------------------------------------------- | -------------------------------------------------- | | Popup | `action.default_popup` or `browser_action.default_popup` | short, task-focused UI opened from the toolbar | | Options page | `options_ui.page` or `options_page` | durable settings and preferences | | Side panel | `side_panel.default_path` or `sidebar_action.default_panel` | persistent companion UI alongside the current tab | | DevTools page | `devtools_page` | debugging or inspection tools for your extension | | New tab override | `chrome_url_overrides.newtab` | full-screen extension-owned browser surface | | Sandbox page | `sandbox.pages` | isolated HTML when sandboxed execution is required | ### Popup Use a popup for fast, focused actions: * quick status checks * one-step commands * small forms or toggles Keep popups small and resilient. They are often opened and closed frequently, so avoid assuming long-lived in-memory UI state. ### Options page Use an options page when you need to configure durable behavior: * account or integration settings * feature toggles * keyboard or workflow preferences Pair options pages with browser storage so settings survive restarts and can be read by background or content-script code. ### Side panel Use a side panel when the extension needs a persistent workspace next to the current page: * research or note-taking companions * tab-aware assistants * page analysis results that should stay visible while you navigate Side panel HTML is still just an extension page entrypoint. The difference is the surface semantics, not the build pipeline. ### DevTools page Use `devtools_page` when the extension provides tooling for developers: * inspection panels * request or DOM diagnostics * project-specific debugging helpers Treat this as a specialized surface. It usually needs stronger messaging patterns because it often coordinates with background logic and inspected tabs. ### New tab override Use `chrome_url_overrides.newtab` when the extension owns the whole new-tab experience. This is usually the best fit for rich, app-like UIs where a popup would be too constrained. ### Sandbox page Use sandbox pages only when the feature truly needs the sandbox model. They are HTML entrypoints too, but they exist for a narrower set of browser-extension security and execution constraints. ## Surface authoring pattern A good default structure for any HTML surface is: 1. keep the HTML file minimal 2. mount one script entry 3. import styles from that script or a linked stylesheet 4. move browser API coordination into dedicated modules That pattern works well for popup, options, side panel, devtools, and new-tab surfaces. ## Best practices * Keep entry HTML minimal and import app code through scripts. * Use relative paths for project assets and `/` paths only for intentional public assets. * Use `pages/` for additional extension pages that are not primary manifest UI entrypoints. * Treat script/link add/remove changes as structural changes that may require restart. * Pick the surface by user interaction model first, then implement it as a thin HTML page entrypoint. * Keep surface-specific state in storage or background coordination instead of assuming the UI page always stays mounted. ## Next steps * Understand update outcomes in [dev update behavior](/docs/workflows/dev-update-behavior). * Persist settings and state with [Storage](/docs/implementation-guide/storage). * Coordinate UI pages with [Messaging](/docs/implementation-guide/messaging). * Learn more about [special folders](/docs/features/special-folders). * Continue with [CSS in development](/docs/implementation-guide/css). # Extension icons and manifest declarations Source: https://extension.js.org/docs/implementation-guide/icons Declare extension icons in manifest fields for branding and action UI. Extension.js validates paths, rewrites references, and emits icon assets reliably. Keep extension branding and action UI consistent by declaring icons in manifest fields that Extension.js can validate, rewrite, and emit predictably. Extension.js processes icon paths from `manifest.json`, resolves public/relative paths, emits icon assets, and watches icon files during development. ## Template example ### `action` action template screenshot An action extension with toolbar icons declared in `manifest.json`. ```bash npm theme={null} npx extension@latest create my-extension --template=action ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=action ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=action ``` Repository: [extension-js/examples/action](https://github.com/extension-js/examples/tree/main/examples/action) ## Icon capabilities | Capability | What it gives you | | --------------------------- | ----------------------------------------------------------- | | Manifest icon field support | Validate and compile icons for supported extension surfaces | | Path resolution | Handle relative and public-root icon paths consistently | | Dev watch updates | Recompile icon changes during local development | | Browser-safe output | Emit icon assets in extension output with stable references | ## Supported icon fields | Manifest field | File type expected | | ----------------------------- | --------------------- | | `action.default_icon` | .png, .jpg, .svg | | `browser_action.default_icon` | .png, .jpg, .svg | | `icons` | .png, .jpg, .svg | | `page_action.default_icon` | .png, .jpg, .svg | | `sidebar_action.default_icon` | .png, .jpg, .svg (\*) | | `browser_action.theme_icons` | .png, .jpg, .svg | > **Note:** Support for `.svg` is currently partial in some browsers for `sidebar_action.default_icon`. Review browser compatibility before using SVGs in this context. ## Sample icon declaration in `manifest.json` ```json theme={null} { "manifest_version": 3, "name": "My Extension", "version": "1.0.0", "icons": { "16": "icons/icon-16.png", "48": "icons/icon-48.png", "128": "icons/icon-128.png" }, "action": { "default_icon": "icons/action-icon.png" } } ``` ## Output path Typical icon outputs: ```plaintext theme={null} icons/ browser_action/ # for theme_icons ``` ## Path behavior * Extension.js resolves relative icon paths from the manifest folder. * Leading `/` and `public/...` resolve to extension public-root semantics. * Extension.js can watch public-folder assets without re-emitting them through the icon feature itself. ## Development behavior * Changing existing icon files triggers recompilation. * Changing manifest icon entrypoint references can require restarting the dev server. * Missing required icon files produce build errors (some optional icon groups can warn instead). ## Best practices * Declare manifest icon fields explicitly instead of relying on incidental asset imports. * Provide multiple icon sizes (`16`, `32`, `48`, `128`) for sharper UI across browser surfaces. * Keep icon filenames stable to reduce manifest churn during development. * Use public-root paths intentionally and test resulting manifest output paths. ## Next steps * Continue with [web-accessible resources](/docs/implementation-guide/web-accessible-resources). * Learn more about [special folders](/docs/features/special-folders). # Extension implementation guide and runtime model Source: https://extension.js.org/docs/implementation-guide/index Build browser extension features with the Extension.js runtime model. Covers manifest contracts, entrypoints, special folders, and browser constraints. This section explains the contracts that matter when you author real extension features. It covers what `manifest.json` controls, which entrypoints Extension.js rewrites, and where special folders fit. It also explains which behaviors are safe to automate and which are bounded by browser-extension constraints. ## Start by need | Need | Read this first | | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | Understand how one manifest becomes browser-specific output | [manifest.json](/docs/implementation-guide/manifest-json) | | Decide which permissions your feature actually needs | [Permissions and host permissions](/docs/implementation-guide/permissions-and-host-permissions) | | Send data between popup, content scripts, and background | [Messaging](/docs/implementation-guide/messaging) | | Persist settings or cache runtime data | [Storage](/docs/implementation-guide/storage) | | Build page-integrated behavior | [Content scripts](/docs/implementation-guide/content-scripts) | | Build event-driven extension-wide logic | [Background scripts / service worker](/docs/implementation-guide/background) | | Expose packaged assets to pages safely | [Web accessible resources](/docs/implementation-guide/web-accessible-resources) | | Keep output paths predictable | [Predictable path resolution](/docs/features/path-resolution) | ## What this section focuses on * Authoring contracts that are easy to miss from CLI docs alone. * Source-backed runtime behavior that affects how your extension loads and reloads. * Browser-extension architecture concerns that still matter after bundling, such as permissions, messaging, storage, and context boundaries. * Practical guidance whether you work manually or with automation agents. ## Important mental model Extension.js helps with compilation, path normalization, manifest rewriting, and development ergonomics. It does not remove the underlying browser-extension boundaries: * Content scripts still run with page-context constraints. * Background service workers still have lifecycle limitations. * Web-accessible resources still need explicit exposure. * Permissions still determine what the extension is allowed to do. * Messaging and storage still need explicit design. ## Good reading order for a new feature 1. Start with [manifest.json](/docs/implementation-guide/manifest-json) to understand the extension surface you are declaring. 2. Read the feature-specific page, such as [content scripts](/docs/implementation-guide/content-scripts) or [background](/docs/implementation-guide/background). 3. Add supporting pieces: [permissions](/docs/implementation-guide/permissions-and-host-permissions), [messaging](/docs/implementation-guide/messaging), [storage](/docs/implementation-guide/storage), and [web accessible resources](/docs/implementation-guide/web-accessible-resources). 4. Validate path assumptions in [Predictable path resolution](/docs/features/path-resolution). 5. Review [Dev update behavior](/docs/workflows/dev-update-behavior) before changing entrypoints during active watch mode. ## Next steps * Start with [manifest.json](/docs/implementation-guide/manifest-json). * Build page integrations with [Content scripts](/docs/implementation-guide/content-scripts). * Review runtime boundaries in [Security checklist](/docs/workflows/security-checklist). # JavaScript and TypeScript in extensions Source: https://extension.js.org/docs/implementation-guide/javascript Ship JavaScript and TypeScript across background, content scripts, and UI pages with one Extension.js pipeline powered by Rspack and SWC transforms. Ship extension behavior across background, content scripts, and UI pages with one JavaScript/TypeScript pipeline. Extension.js collects script entrypoints from `manifest.json`, HTML pages, and special folders. It compiles them with the default SWC (Speedy Web Compiler)-based setup. ## Template examples ### `javascript` javascript template screenshot Vanilla JavaScript extension with a new-tab page. ```bash npm theme={null} npx extension@latest create my-extension --template=javascript ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=javascript ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=javascript ``` Repository: [extension-js/examples/javascript](https://github.com/extension-js/examples/tree/main/examples/javascript) ### `content` content template screenshot Vanilla JavaScript content script injected into web pages. ```bash npm theme={null} npx extension@latest create my-extension --template=content ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=content ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=content ``` Repository: [extension-js/examples/content](https://github.com/extension-js/examples/tree/main/examples/content) ## JavaScript capabilities | Capability | What it gives you | | --------------------- | -------------------------------------------------------------- | | Shared JS/TS pipeline | Compile extension scripts with one default transformer setup | | Entrypoint discovery | Load script entries from manifest, pages, and special folders | | Runtime-safe outputs | Emit predictable script artifacts for each extension context | | Path normalization | Rewrite supported extension-path literals to output-safe paths | ## Supported script entrypoints | Manifest field | File type expected | | --------------------------- | ---------------------------- | | `background.service_worker` | `.js`, `.ts`, `.mjs`, `.tsx` | | `background.scripts` | `.js`, `.ts`, `.mjs`, `.tsx` | | `content_scripts.js` | `.js`, `.ts`, `.mjs`, `.tsx` | | `user_scripts.api_script` | `.js`, `.ts`, `.mjs`, `.tsx` | ## Sample script declaration in `manifest.json` Here is an example of defining JavaScript files in `manifest.json`: ```json theme={null} { "manifest_version": 3, "name": "My Extension", "version": "1.0.0", "background": { "service_worker": "./scripts/background.ts" }, "content_scripts": [ { "matches": [""], "js": ["./scripts/content-script.ts"] } ] } ``` ## Development behavior * **Content scripts:** Extension.js injects hot module replacement (HMR)/remount flow for fast updates. * **Extension pages:** Page scripts follow page HMR behavior. * **Background/service worker:** script updates can trigger hard extension reload depending on change type. * **Entrypoint list changes:** changing manifest script structure may require dev server restart. ## Script locations and conventions * Use `scripts/` at project root for script-centric extension entries. * Use `pages/` for HTML entrypoints with their own scripts. * Keep manifest-referenced paths stable; avoid moving entry files without updating manifest. ## Transform and bundling defaults * SWC is the default transformer for JS/TS/JSX/TSX. * Extension.js uses browser-first resolution (`browser`, `module`, `main`). * Extension.js emits entries predictably and does not split code into separate runtime chunks by default. * Path resolution rewrites static extension-path literals in supported APIs. ## Dynamic import caveats * Content-script dynamic import has runtime constraints; Extension.js uses loader fallbacks where possible. * Service worker lazy-loading has browser limitations; prefer predictable entry loading in extension runtime-critical code. ## Best practices * Keep entry scripts thin and move feature logic into shared modules. * Prefer static import paths for extension APIs so path resolution can normalize them safely. * Avoid unnecessary dynamic import in content scripts and service workers. * Treat manifest script list edits as structural changes in development workflows. ## Next steps * Understand update outcomes in [dev update behavior](/docs/workflows/dev-update-behavior). * Learn how to structure files with [special folders](/docs/features/special-folders). * Continue with [background scripts and service workers](/docs/implementation-guide/background). # JSON resources in extensions Source: https://extension.js.org/docs/implementation-guide/json Use JSON resources like declarative net request rulesets and managed schemas in extensions. Extension.js validates and emits referenced JSON assets correctly. Use JSON resources for extension features like rulesets and managed schemas with validation and predictable output handling. Extension.js processes manifest-referenced JSON resources and validates them for known feature types. It watches them during development and emits output assets when needed. ## JSON capabilities | Capability | What it gives you | | ------------------------- | ------------------------------------------------------------------------------ | | Manifest resource support | Handle Declarative Net Request (DNR) rulesets and managed schema files cleanly | | JSON validation | Fail early on malformed JSON and invalid resource shape | | Dev dependency tracking | Recompile when referenced JSON resources change | | Placeholder replacement | Resolve supported `$EXTENSION_*` placeholders in emitted JSON | ## JSON support Common manifest fields: | Manifest field | File type expected | | ---------------------------------------- | ------------------ | | `declarative_net_request.rule_resources` | .json | | `storage.managed_schema` | .json | ## Sample JSON declaration in `manifest.json` Here is an example of how to declare a JSON file in `manifest.json`: ```json theme={null} { "manifest_version": 3, "name": "My Extension", "version": "1.0.0", "declarative_net_request": { "rule_resources": [ { "id": "ruleset_1", "enabled": true, "path": "rules/ruleset.json" } ] }, "storage": { "managed_schema": "schema/storage-schema.json" } } ``` ## Output path Extension.js emits JSON resources by feature context (for example, DNR and storage schema outputs): ```plaintext theme={null} declarative_net_request/.json storage/managed_schema.json ``` ## Development behavior * Extension.js tracks manifest-referenced JSON files as file dependencies. * JSON edits trigger recompilation. * Some structural manifest changes related to JSON can require you to restart the dev server. ## Validation behavior Extension.js validates: * JSON syntax for referenced resources * DNR ruleset shape (array-based ruleset expectations) * managed storage schema shape (object-based expectations) ## Environment placeholders Extension.js replaces `$EXTENSION_*` placeholders in emitted `.json` assets during compilation, using the same pass as static HTML placeholder replacement. ## Best practices * Keep manifest-referenced JSON focused on browser-required resource formats. * Validate JSON resources in CI before packaging. * Prefer stable file paths in manifest to reduce restart-required structural changes in development. * For app data consumed by code, consider JSON imports in JS/TS modules separately from manifest resource JSON. ## Next steps * Continue with [icons in development](/docs/implementation-guide/icons). * Learn about [manifest development behavior](/docs/implementation-guide/manifest-json). # Locales and internationalization Source: https://extension.js.org/docs/implementation-guide/locales Ship localized extension metadata and UI strings with _locales handling. Extension.js discovers, validates, and emits locale JSON in dev and production. Extension.js discovers locale files next to your manifest, validates that each declared locale has a `messages.json`, and emits locale JSON assets into the browser-specific build. Edits to any locale file are picked up in dev without a full restart. ## Template example ### `action-locales` action-locales template screenshot See localized extension metadata and UI strings with `_locales` support. ```bash npm theme={null} npx extension@latest create my-extension --template=action-locales ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=action-locales ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=action-locales ``` Repository: [extension-js/examples/action-locales](https://github.com/extension-js/examples/tree/main/examples/action-locales) ## Locale capabilities | Capability | What it gives you | | -------------------- | ------------------------------------------------------------------ | | Locale discovery | Detect `_locales//messages.json` from manifest location | | Validation | Catch missing default locale files and unresolved `__MSG_*__` keys | | Build output mapping | Emit locale files in the expected extension output structure | | Dev watch support | Reload on locale file changes during development | ## Expected structure ```plaintext theme={null} manifest.json _locales/ en/ messages.json fr/ messages.json ``` `default_locale` in `manifest.json` should map to an existing `_locales//messages.json`. ## Sample locales declaration in `manifest.json` Here is how to declare locales in `manifest.json`: ```json theme={null} { "manifest_version": 3, "name": "My Extension", "version": "1.0.0", "default_locale": "en", "description": "__MSG_extension_description__" } ``` You would then include JSON files for each locale inside the `_locales` folder: ```plaintext theme={null} _locales/ └── en/ └── messages.json ``` ## Sample `messages.json` file Here is an example of a `messages.json` file used for translations: ```json theme={null} { "extension_name": { "message": "My Extension" }, "extension_description": { "message": "This is a localized description of my extension." } } ``` ## Output path Extension.js emits locale JSON files under: ```plaintext theme={null} _locales//messages.json ``` ## Development behavior * Extension.js adds locale JSON files to compilation dependencies and watches them. * Locale changes trigger extension reload behavior (hard reload), not component-style hot module replacement (HMR). * Extension.js fails validation with actionable diagnostics when required locale files are missing or invalid. ## Validation behavior Extension.js validates: * `default_locale` presence when the project uses `_locales` * existence of `_locales//messages.json` * JSON validity for locale files * `__MSG_*__` references in manifest against default locale keys ### Troubleshooting missing locale keys If your manifest uses `__MSG_extension_description__`, ensure the default locale file contains `extension_description`: ```json theme={null} { "extension_description": { "message": "Localized extension description" } } ``` If the key is missing in the default locale, build/validation diagnostics surface the mismatch. ## Best practices * Keep `messages.json` keys consistent across locales. * Update default locale first, then propagate keys to other locales. * Validate locale JSON in CI to catch malformed files before packaging. * Keep locale files close to manifest (`manifestDir/_locales`) for predictable resolution. ## Next steps * Understand update outcomes in [dev update behavior](/docs/workflows/dev-update-behavior). * Continue with [JSON in development](/docs/implementation-guide/json). * Learn about [manifest development behavior](/docs/implementation-guide/manifest-json). # Manifest.json compilation and output Source: https://extension.js.org/docs/implementation-guide/manifest-json Treat manifest.json as the source of truth for entrypoints and assets. Extension.js compiles, rewrites paths, and emits a browser-ready manifest per target. Keep extension builds predictable by treating `manifest.json` as the source of truth for entrypoints, assets, and browser-specific behavior. Extension.js compiles your manifest and filters browser-prefixed fields. It rewrites runtime paths, validates referenced files, and emits a ready-to-load manifest per target browser. ## Manifest capabilities | Capability | What it gives you | | -------------------------------- | ------------------------------------------------------------------ | | Browser-specific field filtering | Keep one manifest file while emitting target-specific output | | Path normalization | Resolve runtime-safe output paths automatically | | Reference validation | Fail early when HTML/script/CSS/JSON/icon files are missing | | Targeted outputs | Generate manifest artifacts per browser target in `dist/` | ## Where the manifest is read from * `src/manifest.json` (preferred when present) * `manifest.json` at project root Extension.js does not use `public/manifest.json` as the source manifest. If a project has a `package.json`, Extension.js does not treat `public/manifest.json` as a valid fallback. This guard prevents a copied static asset from overwriting the generated manifest. ## What Extension.js does with it During dev/build, the manifest pipeline: 1. emits the manifest asset from your source file 2. filters browser-prefixed keys for the active browser target 3. applies manifest overrides/path normalization for extension outputs 4. validates referenced files (HTML/scripts/CSS/icons/JSON) and fails early when missing ## One manifest, multiple browsers Browser-prefixed keys let you keep one manifest file while still targeting browser-specific behavior: * `chromium:*`, `chrome:*`, `edge:*` * `firefox:*`, `gecko:*` These prefixes can apply to top-level keys and nested manifest fields. Examples: * `chromium:key` * `background.firefox:scripts` * `background.chromium:service_worker` ### Supported manifest fields Common entrypoint-related fields include: | Manifest field | File type expected | | ---------------------------------------- | ---------------------------------- | | `action.default_popup` | .html | | `background.page` | .html | | `background.service_worker` | .js, .jsx, .ts, .tsx, .mjs | | `browser_action.default_popup` | .html | | `chrome_url_overrides.bookmarks` | .html | | `chrome_url_overrides.history` | .html | | `chrome_url_overrides.newtab` | .html | | `content_scripts.js` | .js, .jsx, .ts, .tsx, .mjs | | `content_scripts.css` | .css, .scss, .sass, .less | | `declarative_net_request.rule_resources` | .json | | `devtools_page` | .html | | `icons` | .png, .jpg, ...Other image formats | | `options_ui.page` | .html | | `options_page` | .html | | `page_action.default_popup` | .html | | `sandbox.pages` | .html | | `side_panel.default_path` | .html | | `sidebar_action.default_panel` | .html | | `storage.managed_schema` | .json | | `theme_icons` | .png, .jpg, ...Other image formats | | `user_scripts.api_script` | .js, .jsx, .ts, .tsx, .mjs | | `web_accessible_resources` | .png, .jpg, .css, .js | ## Permissions design The manifest is also where your extension declares what it is allowed to do. Extension.js compiles the manifest, but you still need good permission design. * keep `permissions` small and intentional * keep `host_permissions` as narrow as the feature allows * move non-core capabilities into `optional_permissions` or `optional_host_permissions` where possible * review permission scope whenever content-script matches or background capabilities change For permission strategy, see [Permissions and host permissions](/docs/implementation-guide/permissions-and-host-permissions). ## Output behavior Extension.js rewrites manifest paths to canonical output locations when needed. Two important examples: * `background.service_worker` becomes `background/service_worker.js` * `side_panel.default_path` becomes `sidebar/index.html` Extension.js also normalizes content scripts by manifest entry index: * `content_scripts/content-0.js` * `content_scripts/content-0.css` Those emitted paths are the browser-facing contract. Use source paths in authoring, then let Extension.js rewrite them for output. ## Development behavior * When `manifest.json` changes, Extension.js recompiles and triggers extension hard reload flow. * If manifest entrypoint structure changes (for example, script list changes), Extension.js may require a dev server restart. * Missing files referenced by manifest fields fail compilation with manifest-focused errors. ### Change outcome matrix | Manifest change type | Typical outcome | | ----------------------------------------------------------------------------- | ----------------------------------- | | Update non-structural values (for example, descriptions/permissions metadata) | Hard reload flow | | Update asset path values that still resolve cleanly | Recompile + hard reload flow | | Add/remove script or page entrypoints in manifest | Restart required | | Introduce invalid/missing referenced files | Build error (fix first, then rerun) | ## Best practices * Keep manifest paths relative to the extension source/output model and use leading `/` only when you mean extension output root. * Use browser-prefixed keys instead of maintaining separate manifest files per browser. * Keep entrypoint changes deliberate; adding/removing manifest scripts often changes reload semantics in dev. * Validate icons, JSON resources, and content script assets as part of continuous integration (CI) to catch path regressions early. * Do not place `manifest.json` under `public/`. ## Next steps * Understand update outcomes in [dev update behavior](/docs/workflows/dev-update-behavior). * Design least-privilege access in [Permissions and host permissions](/docs/implementation-guide/permissions-and-host-permissions). * Learn how Extension.js handles [browser-specific fields](/docs/features/browser-specific-fields). * Understand dev update flow in [page reload and hot module replacement (HMR)](/docs/features/reload-and-hmr). # Extension messaging across contexts Source: https://extension.js.org/docs/implementation-guide/messaging Move data across extension contexts with browser messaging APIs. Keep privilege boundaries clear between background, content scripts, and extension pages. Move data across extension contexts explicitly and validate every privileged boundary. Extension.js bundles background scripts, content scripts, and extension pages together. Those contexts still communicate through browser-extension messaging APIs. Good messaging structure keeps your privilege boundaries clear and makes debugging more reliable. ## Context boundaries | From | To | Typical API | | ---------------------------------- | --------------------------------------------- | ---------------------------------------------- | | Popup or options page | Background service worker | `runtime.sendMessage()` | | Content script | Background service worker | `runtime.sendMessage()` or `runtime.connect()` | | Background service worker | Content script | `tabs.sendMessage()` | | Long-lived streaming or state sync | Background service worker and another context | `runtime.connect()` | ## Recommended contract Treat messages as a typed protocol: * include an explicit `type` * validate payload shape before acting * validate sender context for privileged operations * return structured success and error responses ```ts theme={null} type Message = | {type: 'settings:get'} | {type: 'settings:set'; payload: {theme: 'light' | 'dark'}} chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if ( !message || typeof message !== 'object' || typeof message.type !== 'string' ) { sendResponse({ok: false, error: 'invalid_message'}) return } if (message.type === 'settings:get') { sendResponse({ok: true, data: {theme: 'dark'}}) return } if (message.type === 'settings:set') { sendResponse({ok: true}) return } sendResponse({ok: false, error: 'unknown_message'}) }) ``` ## When to use `sendMessage` vs `connect` | Use case | Prefer | | -------------------------------------------------- | ----------------------- | | One request and one response | `runtime.sendMessage()` | | Long-lived stream or multiple updates | `runtime.connect()` | | Communicating with a specific tab's content script | `tabs.sendMessage()` | ## Good architecture * Keep one message handler module per feature area instead of one giant switch file. * Centralize privileged work in the background service worker. * Use content scripts to collect page data, then forward only the minimum required payload. * Keep popup and options pages thin by delegating browser API calls to background code. ## Security rules * Never trust page-derived content just because it came from a content script. * Validate sender identity before performing privileged actions. * Reject unknown message types explicitly. * Avoid exposing generic "run anything" or "fetch anything" commands through messaging. ## Common mistakes * Letting UI pages call privileged APIs directly in many places instead of routing through background. * Sending large arbitrary page snapshots when a small structured payload would be enough. * Treating content scripts as trusted because they are your code, even though they see untrusted page data. * Forgetting to version or migrate message shapes when multiple contexts evolve together. ## Practical patterns ### Popup requests current state Use a single request-response message from popup to background. ### Content script requests privileged work Collect the smallest possible page payload, send it to background, and let background decide whether the action is allowed. ### Live subscription Use `runtime.connect()` only when you truly need continuous updates, not for simple request-response flows. ## Next steps * Persist data with [Storage](/docs/implementation-guide/storage). * Design event-driven logic in [Background scripts / service worker](/docs/implementation-guide/background). * Review boundary safety in [Security checklist](/docs/workflows/security-checklist). # Permissions and host permissions Source: https://extension.js.org/docs/implementation-guide/permissions-and-host-permissions Design least-privilege permission sets for your extension. Covers required, optional, and host permissions to improve user trust and review outcomes. Keep your extension capable without asking for more privilege than the feature requires. Extension.js compiles and validates your extension, but the browser still enforces `permissions`, `host_permissions`, and `optional_*` fields from `manifest.json`. Good permission design improves user trust and reduces store review friction. ## Permission strategy | Need | Prefer | | ------------------------------------------------------ | --------------------------- | | Core API access required for the extension to function | `permissions` | | Host access required on install | `host_permissions` | | Capability that can be requested later | `optional_permissions` | | Domain access that only some users need | `optional_host_permissions` | ## Common patterns ### Background-driven feature Use `permissions` for the browser APIs you need and add only the host patterns required by the feature: ```json theme={null} { "manifest_version": 3, "permissions": ["storage", "tabs", "scripting"], "host_permissions": ["https://example.com/*"] } ``` ### Optional capability If a feature is not required for the first-run experience, keep it optional: ```json theme={null} { "manifest_version": 3, "permissions": ["storage"], "optional_permissions": ["notifications"], "optional_host_permissions": ["https://api.example.com/*"] } ``` ## Practical rules * Ask for API permissions only when the feature truly needs them. * Keep `host_permissions` scoped to the smallest working set of origins. * Prefer optional permissions for secondary or premium features. * Re-audit permissions whenever you add background actions, content-script injection, or remote API calls. ## Permission design by feature type | Feature | Usually involves | | ------------------------------------------ | ------------------------------------------------------- | | Content-script enhancement on a known site | `host_permissions` for that site, sometimes `scripting` | | Popup or options UI only | often no host permissions at all | | Programmatic injection | `scripting` plus matching host permissions | | Cross-tab workflows | `tabs` and sometimes `activeTab` | | Persisted settings | `storage` | | Notifications or badges | `notifications`, `action`-related manifest fields | ## Common mistakes * Using broad wildcards like `` when only one or two origins are needed. * Mixing `host_permissions` into a feature that could instead use a narrower user-triggered workflow. * Forgetting that content scripts, web accessible resource (WAR) access, and programmatic injection all have different security implications. * Documenting permissions in feature docs without explaining why each permission exists. ## Review checklist 1. List every user-facing feature. 2. Map each feature to the exact API and host permissions it needs. 3. Move non-core permissions to optional permissions where possible. 4. Remove stale permissions left over from old experiments. 5. Re-test install prompts and browser-store expectations after changes. ## Next steps * Keep one manifest accurate in [manifest.json](/docs/implementation-guide/manifest-json). * Review page access in [Web accessible resources](/docs/implementation-guide/web-accessible-resources). * Validate boundary handling in [Messaging](/docs/implementation-guide/messaging). * Do a release pass with [Security checklist](/docs/workflows/security-checklist). # Browser storage APIs for extensions Source: https://extension.js.org/docs/implementation-guide/storage Persist settings and runtime state with browser storage APIs. Covers local, sync, and session storage choices and their impact on service worker reliability. Extension.js does not replace or wrap browser storage APIs — it only compiles the code that calls them. The choice of storage area still matters though, because it shapes service-worker reliability, cross-browser parity, and how painful future schema migrations become. ## Choose the right storage area | Storage area | Use when | Notes | | ----------------- | ------------------------------------------------ | ----------------------------------------- | | `storage.local` | durable extension state on one device | best default for most extension data | | `storage.sync` | small user preferences across signed-in browsers | quota-sensitive and slower than local | | `storage.session` | short-lived session state | useful for ephemeral runtime coordination | | `storage.managed` | enterprise-managed policy values | schema-driven, read-only to the extension | ## Recommended defaults * Use `storage.local` for feature state, caches, and durable settings. * Use `storage.sync` only for small, user-meaningful preferences. * Use `storage.session` for state that should not survive a full browser restart. * Treat `storage.managed` as a separate enterprise path, not your general settings mechanism. ## Service-worker-friendly patterns Manifest V3 (MV3) background service workers can stop and restart between events, so do not rely on in-memory state alone. * Restore critical state from storage on demand. * Cache in memory only as an optimization. * Keep startup work small so event handling stays responsive. * Persist versioned settings changes rather than assuming a singleton background runtime. ## Example settings module ```ts theme={null} const SETTINGS_KEY = 'settings' export async function getSettings() { const result = await chrome.storage.local.get(SETTINGS_KEY) return result[SETTINGS_KEY] ?? {theme: 'system'} } export async function setSettings(nextSettings: { theme: 'system' | 'light' | 'dark' }) { await chrome.storage.local.set({[SETTINGS_KEY]: nextSettings}) } ``` ## Storage design checklist 1. Decide which values must survive browser restarts. 2. Decide which values should sync across devices, if any. 3. Version your stored data shape before the first release. 4. Keep a migration path for renamed keys or changed structures. 5. Avoid storing secrets in client-readable extension storage unless you fully accept that any extension code can read them. ## Managed storage If you use `storage.managed`, pair your runtime code with a valid `storage.managed_schema` file in `manifest.json`. Extension.js validates and emits that schema file to the output path. For the JSON resource details, see [JSON](/docs/implementation-guide/json). ## Common mistakes * Using `storage.sync` for large data sets or caches. * Assuming background in-memory variables are durable in MV3. * Storing multiple disconnected keys without a versioning strategy. * Mixing enterprise-managed storage with user-editable settings in the same mental model. ## Next steps * Declare schema-backed enterprise storage in [manifest.json](/docs/implementation-guide/manifest-json). * Review resource handling in [JSON](/docs/implementation-guide/json). * Move state between contexts with [Messaging](/docs/implementation-guide/messaging). # Web accessible resources and build-time merging Source: https://extension.js.org/docs/implementation-guide/web-accessible-resources Expose only the extension assets web pages need. Extension.js merges user-declared web_accessible_resources with build-time requirements per browser target. Expose only the extension assets that web pages or external extension contexts must access. Extension.js merges user-declared `web_accessible_resources` with build-discovered requirements and applies target-specific manifest normalization. ## Template example ### `content` content template screenshot Content scripts use web-accessible resources to inject styles and assets into pages. ```bash npm theme={null} npx extension@latest create my-extension --template=content ``` ```bash pnpm theme={null} pnpx extension@latest create my-extension --template=content ``` ```bash yarn theme={null} yarn dlx extension@latest create my-extension --template=content ``` Repository: [extension-js/examples/content](https://github.com/extension-js/examples/tree/main/examples/content) ## Web-accessible resource capabilities | Capability | What it gives you | | ------------------------------ | -------------------------------------------------------------------------------------- | | Manifest V2/V3 schema handling | Support both array and object `web_accessible_resources` shapes by manifest version | | Build-aware merging | Combine declared resources with required build/runtime entries | | Path normalization | Resolve manifest web accessible resources (WAR) paths to target-safe output references | | Security control surface | Keep access rules explicit by `resources` and `matches` | ## Manifest schema ### Manifest V3 ```json theme={null} { "web_accessible_resources": [ { "resources": ["images/*.png", "fonts/*.woff2"], "matches": [""] } ] } ``` ### Manifest V2 ```json theme={null} { "web_accessible_resources": ["images/*.png", "scripts/*.js", "styles/*.css"] } ``` ## Sample `manifest.json` file If you import assets through supported content-script or build paths, Extension.js adds the required WAR entries automatically. For explicit external access, declare resources manually. ```json theme={null} { "manifest_version": 3, "name": "My Extension", "version": "1.0.0", "web_accessible_resources": [ { "resources": ["images/*.png", "scripts/*.js", "styles/*.css"], "matches": [""] } ], "content_scripts": [ { "matches": ["https://example.com/*"], "css": ["styles/content.css"], "js": ["scripts/content.js"] } ] } ``` ## What Extension.js adds automatically Extension.js can expand WAR entries from build output when the runtime needs those assets to be page-accessible. Common examples include: * assets imported by content scripts * content-script CSS outputs * emitted fonts or static assets referenced by those entries * development-only runtime support needed for reload behavior Automatic inclusion is convenient, but you should still review the final exposure surface. ## Development behavior * In development, Extension.js patches WAR entries to support reload and hot module replacement (HMR) asset access. * Extension.js can auto-include content-script related assets in WAR when needed. * Extension.js normalizes paths by removing the `public/` prefix and leading `/` to produce output-safe paths. ## Output and resolution notes * Extension.js copies resources in `public/` through the public-asset flow, and you can declare them from extension root paths. * Relative WAR resource paths resolve from the manifest folder. * You can use globs, but match patterns must be valid for target browser constraints. * Extension.js drops Manifest V3 (MV3) entries without `resources` during normalization. * Extension.js preserves globs as-is and does not expand them into explicit file lists. ## Runtime usage Use browser runtime URL helpers instead of constructing extension URLs manually: ```ts theme={null} const logoUrl = chrome.runtime.getURL('/images/logo.png') ``` If a page or content script cannot read an asset you expected to be accessible, check: 1. whether the asset is actually present in the output 2. whether the WAR entry exposes the correct `resources` 3. whether the `matches` scope includes the page that is trying to read it 4. whether the asset should instead stay private to extension pages only ## Best practices * Keep WAR entries minimal; expose only what external contexts must read. * Prefer explicit resource paths over broad globs for tighter security posture. * Validate WAR match patterns and resource paths across Chromium and Firefox targets. * Use `chrome.runtime.getURL()`/`browser.runtime.getURL()` for runtime-safe URL creation. * Re-audit WAR after adding content-script imports, fonts, or page-accessible static assets. ### Security checklist * Limit `matches` scope to domains that actually require access. * Avoid broad wildcard resource globs when a small explicit list is sufficient. * Keep WAR entries focused on non-sensitive assets. * Re-audit WAR when adding new content script imports or runtime asset lookups. ### Good vs risky examples ```json theme={null} { "web_accessible_resources": [ { "resources": ["images/logo.png", "fonts/inter.woff2"], "matches": ["https://example.com/*"] } ] } ``` ```json theme={null} { "web_accessible_resources": [ { "resources": ["*"], "matches": [""] } ] } ``` ## Next steps * Understand update outcomes in [dev update behavior](/docs/workflows/dev-update-behavior). * Learn more about [special folders](/docs/features/special-folders). * Continue with [commands reference](/docs/commands/dev). # What is Extension.js? Introduction and overview Source: https://extension.js.org/docs/index Learn what Extension.js does, how to choose the right CLI command, and where to start based on your browser extension development workflow. Build browser extensions for Chrome, Edge, and Firefox with one modern workflow. Extension.js handles manifest compilation, browser-specific output, reload behavior, and packaging so you can focus on product features. Use familiar web tooling like [TypeScript](/docs/languages-and-frameworks/typescript), [React](/docs/languages-and-frameworks/react), [Vue](/docs/languages-and-frameworks/vue), and [Tailwind CSS](/docs/integrations/tailwindcss). Keep direct access to native extension APIs. **CLI version:** Run `extension --version` (or `npx extension@latest --version`) for the build you are using. The [extension package on npm](https://www.npmjs.com/package/extension) lists the latest publish, and [GitHub releases](https://github.com/extension-js/extension.js/releases) track changelog-style notes without tying these docs to a specific patch number. ## Watch the workflow