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:

  1. Read the Vite Asset Manifest
    The plugin detects the Vite major version and parses either manifest.json (Vite ≤4) or .vite/manifest.json (Vite ≥5) using parseJsonAsset to create the viteManifest object.

  2. Collect Vite Files
    A Map named viteFiles maps file → ManifestChunk for quick look-ups during dependency resolution.

  3. Map Rollup Chunks
    The plugin creates a bundleChunks map to resolve imports and dynamic imports across the Rollup output.

  4. Process Content Scripts
    For every declared content script defined in packages/vite-plugin/src/node/contentScripts.ts, the plugin calls compileFileResources (defined in packages/vite-plugin/src/node/compileFileResources.ts) to walk the dependency graph. This gathers assets (static files), css (stylesheets), and imports (dependent scripts).

  5. Build WAR Entries
    For each script, CRXJS constructs a resource object using types defined in packages/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 in moduleScriptResources to prevent duplicate exposure.

  6. Combine Module-Script Resources
    After processing all scripts, any top-level module imports not already referenced are added as separate WAR entries.

  7. Deduplicate and Merge
    WAR entries with identical matches and use_dynamic_url values are merged into single entries, consolidating their resources into a Set to eliminate duplicates.

  8. Apply Browser-Specific Tweaks
    For Firefox, the use_dynamic_url flag is stripped because Firefox implicitly uses dynamic URLs for all WARs.

  9. Include Source Maps and Cleanup
    For each Rollup chunk resource, corresponding .map files 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 unless build.manifest is 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 compileFileResources function in compileFileResources.ts traverses Vite and Rollup metadata to collect all required assets, CSS, and imports for each content script.
  • Smart deduplication: Module scripts are tracked separately in moduleScriptResources and merged to prevent duplicate resource exposure.
  • Browser compatibility: Firefox-specific handling automatically removes use_dynamic_url since 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →