Skip to content

Configuration

ExtForge reads its config from extforge.config.ts (or .js / .mjs) in the project root. The file must export a default config object — use defineConfig for TypeScript inference.

extforge.config.ts
import { defineConfig } from 'extforge';
export default defineConfig({
browsers: ['chrome', 'firefox'],
framework: 'react',
css: 'tailwind',
manifest: {
name: 'My Extension',
version: '1.0.0',
// ...
},
});

defineConfig is an identity function — it returns the object unchanged but gives your editor full TypeScript autocompletion.


Type: 'react' | 'vanilla'
Default: 'react'

Setting framework drives automatic plugin injection. When framework: 'react', ExtForge injects presetReact() before any user plugins so all entry points get the correct JSX transform without extra configuration.

export default defineConfig({
framework: 'react', // presetReact() injected automatically
});

See preset-react for the options you can pass if you need to override the defaults (e.g. Preact or classic runtime).


Type: 'tailwind' | 'vanilla' | 'none'
Default: 'tailwind'

css: 'tailwind' scaffolds a postcss.config.js and tailwind.config.js and assumes Tailwind is installed. It does not inject a Tailwind plugin itself — PostCSS handles that at esbuild’s CSS entry point. Set 'vanilla' to opt out of any preset but keep the CSS pipeline. Set 'none' to disable CSS processing entirely.


Type: Array<'chrome' | 'firefox' | 'edge' | 'safari'>
Default: ['chrome', 'firefox']

Declares which browsers to build for. extforge build iterates this list and produces a separate output under dist/<browser>/. extforge dev defaults to chrome; pass --browser firefox to target a different browser in dev mode.

export default defineConfig({
browsers: ['chrome', 'firefox', 'edge', 'safari'],
});

Each browser gets its own manifest generated from the shared manifest config plus any per-browser overrides. Duplicate entries are deduplicated automatically.

See /reference/config/browsers/ for the full spec.


The manifest object maps to the Manifest V3 fields ExtForge understands. ExtForge generates the final manifest.json from this config — you never write manifest.json by hand.

See /reference/config/manifest/ for the complete field list.

manifest: {
name: 'My Extension',
version: '1.0.0',
description: 'Does something useful.',
}

version must be a dot-separated integer string (e.g. 1.2.3 or 1.2.3.4 for Chrome’s four-part format).

manifest: {
action: {
defaultPopup: 'src/ui/popup/popup.html',
defaultTitle: 'My Extension',
},
}
manifest: {
sidePanel: {
defaultPath: 'src/ui/sidepanel/sidepanel.html',
},
}

sidePanel is Chrome/Edge-only. If you list Firefox in browsers, ExtForge omits the side_panel key from that browser’s manifest automatically. Use a per-browser override if you want a fallback popup on Firefox.

manifest: {
optionsPage: 'src/ui/options/options.html',
}
manifest: {
background: {
entrypoint: 'src/background/index.ts',
},
}

ExtForge emits service_worker on Chrome/Edge/Safari and scripts on Firefox (MV3). You do not need a per-browser override for this field — the manifest generator handles it using the browser capability matrix. See the cross-browser guide for details.

An array of content script declarations. Each entry maps to a content_scripts object in the generated manifest.

manifest: {
contentScripts: [
{
matches: ['https://*.example.com/*'],
js: ['src/content/index.ts'],
css: ['src/content/style.css'],
runAt: 'document_idle',
},
],
}

runAt accepts 'document_start', 'document_end', or 'document_idle'.

manifest: {
webAccessibleResources: [
{
resources: ['assets/*'],
matches: ['https://*.example.com/*'],
},
],
}
manifest: {
permissions: {
required: ['storage', 'activeTab', 'scripting'],
optional: ['tabs'],
host: ['https://*.example.com/*'],
},
}

required permissions are listed under permissions in the manifest. optional maps to optional_permissions. host patterns go under host_permissions (MV3).

