# How CRXJS Handles HTML Inline Scripts in Vite for Chrome Extensions

> Learn how CRXJS handles inline scripts in Vite for Chrome extensions. Discover the internal workings of plugin-htmlInlineScripts for Manifest V3 CSP compliance.

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

---

**The `plugin-htmlInlineScripts` module audits Vite plugin output, extracts inline JavaScript, and dynamically loads it through ES modules to satisfy Manifest V3 CSP requirements.**

Chrome Extension Tools (CRXJS) bridges the gap between Vite's development features and Chrome's strict Content Security Policy (CSP) for Manifest V3 extensions. The [`packages/vite-plugin/src/node/plugin-htmlInlineScripts.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/plugin-htmlInlineScripts.ts) file implements a comprehensive **HTML inline script handling** system that transforms dangerous inline script tags into compliant dynamically-loaded modules without breaking execution order.

## The Manifest V3 Inline Script Problem

Manifest V3 extensions prohibit inline JavaScript execution via Content Security Policy directives. Vite and its ecosystem frequently inject inline scripts during development—for client initialization, hot module replacement, and plugin functionality. Without intervention, these injected scripts cause immediate CSP violations in Chrome extensions.

The CRXJS solution operates as a four-stage pipeline that intercepts, caches, and rewrites script injection patterns before they reach the browser.

## Four-Stage Inline Script Transformation Pipeline

### Stage 1: Auditing Other Plugins with `auditTransformIndexHtml`

The plugin begins by wrapping every Vite plugin's `transformIndexHtml` hook to intercept emitted script tags. The `auditTransformIndexHtml` function (lines [51‑88](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/plugin-htmlInlineScripts.ts#L51-L88)) replaces each plugin's original hook with an `auditor` wrapper that captures all `<script>` elements.

When plugins emit inline scripts, the auditor immediately strips them from the HTML output and stores them in a temporary cache. External scripts with `src` attributes pass through but are tracked for dependency ordering.

### Stage 2: Per-Page Cache Initialization via `prePlugin`

Before any HTML processing begins, the `prePlugin` (lines [90‑109](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/plugin-htmlInlineScripts.ts#L90-L109)) initializes a dedicated cache entry for each page. The system generates a normalized page identifier using the `toKey` helper function (lines [25‑28](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/plugin-htmlInlineScripts.ts#L25-L28)), which converts file paths into consistent cache keys that double as virtual module IDs.

This cache always initializes with Vite's client module (`@vite/client`) to ensure HMR capabilities survive the transformation process.

### Stage 3: Safe Script Extraction with `postPlugin`

After all other plugins complete their HTML transformations, the `postPlugin` (lines [112‑141](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/plugin-htmlInlineScripts.ts#L112-L141)) executes. This hook invokes `extractScriptsAndRemove` from [`packages/vite-plugin/src/node/html.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/html.ts) (lines [10‑17](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/html.ts#L10-L17)), which parses the final HTML and removes all `<script>` tags while preserving their metadata.

The plugin distinguishes inline scripts from external sources using the `isInlineTag` helper (lines [18‑20](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/plugin-htmlInlineScripts.ts#L18-L20)). It stores both the inline script bodies and external script paths in the page-specific cache, then removes them from the HTML entirely.

### Stage 4: Virtual Module Generation and Loading

When the browser requests a script via the special `@crx/inline-script` virtual URL prefix, the plugin's `load` hook (lines [173‑197](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/plugin-htmlInlineScripts.ts#L173-L197)) assembles the final module. This hook concatenates all collected inline script bodies into executable code, builds a JSON array of external module paths, and returns a module that imports the **page script loader** from [`client/es/page-inline-script-loader.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/client/es/page-inline-script-loader.ts).

The generated module structure follows this pattern:

```javascript
import loader from 'client/es/page-inline-script-loader.ts'

// Concatenated inline scripts execute first
console.log('inline from react plugin')

// External scripts loaded dynamically
const SCRIPTS = ["./src/main.ts"]
loader(SCRIPTS)

```

## Runtime Loader Mechanics

The [`client/es/page-inline-script-loader.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/client/es/page-inline-script-loader.ts) module receives the JSON array of script URLs, resolves them relative to the page directory, and injects each as a separate module-type `<script>` tag. Because each script loads as an ES module, the browser respects the original execution order while maintaining strict CSP compliance—no inline scripts remain in the final HTML.

## Configuration Example

Integrate the inline script handler into your Vite configuration for Chrome extension development:

```typescript
// vite.config.ts
import { defineConfig } from 'vite'
import { crx } from '@crxjs/vite-plugin'
import { pluginHtmlInlineScripts } from '@crxjs/chrome-extension-tools/vite-plugin'

export default defineConfig({
  plugins: [
    crx({ manifest: './manifest.json' }),
    // Activate during development to handle inline scripts
    pluginHtmlInlineScripts(),
  ],
})

```

During `vite serve`, the plugin automatically transforms HTML output:

```html
<!-- Original Vite output -->
<script type="module" src="/@vite/client"></script>
<script type="module">console.log('inline script')</script>
<script type="module" src="/src/main.ts"></script>

```

After processing, the HTML contains only:

```html
<!-- After CRXJS processing -->
<script type="module" src="@crx/inline-script/src/main?t=1677623456789"></script>

```

## Summary

- The [`plugin-htmlInlineScripts.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/plugin-htmlInlineScripts.ts) module intercepts all HTML transformations to audit for inline scripts.
- It caches scripts per-page using normalized keys generated by `toKey`, storing data from both inline sources and external modules.
- The system strips all `<script>` tags from HTML output using `extractScriptsAndRemove` and replaces them with a single virtual module loader.
- Virtual modules served via `@crx/inline-script` URLs concatenate inline code and dynamically import external scripts through [`page-inline-script-loader.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/page-inline-script-loader.ts).
- This architecture guarantees CSP compliance for Manifest V3 while preserving Vite's development features and script execution order.

## Frequently Asked Questions

### How does the plugin detect inline scripts versus external scripts?

The `isInlineTag` helper function (lines [18‑20](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/src/node/plugin-htmlInlineScripts.ts#L18-L20)) examines each `<script>` element for the absence of a `src` attribute. Tags without `src` are classified as inline and flagged for extraction and caching, while external scripts are tracked but processed differently during the virtual module generation phase.

### Why is the `@crx/inline-script` virtual module necessary?

Chrome's Manifest V3 CSP prohibits inline JavaScript execution entirely. The virtual module system allows Vite to serve concatenated inline script content as external module files with the `@crx/inline-script` prefix, effectively converting inline code into importable ES modules that bypass CSP restrictions while maintaining the original execution context.

### Does this HTML inline script handling affect production builds?

The `plugin-htmlInlineScripts` primarily targets development mode where Vite injects client code and HMR scripts inline. Production builds typically emit only external script files, but the plugin ensures that any remaining inline scripts—including those from third-party Vite plugins—are automatically externalized to prevent CSP violations in the packaged extension.

### How does the system preserve script execution order?

The plugin maintains execution order by caching scripts in the sequence they appear during the audit phase, then reconstructing that exact order in the virtual module output. The [`page-inline-script-loader.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/page-inline-script-loader.ts) runtime inserts scripts as ES modules in the provided array order, ensuring dependencies execute before dependent code regardless of the asynchronous loading mechanism.