# How the Chrome Extension Manifest Is Transformed by vite-plugin-manifest.ts in CRXJS

> Discover how vite-plugin-manifest.ts transforms your Chrome extension manifest. Learn the three-stage pipeline for production-ready manifest.json files with hashed filenames and Vite dev-server integration.

- Repository: [crxjs/chrome-extension-tools](https://github.com/crxjs/chrome-extension-tools)
- Tags: internals
- Published: 2026-02-28

---

**The [`vite-plugin-manifest.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/vite-plugin-manifest.ts) plugin in the `crxjs/chrome-extension-tools` repository orchestrates a three-stage pipeline—load and validate, transform and emit, and generate bundle—to convert developer-friendly manifest definitions into production-ready Chrome extension [`manifest.json`](https://github.com/crxjs/chrome-extension-tools/blob/main/manifest.json) files with correct hashed filenames and Vite dev-server integration.**

The CRXJS Chrome extension tools use a sophisticated Vite plugin architecture to handle Chrome extension manifests. Understanding how [`vite-plugin-manifest.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/vite-plugin-manifest.ts) transforms your [`manifest.json`](https://github.com/crxjs/chrome-extension-tools/blob/main/manifest.json) is essential for debugging build issues and creating custom plugins that modify extension behavior during the build lifecycle.

## The Three-Stage Transformation Pipeline

The transformation proceeds through three distinct stages that correspond to the standard Vite/Rollup hook order. Each stage handles specific responsibilities, from initial validation to final file emission.

### Stage 1: Load and Initialise

During the initialization phase, the plugin captures the user configuration and prepares the Vite environment for Chrome extension development.

- **`config` hook (lines 45‑55)** – Resolves the user-provided manifest, which may be a plain object or a function returning a manifest. It strictly validates that `manifest_version === 3` and throws an error if the manifest does not meet this requirement.
- **`configResolved` hook (lines 91‑98)** – Captures references to other registered CRX plugins for later use in the transformation pipeline.
- **Development optimization** – When `command === 'serve'`, the plugin augments Vite’s `optimizeDeps.entries` array to ensure all manifest entry files (JavaScript, CSS, HTML, and service workers) are pre-bundled by the dev server.

### Stage 2: Transform and Emit

The `transform` hook (post-enforce) decodes the manifest and allows other plugins to mutate it before emitting the actual script files.

- **Virtual module decoding** – The plugin decodes the emitted manifest module (`manifestId`), which is served as a virtual module [`crx-manifest.js`](https://github.com/crxjs/chrome-extension-tools/blob/main/crx-manifest.js) (referenced as `crx:manifest-loader` in lines 21‑24).
- **Plugin mutation** – The plugin calls **`transformCrxManifest`** on every registered CRX plugin (transformation loop lines 86‑99), allowing other plugins to add fields, rewrite values, or modify the manifest structure.
- **Script emission** – The plugin emits **content-script** files, **CSS synthetic loaders**, and **background scripts** as Rollup chunks. It stores a mapping of each script to a loader record in `contentScripts`.
- **Mode-specific logic** – Different code paths handle file emission for development versus production:
  - **Serve mode (lines 101‑154)** – Registers synthetic loader entries for content scripts to enable HMR.
  - **Build mode (lines 155‑276)** – Emits real Rollup chunks with hashed filenames for optimal caching.

### Stage 3: Generate Bundle and Render

During the `generateBundle` hook (post-enforce), the plugin finalizes filenames, executes final mutation hooks, and writes the [`manifest.json`](https://github.com/crxjs/chrome-extension-tools/blob/main/manifest.json) to disk.

- **Bundle retrieval (lines 311‑315)** – Retrieves the manifest module from the bundle and decodes it for final processing.
- **Filename replacement** – Updates file references in the manifest to match the actual emitted file names:
  - **Development (lines 318‑346)** – Replaces placeholders with synthetic loader URLs like `loader-<hash>.js`.
  - **Production (lines 347‑380)** – Replaces placeholders with the final hashed chunk names (e.g., `contentScript-<hash>.js`).
- **Final plugin hooks (lines 384‑406)** – Executes **`renderCrxManifest`** hooks on all CRX plugins, providing a final opportunity to tweak the manifest after file names are known.
- **Asset copying (lines 410‑457)** – Copies any **manifest assets** (icons, locales, rulesets, etc.) that were not automatically emitted by the bundler.
- **Development loading page (lines 460‑480)** – In serve mode, creates a tiny "loading page" asset that injects the Vite dev-server script into each HTML page.
- **Final write (lines 482‑495)** – Overwrites or creates the final [`manifest.json`](https://github.com/crxjs/chrome-extension-tools/blob/main/manifest.json) in the bundle and removes the temporary [`crx-manifest.js`](https://github.com/crxjs/chrome-extension-tools/blob/main/crx-manifest.js) chunk.

## Practical Implementation Examples

### Basic Vite Configuration

Configure the plugin to load your manifest definition:

```typescript
// vite.config.ts
import { defineConfig } from 'vite'
import { crx } from 'crxjs/vite-plugin'   // re‑exports plugin‑manifest internally

export default defineConfig({
  plugins: [
    crx({
      manifest: './src/manifest.ts',   // can be a plain object or a function
    }),
  ],
})

```

### Modifying the Manifest During Transform

Create a custom CRX plugin to add permissions before files are emitted:

```typescript
// my-permissions-plugin.ts
export const addPermission = {
  name: 'my:add-permission',
  async transformCrxManifest(manifest) {
    // ensure the permission array exists
    ;(manifest.permissions ??= []).push('storage')
    return manifest
  },
}

```

Register the plugin in your Vite config:

```typescript
// vite.config.ts
import { crx } from 'crxjs/vite-plugin'
import { addPermission } from './my-permissions-plugin'

export default {
  plugins: [crx({ manifest: './manifest.ts' }), addPermission],
}

```

### Injecting Build Metadata After Filename Resolution

Use the `renderCrxManifest` hook to add build-specific data after file names are known:

```typescript
export const injectBuildInfo = {
  name: 'my:build-info',
  async renderCrxManifest(manifest, bundle) {
    manifest.version_name = `build-${new Date().toISOString()}`
    return manifest
  },
}

```

### Inspecting the Manifest During Development

The plugin serves the manifest as a virtual module during development. You can inspect it by importing the resolved virtual ID:

```bash
npm run dev

```

Then in your code:

```typescript
// The plugin emits a virtual module `crx-manifest.js`
import manifest from '/@fs/src/manifest.ts'   // Vite resolves the virtual ID

console.log('Decoded manifest during dev:', manifest)

```

## Key Source Files in the Transformation Pipeline

- **[`packages/vite-plugin/src/node/plugin-manifest.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/plugin-manifest.ts)** – Core plugin that loads, transforms, emits, and writes the Chrome extension manifest. ([view source](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/plugin-manifest.ts))
- **[`packages/vite-plugin/src/node/manifest.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/manifest.ts)** – TypeScript definitions for `ManifestV3` that define the shape of the manifest object manipulated by the plugin. ([view source](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/manifest.ts))
- **[`packages/vite-plugin/src/node/helpers.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/helpers.ts)** – Helper functions including `encodeManifest` and `decodeManifest` used for virtual module serialization. ([view source](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/helpers.ts))
- **[`packages/vite-plugin/src/node/files.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/files.ts)** – Functions that discover all files referenced by the manifest, including content scripts, HTML pages, and static assets. ([view source](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/files.ts))
- **[`packages/vite-plugin/src/node/plugin-contentScripts_declared.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/plugin-contentScripts_declared.ts)** – Manages synthetic CSS loader entries for content scripts that have associated CSS files. ([view source](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/plugin-contentScripts_declared.ts))
- **[`packages/vite-plugin/src/node/plugin-optionsProvider.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/plugin-optionsProvider.ts)** – Retrieves the user-provided plugin options including the `manifest` path and `entrypoints` configuration. ([view source](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/plugin-optionsProvider.ts))

## Summary

- The transformation occurs in three distinct stages aligned with Vite/Rollup hooks: initialization, transform/emit, and bundle generation.
- **`transformCrxManifest`** allows mutation before file emission, while **`renderCrxManifest`** runs after filenames are finalized for build-specific modifications.
- The plugin handles different emission strategies for development (synthetic loaders for HMR) versus production (hashed chunks for caching).
- Static assets referenced in the manifest are copied automatically during the `generateBundle` phase if not emitted by the bundler.
- The final [`manifest.json`](https://github.com/crxjs/chrome-extension-tools/blob/main/manifest.json) is written with accurate file references, and the temporary virtual module [`crx-manifest.js`](https://github.com/crxjs/chrome-extension-tools/blob/main/crx-manifest.js) is removed from the output.

## Frequently Asked Questions

### What validates that the manifest uses Manifest V3?

The `config` hook in [`plugin-manifest.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/plugin-manifest.ts) (lines 45‑55) reads the user-provided manifest and strictly validates that `manifest_version === 3`, throwing an error if this requirement is not met.

### How does the plugin handle content scripts differently in development versus production?

During `serve` mode (development), the plugin registers synthetic loader entries for content scripts (lines 101‑154) to enable Vite's dev-server HMR, while in `build` mode (production), it emits actual Rollup chunks with hashed filenames (lines 155‑276) for optimal caching and deployment.

### When can plugins modify the manifest after filenames are known?

The `renderCrxManifest` hook executes during `generateBundle` (lines 384‑406) after file names are updated to their final values, providing a final opportunity for CRX plugins to inject build-specific metadata or modify fields that depend on the emitted output.

### How are static assets like icons processed if not imported in code?

During the `generateBundle` phase (lines 410‑457), the plugin copies any manifest assets—including icons, locales, and rulesets—that were not automatically emitted by the bundler, ensuring all referenced files exist in the final extension package.