How manifest.ts Generates the Chrome Extension manifest.json in the React-Vite Boilerplate
The boilerplate generates the Chrome extension manifest.json from TypeScript using a custom Vite plugin that compiles manifest.ts, optionally transforms it for Firefox compatibility, and writes the final JSON to the output directory.
The jonghakseo/chrome-extension-boilerplate-react-vite repository treats the extension manifest as a TypeScript module rather than static JSON, enabling type safety and dynamic configuration. This approach allows developers to import project metadata directly from package.json and maintain a single source of truth for browser compatibility. Understanding how manifest.ts generates the final manifest.json is essential for customizing permissions, content scripts, and cross-browser deployment.
The Manifest Generation Pipeline
1. Defining the Manifest Source in manifest.ts
The generation process starts at chrome-extension/manifest.ts, which exports a typed JavaScript object that satisfies the shared ManifestType interface. This file imports the project’s package.json to dynamically inject the version number and declares all required Manifest V3 fields—including action, content_scripts, side_panel, and permissions—as a native TypeScript object.
// chrome-extension/manifest.ts
import { readFileSync } from 'node:fs';
import type { ManifestType } from '@extension/shared';
const packageJson = JSON.parse(readFileSync('./package.json', 'utf8'));
const manifest = {
manifest_version: 3,
default_locale: 'en',
name: '__MSG_extensionName__',
version: packageJson.version,
// ... additional fields
} satisfies ManifestType;
export default manifest;
Source: lines 1-85 of manifest.ts in the repository.
2. Wiring the Plugin into Vite (vite.config.mts)
The Vite configuration for the Chrome extension (chrome-extension/vite.config.mts) registers the custom make-manifest-plugin within the plugins array. This integration ensures the manifest generation runs during the build lifecycle.
import makeManifestPlugin from './utils/plugins/make-manifest-plugin.js';
export default defineConfig({
plugins: [
// ... other plugins
makeManifestPlugin({ outDir }), // Registers the generator
],
});
Source: lines 25-31 of vite.config.mts.
3. The make-manifest-plugin.ts Engine
Located at chrome-extension/utils/plugins/make-manifest-plugin.ts, this plugin orchestrates the conversion from TypeScript to JSON through several distinct phases:
Locating the Compiled Module
The plugin resolves the compiled JavaScript output (not the raw TypeScript) using import.meta.dirname to locate manifest.js in the file system:
const manifestFile = resolve(import.meta.dirname, '..', '..', 'manifest.js');
Source: line 11.
Cache-Busting Dynamic Import
To ensure Vite always loads the latest version during development, the plugin appends a timestamp query parameter to bypass module caching. It also handles Windows file paths by converting them to file:// URLs:
const getManifestWithCacheBurst = async () => {
const withCacheBurst = (path: string) => `${path}?${Date.now()}`;
if (platform === 'win32') {
return (await import(withCacheBurst(pathToFileURL(manifestFile).href))).default;
} else {
return (await import(withCacheBurst(manifestFile))).default;
}
};
Source: lines 27-38.
Writing the Final JSON
During the writeBundle lifecycle hook, the plugin invokes makeManifest, which uses ManifestParser.convertManifestToString to serialize the object and writes it to the output directory:
writeFileSync(
manifestPath,
ManifestParser.convertManifestToString(manifest, IS_FIREFOX)
);
Source: line 61.
Plugin Lifecycle Hooks
The plugin implements two critical Vite hooks:
return {
name: 'make-manifest',
buildStart() { this.addWatchFile(manifestFile); },
async writeBundle() {
const outDir = config.outDir;
const manifest = await getManifestWithCacheBurst();
makeManifest(manifest, outDir);
},
};
The buildStart hook adds the manifest file to Vite’s watch list for hot reloading, while writeBundle triggers the actual JSON generation after the bundle is complete.
Source: lines 72-82.
4. Cross-Browser Compatibility with ManifestParserImpl
The @extension/dev-utils package provides the ManifestParserImpl utility at packages/dev-utils/lib/manifest-parser/impl.ts. When the IS_FIREFOX environment variable is true, the parser transforms the Manifest V3 structure into Firefox-compatible syntax—such as converting background.service_worker to background.scripts and removing unsupported fields like sidePanel—before stringifying to JSON.
export const ManifestParserImpl: IManifestParser = {
convertManifestToString: (manifest, isFirefox) => {
if (isFirefox) {
manifest = convertToFirefoxCompatibleManifest(manifest);
}
return JSON.stringify(manifest, null, 2);
},
};
Source: lines 31-38 of impl.ts.
Customizing the Manifest
Adding New Permissions
Modify the permissions array in chrome-extension/manifest.ts to include additional Chrome APIs:
export default {
// ...
permissions: ['storage', 'scripting', 'tabs', 'notifications', 'sidePanel', 'webRequest'],
// ...
} satisfies ManifestType;
After running vite build, the plugin automatically includes these permissions in dist/manifest.json.
Conditional Firefox Configuration
To maintain a single codebase for both browsers, define the full Manifest V3 structure in manifest.ts. When building for Firefox, set the environment variable IS_FIREFOX=1. The ManifestParser automatically strips Chrome-specific fields like sidePanel and reformats the background worker property:
export default {
// ...
permissions: ['storage', 'scripting', 'tabs', 'notifications', 'sidePanel'],
// ...
} satisfies ManifestType;
Note: The sidePanel permission is removed automatically during Firefox conversion; no manual conditional logic is required in the source file.
Summary
- TypeScript Source:
chrome-extension/manifest.tsdefines the manifest as a typed object, importingpackage.jsonfor dynamic versioning. - Vite Integration: The
makeManifestPluginregistered invite.config.mtshooks into the build lifecycle to trigger generation. - Compilation Flow: The plugin loads the compiled
manifest.js(not the.tsfile) using a cache-busting dynamic import to ensure fresh builds. - Output: The final
manifest.jsonis written to the Vite output directory viawriteFileSyncduring thewriteBundlephase. - Firefox Support: The
ManifestParserImplutility optionally transforms the manifest for Firefox compatibility whenIS_FIREFOXis enabled.
Frequently Asked Questions
Where is the generated manifest.json file located?
The plugin writes the file to the Vite output directory (typically dist/ or your configured outDir) at the root level alongside background.js and other assets. The exact path is determined by the outDir parameter passed to makeManifestPlugin in vite.config.mts.
How does the boilerplate handle Firefox compatibility?
When the IS_FIREFOX environment variable is set to true, the ManifestParser.convertManifestToString function (implemented in packages/dev-utils/lib/manifest-parser/impl.ts) rewrites the manifest. It converts background.service_worker to background.scripts, removes the side_panel field, and applies other Firefox-specific transformations before writing the JSON.
Why does the plugin import manifest.js instead of manifest.ts?
Vite compiles TypeScript files during the build process. The plugin resolves and imports the compiled JavaScript output (manifest.js) to execute the module and retrieve the exported manifest object. This ensures the code has been transpiled and any TypeScript-specific syntax (like satisfies) has been stripped before runtime evaluation.
Can I dynamically change the manifest based on the environment?
Yes. Because manifest.ts is a standard TypeScript module, you can use conditional logic, environment variables, or additional imports to modify the exported object. For example, you can check process.env.NODE_ENV to add development-only permissions or content scripts, and these changes will be reflected in the generated manifest.json after the build completes.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →