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

format iife:

+

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