pluginContentScripts vs pluginDeclaredContentScripts vs pluginDynamicContentScripts in CRXJS
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 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) 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) 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, 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) containing:
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, 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:
-
pluginContentScriptsregisters every content script (static or dynamic) in thecontentScriptsmap and handles the final emission of all JavaScript files and loaders. -
When a content script includes CSS in the manifest,
pluginDeclaredContentScriptsintercepts the manifest entry, creates the synthetic CSS module, and updates the entry before the core plugin processes it. -
When code uses the
?scriptquery,pluginDynamicContentScriptsresolves the import, registers a dynamic entry in thecontentScriptsmap, and later rewrites the placeholder during the build'sgenerateBundlestep.
Summary
pluginContentScriptsserves as the core engine insrc/node/plugin-contentScripts.tsthat emits all content script files and loaders, manages thecontentScriptsregistry, and handles both development HMR and production optimization.pluginDeclaredContentScriptsspecifically 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.pluginDynamicContentScriptsresolves?scriptquery imports andimport.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. This module provides the data structures that allow all three plugins to coordinate content script metadata throughout the build process.
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 →