# pluginContentScripts vs pluginDeclaredContentScripts vs pluginDynamicContentScripts in CRXJS

> Understand the distinct roles of pluginContentScripts, pluginDeclaredContentScripts, and pluginDynamicContentScripts in CRXJS. Learn how they manage content script loading, development builds, and dynamic imports for efficient ...

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

---

**The three plugins represent distinct layers of content script handling: `pluginContentScripts` manages the core emission and loading infrastructure for all content scripts, `pluginDeclaredContentScripts` specifically processes manifest-declared CSS by creating synthetic virtual modules during development, and `pluginDynamicContentScripts` resolves dynamic `?script` imports and replaces build-time placeholders with final chunk filenames.**

The CRXJS Vite plugin (`crxjs/chrome-extension-tools`) uses these three specialized plugins to handle different aspects of Chrome extension content scripts. Understanding the functional differences between `pluginContentScripts`, `pluginDeclaredContentScripts`, and `pluginDynamicContentScripts` is essential for debugging build issues and optimizing your extension's loading strategy.

## Core Content Script Emission with pluginContentScripts

The foundational plugin implemented in [`src/node/plugin-contentScripts.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/src/node/plugin-contentScripts.ts) serves as the central registry and emission engine for all content scripts. It runs during both **serve** (development) and **build** (production) phases.

During development, the plugin creates a virtual loader ([`content-dev-loader.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/content-dev-loader.ts)) that imports the Vite client and the actual content script module, enabling Hot Module Replacement (HMR) via the Vite dev server. In production builds, it emits the final JavaScript chunks and, when possible, inlines scripts to eliminate separate loader files entirely.

The plugin maintains a global `contentScripts` map (defined in [`src/node/contentScripts.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/src/node/contentScripts.ts)) that tracks metadata for every content script, including file names, loader names, and match patterns. This registry serves as the single source of truth for the other two plugins.

## Manifest CSS Handling with pluginDeclaredContentScripts

Located in [`src/node/plugin-contentScripts_declared.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/src/node/plugin-contentScripts_declared.ts), this plugin exclusively handles **CSS declared in the manifest** (`content_scripts[].css`). It operates only during the **serve** phase.

Rather than injecting CSS imports directly into JavaScript content scripts, it creates a **synthetic virtual module** for each manifest entry. For a manifest entry with `css: ["a.css", "b.css"]`, the plugin generates a virtual module at `/@crx/content-css/<index>` (defined in [`src/virtualFileIds.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/src/virtualFileIds.ts)) containing:

```javascript
import "/a.css";
import "/b.css";

```

The plugin then rewrites the manifest entry to insert this virtual module as the **first** JavaScript entry, preserving the original content script's code cleanliness while enabling CSS HMR. This approach ensures that style changes trigger instant refreshes without reloading the entire extension.

## Dynamic Script Resolution with pluginDynamicContentScripts

Found in [`src/node/plugin-contentScripts_dynamic.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/src/node/plugin-contentScripts_dynamic.ts), this plugin enables **dynamic script imports** using the `?script` query syntax (e.g., `import "./foo?script&module"`) and `import.meta.CRX_DYNAMIC_SCRIPT_<id>` placeholders.

It functions during both **serve** and **build** phases. In development, it returns deterministic file names for scripts, allowing `import.meta.CRX_DYNAMIC_SCRIPT_<id>` to evaluate to the correct path immediately. During the `generateBundle` phase of production builds, it replaces these placeholders with the actual emitted chunk file names (e.g., `"/<script>.js"`).

The plugin supports three script types: `module`, `loader`, and `iife`, though the `iife` type currently throws an unimplemented error. It registers dynamic entries in the shared `contentScripts` map, ensuring the core plugin can properly emit them.

## How the Three Plugins Interact

The plugins operate as a coordinated pipeline:

1. **`pluginContentScripts`** registers every content script (static or dynamic) in the `contentScripts` map and handles the final emission of all JavaScript files and loaders.

2. When a content script includes CSS in the manifest, **`pluginDeclaredContentScripts`** intercepts the manifest entry, creates the synthetic CSS module, and updates the entry before the core plugin processes it.

3. When code uses the `?script` query, **`pluginDynamicContentScripts`** resolves the import, registers a dynamic entry in the `contentScripts` map, and later rewrites the placeholder during the build's `generateBundle` step.

## Summary

- **`pluginContentScripts`** serves as the core engine in [`src/node/plugin-contentScripts.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/src/node/plugin-contentScripts.ts) that emits all content script files and loaders, manages the `contentScripts` registry, and handles both development HMR and production optimization.
- **`pluginDeclaredContentScripts`** specifically processes manifest-declared CSS during development by creating synthetic virtual modules at `/@crx/content-css/<index>` that enable CSS Hot Module Replacement without polluting JavaScript content scripts.
- **`pluginDynamicContentScripts`** resolves `?script` query imports and `import.meta.CRX_DYNAMIC_SCRIPT_<id>` placeholders, bridging the gap between development file resolution and production chunk emission.

## Frequently Asked Questions

### What happens if I declare CSS in my manifest without using CRXJS?

Without **`pluginDeclaredContentScripts`**, CSS files declared in the manifest would not benefit from Vite's HMR system. The plugin ensures that style changes trigger instant updates by creating a virtual module that imports the CSS files separately from the JavaScript execution context.

### Can I use dynamic script imports in production builds?

Yes. **`pluginDynamicContentScripts`** processes `?script` queries during both development and production. In production, the plugin replaces `import.meta.CRX_DYNAMIC_SCRIPT_<id>` placeholders with the final emitted chunk filenames during the `generateBundle` phase, ensuring correct runtime paths.

### Why does pluginDeclaredContentScripts only run during serve?

The plugin exclusively operates during development because its sole purpose is to enable HMR for manifest-declared CSS by rewriting the manifest on-the-fly. In production builds, CSS is handled differently through the core emission pipeline, making the synthetic virtual module approach unnecessary for static file generation.

### Where is the contentScripts registry defined?

The shared `contentScripts` map and helper functions are defined in **[`src/node/contentScripts.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/src/node/contentScripts.ts)**. This module provides the data structures that allow all three plugins to coordinate content script metadata throughout the build process.