# How CRXJS Automates web_accessible_resources Generation for Chrome Extensions

> Learn how CRXJS automates web_accessible_resources generation for Chrome extensions. Discover its smart analysis of Vite build output for efficient asset management.

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

---

**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`](https://github.com/crxjs/chrome-extension-tools/blob/main/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`](https://github.com/crxjs/chrome-extension-tools/blob/main/manifest.json) or Vite ≥5's [`.vite/manifest.json`](https://github.com/crxjs/chrome-extension-tools/blob/main/.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`](https://github.com/crxjs/chrome-extension-tools/blob/main/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`](https://github.com/crxjs/chrome-extension-tools/blob/main/manifest.json) (Vite ≤4) or [`.vite/manifest.json`](https://github.com/crxjs/chrome-extension-tools/blob/main/.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`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/contentScripts.ts), the plugin calls `compileFileResources` (defined in [`packages/vite-plugin/src/node/compileFileResources.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/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`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/manifest.ts):
   ```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`](https://github.com/crxjs/chrome-extension-tools/blob/main/vite.config.ts):

```typescript
// 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:

```typescript
// 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`](https://github.com/crxjs/chrome-extension-tools/blob/main/manifest.json) after build:

```json
{
  "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`](https://github.com/crxjs/chrome-extension-tools/blob/main/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`](https://github.com/crxjs/chrome-extension-tools/blob/main/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`](https://github.com/crxjs/chrome-extension-tools/blob/main/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`](https://github.com/crxjs/chrome-extension-tools/blob/main/manifest.json) for Vite ≤4 or [`.vite/manifest.json`](https://github.com/crxjs/chrome-extension-tools/blob/main/.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.