Skip to content

Commit

Permalink
fix(worker): throw error when circular worker import is detected and …
Browse files Browse the repository at this point in the history
…support self referencing worker (#16103)
  • Loading branch information
sapphi-red authored Mar 6, 2024
1 parent e92abe5 commit eef9da1
Show file tree
Hide file tree
Showing 13 changed files with 146 additions and 32 deletions.
8 changes: 6 additions & 2 deletions packages/vite/src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ export interface LegacyOptions {

export interface ResolvedWorkerOptions {
format: 'es' | 'iife'
plugins: () => Promise<Plugin[]>
plugins: (bundleChain: string[]) => Promise<Plugin[]>
rollupOptions: RollupOptions
}

Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -719,6 +721,7 @@ export async function resolveConfig(
...resolved,
isWorker: true,
mainConfig: resolved,
bundleChain,
}
const resolvedWorkerPlugins = await resolvePlugins(
workerResolved,
Expand Down Expand Up @@ -760,6 +763,7 @@ export async function resolveConfig(
ssr,
isWorker: false,
mainConfig: null,
bundleChain: [],
isProduction,
plugins: userPlugins,
css: resolveCSSOptions(config.css),
Expand Down
31 changes: 21 additions & 10 deletions packages/vite/src/node/plugins/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
},
Expand Down Expand Up @@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -335,24 +345,25 @@ 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
}
}

return {
code: `export default function WorkerWrapper(options) {
return new ${workerConstructor}(
${JSON.stringify(url)},
${urlCode},
${workerTypeOption}
);
}`,
Expand Down
34 changes: 21 additions & 13 deletions packages/vite/src/node/plugins/workerImportMetaUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
16 changes: 14 additions & 2 deletions playground/worker/__tests__/es/worker-es.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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'))
Expand Down Expand Up @@ -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',
)
})
15 changes: 14 additions & 1 deletion playground/worker/__tests__/iife/worker-iife.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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'))
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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',
)
})
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions playground/worker/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ <h2 class="format-iife">format iife:</h2>
</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' })
Expand Down
13 changes: 13 additions & 0 deletions playground/worker/self-reference-url-worker.js
Original file line number Diff line number Diff line change
@@ -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}`)
})
13 changes: 13 additions & 0 deletions playground/worker/self-reference-worker.js
Original file line number Diff line number Diff line change
@@ -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}`)
})
16 changes: 16 additions & 0 deletions playground/worker/worker/main-module.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`
})

0 comments on commit eef9da1

Please sign in to comment.