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' | CssProcessor
Default: 'tailwind'
The CSS strategy applied to src/styles/globals.css and src/styles/content.css:
'tailwind'— compiles each stylesheet with the Tailwind CLI (minified in production).extforge initalso scaffoldstailwind.config.jsandpostcss.config.js. Falls back to a plain copy if the CLI isn’t installed.'vanilla'— copies the stylesheets through untouched (plain CSS).'none'— same copy-through behaviour; use it when you handle CSS yourself.- A custom
CssProcessorobject — bring any toolchain (Sass, Lightning CSS, UnoCSS, a PostCSS pipeline…) via an inlinetransformfunction or a CLIcommand.
// Inline transformexport default defineConfig({ css: { name: 'my-minifier', transform: ({ code, dev }) => dev ? code : minify(code) },});For the full set of options — custom processors, CLI commands, and the onCssTransform plugin hook — see the Styling guide.
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/*'], excludeMatches: ['https://*.example.com/admin/*'], js: ['src/content/index.ts'], css: ['src/content/style.css'], runAt: 'document_idle', allFrames: true, matchAboutBlank: true, world: 'ISOLATED', }, ],}| Key | Manifest key | Notes |
|---|---|---|
matches |
matches |
Required. |
excludeMatches |
exclude_matches |
Patterns subtracted from matches. |
js / css |
js / css |
|
runAt |
run_at |
'document_start' | 'document_end' | 'document_idle'. Defaults to 'document_idle'. |
allFrames |
all_frames |
Inject into every frame, not just the top one — what iframe-aware inspectors and overlays need. |
matchAboutBlank |
match_about_blank |
Also inject into about:blank / about:srcdoc frames whose creator matches. |
world |
world |
'MAIN' runs in the page’s own JS realm (no extension APIs); 'ISOLATED' is the default sandbox. |
Only runAt has a default. Every other optional key is omitted from the generated manifest when unset, so the browser’s own default applies and manifests for configs that don’t use them are unchanged.
world needs Chrome/Edge 111+, Firefox 128+ or Safari 18+. Because Firefox below 128 ignores the key silently — running a 'MAIN' script in the isolated world instead — declaring world raises the Firefox build’s browser_specific_settings.gecko.strict_min_version from 109.0 to 128.0 rather than shipping a manifest Firefox would quietly misread.
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 (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.