Skip to content

Commit eef9da1

Browse files
authored
fix(worker): throw error when circular worker import is detected and support self referencing worker (#16103)
1 parent e92abe5 commit eef9da1

13 files changed

+146
-32
lines changed

packages/vite/src/node/config.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ export interface LegacyOptions {
328328

329329
export interface ResolvedWorkerOptions {
330330
format: 'es' | 'iife'
331-
plugins: () => Promise<Plugin[]>
331+
plugins: (bundleChain: string[]) => Promise<Plugin[]>
332332
rollupOptions: RollupOptions
333333
}
334334

@@ -357,6 +357,8 @@ export type ResolvedConfig = Readonly<
357357
// in nested worker bundle to find the main config
358358
/** @internal */
359359
mainConfig: ResolvedConfig | null
360+
/** @internal list of bundle entry id. used to detect recursive worker bundle. */
361+
bundleChain: string[]
360362
isProduction: boolean
361363
envDir: string
362364
env: Record<string, any>
@@ -689,7 +691,7 @@ export async function resolveConfig(
689691
)
690692
}
691693

692-
const createWorkerPlugins = async function () {
694+
const createWorkerPlugins = async function (bundleChain: string[]) {
693695
// Some plugins that aren't intended to work in the bundling of workers (doing post-processing at build time for example).
694696
// And Plugins may also have cached that could be corrupted by being used in these extra rollup calls.
695697
// So we need to separate the worker plugin from the plugin that vite needs to run.
@@ -719,6 +721,7 @@ export async function resolveConfig(
719721
...resolved,
720722
isWorker: true,
721723
mainConfig: resolved,
724+
bundleChain,
722725
}
723726
const resolvedWorkerPlugins = await resolvePlugins(
724727
workerResolved,
@@ -760,6 +763,7 @@ export async function resolveConfig(
760763
ssr,
761764
isWorker: false,
762765
mainConfig: null,
766+
bundleChain: [],
763767
isProduction,
764768
plugins: userPlugins,
765769
css: resolveCSSOptions(config.css),

packages/vite/src/node/plugins/worker.ts

+21-10
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { ResolvedConfig } from '../config'
55
import type { Plugin } from '../plugin'
66
import type { ViteDevServer } from '../server'
77
import { ENV_ENTRY, ENV_PUBLIC_PATH } from '../constants'
8-
import { getHash, injectQuery, urlRE } from '../utils'
8+
import { getHash, injectQuery, prettifyUrl, urlRE } from '../utils'
99
import {
1010
createToImportMetaURLBasedRelativeRuntime,
1111
onRollupWarning,
@@ -50,13 +50,22 @@ async function bundleWorkerEntry(
5050
config: ResolvedConfig,
5151
id: string,
5252
): Promise<OutputChunk> {
53+
const input = cleanUrl(id)
54+
const newBundleChain = [...config.bundleChain, input]
55+
if (config.bundleChain.includes(input)) {
56+
throw new Error(
57+
'Circular worker imports detected. Vite does not support it. ' +
58+
`Import chain: ${newBundleChain.map((id) => prettifyUrl(id, config.root)).join(' -> ')}`,
59+
)
60+
}
61+
5362
// bundle the file as entry to support imports
5463
const { rollup } = await import('rollup')
5564
const { plugins, rollupOptions, format } = config.worker
5665
const bundle = await rollup({
5766
...rollupOptions,
58-
input: cleanUrl(id),
59-
plugins: await plugins(),
67+
input,
68+
plugins: await plugins(newBundleChain),
6069
onwarn(warning, warn) {
6170
onRollupWarning(warning, warn, config)
6271
},
@@ -262,8 +271,6 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
262271
const workerMatch = workerOrSharedWorkerRE.exec(id)
263272
if (!workerMatch) return
264273

265-
// stringified url or `new URL(...)`
266-
let url: string
267274
const { format } = config.worker
268275
const workerConstructor =
269276
workerMatch[1] === 'sharedworker' ? 'SharedWorker' : 'Worker'
@@ -277,8 +284,11 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
277284
name: options?.name
278285
}`
279286

287+
let urlCode: string
280288
if (isBuild) {
281-
if (inlineRE.test(id)) {
289+
if (isWorker && this.getModuleInfo(cleanUrl(id))?.isEntry) {
290+
urlCode = 'self.location.href'
291+
} else if (inlineRE.test(id)) {
282292
const chunk = await bundleWorkerEntry(config, id)
283293
const encodedJs = `const encodedJs = "${Buffer.from(
284294
chunk.code,
@@ -335,24 +345,25 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
335345
map: { mappings: '' },
336346
}
337347
} else {
338-
url = await workerFileToUrl(config, id)
348+
urlCode = JSON.stringify(await workerFileToUrl(config, id))
339349
}
340350
} else {
341-
url = await fileToUrl(cleanUrl(id), config, this)
351+
let url = await fileToUrl(cleanUrl(id), config, this)
342352
url = injectQuery(url, `${WORKER_FILE_ID}&type=${workerType}`)
353+
urlCode = JSON.stringify(url)
343354
}
344355

345356
if (urlRE.test(id)) {
346357
return {
347-
code: `export default ${JSON.stringify(url)}`,
358+
code: `export default ${urlCode}`,
348359
map: { mappings: '' }, // Empty sourcemap to suppress Rollup warning
349360
}
350361
}
351362

352363
return {
353364
code: `export default function WorkerWrapper(options) {
354365
return new ${workerConstructor}(
355-
${JSON.stringify(url)},
366+
${urlCode},
356367
${workerTypeOption}
357368
);
358369
}`,

packages/vite/src/node/plugins/workerImportMetaUrl.ts

+21-13
Original file line numberDiff line numberDiff line change
@@ -165,22 +165,30 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
165165
: slash(path.resolve(path.dirname(id), url))
166166
}
167167

168-
let builtUrl: string
169-
if (isBuild) {
170-
builtUrl = await workerFileToUrl(config, file)
168+
if (
169+
isBuild &&
170+
config.isWorker &&
171+
this.getModuleInfo(cleanUrl(file))?.isEntry
172+
) {
173+
s.update(expStart, expEnd, 'self.location.href')
171174
} else {
172-
builtUrl = await fileToUrl(cleanUrl(file), config, this)
173-
builtUrl = injectQuery(
174-
builtUrl,
175-
`${WORKER_FILE_ID}&type=${workerType}`,
175+
let builtUrl: string
176+
if (isBuild) {
177+
builtUrl = await workerFileToUrl(config, file)
178+
} else {
179+
builtUrl = await fileToUrl(cleanUrl(file), config, this)
180+
builtUrl = injectQuery(
181+
builtUrl,
182+
`${WORKER_FILE_ID}&type=${workerType}`,
183+
)
184+
}
185+
s.update(
186+
expStart,
187+
expEnd,
188+
// add `'' +` to skip vite:asset-import-meta-url plugin
189+
`new URL('' + ${JSON.stringify(builtUrl)}, import.meta.url)`,
176190
)
177191
}
178-
s.update(
179-
expStart,
180-
expEnd,
181-
// add `'' +` to skip vite:asset-import-meta-url plugin
182-
`new URL('' + ${JSON.stringify(builtUrl)}, import.meta.url)`,
183-
)
184192
}
185193

186194
if (s) {

playground/worker/__tests__/es/worker-es.spec.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from 'node:fs'
22
import path from 'node:path'
33
import { describe, expect, test } from 'vitest'
4-
import { isBuild, page, testDir, untilUpdated } from '~utils'
4+
import { expectWithRetry, isBuild, page, testDir, untilUpdated } from '~utils'
55

66
test('normal', async () => {
77
await untilUpdated(() => page.textContent('.pong'), 'pong', true)
@@ -111,7 +111,7 @@ describe.runIf(isBuild)('build', () => {
111111
test('inlined code generation', async () => {
112112
const assetsDir = path.resolve(testDir, 'dist/es/assets')
113113
const files = fs.readdirSync(assetsDir)
114-
expect(files.length).toBe(32)
114+
expect(files.length).toBe(34)
115115
const index = files.find((f) => f.includes('main-module'))
116116
const content = fs.readFileSync(path.resolve(assetsDir, index), 'utf-8')
117117
const worker = files.find((f) => f.includes('my-worker'))
@@ -228,3 +228,15 @@ test('import.meta.glob with eager in worker', async () => {
228228
true,
229229
)
230230
})
231+
232+
test('self reference worker', async () => {
233+
expectWithRetry(() => page.textContent('.self-reference-worker')).toBe(
234+
'pong: main\npong: nested\n',
235+
)
236+
})
237+
238+
test('self reference url worker', async () => {
239+
expectWithRetry(() => page.textContent('.self-reference-url-worker')).toBe(
240+
'pong: main\npong: nested\n',
241+
)
242+
})

playground/worker/__tests__/iife/worker-iife.spec.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from 'node:fs'
22
import path from 'node:path'
33
import { describe, expect, test } from 'vitest'
44
import {
5+
expectWithRetry,
56
isBuild,
67
isServe,
78
page,
@@ -74,7 +75,7 @@ describe.runIf(isBuild)('build', () => {
7475
test('inlined code generation', async () => {
7576
const assetsDir = path.resolve(testDir, 'dist/iife/assets')
7677
const files = fs.readdirSync(assetsDir)
77-
expect(files.length).toBe(20)
78+
expect(files.length).toBe(22)
7879
const index = files.find((f) => f.includes('main-module'))
7980
const content = fs.readFileSync(path.resolve(assetsDir, index), 'utf-8')
8081
const worker = files.find((f) => f.includes('worker_entry-my-worker'))
@@ -160,6 +161,18 @@ test('import.meta.glob eager in worker', async () => {
160161
)
161162
})
162163

164+
test('self reference worker', async () => {
165+
expectWithRetry(() => page.textContent('.self-reference-worker')).toBe(
166+
'pong: main\npong: nested\n',
167+
)
168+
})
169+
170+
test('self reference url worker', async () => {
171+
expectWithRetry(() => page.textContent('.self-reference-url-worker')).toBe(
172+
'pong: main\npong: nested\n',
173+
)
174+
})
175+
163176
test.runIf(isServe)('sourcemap boundary', async () => {
164177
const response = page.waitForResponse(/my-worker.ts\?worker_file&type=module/)
165178
await page.goto(viteTestUrl)

playground/worker/__tests__/relative-base/worker-relative-base.spec.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from 'node:fs'
22
import path from 'node:path'
33
import { describe, expect, test } from 'vitest'
4-
import { isBuild, page, testDir, untilUpdated } from '~utils'
4+
import { expectWithRetry, isBuild, page, testDir, untilUpdated } from '~utils'
55

66
test('normal', async () => {
77
await untilUpdated(() => page.textContent('.pong'), 'pong', true)
@@ -161,3 +161,15 @@ test('import.meta.glob with eager in worker', async () => {
161161
true,
162162
)
163163
})
164+
165+
test('self reference worker', async () => {
166+
expectWithRetry(() => page.textContent('.self-reference-worker')).toBe(
167+
'pong: main\npong: nested\n',
168+
)
169+
})
170+
171+
test('self reference url worker', async () => {
172+
expectWithRetry(() => page.textContent('.self-reference-url-worker')).toBe(
173+
'pong: main\npong: nested\n',
174+
)
175+
})

playground/worker/__tests__/sourcemap-hidden/worker-sourcemap-hidden.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe.runIf(isBuild)('build', () => {
1010

1111
const files = fs.readdirSync(assetsDir)
1212
// should have 2 worker chunk
13-
expect(files.length).toBe(40)
13+
expect(files.length).toBe(44)
1414
const index = files.find((f) => f.includes('main-module'))
1515
const content = fs.readFileSync(path.resolve(assetsDir, index), 'utf-8')
1616
const indexSourcemap = getSourceMapUrl(content)

playground/worker/__tests__/sourcemap-inline/worker-sourcemap-inline.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe.runIf(isBuild)('build', () => {
1010

1111
const files = fs.readdirSync(assetsDir)
1212
// should have 2 worker chunk
13-
expect(files.length).toBe(20)
13+
expect(files.length).toBe(22)
1414
const index = files.find((f) => f.includes('main-module'))
1515
const content = fs.readFileSync(path.resolve(assetsDir, index), 'utf-8')
1616
const indexSourcemap = getSourceMapUrl(content)

playground/worker/__tests__/sourcemap/worker-sourcemap.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe.runIf(isBuild)('build', () => {
99
const assetsDir = path.resolve(testDir, 'dist/iife-sourcemap/assets')
1010
const files = fs.readdirSync(assetsDir)
1111
// should have 2 worker chunk
12-
expect(files.length).toBe(40)
12+
expect(files.length).toBe(44)
1313
const index = files.find((f) => f.includes('main-module'))
1414
const content = fs.readFileSync(path.resolve(assetsDir, index), 'utf-8')
1515
const indexSourcemap = getSourceMapUrl(content)

playground/worker/index.html

+12
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,18 @@ <h2 class="format-iife">format iife:</h2>
152152
</p>
153153
<code class="importMetaGlobEager-worker"></code>
154154

155+
<p>
156+
self reference worker
157+
<span class="classname">.self-reference-worker</span>
158+
</p>
159+
<code class="self-reference-worker"></code>
160+
161+
<p>
162+
new Worker(new URL('../self-reference-url-worker.js', import.meta.url))
163+
<span class="classname">.self-reference-url-worker</span>
164+
</p>
165+
<code class="self-reference-url-worker"></code>
166+
155167
<p>
156168
new Worker(new URL('../deeply-nested-worker.js', import.meta.url), { type:
157169
'module' })
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
self.addEventListener('message', (e) => {
2+
if (e.data === 'main') {
3+
const selfWorker = new Worker(
4+
new URL('./self-reference-url-worker.js', import.meta.url),
5+
)
6+
selfWorker.postMessage('nested')
7+
selfWorker.addEventListener('message', (e) => {
8+
self.postMessage(e.data)
9+
})
10+
}
11+
12+
self.postMessage(`pong: ${e.data}`)
13+
})
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import SelfWorker from './self-reference-worker?worker'
2+
3+
self.addEventListener('message', (e) => {
4+
if (e.data === 'main') {
5+
const selfWorker = new SelfWorker()
6+
selfWorker.postMessage('nested')
7+
selfWorker.addEventListener('message', (e) => {
8+
self.postMessage(e.data)
9+
})
10+
}
11+
12+
self.postMessage(`pong: ${e.data}`)
13+
})

playground/worker/worker/main-module.js

+16
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import mySharedWorker from '../my-shared-worker?sharedworker&name=shared'
55
import TSOutputWorker from '../possible-ts-output-worker?worker'
66
import NestedWorker from '../worker-nested-worker?worker'
77
import { mode } from '../modules/workerImport'
8+
import SelfReferenceWorker from '../self-reference-worker?worker'
89

910
function text(el, text) {
1011
document.querySelector(el).textContent = text
@@ -158,3 +159,18 @@ importMetaGlobEagerWorker.postMessage('1')
158159
importMetaGlobEagerWorker.addEventListener('message', (e) => {
159160
text('.importMetaGlobEager-worker', JSON.stringify(e.data))
160161
})
162+
163+
const selfReferenceWorker = new SelfReferenceWorker()
164+
selfReferenceWorker.postMessage('main')
165+
selfReferenceWorker.addEventListener('message', (e) => {
166+
document.querySelector('.self-reference-worker').textContent += `${e.data}\n`
167+
})
168+
169+
const selfReferenceUrlWorker = new Worker(
170+
new URL('../self-reference-url-worker.js', import.meta.url),
171+
)
172+
selfReferenceUrlWorker.postMessage('main')
173+
selfReferenceUrlWorker.addEventListener('message', (e) => {
174+
document.querySelector('.self-reference-url-worker').textContent +=
175+
`${e.data}\n`
176+
})

0 commit comments

Comments
 (0)