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.
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.
framework
Section titled “framework”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.
browsers
Section titled “browsers”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.
manifest
Section titled “manifest”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.
name and version
Section titled “name and version”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).
action (popup)
Section titled “action (popup)”manifest: { action: { defaultPopup: 'src/ui/popup/popup.html', defaultTitle: 'My Extension', },}sidePanel
Section titled “sidePanel”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.
optionsPage
Section titled “optionsPage”manifest: { optionsPage: 'src/ui/options/options.html',}background.serviceWorker
Section titled “background.serviceWorker”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.
contentScripts
Section titled “contentScripts”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'.
webAccessibleResources
Section titled “webAccessibleResources”manifest: { webAccessibleResources: [ { resources: ['assets/*'], matches: ['https://*.example.com/*'], }, ],}permissions
Section titled “permissions”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/.
Per-browser manifest overrides
Section titled “Per-browser manifest overrides”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
Section titled “plugins”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.
Worked example
Section titled “Worked example”A config for an extension with a popup, a content script, a side panel, and two browser targets:
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, },});Config resolution order
Section titled “Config resolution order”extforge.config.ts(or.js/.mjs) is loaded from the project root.- Default values fill in missing fields.
- The Zod schema validates the result; unknown keys are passed through (
passthrough()). - Built-in plugins (
presetReact(), etc.) are prepended to the plugin list. onConfigResolvedhooks from all plugins fire.
If the config file is missing, loadExtForgeConfig falls back to the defaults and logs a warning. It does not error unless EXTFORGE_STRICT_CONFIG=1 is set.