# How manifest.ts Generates the Chrome Extension manifest.json in the React-Vite Boilerplate

> Discover how manifest.ts generates your Chrome extension manifest.json using a custom Vite plugin in React Vite Boilerplate. Learn about optional Firefox compatibility transforms.

- Repository: [JongHak Seo/chrome-extension-boilerplate-react-vite](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite)
- Tags: how-to-guide
- Published: 2026-03-05

---

**The boilerplate generates the Chrome extension manifest.json from TypeScript using a custom Vite plugin that compiles [`manifest.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/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`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/package.json) and maintain a single source of truth for browser compatibility. Understanding how [`manifest.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/manifest.ts) generates the final [`manifest.json`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/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`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/manifest.ts)

The generation process starts at [`chrome-extension/manifest.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/chrome-extension/manifest.ts), which exports a typed JavaScript object that satisfies the shared `ManifestType` interface. This file imports the project’s [`package.json`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/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.

```ts
// 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`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/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.

```ts
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`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/make-manifest-plugin.ts) Engine

Located at [`chrome-extension/utils/plugins/make-manifest-plugin.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/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`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/manifest.js) in the file system:

```ts
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:

```ts
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:

```ts
writeFileSync(
  manifestPath,
  ManifestParser.convertManifestToString(manifest, IS_FIREFOX)
);

```

*Source:* line 61.

**Plugin Lifecycle Hooks**

The plugin implements two critical Vite hooks:

```ts
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`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/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.

```ts
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`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/impl.ts).

## Customizing the Manifest

### Adding New Permissions

Modify the permissions array in [`chrome-extension/manifest.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/chrome-extension/manifest.ts) to include additional Chrome APIs:

```ts
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`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/dist/manifest.json).

### Conditional Firefox Configuration

To maintain a single codebase for both browsers, define the full Manifest V3 structure in [`manifest.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/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:

```ts
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`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/chrome-extension/manifest.ts) defines the manifest as a typed object, importing [`package.json`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/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`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/manifest.js) (not the `.ts` file) using a cache-busting dynamic import to ensure fresh builds.
- **Output:** The final [`manifest.json`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/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`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/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`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/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`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/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`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/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`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/manifest.json) after the build completes.