Skip to content

Commit d5e1c9a

Browse files
committed
fix: use Vite build API for IIFE bundling to fix Vite 6+ compatibility
Vite 6+ tracks plugins internally using WeakMaps, causing errors when reusing plugins in a separate Rollup build. This rewrites bundleIife() to use viteBuild() with direct rollupOptions instead of raw Rollup. Key changes: - Use configFile: false to avoid loading user's config with incompatible settings - Use direct rollupOptions with format: 'iife' instead of lib mode - Copy resolve settings (alias, extensions, conditions) from dev server - Filter out manifest.json and .vite/ assets from output Works with Vite 3, 5, 6, and 7.
1 parent d1ec973 commit d5e1c9a

File tree

8 files changed

+216
-66
lines changed

8 files changed

+216
-66
lines changed

packages/vite-plugin/src/node/fileWriter-rxjs.ts

Lines changed: 50 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,7 @@ import * as lexer from 'es-module-lexer'
22
import fsx from 'fs-extra'
33
import { readFile } from 'fs/promises'
44
import MagicString from 'magic-string'
5-
import {
6-
OutputAsset,
7-
OutputChunk,
8-
OutputOptions,
9-
rollup,
10-
RollupOptions,
11-
} from 'rollup'
5+
import { OutputAsset, OutputChunk, RollupOutput } from 'rollup'
126
import {
137
BehaviorSubject,
148
filter,
@@ -26,8 +20,8 @@ import {
2620
toArray,
2721
} from 'rxjs'
2822
import {
23+
build as viteBuild,
2924
ErrorPayload,
30-
resolveConfig,
3125
ResolvedConfig,
3226
ViteDevServer,
3327
} from 'vite'
@@ -39,35 +33,6 @@ import convertSourceMap from 'convert-source-map'
3933

4034
const { outputFile } = fsx
4135

42-
const buildConfigCache = new Map<string, ResolvedConfig>()
43-
44-
const getBuildConfigCacheKey = (server: ViteDevServer) => {
45-
return `${server.config.root}:${server.config.mode}:${
46-
server.config.configFile ?? 'inline'
47-
}`
48-
}
49-
50-
async function resolveBuildConfig(
51-
server: ViteDevServer,
52-
): Promise<ResolvedConfig> {
53-
const cacheKey = getBuildConfigCacheKey(server)
54-
const cached = buildConfigCache.get(cacheKey)
55-
if (cached) return cached
56-
const configFile = server.config.configFile ?? false
57-
const resolved = await resolveConfig(
58-
{
59-
root: server.config.root,
60-
mode: server.config.mode,
61-
logLevel: 'silent',
62-
configFile,
63-
},
64-
'build',
65-
server.config.mode,
66-
)
67-
buildConfigCache.set(cacheKey, resolved)
68-
return resolved
69-
}
70-
7136
const getIifeGlobalName = (fileName: string) => {
7237
const base = fileName.split('/').pop() ?? fileName
7338
const sanitized = base.replace(/\W+/g, '_').replace(/^_+/, '')
@@ -91,35 +56,50 @@ async function bundleIife(
9156
script: CrxDevScriptId,
9257
fileName: string,
9358
) {
94-
const buildConfig = await resolveBuildConfig(server)
9559
const input = resolveScriptInput(server, script.id)
96-
const rollupOptions: RollupOptions = {
97-
input,
98-
plugins: buildConfig.plugins.filter(
99-
(plugin) => !plugin.name?.startsWith('crx:'),
100-
),
101-
external: buildConfig.build.rollupOptions?.external,
102-
onwarn: buildConfig.build.rollupOptions?.onwarn,
103-
treeshake: buildConfig.build.rollupOptions?.treeshake,
104-
}
105-
const rollupOutput = [buildConfig.build.rollupOptions?.output].flat()[0]
106-
const { dir, file, manualChunks, ...baseOutputOptions } = (rollupOutput ??
107-
{}) as OutputOptions
108-
const sourcemap = buildConfig.build.sourcemap === 'inline' ? 'inline' : false
109-
const outputOptions: OutputOptions = {
110-
...baseOutputOptions,
111-
format: 'iife',
112-
inlineDynamicImports: true,
113-
entryFileNames: fileName,
114-
assetFileNames:
115-
baseOutputOptions.assetFileNames ?? 'assets/[name][extname]',
116-
name: getIifeGlobalName(fileName),
117-
sourcemap,
118-
}
60+
const sourcemap =
61+
server.config.build.sourcemap === 'inline' ? 'inline' : false
62+
63+
// Use Vite's build API instead of raw Rollup to avoid plugin compatibility issues
64+
// (Vite 6+ plugins are tracked in WeakMaps and can't be reused in separate Rollup builds)
65+
// We use configFile: false to avoid loading user's config which may have incompatible settings
66+
const result = await viteBuild({
67+
root: server.config.root,
68+
mode: server.config.mode,
69+
configFile: false, // Don't load user's config - use minimal IIFE-specific settings
70+
logLevel: 'silent',
71+
resolve: {
72+
// Copy resolve settings from the dev server for consistency
73+
alias: server.config.resolve.alias,
74+
extensions: server.config.resolve.extensions,
75+
conditions: server.config.resolve.conditions,
76+
},
77+
build: {
78+
write: false, // Don't write to disk, we'll handle that
79+
manifest: false, // Don't generate Vite manifest
80+
rollupOptions: {
81+
input,
82+
output: {
83+
format: 'iife',
84+
name: getIifeGlobalName(fileName),
85+
entryFileNames: fileName,
86+
inlineDynamicImports: true, // Required for IIFE format
87+
sourcemap,
88+
},
89+
},
90+
minify: false,
91+
copyPublicDir: false,
92+
},
93+
})
11994

120-
const bundle = await rollup(rollupOptions)
121-
const { output } = await bundle.generate(outputOptions)
122-
await bundle.close()
95+
// viteBuild with write: false returns RollupOutput or RollupOutput[]
96+
const outputs = Array.isArray(result) ? result : [result]
97+
const firstOutput = outputs[0]
98+
const output = 'output' in firstOutput ? firstOutput.output : undefined
99+
100+
if (!output) {
101+
throw new Error(`Unable to generate IIFE bundle for "${script.id}"`)
102+
}
123103

124104
const entryChunk = output.find(
125105
(item): item is OutputChunk => isOutputChunk(item) && item.isEntry,
@@ -128,7 +108,12 @@ async function bundleIife(
128108
throw new Error(`Unable to generate IIFE bundle for "${script.id}"`)
129109
}
130110

131-
const assets = output.filter(isOutputAsset)
111+
const assets = output.filter(isOutputAsset).filter(
112+
// Filter out manifest.json to avoid overwriting extension manifest
113+
(asset) =>
114+
asset.fileName !== 'manifest.json' &&
115+
!asset.fileName.startsWith('.vite/'),
116+
)
132117
const extraChunks = output.filter(
133118
(item): item is OutputChunk => isOutputChunk(item) && !item.isEntry,
134119
)

playgrounds/iife-demo/manifest.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { defineManifest } from '@crxjs/vite-plugin'
2+
3+
export default defineManifest({
4+
manifest_version: 3,
5+
name: 'IIFE Content Script Demo',
6+
version: '1.0.0',
7+
description: 'Demo of IIFE content scripts for main-world injection',
8+
icons: {
9+
48: 'public/icon.png',
10+
},
11+
permissions: ['scripting'],
12+
host_permissions: ['<all_urls>'],
13+
background: {
14+
service_worker: 'src/background.ts',
15+
type: 'module',
16+
},
17+
})

playgrounds/iife-demo/package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "play-iife-demo",
3+
"type": "module",
4+
"version": "1.0.0",
5+
"private": true,
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "vite build"
9+
},
10+
"devDependencies": {
11+
"@crxjs/vite-plugin": "workspace:*",
12+
"@types/chrome": "^0.0.313",
13+
"@types/node": "^22.13.14",
14+
"typescript": "~5.7.2",
15+
"vite": "^6.2.0"
16+
},
17+
"dependenciesMeta": {
18+
"@crxjs/vite-plugin": {
19+
"injected": true
20+
}
21+
}
22+
}
897 Bytes
Loading
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Background script - registers IIFE content script for main-world injection
2+
declare const self: ServiceWorkerGlobalScope
3+
export {}
4+
5+
import mainWorldScript from './main-world?iife'
6+
7+
console.log('[CRXJS] Background script loaded')
8+
console.log('[CRXJS] Registering main-world script:', mainWorldScript)
9+
10+
const script: chrome.scripting.RegisteredContentScript = {
11+
id: 'main-world-script',
12+
js: [mainWorldScript],
13+
matches: ['<all_urls>'],
14+
world: 'MAIN',
15+
runAt: 'document_start',
16+
}
17+
18+
async function registerScript() {
19+
// Check if script already exists (for hot reload scenarios)
20+
const existing = await chrome.scripting.getRegisteredContentScripts({
21+
ids: [script.id],
22+
})
23+
if (existing.length) {
24+
await chrome.scripting.updateContentScripts([script])
25+
console.log('[CRXJS] Updated main-world content script')
26+
} else {
27+
await chrome.scripting.registerContentScripts([script])
28+
console.log('[CRXJS] Registered main-world content script')
29+
}
30+
}
31+
32+
registerScript()
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// This script runs in the MAIN world - it has access to the page's JavaScript context!
2+
// Unlike regular content scripts that run in an isolated world, this can:
3+
// - Access window.* variables set by the page
4+
// - Call functions defined by the page
5+
// - Intercept/modify page behavior
6+
7+
console.log('[CRXJS IIFE] Running in MAIN world!')
8+
console.log(
9+
'[CRXJS IIFE] window object is the real page2112313 window:',
10+
window.location.href,
11+
)
12+
13+
// Example: Add a visible indicator that the script is running
14+
function addIndicator() {
15+
const indicator = document.createElement('div')
16+
indicator.id = 'crxjs-iife-indicator'
17+
indicator.innerHTML = `
18+
<div style="
19+
position: fixed;
20+
bottom: 20px;
21+
right: 20px;
22+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
23+
color: white;
24+
padding: 12px 20px;
25+
border-radius: 8px;
26+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
27+
font-size: 14px;
28+
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
29+
z-index: 999999;
30+
cursor: pointer;
31+
transition: transform 0.2s, box-shadow 0.2s;
32+
" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
33+
<strong>Hello crxjs community</strong><br>
34+
<small>Running in MAIN world</small>
35+
</div>
36+
`
37+
indicator.onclick = () => {
38+
alert(
39+
"This script has access to the page's JavaScript context!\n\nwindow.location: " +
40+
window.location.href,
41+
)
42+
}
43+
document.body.appendChild(indicator)
44+
}
45+
46+
// Wait for DOM to be ready
47+
if (document.readyState === 'loading') {
48+
document.addEventListener('DOMContentLoaded', addIndicator)
49+
} else {
50+
addIndicator()
51+
}
52+
53+
// Example: Intercept fetch (only possible in MAIN world!)
54+
const originalFetch = window.fetch
55+
window.fetch = async function (...args) {
56+
console.log('[CRXJS IIFE] Intercepted fetch:', args[0])
57+
return originalFetch.apply(this, args)
58+
}
59+
60+
console.log(
61+
'[CRXJS IIFE] Fetch interceptor installed - try making a fetch request!',
62+
)
63+
64+
export {}
65+
// test change 1768659751
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { crx } from '@crxjs/vite-plugin'
2+
import { defineConfig } from 'vite'
3+
import manifest from './manifest'
4+
5+
export default defineConfig({
6+
plugins: [crx({ manifest })],
7+
build: { minify: false },
8+
})

pnpm-lock.yaml

Lines changed: 22 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)