# How the CRXJS Vite Plugin Integrates with Vite's Core Build System for Chrome Extension Development

> Learn how the CRXJS Vite plugin seamlessly integrates with Vite's build system using standard hooks. Optimize your Chrome extension development without altering Vite's core.

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

---

**The CRXJS Vite plugin integrates with Vite by implementing standard Vite plugin hooks—such as `config`, `resolveId`, `load`, and `generateBundle`—to inject Chrome extension-specific logic without modifying Vite's internal architecture.**

The **CRXJS** (`@crxjs/vite-plugin`) package provides a specialized Vite plugin that transforms a standard Vite project into a fully functional Chrome extension build pipeline. By adhering strictly to Vite's public plugin API, the plugin orchestrates manifest generation, content script bundling, and hot module replacement (HMR) while allowing Vite's core build system to handle module resolution, transformation, and optimization as it would for any web application.

## Plugin Architecture and Composition

### The crx() Function and Plugin Composition

The entry point for the integration is the `crx` function exported from [`packages/vite-plugin/src/node/index.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/index.ts). Rather than implementing a single monolithic plugin, `crx()` composes an array of specialized sub-plugins that together handle distinct aspects of Chrome extension development.

```typescript
// packages/vite-plugin/src/node/index.ts
return [
  pluginOptionsProvider(options),
  pluginBackground(),
  pluginContentScripts(),
  // ... additional CRXJS plugins
].flat();

```

This composition pattern allows CRXJS to insert hooks at precise points in Vite's lifecycle. The flattened array of `PluginOption` objects is returned to Vite, which executes them according to its standard plugin ordering logic.

### Virtual Module Resolution

CRXJS leverages Vite's virtual module system to inject generated code without writing temporary files to disk. The plugin defines virtual file IDs in [`packages/vite-plugin/src/node/virtualFileIds.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/virtualFileIds.ts), including `manifestId`, `stubId`, `preambleId`, and `contentHmrPortId`.

During the `resolveId` and `load` phases, the plugin intercepts requests for these virtual modules:

```typescript
// packages/vite-plugin/src/node/plugin-manifest.ts
if (source === manifestId) return manifestId;
if (id === manifestId) return encodeManifest(manifest);

```

This technique allows the manifest JSON and content script loaders to participate in Vite's module graph as first-class citizens, enabling dependency tracking and HMR.

## Lifecycle Hook Integration

### Configuration Phase

During the `config` hook, CRXJS performs initial validation and setup. The `pluginOptionsProvider` and `pluginManifest` (pre-hook) read the user-provided `manifest` object, validate Manifest V3 compliance, and add manifest-referenced files to Vite's dependency optimization entries.

```typescript
// plugin-manifest:config hook
// Adds content scripts, background worker, and HTML pages to optimizeDeps.entries

```

This ensures Vite's dev server crawls and pre-bundles all extension entry points during startup, preventing cold-start delays when loading the extension in Chrome.

### Build Phase

During `buildStart`, `resolveId`, `load`, and `transform`, CRXJS orchestrates the generation of extension-specific assets. The `pluginContentScripts` sub-plugin manages the `RxMap` metadata store ([`packages/vite-plugin/src/node/contentScripts.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/contentScripts.ts)) to track content script IDs, reference IDs, and loader names.

The `transform` hook in `pluginManifest` (post-hook) runs manifest transform hooks supplied by other CRXJS plugins via `transformCrxManifest`. During development, it registers synthetic CSS entries and creates in-memory content script loader metadata. During production builds, it emits content script chunks and background scripts as Rollup reference IDs.

### Bundle Generation

In the `generateBundle` hook, CRXJS finalizes the extension package. The `pluginManifest` (finalisation) rewrites the emitted manifest with final file names (or reference IDs), copies static assets (icons, locales), writes a loading-page placeholder for dev servers, and emits the final [`manifest.json`](https://github.com/crxjs/chrome-extension-tools/blob/main/manifest.json) asset.

The `closeBundle` hook triggers cleanup in `pluginFileWriter` and `pluginContentScripts`, finalizing temporary subscriptions and HMR resources.

## Content Script Orchestration and HMR

### The RxMap Metadata Store

Content scripts require special handling because they must be injected into arbitrary web pages rather than loaded as standard web assets. CRXJS centralizes content script state in an `RxMap` ([`packages/vite-plugin/src/node/contentScripts.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/contentScripts.ts)), a reactive map that stores metadata including script type, ID, match patterns, Rollup reference IDs, and loader file names.