The full permission list is in /reference/config/manifest/.

Use manifest.browserOverrides to merge browser-specific fields into the generated manifest. Overrides are shallow-merged over the base manifest.

manifest: {
background: {
entrypoint: 'src/background/index.ts',
},
browserOverrides: {
firefox: {
firefoxId: 'my-extension@example.com',
},
safari: {
// Safari doesn't support sidePanel; nothing to override here —
// ExtForge omits it automatically.
},
},
}

See cross-browser guide for common override patterns.


build: {
outDir: 'dist', // output root; per-browser dirs land under here
srcDir: 'src', // source root for resolving relative paths
sourcemap: false, // generate source maps (true in dev)
esbuild: { // pass-through to esbuild BuildOptions
target: 'es2020',
define: { 'process.env.NODE_ENV': '"production"' },
},
}

esbuild is a free-form object that merges into every esbuild invocation. Use it for define, target, external, minify, or any other esbuild option. Plugin hooks can override per-entry options via onBuildEntry.

See /reference/config/build/ for all fields.


dev: {
port: 35729, // HMR WebSocket port
host: 'localhost', // HMR host
debounce: 150, // file-change debounce in ms
open: false, // open browser on start
strictCompat: false, // treat compat warnings as errors in dev
}

debounce controls how long ExtForge waits after the last file change before triggering a reload. Raising it can help if file writes trigger multiple events in quick succession.

See /reference/config/dev/ and the HMR guide.


plugins: [
myPlugin(),
anotherPlugin({ option: true }),
]

User plugins are appended after built-in plugins. The framework preset (presetReact(), etc.) always runs first.

See the plugins guide for writing plugins, or /reference/plugins/api/ for the hook reference.


A config for an extension with a popup, a content script, a side panel, and two browser targets:

extforge.config.ts
import { defineConfig } from 'extforge';
export default defineConfig({
browsers: ['chrome', 'firefox'],
framework: 'react',
css: 'tailwind',
manifest: {
name: 'Page Annotator',
version: '1.0.0',
description: 'Annotate any web page.',
action: {
defaultPopup: 'src/ui/popup/popup.html',
defaultTitle: 'Page Annotator',
},
sidePanel: {
defaultPath: 'src/ui/sidepanel/sidepanel.html',
},
background: {
entrypoint: 'src/background/index.ts',
},
contentScripts: [
{
matches: ['<all_urls>'],
js: ['src/content/index.ts'],
css: ['src/content/style.css'],
runAt: 'document_idle',
},
],
permissions: {
required: ['storage', 'activeTab', 'scripting', 'sidePanel'],
optional: ['tabs'],
host: [],
},
webAccessibleResources: [
{
resources: ['assets/*'],
matches: ['<all_urls>'],
},
],
browserOverrides: {
firefox: {
// Firefox does not support sidePanel; provide a fallback action popup
action: {
defaultPopup: 'src/ui/sidepanel/sidepanel.html',
defaultTitle: 'Page Annotator',
},
firefoxId: 'page-annotator@example.com',
},
},
},
build: {
outDir: 'dist',
srcDir: 'src',
sourcemap: false,
esbuild: {
target: 'es2020',
},
},
dev: {
port: 35729,
debounce: 150,
},
});

  1. extforge.config.ts (or .js / .mjs) is loaded from the project root.
  2. Default values fill in missing fields.
  3. The Zod schema validates the result; unknown keys are passed through (passthrough()).
  4. Built-in plugins (presetReact(), etc.) are prepended to the plugin list.
  5. onConfigResolved hooks from all plugins fire.

If the config file is missing, loadExtForgeConfig falls back to the defaults (which are valid) and logs a warning — no error.

If the config is present but invalid, validation fails hard by default: loadExtForgeConfig throws an extforge.config is invalid error. Set EXTFORGE_STRICT_CONFIG=0 to downgrade validation failures to a warning while you migrate.