diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index 8ff0963d321692..7b321828341c6d 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -156,7 +156,7 @@ function encodeWorkerAssetFileName( ): string { const { fileNameHash } = workerCache const hash = getHash(fileName) - if (!fileNameHash.get(fileName)) { + if (!fileNameHash.get(hash)) { fileNameHash.set(hash, fileName) } return `__VITE_WORKER_ASSET__${hash}__` @@ -297,10 +297,13 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { while ((match = workerAssetUrlRE.exec(code))) { const [full, hash] = match const filename = fileNameHash.get(hash)! - const outputFilepath = path.relative( + let outputFilepath = path.relative( path.dirname(chunk.fileName), filename ) + if (!outputFilepath.startsWith('.')) { + outputFilepath = './' + outputFilepath + } const replacement = JSON.stringify(outputFilepath).slice(1, -1) s.overwrite(match.index, match.index + full.length, replacement, { contentOnly: true diff --git a/playground/worker/__tests__/relative-base/relative-base-worker.spec.ts b/playground/worker/__tests__/relative-base/relative-base-worker.spec.ts new file mode 100644 index 00000000000000..ff633f66b05ea2 --- /dev/null +++ b/playground/worker/__tests__/relative-base/relative-base-worker.spec.ts @@ -0,0 +1,127 @@ +import fs from 'fs' +import path from 'path' +import type { Page } from 'playwright-chromium' +import { isBuild, page, testDir, untilUpdated } from '~utils' + +test('normal', async () => { + await page.click('.ping') + await untilUpdated(() => page.textContent('.pong'), 'pong') + await untilUpdated( + () => page.textContent('.mode'), + isBuild ? 'production' : 'development' + ) + await untilUpdated( + () => page.textContent('.bundle-with-plugin'), + 'worker bundle with plugin success!' + ) +}) + +test('TS output', async () => { + await page.click('.ping-ts-output') + await untilUpdated(() => page.textContent('.pong-ts-output'), 'pong') +}) + +test('inlined', async () => { + await page.click('.ping-inline') + await untilUpdated(() => page.textContent('.pong-inline'), 'pong') +}) + +const waitSharedWorkerTick = ( + (resolvedSharedWorkerCount: number) => async (page: Page) => { + await untilUpdated(async () => { + const count = await page.textContent('.tick-count') + // ignore the initial 0 + return count === '1' ? 'page loaded' : '' + }, 'page loaded') + // test.concurrent sequential is not guaranteed + // force page to wait to ensure two pages overlap in time + resolvedSharedWorkerCount++ + if (resolvedSharedWorkerCount < 2) return + + await untilUpdated(() => { + return resolvedSharedWorkerCount === 2 ? 'all pages loaded' : '' + }, 'all pages loaded') + } +)(0) + +test.each([[true], [false]])('shared worker', async (doTick) => { + if (doTick) { + await page.click('.tick-shared') + } + await waitSharedWorkerTick(page) +}) + +test('worker emitted and import.meta.url in nested worker (serve)', async () => { + expect(await page.textContent('.nested-worker')).toMatch( + 'worker-nested-worker' + ) + expect(await page.textContent('.nested-worker-module')).toMatch('sub-worker') + expect(await page.textContent('.nested-worker-constructor')).toMatch( + '"type":"constructor"' + ) +}) + +describe.runIf(isBuild)('build', () => { + // assert correct files + test('inlined code generation', async () => { + const chunksDir = path.resolve(testDir, 'dist/chunks') + const files = fs.readdirSync(chunksDir) + const index = files.find((f) => f.includes('main-module')) + const content = fs.readFileSync(path.resolve(chunksDir, index), 'utf-8') + const workerEntriesDir = path.resolve(testDir, 'dist/worker-entries') + const workerFiles = fs.readdirSync(workerEntriesDir) + const worker = workerFiles.find((f) => f.includes('worker_entry.my-worker')) + const workerContent = fs.readFileSync( + path.resolve(workerEntriesDir, worker), + 'utf-8' + ) + + // worker should have all imports resolved and no exports + expect(workerContent).not.toMatch(`import`) + expect(workerContent).not.toMatch(`export`) + // chunk + expect(content).toMatch(`new Worker("../worker-entries/`) + expect(content).toMatch(`new SharedWorker("../worker-entries/`) + // inlined + expect(content).toMatch(`(window.URL||window.webkitURL).createObjectURL`) + expect(content).toMatch(`window.Blob`) + }) + + test('worker emitted and import.meta.url in nested worker (build)', async () => { + expect(await page.textContent('.nested-worker-module')).toMatch( + '"type":"module"' + ) + expect(await page.textContent('.nested-worker-constructor')).toMatch( + '"type":"constructor"' + ) + }) +}) + +test('module worker', async () => { + expect(await page.textContent('.shared-worker-import-meta-url')).toMatch( + 'A string' + ) +}) + +// TODO: Maybe we can modify classic-worker.js and classic-shared-worker.js to take into account the different asset paths +test('classic worker', async () => { + expect(await page.textContent('.classic-worker')).toMatch('A classic') + expect(await page.textContent('.classic-shared-worker')).toMatch('A classic') +}) + +test('emit chunk', async () => { + expect(await page.textContent('.emit-chunk-worker')).toMatch( + '["A string",{"type":"emit-chunk-sub-worker","data":"A string"},{"type":"module-and-worker:worker","data":"A string"},{"type":"module-and-worker:module","data":"module and worker"},{"type":"emit-chunk-sub-worker","data":{"module":"module and worker","msg1":"module1","msg2":"module2","msg3":"module3"}}]' + ) + expect(await page.textContent('.emit-chunk-dynamic-import-worker')).toMatch( + '"A string./"' + ) +}) + +test('import.meta.glob in worker', async () => { + expect(await page.textContent('.importMetaGlob-worker')).toMatch('["') +}) + +test('import.meta.glob with eager in worker', async () => { + expect(await page.textContent('.importMetaGlobEager-worker')).toMatch('["') +}) diff --git a/playground/worker/__tests__/relative-base/vite.config.js b/playground/worker/__tests__/relative-base/vite.config.js new file mode 100644 index 00000000000000..6837b8ef975a3d --- /dev/null +++ b/playground/worker/__tests__/relative-base/vite.config.js @@ -0,0 +1 @@ +module.exports = require('../../vite.config-relative-base') diff --git a/playground/worker/classic-shared-worker.js b/playground/worker/classic-shared-worker.js index e629208f05cf0a..eed61e028f1e01 100644 --- a/playground/worker/classic-shared-worker.js +++ b/playground/worker/classic-shared-worker.js @@ -1,4 +1,7 @@ -importScripts(`/${self.location.pathname.split('/')[1]}/classic.js`) +let base = `/${self.location.pathname.split('/')[1]}` +if (base === `/worker-entries`) base = '' // relative base + +importScripts(`${base}/classic.js`) self.onconnect = (event) => { const port = event.ports[0] diff --git a/playground/worker/classic-worker.js b/playground/worker/classic-worker.js index 238857acf00a58..55486818a8ada7 100644 --- a/playground/worker/classic-worker.js +++ b/playground/worker/classic-worker.js @@ -1,4 +1,7 @@ -importScripts(`/${self.location.pathname.split("/")[1]}/classic.js`) +let base = `/${self.location.pathname.split('/')[1]}` +if (base === `/worker-entries`) base = '' // relative base + +importScripts(`${base}/classic.js`) self.addEventListener('message', () => { self.postMessage(self.constant) diff --git a/playground/worker/package.json b/playground/worker/package.json index 52cdac8c27c051..d66a96e84d59f2 100644 --- a/playground/worker/package.json +++ b/playground/worker/package.json @@ -18,6 +18,9 @@ "dev:sourcemap-inline": "cross-env WORKER_MODE=inline vite --config ./vite.config-sourcemap.js dev", "build:sourcemap-inline": "cross-env WORKER_MODE=inline vite --config ./vite.config-sourcemap.js build", "preview:sourcemap-inline": "cross-env WORKER_MODE=inline vite --config ./vite.config-sourcemap.js preview", + "dev:relative-base": "cross-env WORKER_MODE=inline vite --config ./vite.config-relative-base.js dev", + "build:relative-base": "cross-env WORKER_MODE=inline vite --config ./vite.config-relative-base.js build", + "preview:relative-base": "cross-env WORKER_MODE=inline vite --config ./vite.config-relative-base.js preview", "debug": "node --inspect-brk ../../packages/vite/bin/vite" }, "devDependencies": { diff --git a/playground/worker/vite.config-relative-base.js b/playground/worker/vite.config-relative-base.js new file mode 100644 index 00000000000000..dd10caa205f60b --- /dev/null +++ b/playground/worker/vite.config-relative-base.js @@ -0,0 +1,42 @@ +const vueJsx = require('@vitejs/plugin-vue-jsx') +const vite = require('vite') +const path = require('path') + +module.exports = vite.defineConfig({ + base: './', + enforce: 'pre', + worker: { + format: 'es', + plugins: [vueJsx()], + rollupOptions: { + output: { + assetFileNames: 'worker-assets/worker_asset.[name]-[hash].[ext]', + chunkFileNames: 'worker-chunks/worker_chunk.[name]-[hash].js', + entryFileNames: 'worker-entries/worker_entry.[name]-[hash].js' + } + } + }, + build: { + outDir: 'dist', + rollupOptions: { + output: { + assetFileNames: 'other-assets/[name]-[hash].[ext]', + chunkFileNames: 'chunks/[name]-[hash].js', + entryFileNames: 'entries/[name]-[hash].js' + } + } + }, + plugins: [ + { + name: 'resolve-format-es', + transform(code, id) { + if (id.includes('main.js')) { + return code.replace( + `/* flag: will replace in vite config import("./format-es.js") */`, + `import("./main-format-es")` + ) + } + } + } + ] +})