Skip to content

Commit

Permalink
fix(web-worker): expose globals on self (#6170)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va authored Jul 23, 2024
1 parent 57d23ce commit 12bb567
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 54 deletions.
4 changes: 2 additions & 2 deletions docs/guide/mocking.md
Original file line number Diff line number Diff line change
Expand Up @@ -759,9 +759,9 @@ it('the value is restored before running an other test', () => {

```ts
// vitest.config.ts
export default {
export default defineConfig({
test: {
unstubAllEnvs: true,
}
}
})
```
58 changes: 35 additions & 23 deletions packages/web-worker/src/shared-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,10 @@ import {
MessageChannel,
type MessagePort as NodeMessagePort,
} from 'node:worker_threads'
import type { InlineWorkerContext, Procedure } from './types'
import type { Procedure } from './types'
import { InlineWorkerRunner } from './runner'
import { debug, getFileIdFromUrl, getRunnerOptions } from './utils'

interface SharedInlineWorkerContext
extends Omit<
InlineWorkerContext,
'onmessage' | 'postMessage' | 'self' | 'global'
> {
onconnect: Procedure | null
self: SharedInlineWorkerContext
global: SharedInlineWorkerContext
}

function convertNodePortToWebPort(port: NodeMessagePort): MessagePort {
if (!('addEventListener' in port)) {
Object.defineProperty(port, 'addEventListener', {
Expand Down Expand Up @@ -79,33 +69,55 @@ export function createSharedWorkerConstructor(): typeof SharedWorker {
super()

const name = typeof options === 'string' ? options : options?.name

// should be equal to SharedWorkerGlobalScope
const context: SharedInlineWorkerContext = {
onconnect: null,
name,
let selfProxy: typeof globalThis

const context = {
onmessage: null,
onmessageerror: null,
onerror: null,
onlanguagechange: null,
onoffline: null,
ononline: null,
onrejectionhandled: null,
onrtctransform: null,
onunhandledrejection: null,
origin: typeof location !== 'undefined' ? location.origin : 'http://localhost:3000',
importScripts: () => {
throw new Error(
'[vitest] `importScripts` is not supported in Vite workers. Please, consider using `import` instead.',
)
},
crossOriginIsolated: false,
onconnect: null as ((e: MessageEvent) => void) | null,
name: name || '',
close: () => this.port.close(),
dispatchEvent: (event: Event) => {
return this._vw_workerTarget.dispatchEvent(event)
},
addEventListener: (...args) => {
return this._vw_workerTarget.addEventListener(...args)
addEventListener: (...args: any[]) => {
return this._vw_workerTarget.addEventListener(...args as [any, any])
},
removeEventListener: this._vw_workerTarget.removeEventListener,
get self() {
return context
},
get global() {
return context
return selfProxy
},
}

selfProxy = new Proxy(context, {
get(target, prop, receiver) {
if (Reflect.has(target, prop)) {
return Reflect.get(target, prop, receiver)
}
return Reflect.get(globalThis, prop, receiver)
},
}) as any

const channel = new MessageChannel()
this.port = convertNodePortToWebPort(channel.port1)
this._vw_workerPort = convertNodePortToWebPort(channel.port2)

this._vw_workerTarget.addEventListener('connect', (e) => {
context.onconnect?.(e)
context.onconnect?.(e as MessageEvent)
})

const runner = new InlineWorkerRunner(runnerOptions, context)
Expand Down
16 changes: 0 additions & 16 deletions packages/web-worker/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,3 @@ export type CloneOption = 'native' | 'ponyfill' | 'none'
export interface DefineWorkerOptions {
clone: CloneOption
}

export interface InlineWorkerContext {
onmessage: Procedure | null
name?: string
close: () => void
dispatchEvent: (e: Event) => void
addEventListener: (e: string, fn: Procedure) => void
removeEventListener: (e: string, fn: Procedure) => void
postMessage: (
data: any,
transfer?: Transferable[] | StructuredSerializeOptions
) => void
self: InlineWorkerContext
global: InlineWorkerContext
importScripts?: any
}
48 changes: 35 additions & 13 deletions packages/web-worker/src/worker.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {
CloneOption,
DefineWorkerOptions,
InlineWorkerContext,
Procedure,
} from './types'
import { InlineWorkerRunner } from './runner'
Expand Down Expand Up @@ -45,22 +44,39 @@ export function createWorkerConstructor(
constructor(url: URL | string, options?: WorkerOptions) {
super()

// should be equal to DedicatedWorkerGlobalScope
const context: InlineWorkerContext = {
onmessage: null,
name: options?.name,
let selfProxy: typeof globalThis

// should be in sync with DedicatedWorkerGlobalScope, but without globalThis
const context = {
onmessage: null as null | ((e: MessageEvent) => void),
onmessageerror: null,
onerror: null,
onlanguagechange: null,
onoffline: null,
ononline: null,
onrejectionhandled: null,
onrtctransform: null,
onunhandledrejection: null,
origin: typeof location !== 'undefined' ? location.origin : 'http://localhost:3000',
importScripts: () => {
throw new Error(
'[vitest] `importScripts` is not supported in Vite workers. Please, consider using `import` instead.',
)
},
crossOriginIsolated: false,
name: options?.name || '',
close: () => this.terminate(),
dispatchEvent: (event: Event) => {
return this._vw_workerTarget.dispatchEvent(event)
},
addEventListener: (...args) => {
addEventListener: (...args: any[]) => {
if (args[1]) {
this._vw_insideListeners.set(args[0], args[1])
}
return this._vw_workerTarget.addEventListener(...args)
return this._vw_workerTarget.addEventListener(...args as [any, any])
},
removeEventListener: this._vw_workerTarget.removeEventListener,
postMessage: (...args) => {
postMessage: (...args: any[]) => {
if (!args.length) {
throw new SyntaxError(
'"postMessage" requires at least one argument.',
Expand All @@ -76,15 +92,21 @@ export function createWorkerConstructor(
this.dispatchEvent(event)
},
get self() {
return context
},
get global() {
return context
return selfProxy
},
}

selfProxy = new Proxy(context, {
get(target, prop, receiver) {
if (Reflect.has(target, prop)) {
return Reflect.get(target, prop, receiver)
}
return globalThis[prop as 'crypto']
},
}) as any

this._vw_workerTarget.addEventListener('message', (e) => {
context.onmessage?.(e)
context.onmessage?.(e as MessageEvent)
})

this.addEventListener('message', (e) => {
Expand Down
3 changes: 3 additions & 0 deletions packages/web-worker/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": ["ESNext", "WebWorker"]
},
"exclude": ["./dist"]
}
8 changes: 8 additions & 0 deletions test/core/src/web-worker/worker-globals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
self.onmessage = () => {
self.postMessage({
crypto: !!self.crypto,
caches: !!self.caches,
location: !!self.location,
origin: self.origin,
})
}
23 changes: 23 additions & 0 deletions test/core/test/web-worker-jsdom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import '@vitest/web-worker'

import { expect, it } from 'vitest'
import GlobalsWorker from '../src/web-worker/worker-globals?worker'

it('worker with invalid url throws an error', async () => {
const url = import.meta.url
Expand Down Expand Up @@ -35,3 +36,25 @@ it('throws an error on invalid path', async () => {
}
expect(event.error.message).toContain('Failed to load')
})

it('returns globals on self correctly', async () => {
const worker = new GlobalsWorker()
await new Promise<void>((resolve, reject) => {
worker.onmessage = (e) => {
try {
expect(e.data).toEqual({
crypto: !!globalThis.crypto,
location: !!globalThis.location,
caches: !!globalThis.caches,
origin: 'http://localhost:3000',
})
resolve()
}
catch (err) {
reject(err)
}
}
worker.onerror = reject
worker.postMessage(null)
})
})
23 changes: 23 additions & 0 deletions test/core/test/web-worker-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import MyObjectWorker from '../src/web-worker/objectWorker?worker'
import MyEventListenerWorker from '../src/web-worker/eventListenerWorker?worker'
import MySelfWorker from '../src/web-worker/selfWorker?worker'
import MySharedWorker from '../src/web-worker/sharedWorker?sharedworker'
import GlobalsWorker from '../src/web-worker/worker-globals?worker'

const major = Number(version.split('.')[0].slice(1))

Expand Down Expand Up @@ -269,3 +270,25 @@ it('doesn\'t trigger events, if closed', async () => {
setTimeout(resolve, 100)
})
})

it('returns globals on self correctly', async () => {
const worker = new GlobalsWorker()
await new Promise<void>((resolve, reject) => {
worker.onmessage = (e) => {
try {
expect(e.data).toEqual({
crypto: !!globalThis.crypto,
location: !!globalThis.location,
caches: !!globalThis.caches,
origin: 'http://localhost:3000',
})
resolve()
}
catch (err) {
reject(err)
}
}
worker.onerror = reject
worker.postMessage(null)
})
})

0 comments on commit 12bb567

Please sign in to comment.