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/*'], 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 (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.