Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(worker): using data URLs for inline shared worker #12014

Merged
merged 10 commits into from Mar 18, 2023
2 changes: 1 addition & 1 deletion docs/config/worker-options.md
Expand Up @@ -5,7 +5,7 @@ Options related to Web Workers.
## worker.format

- **Type:** `'es' | 'iife'`
- **Default:** `iife`
- **Default:** `'iife'`

Output format for worker bundle.

Expand Down
43 changes: 29 additions & 14 deletions packages/vite/src/node/plugins/worker.ts
Expand Up @@ -287,25 +287,40 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
: 'classic'
: 'module'
const workerOptions = workerType === 'classic' ? '' : ',{type: "module"}'

if (isBuild) {
getDepsOptimizer(config, ssr)?.registerWorkersSource(id)
if (query.inline != null) {
const chunk = await bundleWorkerEntry(config, id, query)
// inline as blob data url
return {
code: `const encodedJs = "${Buffer.from(chunk.code).toString(
'base64',
)}";
const blob = typeof window !== "undefined" && window.Blob && new Blob([atob(encodedJs)], { type: "text/javascript;charset=utf-8" });
export default function WorkerWrapper() {
const objURL = blob && (window.URL || window.webkitURL).createObjectURL(blob);
try {
return objURL ? new ${workerConstructor}(objURL) : new ${workerConstructor}("data:application/javascript;base64," + encodedJs${workerOptions});
} finally {
objURL && (window.URL || window.webkitURL).revokeObjectURL(objURL);
}
}`,
const encodedJs = `const encodedJs = "${Buffer.from(
chunk.code,
).toString('base64')}";`

const code =
// Using blob URL for SharedWorker results in multiple instances of a same worker
workerConstructor === 'Worker'
? `${encodedJs}
const blob = typeof window !== "undefined" && window.Blob && new Blob([atob(encodedJs)], { type: "text/javascript;charset=utf-8" });
export default function WorkerWrapper() {
let objURL;
try {
objURL = blob && (window.URL || window.webkitURL).createObjectURL(blob);
if (!objURL) throw ''
return new ${workerConstructor}(objURL)
} catch(e) {
return new ${workerConstructor}("data:application/javascript;base64," + encodedJs${workerOptions});
} finally {
objURL && (window.URL || window.webkitURL).revokeObjectURL(objURL);
}
}`
: `${encodedJs}
export default function WorkerWrapper() {
return new ${workerConstructor}("data:application/javascript;base64," + encodedJs${workerOptions});
}
`

return {
code,
// Empty sourcemap to suppress Rollup warning
map: { mappings: '' },
}
Expand Down
13 changes: 12 additions & 1 deletion playground/worker/__tests__/es/es-worker.spec.ts
Expand Up @@ -34,6 +34,10 @@ test('shared worker', async () => {
await untilUpdated(() => page.textContent('.tick-count'), 'pong', true)
})

test('inline shared worker', async () => {
await untilUpdated(() => page.textContent('.pong-shared-inline'), 'pong')
})

test('worker emitted and import.meta.url in nested worker (serve)', async () => {
await untilUpdated(
() => page.textContent('.nested-worker'),
Expand Down Expand Up @@ -72,9 +76,16 @@ describe.runIf(isBuild)('build', () => {
// chunk
expect(content).toMatch(`new Worker("/es/assets`)
expect(content).toMatch(`new SharedWorker("/es/assets`)
// inlined
// inlined worker
expect(content).toMatch(`(window.URL||window.webkitURL).createObjectURL`)
expect(content).toMatch(`window.Blob`)
expect(content).toMatch(
/try\{if\(\w+=\w+&&\(window\.URL\|\|window\.webkitURL\)\.createObjectURL\(\w+\),!\w+\)throw""/,
)
// inlined shared worker
expect(content).toMatch(
`return new SharedWorker("data:application/javascript;base64,"+`,
)
})

test('worker emitted and import.meta.url in nested worker (build)', async () => {
Expand Down
4 changes: 4 additions & 0 deletions playground/worker/__tests__/iife/iife-worker.spec.ts
Expand Up @@ -29,6 +29,10 @@ test('shared worker', async () => {
await untilUpdated(() => page.textContent('.tick-count'), 'pong')
})

test('inline shared worker', async () => {
await untilUpdated(() => page.textContent('.pong-shared-inline'), 'pong')
})

test('worker emitted and import.meta.url in nested worker (serve)', async () => {
await untilUpdated(() => page.textContent('.nested-worker'), '/worker-nested')
await untilUpdated(
Expand Down
Expand Up @@ -35,6 +35,10 @@ test('shared worker', async () => {
await untilUpdated(() => page.textContent('.tick-count'), 'pong', true)
})

test('inline shared worker', async () => {
await untilUpdated(() => page.textContent('.pong-shared-inline'), 'pong')
})

test('worker emitted and import.meta.url in nested worker (serve)', async () => {
await untilUpdated(
() => page.textContent('.nested-worker'),
Expand Down
Expand Up @@ -10,7 +10,7 @@ describe.runIf(isBuild)('build', () => {

const files = fs.readdirSync(assetsDir)
// should have 2 worker chunk
expect(files.length).toBe(31)
expect(files.length).toBe(32)
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
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(31)
expect(files.length).toBe(32)
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
6 changes: 6 additions & 0 deletions playground/worker/index.html
Expand Up @@ -38,6 +38,12 @@ <h2 class="format-iife">format iife:</h2>
</p>
<code class="tick-count"></code>

<p>
import InlineSharedWorker from '../my-shared-worker?sharedworker&inline'
<span class="classname">.pong-shared-inline</span>
</p>
<code class="pong-shared-inline"></code>

<p>
new Worker(new URL('./url-worker.js', import.meta.url), { type: 'module' })
<span class="classname">.worker-import-meta-url</span>
Expand Down
13 changes: 13 additions & 0 deletions playground/worker/my-inline-shared-worker.ts
@@ -0,0 +1,13 @@
let inlineSharedWorkerCount = 0

// @ts-expect-error onconnect exists in worker
self.onconnect = (event) => {
inlineSharedWorkerCount++
const port = event.ports[0]
if (inlineSharedWorkerCount >= 2) {
port.postMessage('pong')
}
}

// for sourcemap
console.log('my-inline-shared-worker.js')
11 changes: 4 additions & 7 deletions playground/worker/my-shared-worker.ts
@@ -1,14 +1,11 @@
const ports = new Set()
let sharedWorkerCount = 0

// @ts-expect-error onconnect exists in worker
self.onconnect = (event) => {
sharedWorkerCount++
const port = event.ports[0]
ports.add(port)
port.postMessage('pong')
port.onmessage = () => {
ports.forEach((p: any) => {
p.postMessage('pong')
})
if (sharedWorkerCount >= 2) {
port.postMessage('pong')
}
}

Expand Down
26 changes: 21 additions & 5 deletions playground/worker/worker/main-module.js
@@ -1,5 +1,6 @@
import myWorker from '../my-worker.ts?worker'
import InlineWorker from '../my-worker.ts?worker&inline'
import InlineSharedWorker from '../my-inline-shared-worker?sharedworker&inline'
import mySharedWorker from '../my-shared-worker?sharedworker&name=shared'
import TSOutputWorker from '../possible-ts-output-worker?worker'
import NestedWorker from '../worker-nested-worker?worker'
Expand All @@ -26,11 +27,26 @@ inlineWorker.addEventListener('message', (e) => {
text('.pong-inline', e.data.msg)
})

const sharedWorker = new mySharedWorker()
sharedWorker.port.addEventListener('message', (event) => {
text('.tick-count', event.data)
})
sharedWorker.port.start()
const startSharedWorker = () => {
const sharedWorker = new mySharedWorker()
sharedWorker.port.addEventListener('message', (event) => {
text('.tick-count', event.data)
})
sharedWorker.port.start()
}
startSharedWorker()
startSharedWorker()

const startInlineSharedWorker = () => {
const inlineSharedWorker = new InlineSharedWorker()
inlineSharedWorker.port.addEventListener('message', (event) => {
text('.pong-shared-inline', event.data)
})
inlineSharedWorker.port.start()
}

startInlineSharedWorker()
startInlineSharedWorker()

const tsOutputWorker = new TSOutputWorker()
tsOutputWorker.postMessage('ping')
Expand Down