Content scripts have unique requirements in Chrome extensions. CRXJS provides specialized configuration options to handle content script loading, CSS injection, and Hot Module Replacement (HMR).
Overview
Content script options are configured under the contentScripts key in your CRXJS configuration:
import { crx } from '@crxjs/vite-plugin'
crx({
manifest,
contentScripts: {
preambleCode: false,
hmrTimeout: 5000,
injectCss: true
}
})
Configuration Options
preambleCode
contentScripts.preambleCode
string | false | undefined
Code injected before content scripts execute. Used to set up framework-specific functionality like React Fast Refresh.
Automatic Configuration
When using React with @vitejs/plugin-react, CRXJS automatically detects and applies the React preamble:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { crx } from '@crxjs/vite-plugin'
export default defineConfig({
plugins: [
react(),
crx({ manifest })
// preambleCode is automatically configured for React Fast Refresh
]
})
CRXJS automatically uses @vitejs/plugin-react’s preambleCode to enable Fast Refresh in content scripts. No manual configuration needed for React projects.
Manual Configuration
crx({
manifest,
contentScripts: {
// Custom preamble code
preambleCode: `
window.__vite_plugin_react_preamble_installed__ = true;
// Your custom initialization code
`
}
})
Disable Preamble
crx({
manifest,
contentScripts: {
// Disable preamble entirely
preambleCode: false
}
})
Disabling preambleCode when using React will break Fast Refresh in content scripts during development.
hmrTimeout
contentScripts.hmrTimeout
Timeout in milliseconds for establishing HMR WebSocket connections in content scripts.Default: 5000 (5 seconds)
When to Adjust
Increase the timeout if:
- Your development server starts slowly
- You’re experiencing network latency
- Content scripts are loading before the dev server is ready
crx({
manifest,
contentScripts: {
hmrTimeout: 10000 // 10 seconds
}
})
How It Works
During development, CRXJS injects an HMR client into content scripts that connects to Vite’s dev server via WebSocket. The hmrTimeout controls how long the client waits for the connection to establish before timing out.
// Example HMR timeout behavior
try {
await connectToHMRServer({ timeout: 5000 })
console.log('HMR connected')
} catch (error) {
console.error('HMR connection timeout')
}
If you see “HMR connection timeout” warnings in the console, try increasing hmrTimeout.
MAIN World Content Scripts
Content scripts with world: "MAIN" do not support HMR and will always require a full page reload:
// manifest.json
{
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["src/content.ts"],
"world": "MAIN" // No HMR support
}
]
}
Content scripts running in world: "MAIN" don’t support HMR. Changes require reloading the page. CRXJS will warn you about these scripts during development.
injectCss
Controls how CSS imported by content scripts is handled in the built extension.Default: true
When injectCss: true (default)
CSS is injected into the page via JavaScript at runtime:
// Your content script
import './styles.css'
// At runtime, CRXJS:
// 1. Bundles CSS with the content script
// 2. Injects it via <style> tag when the script loads
Pros:
- CSS loads immediately with the content script
- No additional web_accessible_resources entries needed
- Simpler manifest configuration
Cons:
- Small JavaScript overhead for injection
- CSS isn’t cached separately
When injectCss: false
CSS files are emitted separately and added to web_accessible_resources:
crx({
manifest,
contentScripts: {
injectCss: false
}
})
With injectCss: false:
// Your content script
import './styles.css'
// At build time, CRXJS:
// 1. Emits styles.css as a separate file
// 2. Adds it to web_accessible_resources in manifest
// 3. You must manually load it in your content script
Manual CSS loading:
// content.ts
import stylesUrl from './styles.css?url'
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = chrome.runtime.getURL(stylesUrl)
document.head.appendChild(link)
Pros:
- Full control over when/how CSS loads
- CSS can be cached separately
- Useful for conditional CSS loading
Cons:
- Requires manual loading code
- More complex setup
Use injectCss: true (default) for most cases. Only set to false if you need fine-grained control over CSS loading.
Content Script Types
CRXJS handles three types of content scripts differently:
Module Scripts (default)
Standard ES module content scripts:
{
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["src/content.ts"]
}
]
}
- Supports HMR
- Runs in ISOLATED world by default
- Can use
import/export
- Supports dynamic imports
Loader Scripts
When content scripts have imports or dynamic imports, CRXJS automatically creates a loader:
// content.ts
import { helper } from './helper'
import('./dynamic-feature')
// CRXJS creates:
// - content.ts.js (your bundled script)
// - content.ts-loader.js (loader that imports your script)
// Manifest points to the loader
When loaders are created:
- Script has imports
- Script has dynamic imports
- Script exports functions (e.g.,
onExecute)
When loaders are skipped:
- Script has no imports
- Script has no dynamic imports
- Script has no exports
In this case, the script is wrapped in an IIFE for scope isolation:
(function(){
// Your content script code
})()
MAIN World Scripts
Content scripts that run in the main page context:
{
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["src/content.ts"],
"world": "MAIN"
}
]
}
- No HMR support (requires page reload)
- Cannot use
chrome APIs
- Can access page JavaScript directly
- CRXJS will log a warning during development
Dynamic Content Scripts
CRXJS supports dynamically injected content scripts via chrome.scripting.executeScript:
// background.ts
import contentScript from './content.ts?script'
chrome.scripting.executeScript({
target: { tabId },
files: [contentScript]
})
Dynamic scripts are automatically:
- Added to
web_accessible_resources
- Configured with appropriate match patterns
- Bundled with their dependencies
Development vs Production
Development Mode
During development (vite dev):
// CRXJS injects:
// 1. Preamble code (if configured)
// 2. Vite client for HMR
// 3. HMR WebSocket connection
// 4. Your content script
// Generated loader:
import 'http://localhost:5173/preamble.js'
import 'http://localhost:5173/@vite/client'
import 'http://localhost:5173/src/content.ts'
- All resources loaded from dev server
- HMR enabled (except MAIN world scripts)
- Source maps available
Production Build
During build (vite build):
// CRXJS creates:
// - Bundled content script
// - Minified loader (if needed)
// - Optimized CSS (if injectCss: false)
// Generated loader:
import './content.js'
- All resources bundled and optimized
- No HMR overhead
- Minimal loader code
Complete Example
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { crx } from '@crxjs/vite-plugin'
import manifest from './manifest.json'
export default defineConfig(({ mode }) => {
const isDev = mode === 'development'
return {
plugins: [
react(),
crx({
manifest,
contentScripts: {
// React preamble auto-configured
// Override only if needed:
// preambleCode: false,
// Increase timeout for slower networks
hmrTimeout: isDev ? 10000 : 5000,
// Auto-inject CSS (default)
injectCss: true
}
})
]
}
})
Troubleshooting
HMR Not Working
- Check console for timeout warnings
- Increase
hmrTimeout
- Verify dev server is running
- Check if script uses
world: "MAIN" (not supported)
CSS Not Loading
- With
injectCss: true - CSS should auto-inject
- With
injectCss: false - Ensure manual loading code is present
- Check
web_accessible_resources in built manifest
React Fast Refresh Not Working
- Ensure
@vitejs/plugin-react is installed
- Check that
preambleCode isn’t set to false
- Verify React plugin is before CRXJS plugin
See Also