fix(worker): throw error when circular worker import is detected and support self referencing worker (#16103)

This commit is contained in:
翠 / green 2024-03-06 20:03:33 +09:00 committed by GitHub
parent e92abe5816
commit eef9da13d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 146 additions and 32 deletions

View File

@ -328,7 +328,7 @@ export interface LegacyOptions {
export interface ResolvedWorkerOptions {
format: 'es' | 'iife'
plugins: () => Promise<Plugin[]>
plugins: (bundleChain: string[]) => Promise<Plugin[]>
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<string, any>
@ -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),

View File

@ -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<OutputChunk> {
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}
);
}`,

View File

@ -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) {

View File

@ -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',
)
})

View File

@ -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)

View File

@ -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',
)
})

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -152,6 +152,18 @@
</p>
<code class="importMetaGlobEager-worker"></code>
<p>
self reference worker
<span class="classname">.self-reference-worker</span>
</p>
<code class="self-reference-worker"></code>
<p>
new Worker(new URL('../self-reference-url-worker.js', import.meta.url))
<span class="classname">.self-reference-url-worker</span>
</p>
<code class="self-reference-url-worker"></code>
<p>
new Worker(new URL('../deeply-nested-worker.js', import.meta.url), { type:
'module' })

View File

@ -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}`)
})

View File

@ -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}`)
})

View File

@ -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`
})