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.ts defines the manifest as a typed object, importing package.json for dynamic versioning.
  • Vite Integration: The makeManifestPlugin registered in vite.config.mts hooks into the build lifecycle to trigger generation.
  • Compilation Flow: The plugin loads the compiled manifest.js (not the .ts file) using a cache-busting dynamic import to ensure fresh builds.
  • Output: The final manifest.json is written to the Vite output directory via writeFileSync during the writeBundle phase.
  • Firefox Support: The ManifestParserImpl utility optionally transforms the manifest for Firefox compatibility when IS_FIREFOX is 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →