extro

Manifest Generation

How Extro turns your file tree and config into a complete Manifest V3.

You never write manifest.json. Extro generates it from three inputs: the surfaces in your tree, your extro.config.ts, and your package.json. This page shows what you write and what comes out.

Identity

Identity fields resolve from extro.config.ts first, then package.json:

Manifest fieldFrom configFallback
namenamepackage.json name
versionversionpackage.json version
descriptiondescriptionpackage.json description
iconsiconsthe icons/ directory
extro.config.ts
import { defineConfig } from "extrojs"

export default defineConfig({
  name: "Cool Tabs",
  description: "Tabs, but cool.",
})
manifest.json (generated)
{
  "manifest_version": 3,
  "name": "Cool Tabs",
  "description": "Tabs, but cool.",
  "version": "0.1.0"
}

Surfaces

Each surface present in your tree contributes its manifest fields automatically: action for a popup, options_ui for options, side_panel for a side panel, background.service_worker for a background script, content_scripts for a content script. See Surfaces for each fragment.

Permissions

extro.config.ts
export default defineConfig({
  permissions: ["storage", "alarms"],
  hostPermissions: ["https://api.example.com/*"],
})

If you omit these, Extro adds defaults based on the surfaces present:

When presentDefault
background/permissions: ["storage"]
content/host_permissions matching the content script's matches

Supplying your own permissions (or hostPermissions) replaces the defaults entirely; the lists are yours.

Content script matches

extro.config.ts
export default defineConfig({
  content: { matches: ["https://example.com/*"] },
})

content.matches controls which URLs the content script (and CSUI) is injected into. Defaults to ["<all_urls>"].

How fields resolve

The manifest is built in layers, each overriding the last:

framework defaults  →  promoted fields (name, permissions, ...)  →  EXTRO_CRX_KEY  →  manifest {}  →  transformManifest()

Reach for the lowest layer that does the job: a promoted field if one exists, manifest for a field Extro doesn't model, transformManifest for anything computed.

The manifest escape hatch

A raw Partial<ManifestV3> merged over the promoted fields:

extro.config.ts
export default defineConfig({
  manifest: {
    commands: {
      _execute_action: { suggested_key: { default: "Ctrl+Shift+E" } },
    },
  },
})

transformManifest

The final hook. It receives the fully generated manifest and may mutate it or return a replacement:

extro.config.ts
export default defineConfig({
  transformManifest(manifest) {
    manifest.minimum_chrome_version = "114"
    if (process.env.EXTRO_PUBLIC_BETA) {
      manifest.name = `${manifest.name} (beta)`
    }
  },
})

Pitfall

transformManifest runs last, so it can override anything Extro generated, including the relaxed content security policy that dev mode needs. If you set content_security_policy here, HMR can stop working in extro dev.

Pinning the extension ID

Set the EXTRO_CRX_KEY environment variable to a base64 public key and Extro writes it to manifest.key, pinning a stable extension ID across machines and reloads. Useful for OAuth redirect URIs and externally_connectable. See Environment Variables.

Dev mode differences

In extro dev, the generated manifest differs from prod in two ways: the content security policy allows the Vite dev server and the dev WebSockets, and a background service worker is always present (it hosts the dev bridge, even if you have no background file). The tabs permission is also added in dev for reloads, even when you supply your own permissions list. None of this ships in extro build.

On this page