Sub-plugins like `pluginContentScripts` and `pluginContentScripts_css` update this map throughout the build process, ensuring the final manifest generation step can resolve the correct filenames for each content script entry.

### Development Loaders and HMR

During development (`serve`), CRXJS emits **content script loaders** via the `configureServer` hook in `pluginContentScripts`. These loaders are small IIFE scripts injected into web pages that dynamically load the actual content script bundles.

The plugin injects a React preamble when the React plugin is present and sets up a custom HMR port (`contentHmrPortId`) for content scripts. This port is implemented in [`packages/vite-plugin/src/client/es/hmr-content-port.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/client/es/hmr-content-port.ts) and allows Chrome's `chrome.runtime.onMessage`-based HMR to function without interfering with Vite's standard HMR pipeline for the popup or options pages.

```typescript
// Generated dev loader structure (simplified)
(function() {
  // HMR port connection
  const port = chrome.runtime.connect({ name: 'contentHmrPort' });
  // Dynamic import of the actual content script
  import('/@fs/src/content.ts');
})();

```

## Summary

- **CRXJS acts as a standard Vite plugin** by returning an array of `PluginOption` objects from the `crx()` function in [`packages/vite-plugin/src/node/index.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/index.ts), allowing seamless integration with existing Vite configurations.
- **Virtual modules** (`manifestId`, `stubId`, `contentHmrPortId`) enable CRXJS to inject generated code into Vite's module graph without writing temporary files, handled in [`packages/vite-plugin/src/node/virtualFileIds.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/virtualFileIds.ts).
- **Lifecycle hook orchestration** allows CRXJS to perform manifest validation during `config`, emit content script chunks during `transform`, and finalize the extension package during `generateBundle` in [`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).
- **Content script management** relies on an `RxMap` metadata store ([`packages/vite-plugin/src/node/contentScripts.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/contentScripts.ts)) to track script IDs and reference IDs, enabling proper bundling of content scripts that run in isolated contexts.
- **Development HMR** for content scripts uses a dedicated port system (`contentHmrPortId`) implemented in [`packages/vite-plugin/src/client/es/hmr-content-port.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/client/es/hmr-content-port.ts), allowing hot reloading without interfering with Vite's standard HMR workflow.

## Frequently Asked Questions

### How does CRXJS handle Manifest V3 validation?

CRXJS validates the manifest during the `config` hook in `pluginManifest`. It reads the user-provided manifest object—either passed directly or via `defineManifest`—and verifies that it conforms to Chrome's Manifest V3 schema. If validation fails, the plugin throws an error during the Vite configuration phase, preventing the build from starting with an invalid extension structure.

### Can I use standard Vite plugins like React or Vue with CRXJS?

Yes. CRXJS is designed to work alongside standard Vite plugins. The `crx()` function returns a standard Vite plugin array that can be spread into your `plugins` configuration alongside React, Vue, or TypeScript plugins. CRXJS specifically detects the presence of the React plugin to inject the necessary preamble code into content script loaders during development, ensuring compatibility with React's runtime requirements.

### How does CRXJS manage content script hot reloading?

During development, CRXJS injects a custom HMR runtime into content scripts through the `contentHmrPortId` virtual module. The `pluginContentScripts` sub-plugin generates dev loaders that establish a `chrome.runtime.connect` port named `contentHmrPort`. This port receives reload signals from Vite's dev server via [`packages/vite-plugin/src/client/es/hmr-content-port.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/client/es/hmr-content-port.ts), allowing content scripts to reload independently of the background service worker or popup pages without requiring a full extension reload.

### What happens to static assets like icons and locales during the build?

CRXJS handles static assets during the `generateBundle` hook in `pluginManifest`. After Rollup processes all JavaScript and CSS chunks, the plugin copies missing manifest assets—such as icons referenced in the manifest's `icons` field or locale directories—from the project root to the output directory. It also rewrites the final [`manifest.json`](https://github.com/crxjs/chrome-extension-tools/blob/main/manifest.json) to reference the correct hashed filenames for emitted chunks, ensuring all assets are correctly bundled and referenced in the production extension package.