How the crx() Function Composes and Manages Sub-Plugins in CRXJS
The crx() function acts as a lightweight orchestrator that clears shared reactive state, instantiates a chronologically ordered array of specialized Vite plugin factories, and returns a flattened list of plugins where each handles a specific Chrome extension build concern.
The crx() function serves as the primary entry point for CRXJS (crxjs/chrome-extension-tools), the Vite plugin toolchain for building Chrome and Firefox extensions. Located in packages/vite-plugin/src/node/index.ts, this composer delegates to a curated pipeline of sub-plugins that manage manifest generation, service workers, content scripts, and hot-module replacement rather than implementing build logic itself.
Plugin Orchestration in the Main Entry Point
At its core, crx() is a factory function that constructs and returns a flattened array of Vite PluginOptions. The implementation first clears the shared contentScripts state to prevent stale data during Vite config re-evaluation, then builds an ordered list of plugin factories:
// packages/vite-plugin/src/node/index.ts
contentScripts.clear()
return [
pluginOptionsProvider(options),
pluginBackground(),
pluginContentScripts(),
pluginDeclaredContentScripts(),
pluginDynamicContentScripts(),
pluginFileWriter(),
pluginFileWriterPublic(),
pluginFileWriterPolyfill(),
pluginHtmlInlineScripts(),
pluginWebAccessibleResources(),
pluginContentScriptsCss(),
pluginHMR(),
pluginManifest(),
pluginPrint(),
].flat()
The .flat() call ensures that sub-plugins returning multiple plugins—such as pluginBackground() and pluginFileWriter() variants—are expanded into a single-level array that Vite can process sequentially.
Critical Plugin Ordering for Extension Builds
The sequence of the fourteen plugin factories mirrors the chronology of a Chrome extension build process, ensuring each stage sees artifacts produced by previous stages:
pluginOptionsProvider– Injects user-supplied options (manifest, browser target) intoapi.crx.optionsfor downstream consumption.pluginBackground– Registers loaders for the service worker and prepares thebackgroundmanifest entry.pluginContentScripts– Discovers and tracks content scripts via the reactivecontentScriptsmap.pluginDeclaredContentScripts– Processes statically declared content scripts from the manifest.pluginDynamicContentScripts– Handles runtime-generated content scripts.pluginFileWriter– Emits core generated assets (loaders, manifests).pluginFileWriterPublic– Handles public directory file emission.pluginFileWriterPolyfill– Injects necessary polyfills for extension APIs.pluginHtmlInlineScripts– Processes HTML pages and inline script injection.pluginWebAccessibleResources– Configuresweb_accessible_resourcesmanifest entries.pluginContentScriptsCss– Extracts and processes CSS imported by content scripts.pluginHMR– Sets up hot-module-replacement for service workers and content scripts.pluginManifest– Finalizes and writes themanifest.jsonwith all accumulated modifications.pluginPrint– Provides optional debug logging of the resolved build state.
Because Vite executes plugin hooks in registration order (with enforce: 'pre'/'post' providing fine-grained control), this explicit sequence guarantees that manifest finalization occurs after all assets are emitted.
State Management via the contentScripts RxMap
crx() manages shared build state through contentScripts, a reactive map (RxMap) defined in src/node/contentScripts.ts that stores rich descriptors for every content script (type, IDs, hashes, loader names, CSS, etc.).
- Population: Sub-plugins like
pluginContentScripts,pluginDeclaredContentScripts, andpluginDynamicContentScriptspopulate this map during the build discovery phase. - Consumption: File-writer plugins (
pluginFileWriter,pluginFileWriterPublic) read from the map when emitting loader files and constructing the final manifest. - Key Strategy: The map uses a many-to-one lookup strategy supporting multiple keys (
refId,id,fileName,loaderName,resolvedId,scriptId) so any reference (e.g., RolluprefId) resolves to the sameContentScriptobject.
This reactive structure eliminates tightly-coupled imports between plugins and enables real-time build updates as new content scripts are discovered.
Inside Key Sub-Plugin Implementations
Background Service Worker Handling
pluginBackground in src/node/plugin-background.ts registers a development loader for the service worker and injects the appropriate background entry into the manifest. It retrieves browser-specific options via getOptions and behaves differently by mode:
- Serve mode: Builds an import-based loader that pulls the HMR client and the service worker script.
- Build mode: Emits a static loader asset via
emitFileand rewritesmanifest.backgroundto reference the generated file.
This factory returns a pair of plugins (client resolver and loader generator), which crx() automatically flattens into the main array.
The File Writer Chain
The trio of pluginFileWriter, pluginFileWriterPublic, and pluginFileWriterPolyfill ensures generated assets are emitted correctly for both development and production. By separating these concerns into distinct plugins, CRXJS maintains clean boundaries between core extension files, static public assets, and runtime polyfills.
Practical Usage
Developers interact only with the composed crx() function; the sub-plugin architecture remains an internal implementation detail:
// vite.config.ts
import { defineConfig } from 'vite'
import { crx } from 'crxjs/vite-plugin'
export default defineConfig({
plugins: [
crx({
manifest: {
name: 'My Extension',
version: '1.0.0',
manifest_version: 3,
background: { service_worker: 'src/background.ts' },
content_scripts: [
{
matches: ['<all_urls>'],
js: ['src/content.ts'],
},
],
},
browser: 'chrome',
}),
],
})
For advanced customization, the reactive content-script map can be accessed in custom plugins:
import { contentScripts } from 'crxjs/vite-plugin/src/node/contentScripts'
export const myPlugin = () => ({
name: 'my:inspect',
configResolved() {
console.log('Discovered:', Array.from(contentScripts.values()))
},
})
Summary
crx()is a thin composer located inpackages/vite-plugin/src/node/index.tsthat returns a flattened, ordered array of specialized Vite plugins.- Plugin order is chronological, ensuring each build stage (discovery → emission → finalization) executes in the correct sequence.
- State is shared reactively through the
contentScriptsRxMap, enabling loose coupling between content-script discovery and file-writing phases. - Sub-plugins are modular factories, with complex concerns like background service workers and file writing split into focused, testable units.
Frequently Asked Questions
What specific sub-plugins does the crx() function instantiate?
According to the source in packages/vite-plugin/src/node/index.ts, crx() instantiates fourteen plugin factories: pluginOptionsProvider, pluginBackground, pluginContentScripts, pluginDeclaredContentScripts, pluginDynamicContentScripts, pluginFileWriter, pluginFileWriterPublic, pluginFileWriterPolyfill, pluginHtmlInlineScripts, pluginWebAccessibleResources, pluginContentScriptsCss, pluginHMR, pluginManifest, and pluginPrint. The .flat() method expands any sub-arrays into a single-level plugin list for Vite.
How does crx() prevent stale data during Vite watch mode?
The function calls contentScripts.clear() immediately upon invocation to reset the shared RxMap state. This prevents stale content-script descriptors from persisting when the Vite configuration is re-evaluated during file watching or hot updates.
Why is plugin ordering critical in the CRXJS architecture?
Vite executes plugin hooks in the order they are registered. The CRXJS sequence ensures that options are available before background scripts are processed, that content scripts are discovered before CSS extraction runs, and that the final manifest is written only after all file writers have emitted their assets. Violating this order would cause missing references in the generated manifest.json.
Can individual sub-plugins be used independently without calling crx()?
While each sub-plugin is implemented as an isolated factory function in its own file (e.g., src/node/plugin-background.ts), the public API does not export them individually. Using crx() is the supported method because it guarantees correct plugin ordering, initializes the shared contentScripts state, and ensures all inter-plugin dependencies are satisfied.
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 →