diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts
index 2e18cc8b30ba10..21fe544497262a 100644
--- a/packages/vite/src/node/config.ts
+++ b/packages/vite/src/node/config.ts
@@ -328,7 +328,7 @@ export interface LegacyOptions {
export interface ResolvedWorkerOptions {
format: 'es' | 'iife'
- plugins: () => Promise
+ plugins: (bundleChain: string[]) => Promise
rollupOptions: RollupOptions
}
@@ -357,6 +357,8 @@ export type ResolvedConfig = Readonly<
// in nested worker bundle to find the main config
/** @internal */
mainConfig: ResolvedConfig | null
+ /** @internal list of bundle entry id. used to detect recursive worker bundle. */
+ bundleChain: string[]
isProduction: boolean
envDir: string
env: Record
@@ -689,7 +691,7 @@ export async function resolveConfig(
)
}
- const createWorkerPlugins = async function () {
+ const createWorkerPlugins = async function (bundleChain: string[]) {
// Some plugins that aren't intended to work in the bundling of workers (doing post-processing at build time for example).
// And Plugins may also have cached that could be corrupted by being used in these extra rollup calls.
// So we need to separate the worker plugin from the plugin that vite needs to run.
@@ -719,6 +721,7 @@ export async function resolveConfig(
...resolved,
isWorker: true,
mainConfig: resolved,
+ bundleChain,
}
const resolvedWorkerPlugins = await resolvePlugins(
workerResolved,
@@ -760,6 +763,7 @@ export async function resolveConfig(
ssr,
isWorker: false,
mainConfig: null,
+ bundleChain: [],
isProduction,
plugins: userPlugins,
css: resolveCSSOptions(config.css),
diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts
index 89df47a69defd3..22201fa8816cf8 100644
--- a/packages/vite/src/node/plugins/worker.ts
+++ b/packages/vite/src/node/plugins/worker.ts
@@ -5,7 +5,7 @@ import type { ResolvedConfig } from '../config'
import type { Plugin } from '../plugin'
import type { ViteDevServer } from '../server'
import { ENV_ENTRY, ENV_PUBLIC_PATH } from '../constants'
-import { getHash, injectQuery, urlRE } from '../utils'
+import { getHash, injectQuery, prettifyUrl, urlRE } from '../utils'
import {
createToImportMetaURLBasedRelativeRuntime,
onRollupWarning,
@@ -50,13 +50,22 @@ async function bundleWorkerEntry(
config: ResolvedConfig,
id: string,
): Promise {
+ const input = cleanUrl(id)
+ const newBundleChain = [...config.bundleChain, input]
+ if (config.bundleChain.includes(input)) {
+ throw new Error(
+ 'Circular worker imports detected. Vite does not support it. ' +
+ `Import chain: ${newBundleChain.map((id) => prettifyUrl(id, config.root)).join(' -> ')}`,
+ )
+ }
+
// bundle the file as entry to support imports
const { rollup } = await import('rollup')
const { plugins, rollupOptions, format } = config.worker
const bundle = await rollup({
...rollupOptions,
- input: cleanUrl(id),
- plugins: await plugins(),
+ input,
+ plugins: await plugins(newBundleChain),
onwarn(warning, warn) {
onRollupWarning(warning, warn, config)
},
@@ -262,8 +271,6 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
const workerMatch = workerOrSharedWorkerRE.exec(id)
if (!workerMatch) return
- // stringified url or `new URL(...)`
- let url: string
const { format } = config.worker
const workerConstructor =
workerMatch[1] === 'sharedworker' ? 'SharedWorker' : 'Worker'
@@ -277,8 +284,11 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
name: options?.name
}`
+ let urlCode: string
if (isBuild) {
- if (inlineRE.test(id)) {
+ if (isWorker && this.getModuleInfo(cleanUrl(id))?.isEntry) {
+ urlCode = 'self.location.href'
+ } else if (inlineRE.test(id)) {
const chunk = await bundleWorkerEntry(config, id)
const encodedJs = `const encodedJs = "${Buffer.from(
chunk.code,
@@ -335,16 +345,17 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
map: { mappings: '' },
}
} else {
- url = await workerFileToUrl(config, id)
+ urlCode = JSON.stringify(await workerFileToUrl(config, id))
}
} else {
- url = await fileToUrl(cleanUrl(id), config, this)
+ let url = await fileToUrl(cleanUrl(id), config, this)
url = injectQuery(url, `${WORKER_FILE_ID}&type=${workerType}`)
+ urlCode = JSON.stringify(url)
}
if (urlRE.test(id)) {
return {
- code: `export default ${JSON.stringify(url)}`,
+ code: `export default ${urlCode}`,
map: { mappings: '' }, // Empty sourcemap to suppress Rollup warning
}
}
@@ -352,7 +363,7 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
return {
code: `export default function WorkerWrapper(options) {
return new ${workerConstructor}(
- ${JSON.stringify(url)},
+ ${urlCode},
${workerTypeOption}
);
}`,
diff --git a/packages/vite/src/node/plugins/workerImportMetaUrl.ts b/packages/vite/src/node/plugins/workerImportMetaUrl.ts
index 4460c71cf3e836..0a7b34d4ff3dc8 100644
--- a/packages/vite/src/node/plugins/workerImportMetaUrl.ts
+++ b/packages/vite/src/node/plugins/workerImportMetaUrl.ts
@@ -165,22 +165,30 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
: slash(path.resolve(path.dirname(id), url))
}
- let builtUrl: string
- if (isBuild) {
- builtUrl = await workerFileToUrl(config, file)
+ if (
+ isBuild &&
+ config.isWorker &&
+ this.getModuleInfo(cleanUrl(file))?.isEntry
+ ) {
+ s.update(expStart, expEnd, 'self.location.href')
} else {
- builtUrl = await fileToUrl(cleanUrl(file), config, this)
- builtUrl = injectQuery(
- builtUrl,
- `${WORKER_FILE_ID}&type=${workerType}`,
+ let builtUrl: string
+ if (isBuild) {
+ builtUrl = await workerFileToUrl(config, file)
+ } else {
+ builtUrl = await fileToUrl(cleanUrl(file), config, this)
+ builtUrl = injectQuery(
+ builtUrl,
+ `${WORKER_FILE_ID}&type=${workerType}`,
+ )
+ }
+ s.update(
+ expStart,
+ expEnd,
+ // add `'' +` to skip vite:asset-import-meta-url plugin
+ `new URL('' + ${JSON.stringify(builtUrl)}, import.meta.url)`,
)
}
- s.update(
- expStart,
- expEnd,
- // add `'' +` to skip vite:asset-import-meta-url plugin
- `new URL('' + ${JSON.stringify(builtUrl)}, import.meta.url)`,
- )
}
if (s) {
diff --git a/playground/worker/__tests__/es/worker-es.spec.ts b/playground/worker/__tests__/es/worker-es.spec.ts
index 48462ad8bc8077..748cd2b0592bb2 100644
--- a/playground/worker/__tests__/es/worker-es.spec.ts
+++ b/playground/worker/__tests__/es/worker-es.spec.ts
@@ -1,7 +1,7 @@
import fs from 'node:fs'
import path from 'node:path'
import { describe, expect, test } from 'vitest'
-import { isBuild, page, testDir, untilUpdated } from '~utils'
+import { expectWithRetry, isBuild, page, testDir, untilUpdated } from '~utils'
test('normal', async () => {
await untilUpdated(() => page.textContent('.pong'), 'pong', true)
@@ -111,7 +111,7 @@ describe.runIf(isBuild)('build', () => {
test('inlined code generation', async () => {
const assetsDir = path.resolve(testDir, 'dist/es/assets')
const files = fs.readdirSync(assetsDir)
- expect(files.length).toBe(32)
+ expect(files.length).toBe(34)
const index = files.find((f) => f.includes('main-module'))
const content = fs.readFileSync(path.resolve(assetsDir, index), 'utf-8')
const worker = files.find((f) => f.includes('my-worker'))
@@ -228,3 +228,15 @@ test('import.meta.glob with eager in worker', async () => {
true,
)
})
+
+test('self reference worker', async () => {
+ expectWithRetry(() => page.textContent('.self-reference-worker')).toBe(
+ 'pong: main\npong: nested\n',
+ )
+})
+
+test('self reference url worker', async () => {
+ expectWithRetry(() => page.textContent('.self-reference-url-worker')).toBe(
+ 'pong: main\npong: nested\n',
+ )
+})
diff --git a/playground/worker/__tests__/iife/worker-iife.spec.ts b/playground/worker/__tests__/iife/worker-iife.spec.ts
index 3434fe756194da..77547e8426fbd1 100644
--- a/playground/worker/__tests__/iife/worker-iife.spec.ts
+++ b/playground/worker/__tests__/iife/worker-iife.spec.ts
@@ -2,6 +2,7 @@ import fs from 'node:fs'
import path from 'node:path'
import { describe, expect, test } from 'vitest'
import {
+ expectWithRetry,
isBuild,
isServe,
page,
@@ -74,7 +75,7 @@ describe.runIf(isBuild)('build', () => {
test('inlined code generation', async () => {
const assetsDir = path.resolve(testDir, 'dist/iife/assets')
const files = fs.readdirSync(assetsDir)
- expect(files.length).toBe(20)
+ expect(files.length).toBe(22)
const index = files.find((f) => f.includes('main-module'))
const content = fs.readFileSync(path.resolve(assetsDir, index), 'utf-8')
const worker = files.find((f) => f.includes('worker_entry-my-worker'))
@@ -160,6 +161,18 @@ test('import.meta.glob eager in worker', async () => {
)
})
+test('self reference worker', async () => {
+ expectWithRetry(() => page.textContent('.self-reference-worker')).toBe(
+ 'pong: main\npong: nested\n',
+ )
+})
+
+test('self reference url worker', async () => {
+ expectWithRetry(() => page.textContent('.self-reference-url-worker')).toBe(
+ 'pong: main\npong: nested\n',
+ )
+})
+
test.runIf(isServe)('sourcemap boundary', async () => {
const response = page.waitForResponse(/my-worker.ts\?worker_file&type=module/)
await page.goto(viteTestUrl)
diff --git a/playground/worker/__tests__/relative-base/worker-relative-base.spec.ts b/playground/worker/__tests__/relative-base/worker-relative-base.spec.ts
index e05bf8aed72a0b..ae791ff7f36fe4 100644
--- a/playground/worker/__tests__/relative-base/worker-relative-base.spec.ts
+++ b/playground/worker/__tests__/relative-base/worker-relative-base.spec.ts
@@ -1,7 +1,7 @@
import fs from 'node:fs'
import path from 'node:path'
import { describe, expect, test } from 'vitest'
-import { isBuild, page, testDir, untilUpdated } from '~utils'
+import { expectWithRetry, isBuild, page, testDir, untilUpdated } from '~utils'
test('normal', async () => {
await untilUpdated(() => page.textContent('.pong'), 'pong', true)
@@ -161,3 +161,15 @@ test('import.meta.glob with eager in worker', async () => {
true,
)
})
+
+test('self reference worker', async () => {
+ expectWithRetry(() => page.textContent('.self-reference-worker')).toBe(
+ 'pong: main\npong: nested\n',
+ )
+})
+
+test('self reference url worker', async () => {
+ expectWithRetry(() => page.textContent('.self-reference-url-worker')).toBe(
+ 'pong: main\npong: nested\n',
+ )
+})
diff --git a/playground/worker/__tests__/sourcemap-hidden/worker-sourcemap-hidden.spec.ts b/playground/worker/__tests__/sourcemap-hidden/worker-sourcemap-hidden.spec.ts
index 157d9f7d47b26a..f5034cbe6c0d05 100644
--- a/playground/worker/__tests__/sourcemap-hidden/worker-sourcemap-hidden.spec.ts
+++ b/playground/worker/__tests__/sourcemap-hidden/worker-sourcemap-hidden.spec.ts
@@ -10,7 +10,7 @@ describe.runIf(isBuild)('build', () => {
const files = fs.readdirSync(assetsDir)
// should have 2 worker chunk
- expect(files.length).toBe(40)
+ expect(files.length).toBe(44)
const index = files.find((f) => f.includes('main-module'))
const content = fs.readFileSync(path.resolve(assetsDir, index), 'utf-8')
const indexSourcemap = getSourceMapUrl(content)
diff --git a/playground/worker/__tests__/sourcemap-inline/worker-sourcemap-inline.spec.ts b/playground/worker/__tests__/sourcemap-inline/worker-sourcemap-inline.spec.ts
index 9a10908abdc1ec..3e7392b82d8a7b 100644
--- a/playground/worker/__tests__/sourcemap-inline/worker-sourcemap-inline.spec.ts
+++ b/playground/worker/__tests__/sourcemap-inline/worker-sourcemap-inline.spec.ts
@@ -10,7 +10,7 @@ describe.runIf(isBuild)('build', () => {
const files = fs.readdirSync(assetsDir)
// should have 2 worker chunk
- expect(files.length).toBe(20)
+ expect(files.length).toBe(22)
const index = files.find((f) => f.includes('main-module'))
const content = fs.readFileSync(path.resolve(assetsDir, index), 'utf-8')
const indexSourcemap = getSourceMapUrl(content)
diff --git a/playground/worker/__tests__/sourcemap/worker-sourcemap.spec.ts b/playground/worker/__tests__/sourcemap/worker-sourcemap.spec.ts
index 0dd723413e5a4c..e3e98c036852af 100644
--- a/playground/worker/__tests__/sourcemap/worker-sourcemap.spec.ts
+++ b/playground/worker/__tests__/sourcemap/worker-sourcemap.spec.ts
@@ -9,7 +9,7 @@ describe.runIf(isBuild)('build', () => {
const assetsDir = path.resolve(testDir, 'dist/iife-sourcemap/assets')
const files = fs.readdirSync(assetsDir)
// should have 2 worker chunk
- expect(files.length).toBe(40)
+ expect(files.length).toBe(44)
const index = files.find((f) => f.includes('main-module'))
const content = fs.readFileSync(path.resolve(assetsDir, index), 'utf-8')
const indexSourcemap = getSourceMapUrl(content)
diff --git a/playground/worker/index.html b/playground/worker/index.html
index 3080bc7bfb5a1a..c1944046af6998 100644
--- a/playground/worker/index.html
+++ b/playground/worker/index.html
@@ -152,6 +152,18 @@
+
+ self reference worker
+ .self-reference-worker
+
+
+
+
+ new Worker(new URL('../self-reference-url-worker.js', import.meta.url))
+ .self-reference-url-worker
+
+
+
new Worker(new URL('../deeply-nested-worker.js', import.meta.url), { type:
'module' })
diff --git a/playground/worker/self-reference-url-worker.js b/playground/worker/self-reference-url-worker.js
new file mode 100644
index 00000000000000..54b23f32169de7
--- /dev/null
+++ b/playground/worker/self-reference-url-worker.js
@@ -0,0 +1,13 @@
+self.addEventListener('message', (e) => {
+ if (e.data === 'main') {
+ const selfWorker = new Worker(
+ new URL('./self-reference-url-worker.js', import.meta.url),
+ )
+ selfWorker.postMessage('nested')
+ selfWorker.addEventListener('message', (e) => {
+ self.postMessage(e.data)
+ })
+ }
+
+ self.postMessage(`pong: ${e.data}`)
+})
diff --git a/playground/worker/self-reference-worker.js b/playground/worker/self-reference-worker.js
new file mode 100644
index 00000000000000..76f7608d9aa345
--- /dev/null
+++ b/playground/worker/self-reference-worker.js
@@ -0,0 +1,13 @@
+import SelfWorker from './self-reference-worker?worker'
+
+self.addEventListener('message', (e) => {
+ if (e.data === 'main') {
+ const selfWorker = new SelfWorker()
+ selfWorker.postMessage('nested')
+ selfWorker.addEventListener('message', (e) => {
+ self.postMessage(e.data)
+ })
+ }
+
+ self.postMessage(`pong: ${e.data}`)
+})
diff --git a/playground/worker/worker/main-module.js b/playground/worker/worker/main-module.js
index 6a7a9c6f0471ea..e210654402ab84 100644
--- a/playground/worker/worker/main-module.js
+++ b/playground/worker/worker/main-module.js
@@ -5,6 +5,7 @@ import mySharedWorker from '../my-shared-worker?sharedworker&name=shared'
import TSOutputWorker from '../possible-ts-output-worker?worker'
import NestedWorker from '../worker-nested-worker?worker'
import { mode } from '../modules/workerImport'
+import SelfReferenceWorker from '../self-reference-worker?worker'
function text(el, text) {
document.querySelector(el).textContent = text
@@ -158,3 +159,18 @@ importMetaGlobEagerWorker.postMessage('1')
importMetaGlobEagerWorker.addEventListener('message', (e) => {
text('.importMetaGlobEager-worker', JSON.stringify(e.data))
})
+
+const selfReferenceWorker = new SelfReferenceWorker()
+selfReferenceWorker.postMessage('main')
+selfReferenceWorker.addEventListener('message', (e) => {
+ document.querySelector('.self-reference-worker').textContent += `${e.data}\n`
+})
+
+const selfReferenceUrlWorker = new Worker(
+ new URL('../self-reference-url-worker.js', import.meta.url),
+)
+selfReferenceUrlWorker.postMessage('main')
+selfReferenceUrlWorker.addEventListener('message', (e) => {
+ document.querySelector('.self-reference-url-worker').textContent +=
+ `${e.data}\n`
+})