How CRXJS Automates web_accessible_resources Generation for Chrome Extensions
CRXJS automatically generates the web_accessible_resources array by analyzing Vite's build output, using a catch-all pattern during development and a precise dependency-graph analysis during production to expose only the assets required by each content script.
The @crxjs/vite-plugin eliminates manual manifest maintenance by programmatically creating the web_accessible_resources (WAR) section from your actual build artifacts. This automatic generation ensures that only necessary files—JavaScript chunks, CSS, and static assets—are exposed to web pages, while supporting dynamic content scripts and browser-specific optimizations.
The plugin-webAccessibleResources Architecture
The core logic resides in packages/vite-plugin/src/node/plugin-webAccessibleResources.ts, which exports a Vite plugin that hooks into the renderCrxManifest lifecycle. This plugin operates in two distinct phases depending on the Vite mode, ensuring assets are accessible during development while maintaining a minimal security footprint in production builds.
Development vs. Production WAR Generation
CRXJS employs fundamentally different strategies for development (serve) and production (build) modes, balancing convenience against runtime security.
Development Mode: The Catch-All Pattern
During serve mode, CRXJS injects a permissive WAR entry to ensure all assets are accessible from the dev server. The renderCrxManifest function adds a WAR object configured with matches: <all_urls> and resources: ['**/*', '*']. This configuration allows hot-module replacement and dynamic asset loading without pre-calculating the dependency graph. For Firefox, the use_dynamic_url flag is stripped because it is handled implicitly by the browser.
Production Mode: Dependency-Graph Analysis
In build mode, CRXJS derives a minimal, accurate WAR list from the actual assets produced by Vite. The plugin reads the Vite asset manifest—detecting Vite ≤4's manifest.json or Vite ≥5's .vite/manifest.json—walks the Rollup chunk graph, and maps each content script's dependencies to specific resource entries. This process is orchestrated by packages/vite-plugin/src/node/plugin-manifest.ts, which invokes the WAR generation logic after the bundle is complete.
The 9-Step Production Build Pipeline
The renderCrxManifest implementation executes the following algorithm to construct the final WAR array:
-
Read the Vite Asset Manifest
The plugin detects the Vite major version and parses eithermanifest.json(Vite ≤4) or.vite/manifest.json(Vite ≥5) usingparseJsonAssetto create theviteManifestobject. -
Collect Vite Files
AMapnamedviteFilesmapsfile → ManifestChunkfor quick look-ups during dependency resolution. -
Map Rollup Chunks
The plugin creates abundleChunksmap to resolve imports and dynamic imports across the Rollup output. -
Process Content Scripts
For every declared content script defined inpackages/vite-plugin/src/node/contentScripts.ts, the plugin callscompileFileResources(defined inpackages/vite-plugin/src/node/compileFileResources.ts) to walk the dependency graph. This gathersassets(static files),css(stylesheets), andimports(dependent scripts). -
Build WAR Entries
For each script, CRXJS constructs a resource object using types defined inpackages/vite-plugin/src/node/manifest.ts:{ matches: isDynamicScript ? [...dynamicScriptMatches] : matches, resources: [...assets, ...imports], use_dynamic_url: isDynamicScript ? dynamicScriptDynamicUrl : false, }Module scripts (
type === 'module') are stored temporarily inmoduleScriptResourcesto prevent duplicate exposure. -
Combine Module-Script Resources
After processing all scripts, any top-level module imports not already referenced are added as separate WAR entries. -
Deduplicate and Merge
WAR entries with identicalmatchesanduse_dynamic_urlvalues are merged into single entries, consolidating theirresourcesinto aSetto eliminate duplicates. -
Apply Browser-Specific Tweaks
For Firefox, theuse_dynamic_urlflag is stripped because Firefox implicitly uses dynamic URLs for all WARs. -
Include Source Maps and Cleanup
For each Rollup chunk resource, corresponding.mapfiles are added if present. Finally, if no WAR entries remain, the field is removed from the manifest, and the temporary Vite manifest file is deleted unlessbuild.manifestis explicitly enabled.
Configuration and Usage Examples
To leverage automatic WAR generation, configure your content scripts in vite.config.ts:
// vite.config.ts
import { defineConfig } from 'vite'
import { crx } from '@crxjs/vite-plugin'
export default defineConfig({
plugins: [crx({
contentScripts: {
myContent: {
matches: ['https://example.com/*'],
js: ['src/content.ts'],
css: ['src/content.css'],
// isDynamicScript: true, // Enable for dynamic scripts
},
},
})],
})
Access resources dynamically in your content scripts:
// src/content.ts
if (chrome.runtime.getURL) {
// Works because CRXJS added WAR entry with use_dynamic_url: true
const assetUrl = chrome.runtime.getURL('assets/icon.png')
}
The resulting manifest.json after build:
{
"web_accessible_resources": [
{
"matches": ["https://example.com/*"],
"resources": ["src/content.js", "src/content.css", "assets/icon.png"],
"use_dynamic_url": false
},
{
"matches": ["http://*/*", "https://*/*"],
"resources": ["src/dynamicScript.js"],
"use_dynamic_url": true
}
]
}
Summary
- Dual-mode operation: Development uses a catch-all pattern (
<all_urls>with**/*) while production generates minimal, precise WAR entries. - Dependency-graph walking: The
compileFileResourcesfunction incompileFileResources.tstraverses Vite and Rollup metadata to collect all required assets, CSS, and imports for each content script. - Smart deduplication: Module scripts are tracked separately in
moduleScriptResourcesand merged to prevent duplicate resource exposure. - Browser compatibility: Firefox-specific handling automatically removes
use_dynamic_urlsince the browser handles this implicitly. - Cleanup: Temporary Vite manifest files are removed from the final bundle unless specifically retained via
build.manifest.
Frequently Asked Questions
How does CRXJS handle web_accessible_resources during development?
During development (serve mode), CRXJS injects a single catch-all WAR entry with matches: <all_urls> and resources: ['**/*', '*'] via the renderCrxManifest function in plugin-webAccessibleResources.ts. This ensures all assets are accessible from the dev server without requiring a full build analysis, enabling hot-module replacement and dynamic loading.
Why does CRXJS remove the use_dynamic_url flag for Firefox?
According to the source code in packages/vite-plugin/src/node/plugin-webAccessibleResources.ts, Firefox implicitly uses dynamic URLs for all web accessible resources, making the use_dynamic_url flag redundant. The plugin detects the target browser and strips this property to ensure compatibility with Firefox's manifest specification.
How does CRXJS prevent duplicate resources in the final manifest?
The plugin tracks module scripts separately in a moduleScriptResources collection during the initial pass. After processing all content scripts, it removes module imports that appear in this collection from other WAR entries. Finally, it merges WAR objects with identical matches and use_dynamic_url values into single entries using a Set to deduplicate resource paths.
What happens to the Vite manifest.json file during the CRXJS build?
CRXJS reads the Vite asset manifest—located at manifest.json for Vite ≤4 or .vite/manifest.json for Vite ≥5—to identify build outputs. After generating the Chrome Extension manifest, the plugin deletes this temporary file from the final bundle unless the user explicitly enables build.manifest in their Vite configuration.
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 →