How defineDynamicResource Simplifies Web Accessible Resources in CRXJS Chrome Extensions

The defineDynamicResource function exported from packages/vite-plugin/src/node/defineManifest.ts creates a type-safe placeholder entry for dynamic web accessible resources that the CRXJS Vite plugin automatically resolves to actual bundled file names during the build process.

The defineDynamicResource helper in the crxjs/chrome-extension-tools repository eliminates manual enumeration of script hashes in Chrome Extension Manifest V3 files. This function abstracts the boilerplate required to declare resources that will be injected at runtime, while the build system handles the concrete file name resolution and cross-browser compatibility.

How defineDynamicResource Works in the Build Pipeline

Creating the Placeholder Entry

When invoked, defineDynamicResource returns a manifest entry object containing three key properties. The matches field specifies URL patterns that may request the resource. The resources array contains a single sentinel value DYNAMIC_RESOURCE (the literal string <dynamic_resource> defined at lines 71-72 of defineManifest.ts). Finally, the use_dynamic_url boolean flag tells Chrome whether to generate a dynamic chrome.runtime.getURL for the resource.

Default Configuration Values

The function provides sensible defaults when options are omitted. If you call it without arguments, it supplies the most permissive match set—http://*/* and https://*/*—and sets use_dynamic_url to false (lines 60-62). This zero-config approach works out-of-the-box while still allowing explicit overrides for specific domains.

Build-Time Processing in plugin-webAccessibleResources.ts

During the renderCrxManifest hook in packages/vite-plugin/src/node/plugin-webAccessibleResources.ts, the plugin scans the user's web_accessible_resources array for the DYNAMIC_RESOURCE placeholder. When detected, the plugin executes three critical operations (lines 94-104):

  1. Strips the placeholder from the declaration to ensure the final manifest never contains the literal string <dynamic_resource> (lines 98-102).
  2. Collects match patterns into a Set named dynamicScriptMatches, aggregating all URLs specified by the developer.
  3. Propagates the flag by storing the use_dynamic_url value in dynamicScriptDynamicUrl for later application to generated resources (lines 103-104).

Resolving Dynamic Scripts to Real Resources

After Vite emits the build file manifest, the plugin walks through all content-script entries. For each script identified as dynamic (isDynamicScript === true), the plugin assigns the collected dynamicScriptMatches and the use_dynamic_url flag to the generated web accessible resource entry. The placeholder transforms into actual file names—including the bundled script hash and any imported assets—ensuring the runtime can access the correct URLs.

Final Manifest Cleanup

The plugin performs post-processing to ensure valid output. Duplicate web accessible resource entries are merged, and the placeholder is completely removed from the final JSON. For browsers that do not support the use_dynamic_url property (such as Firefox), the plugin strips this field (lines 26-29), maintaining cross-browser compatibility without manual intervention.

Implementing defineDynamicResource in Your Extension

Declaring Dynamic Resources in the Manifest

Define your dynamic web accessible resources in the manifest configuration file using the helper function:

// src/manifest.ts
import { defineManifest, defineDynamicResource } from 'crx/vite-plugin'

export const manifest = defineManifest({
  name: 'My Extension',
  version: '1.0',
  manifest_version: 3,
  web_accessible_resources: [
    // All dynamic scripts can be loaded from any HTTPS origin
    defineDynamicResource({
      matches: ['https://example.com/*'],
      use_dynamic_url: true,
    }),
  ],
  // …other manifest fields…
})

This call produces a web accessible resource entry containing the placeholder <dynamic_resource> and your specified match pattern.

Internal Build Transformation

The plugin transforms your declaration during the build. Here is the simplified internal logic from plugin-webAccessibleResources.ts:

// Internally (simplified from lines 94-104)
for (const r of manifest.web_accessible_resources) {
  if (r.resources.includes(DYNAMIC_RESOURCE)) {
    // Remove placeholder, collect matches, remember flag
    const matches = new Set(r.matches)
    const useDynamic = r.use_dynamic_url ?? false
    
    // Later, when Vite emits a dynamic script file:
    web_accessible_resources.push({
      matches: [...matches],
      resources: ['assets/dynamic-script.abc123.js'],
      use_dynamic_url: useDynamic,
    })
  }
}

Accessing Dynamic URLs at Runtime

When use_dynamic_url is enabled, Chrome rewrites chrome.runtime.getURL to return temporary URLs that remain valid even when script hashes change:

// content script (dynamic)
import scriptUrl from './dynamic-script?script'

// Chrome resolves it to the actual bundled file
chrome.runtime.getURL(scriptUrl).then(url => {
  const s = document.createElement('script')
  s.src = url
  document.head.append(s)
})

Summary

  • defineDynamicResource creates a placeholder entry in packages/vite-plugin/src/node/defineManifest.ts with the sentinel value DYNAMIC_RESOURCE (<dynamic_resource>), match patterns, and the use_dynamic_url flag.
  • The default configuration provides permissive URL matches (http://*/*, https://*/*) and disables dynamic URLs by default for immediate usability.
  • During the renderCrxManifest hook in plugin-webAccessibleResources.ts, the plugin strips the placeholder, aggregates matches into dynamicScriptMatches, and preserves the use_dynamic_url setting.
  • The system automatically resolves placeholder entries to actual bundled file names (including content hashes) for all scripts marked with isDynamicScript === true.
  • Cross-browser compatibility is handled by stripping use_dynamic_url for unsupported browsers like Firefox (lines 26-29).

Frequently Asked Questions

What is the DYNAMIC_RESOURCE sentinel value?

The DYNAMIC_RESOURCE constant is a literal string value <dynamic_resource> defined in packages/vite-plugin/src/node/defineManifest.ts at lines 71-72. It serves as a marker that the plugin-webAccessibleResources.ts build hook detects and replaces with actual file names during the Vite build process. This sentinel allows developers to declare "any script that will be injected at runtime" without knowing the final hashed file names in advance.

Does defineDynamicResource work with Firefox?

Yes, but with compatibility adjustments. The use_dynamic_url flag is specific to Chrome's Manifest V3 implementation. During final manifest cleanup in plugin-webAccessibleResources.ts (lines 26-29), the plugin detects Firefox and strips the use_dynamic_url property from the output. The dynamic resource handling itself works across browsers, but the dynamic URL generation feature is Chrome-specific.

How does use_dynamic_url affect chrome.runtime.getURL?

When use_dynamic_url is set to true, Chrome generates a dynamic URL via chrome.runtime.getURL that remains stable even when the underlying file hash changes between builds. This allows content scripts to inject sub-resources using chrome.runtime.getURL without hardcoding specific asset hashes. The CRXJS plugin propagates this flag from your defineDynamicResource call to the final generated web accessible resource entries.

Where does the actual file name substitution happen?

The substitution occurs in packages/vite-plugin/src/node/plugin-webAccessibleResources.ts during the renderCrxManifest hook. The plugin scans for entries containing DYNAMIC_RESOURCE (lines 94-103), removes the placeholder, and later assigns actual file names (like assets/dynamic-script.abc123.js) when processing content scripts where isDynamicScript === true. This happens after Vite has generated the final asset hashes but before the manifest is written to disk.

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 →