How the Chrome Extension Manifest Is Transformed by vite-plugin-manifest.ts in CRXJS
The 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 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 transforms your 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.
confighook (lines 45‑55) – Resolves the user-provided manifest, which may be a plain object or a function returning a manifest. It strictly validates thatmanifest_version === 3and throws an error if the manifest does not meet this requirement.configResolvedhook (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’soptimizeDeps.entriesarray 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 modulecrx-manifest.js(referenced ascrx:manifest-loaderin lines 21‑24). - Plugin mutation – The plugin calls
transformCrxManifeston 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 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).
- Development (lines 318‑346) – Replaces placeholders with synthetic loader URLs like
- Final plugin hooks (lines 384‑406) – Executes
renderCrxManifesthooks 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.jsonin the bundle and removes the temporarycrx-manifest.jschunk.
Practical Implementation Examples
Basic Vite Configuration
Configure the plugin to load your manifest definition:
// 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:
// 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:
// 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:
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:
npm run dev
Then in your code:
// 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– Core plugin that loads, transforms, emits, and writes the Chrome extension manifest. (view source)packages/vite-plugin/src/node/manifest.ts– TypeScript definitions forManifestV3that define the shape of the manifest object manipulated by the plugin. (view source)packages/vite-plugin/src/node/helpers.ts– Helper functions includingencodeManifestanddecodeManifestused for virtual module serialization. (view source)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)packages/vite-plugin/src/node/plugin-contentScripts_declared.ts– Manages synthetic CSS loader entries for content scripts that have associated CSS files. (view source)packages/vite-plugin/src/node/plugin-optionsProvider.ts– Retrieves the user-provided plugin options including themanifestpath andentrypointsconfiguration. (view source)
Summary
- The transformation occurs in three distinct stages aligned with Vite/Rollup hooks: initialization, transform/emit, and bundle generation.
transformCrxManifestallows mutation before file emission, whilerenderCrxManifestruns 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
generateBundlephase if not emitted by the bundler. - The final
manifest.jsonis written with accurate file references, and the temporary virtual modulecrx-manifest.jsis removed from the output.
Frequently Asked Questions
What validates that the manifest uses Manifest V3?
The config hook in 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.
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 →