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

feat(worker): allow passing options to worker constructors #6145

Closed
wants to merge 12 commits into from
9 changes: 7 additions & 2 deletions packages/playground/worker/__tests__/worker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ test('inlined', async () => {
await untilUpdated(() => page.textContent('.pong-inline'), 'pong')
})

test('worker options', async () => {
await page.click('.ping-classic')
await untilUpdated(() => page.textContent('.pong-classic'), 'pong')
})

const waitSharedWorkerTick = (
(resolvedSharedWorkerCount: number) => async (page: Page) => {
await untilUpdated(async () => {
Expand Down Expand Up @@ -52,8 +57,8 @@ if (isBuild) {
test('inlined code generation', async () => {
const assetsDir = path.resolve(testDir, 'dist/assets')
const files = fs.readdirSync(assetsDir)
// should have 3 worker chunk
expect(files.length).toBe(4)
// should have 4 worker chunk
expect(files.length).toBe(5)
const index = files.find((f) => f.includes('index'))
const content = fs.readFileSync(path.resolve(assetsDir, index), 'utf-8')
const worker = files.find((f) => f.includes('my-worker'))
Expand Down
7 changes: 7 additions & 0 deletions packages/playground/worker/classic-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
self.onmessage = (e) => {
if (e.data === 'ping') {
// Ensure that we are not in a module worker (calling importScripts in module workers throws an error).
self.importScripts()
self.postMessage('pong')
}
}
13 changes: 13 additions & 0 deletions packages/playground/worker/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<button class="ping-inline">Ping Inline Worker</button>
<div>Response from inline worker: <span class="pong-inline"></span></div>

<button class="ping-classic">Ping Classic Worker</button>
<div>Response from classic worker: <span class="pong-classic"></span></div>

<button class="ping-ts-output">Ping Possible Compiled TS Worker</button>
<div>
Response from worker imported from code that might be compiled TS:
Expand All @@ -22,6 +25,7 @@
<script type="module">
import Worker from './my-worker?worker'
import InlineWorker from './my-worker?worker&inline'
import ClassicWorker from './classic-worker?worker'
import SharedWorker from './my-shared-worker?sharedworker&name=shared'
import TSOutputWorker from './possible-ts-output-worker?worker'
import { mode } from './workerImport'
Expand All @@ -47,6 +51,15 @@
inlineWorker.postMessage('ping')
})

const classicWorker = new ClassicWorker({ type: 'classic' })
classicWorker.addEventListener('message', (e) => {
text('.pong-classic', e.data)
})

document.querySelector('.ping-classic').addEventListener('click', () => {
classicWorker.postMessage('ping')
})

const sharedWorker = new SharedWorker()
document.querySelector('.tick-shared').addEventListener('click', () => {
sharedWorker.port.postMessage('tick')
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ declare module '*.pdf' {
// web worker
declare module '*?worker' {
const workerConstructor: {
new (): Worker
new (workerOptions?: WorkerOptions): Worker
}
export default workerConstructor
}
Expand Down
54 changes: 38 additions & 16 deletions packages/vite/src/node/plugins/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ function parseWorkerRequest(id: string): Record<string, string> | null {
}

const WorkerFileId = 'worker_file'
const ClassicWorkerQuery = 'classic'

const esImportEnv = `import ${JSON.stringify(ENV_PUBLIC_PATH)}\n`
const classicImportEnv = `self.importScripts(${JSON.stringify(
ENV_PUBLIC_PATH
)});\n`

export function webWorkerPlugin(config: ResolvedConfig): Plugin {
const isBuild = config.command === 'build'
Expand All @@ -40,8 +46,11 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
async transform(_, id) {
const query = parseWorkerRequest(id)
if (query && query[WorkerFileId] != null) {
// For classic workers where `import` is unavailable, we use `importScripts` to inject env.
const importEnvCode =
query[ClassicWorkerQuery] != null ? classicImportEnv : esImportEnv
return {
code: `import '${ENV_PUBLIC_PATH}'\n` + _
code: importEnvCode + _
}
}
if (
Expand All @@ -51,7 +60,9 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
return
}

let url: string
const workerConstructor =
query.sharedworker != null ? 'SharedWorker' : 'Worker'

if (isBuild) {
// bundle the file as entry to support imports
const rollup = require('rollup') as typeof Rollup
Expand All @@ -77,41 +88,52 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
// inline as blob data url
return `const encodedJs = "${content.toString('base64')}";
const blob = typeof window !== "undefined" && window.Blob && new Blob([atob(encodedJs)], { type: "text/javascript;charset=utf-8" });
export default function WorkerWrapper() {
export default function WorkerWrapper(workerOptions) {
const objURL = blob && (window.URL || window.webkitURL).createObjectURL(blob);
try {
return objURL ? new Worker(objURL) : new Worker("data:application/javascript;base64," + encodedJs, {type: "module"});
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping @andreasg123

I don't quite understand why we didn't pass {type: "module"} to new Worker(objURL) (#4674). An oversight maybe?

return new Worker(
objURL || ("data:application/javascript;base64," + encodedJs),
Object.assign({ type: "module" }, workerOptions)
);
} finally {
objURL && (window.URL || window.webkitURL).revokeObjectURL(objURL);
}
}`
} else {
// isBuild: true, inline: false
const basename = path.parse(cleanUrl(id)).name
const contentHash = getAssetHash(content)
const fileName = path.posix.join(
config.build.assetsDir,
`${basename}.${contentHash}.js`
)
url = `__VITE_ASSET__${this.emitFile({
const url = `__VITE_ASSET__${this.emitFile({
fileName,
type: 'asset',
source: code
})}__`

return `export default function WorkerWrapper(workerOptions) {
return new ${workerConstructor}(
${JSON.stringify(url)},
Object.assign({ type: "module" }, workerOptions)
)
}`
}
} else {
url = await fileToUrl(cleanUrl(id), config, this)
// isBuild: false
let url = await fileToUrl(cleanUrl(id), config, this)
url = injectQuery(url, WorkerFileId)
}

const workerConstructor =
query.sharedworker != null ? 'SharedWorker' : 'Worker'
const workerOptions = { type: 'module' }

return `export default function WorkerWrapper() {
return new ${workerConstructor}(${JSON.stringify(
url
)}, ${JSON.stringify(workerOptions, null, 2)})
}`
return `export default function WorkerWrapper(workerOptions) {
workerOptions = Object.assign({ type: "module" }, workerOptions)
let url = ${JSON.stringify(url)}
if (workerOptions.type !== "module") {
url += ${JSON.stringify('&' + ClassicWorkerQuery)}
}
return new ${workerConstructor}(url, workerOptions)
}`
}
}
}
}