From 5f3a49fd16e37fdbcc17f94329df244edfdd190e Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 5 Dec 2022 09:14:14 +0100 Subject: [PATCH 01/11] fix: make web-worker implementation more compatible with spec --- packages/web-worker/README.md | 13 ++- packages/web-worker/package.json | 2 + packages/web-worker/src/pure.ts | 184 ++++++++++++++++++++----------- pnpm-lock.yaml | 16 ++- 4 files changed, 147 insertions(+), 68 deletions(-) diff --git a/packages/web-worker/README.md b/packages/web-worker/README.md index 326a6f4f1c9c..f80408fa66c6 100644 --- a/packages/web-worker/README.md +++ b/packages/web-worker/README.md @@ -37,14 +37,16 @@ export default defineConfig({ ```ts // worker.ts -import '@vitest/web-worker' -import MyWorker from '../worker?worker' - self.onmessage = (e) => { self.postMessage(`${e.data} world`) } +``` +```ts // worker.test.ts +import '@vitest/web-worker' +import MyWorker from '../worker?worker' + let worker = new MyWorker() // new Worker is also supported worker = new Worker(new URL('../src/worker.ts', import.meta.url)) @@ -55,6 +57,9 @@ worker.onmessage = (e) => { } ``` -## Notice +## Notes - Does not support `onmessage = () => {}`. Please, use `self.onmessage = () => {}`. +- Transferring Buffer will not change its `byteLength`. +- You have access to shared global space as your tests. +- Requires Node 17, if you want to use native `structuredClone`. Otherwise, it fallbacks to [polyfill](https://github.com/ungap/structured-clone). You can configure this behavior by passing down `clone` option (`'native' | 'ponyfill' | 'none'`) to `defineWebWorker` or using `VITEST_WEB_WORKER_CLONE` environmental variable. diff --git a/packages/web-worker/package.json b/packages/web-worker/package.json index cf4e7025133d..8f75275a9bfa 100644 --- a/packages/web-worker/package.json +++ b/packages/web-worker/package.json @@ -40,6 +40,8 @@ "vite-node": "workspace:*" }, "devDependencies": { + "@types/ungap__structured-clone": "^0.3.0", + "@ungap/structured-clone": "^1.0.1", "rollup": "^2.79.1" } } diff --git a/packages/web-worker/src/pure.ts b/packages/web-worker/src/pure.ts index 1416c95be0f1..cbff38a56c3f 100644 --- a/packages/web-worker/src/pure.ts +++ b/packages/web-worker/src/pure.ts @@ -1,6 +1,7 @@ /* eslint-disable no-restricted-imports */ import { VitestRunner } from 'vitest/node' import type { WorkerGlobalState } from 'vitest' +import ponyfillStructuredClone from '@ungap/structured-clone' function getWorkerState(): WorkerGlobalState { // @ts-expect-error untyped global @@ -9,38 +10,20 @@ function getWorkerState(): WorkerGlobalState { type Procedure = (...args: any[]) => void -class Bridge { - private callbacks: Record = {} +type CloneOption = 'native' | 'ponyfill' | 'none' - public on(event: string, fn: Procedure) { - this.callbacks[event] ??= [] - this.callbacks[event].push(fn) - } - - public off(event: string, fn: Procedure) { - if (this.callbacks[event]) - this.callbacks[event] = this.callbacks[event].filter(f => f !== fn) - } - - public removeEvents(event: string) { - this.callbacks[event] = [] - } - - public clear() { - this.callbacks = {} - } - - public emit(event: string, ...data: any[]) { - return (this.callbacks[event] || []).map(fn => fn(...data)) - } +interface DefineWorkerOptions { + clone: CloneOption } 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) => void + postMessage: (data: any, transfer?: Transferable[] | StructuredSerializeOptions) => void self: InlineWorkerContext global: InlineWorkerContext importScripts?: any @@ -53,22 +36,70 @@ class InlineWorkerRunner extends VitestRunner { prepareContext(context: Record) { const ctx = super.prepareContext(context) - // not supported for now - // need to be async - this.context.self.importScripts = () => {} + // not supported for now, need to be async + const importScripts = () => { + throw new Error('[vitest] `importScripts` is not supported in Vite workers. Please, consider using `import` instead.') + } return Object.assign(ctx, this.context, { - importScripts: () => {}, + importScripts, + }) + } +} + +function assertGlobalExists(name: string) { + if (!(name in globalThis)) + throw new Error(`[@vitest/web-worker] Cannot initiate a custom Web Worker. "${name}" is not supported in this environment. Please, consider using jsdom or happy-dom environment or upgrade your Node.js version.`) +} + +function createClonedMessageEvent(data: any, transferOrOptions: StructuredSerializeOptions | Transferable[] | undefined, clone: CloneOption) { + const transfer = Array.isArray(transferOrOptions) ? transferOrOptions : transferOrOptions?.transfer + + if (typeof structuredClone === 'function' && clone === 'native') { + return new MessageEvent('message', { + data: structuredClone(data, { transfer }), + origin: window.location.origin, + }) + } + if (clone !== 'none') { + transfer?.length && console.warn( + '[@vitest/web-worker] `structuredClone` is not supported in this environment. ' + + 'Falling back to polyfill, your transferable options will be lost. ' + + 'Set `VITEST_WEB_WORKER_CLONE` environmental variable to "none", if you don\'t want to loose it.', + ) + return new MessageEvent('message', { + data: ponyfillStructuredClone(data, { lossy: true }), + origin: window.location.origin, }) } + return new MessageEvent('message', { + data, + origin: window.location.origin, + }) } -export function defineWebWorker() { - if ('Worker' in globalThis) +function createMessageEvent(data: any, transferOrOptions: StructuredSerializeOptions | Transferable[] | undefined, clone: CloneOption) { + try { + return createClonedMessageEvent(data, transferOrOptions, clone) + } + catch (error) { + return new ErrorEvent('messageerror', { + error, + message: error instanceof Error ? error.message : undefined, + }) + } +} + +export function defineWebWorker(options?: DefineWorkerOptions) { + if (typeof Worker !== 'undefined' && '__VITEST_WEB_WORKER__' in globalThis.Worker) return + assertGlobalExists('window') + assertGlobalExists('EventTarget') + assertGlobalExists('MessageEvent') + const { config, rpc, mockMap, moduleCache } = getWorkerState() - const options = { + const runnerOptions = { fetchModule(id: string) { return rpc.fetch(id) }, @@ -82,9 +113,14 @@ export function defineWebWorker() { base: config.base, } - globalThis.Worker = class Worker { - private inside = new Bridge() - private outside = new Bridge() + const cloneType = (options?.clone ?? process.env.VITEST_WEB_WORKER_CLONE ?? 'native') as CloneOption + + globalThis.Worker = class Worker extends EventTarget { + static __VITEST_WEB_WORKER__ = true + + private inside = new EventTarget() + private insideListeners = new Map() + private outsideListeners = new Map() private messageQueue: any[] | null = [] @@ -92,17 +128,29 @@ export function defineWebWorker() { public onmessageerror: null | Procedure = null public onerror: null | Procedure = null - constructor(url: URL | string) { + constructor(url: URL | string, options?: WorkerOptions) { + super() + + // should equal to DedicatedWorkerGlobalScope const context: InlineWorkerContext = { onmessage: null, + name: options?.name, + close: () => this.terminate(), dispatchEvent: (event: Event) => { - this.inside.emit(event.type, event) - return true + return this.inside.dispatchEvent(event) }, - addEventListener: this.inside.on.bind(this.inside), - removeEventListener: this.inside.off.bind(this.inside), - postMessage: (data) => { - this.outside.emit('message', { data }) + addEventListener: (...args) => { + if (args[1]) + this.insideListeners.set(args[0], args[1]) + return this.inside.addEventListener(...args) + }, + removeEventListener: this.inside.removeEventListener, + postMessage: (...args) => { + if (!args.length) + throw new SyntaxError('"postMessage" requires at least one argument.') + + const event = createMessageEvent(args[0], args[1], cloneType) + this.dispatchEvent(event) }, get self() { return context @@ -112,15 +160,19 @@ export function defineWebWorker() { }, } - this.inside.on('message', (e) => { + this.inside.addEventListener('message', (e) => { context.onmessage?.(e) }) - this.outside.on('message', (e) => { + this.addEventListener('message', (e) => { this.onmessage?.(e) }) - const runner = new InlineWorkerRunner(options, context) + this.addEventListener('messageerror', (e) => { + this.onmessageerror?.(e) + }) + + const runner = new InlineWorkerRunner(runnerOptions, context) const id = (url instanceof URL ? url.toString() : url).replace(/^file:\/+/, '/') @@ -131,38 +183,46 @@ export function defineWebWorker() { const q = this.messageQueue this.messageQueue = null if (q) - q.forEach(this.postMessage, this) + q.forEach(([data, transfer]) => this.postMessage(data, transfer), this) }).catch((e) => { - this.outside.emit('error', e) + const error = new ErrorEvent('error', { + error: e, + message: e.message, + }) + this.dispatchEvent(error) this.onerror?.(e) console.error(e) }) }) } - dispatchEvent(event: Event) { - this.outside.emit(event.type, event) - return true + addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void { + if (callback) + this.outsideListeners.set(type, callback) + return this.addEventListener(type, callback, options) } - addEventListener(event: string, fn: Procedure) { - this.outside.on(event, fn) - } + postMessage(...args: [any, StructuredSerializeOptions | Transferable[] | undefined]): void { + if (!args.length) + throw new SyntaxError('"postMessage" requires at least one argument.') - removeEventListener(event: string, fn: Procedure) { - this.outside.off(event, fn) - } + const [data, transferOrOptions] = args + if (this.messageQueue != null) { + this.messageQueue.push([data, transferOrOptions]) + return + } - postMessage(data: any) { - if (this.messageQueue != null) - this.messageQueue.push(data) - else - this.inside.emit('message', { data }) + const event = createMessageEvent(data, transferOrOptions, cloneType) + this.inside.dispatchEvent(event) } terminate() { - this.outside.clear() - this.inside.clear() + this.outsideListeners.forEach((fn, type) => { + this.removeEventListener(type, fn) + }) + this.insideListeners.forEach((fn, type) => { + this.inside.removeEventListener(type, fn) + }) } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94159cf4a101..7bd69bd3ee29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -895,11 +895,15 @@ importers: packages/web-worker: specifiers: + '@types/ungap__structured-clone': ^0.3.0 + '@ungap/structured-clone': ^1.0.1 rollup: ^2.79.1 vite-node: workspace:* dependencies: vite-node: link:../vite-node devDependencies: + '@types/ungap__structured-clone': 0.3.0 + '@ungap/structured-clone': 1.0.1 rollup: 2.79.1 packages/ws-client: @@ -7585,6 +7589,10 @@ packages: source-map: 0.6.1 dev: true + /@types/ungap__structured-clone/0.3.0: + resolution: {integrity: sha512-eBWREUhVUGPze+bUW22AgUr05k8u+vETzuYdLYSvWqGTUe0KOf+zVnOB1qER5wMcw8V6D9Ar4DfJmVvD1yu0kQ==} + dev: true + /@types/unist/2.0.6: resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} dev: true @@ -7778,8 +7786,12 @@ packages: eslint-visitor-keys: 3.3.0 dev: true - /@unocss/astro/0.47.6_rollup@2.79.1+vite@4.0.0: - resolution: {integrity: sha512-8lR4KwuCeVxOTKk6g6hx6VUHhW1u+hki8oRsJaKEB0s5iUPmY6rCNtb/iaBJdceY11bZMMy5LZHJFTkod/T/zg==} + /@ungap/structured-clone/1.0.1: + resolution: {integrity: sha512-zKVyTt6rELvPXYwcVPTJcPFtY0AckN5A7xWuc7owBqR0FdtuDYhE9MZZUi6IY1kZUQFSXV1B3UOOIyLkVHYd2w==} + dev: true + + /@unocss/astro/0.46.0_vite@3.2.3: + resolution: {integrity: sha512-IHUQ5JpNjc2szW4Y+Vau6QpoZLc+4109R6QMFwjOXwFa88GVmh510GKKmNTIP0f3V/knPdlhu5TWzORNhQUhMw==} dependencies: '@unocss/core': 0.47.6 '@unocss/reset': 0.47.6 From 7a64723c56bfe4843099e589cc922d1a5dd851f0 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 5 Dec 2022 11:58:55 +0100 Subject: [PATCH 02/11] chore: cleanup --- packages/web-worker/src/pure.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/web-worker/src/pure.ts b/packages/web-worker/src/pure.ts index cbff38a56c3f..2884d69631f6 100644 --- a/packages/web-worker/src/pure.ts +++ b/packages/web-worker/src/pure.ts @@ -36,7 +36,7 @@ class InlineWorkerRunner extends VitestRunner { prepareContext(context: Record) { const ctx = super.prepareContext(context) - // not supported for now, need to be async + // not supported for now, we can't synchronously load modules const importScripts = () => { throw new Error('[vitest] `importScripts` is not supported in Vite workers. Please, consider using `import` instead.') } @@ -48,7 +48,7 @@ class InlineWorkerRunner extends VitestRunner { function assertGlobalExists(name: string) { if (!(name in globalThis)) - throw new Error(`[@vitest/web-worker] Cannot initiate a custom Web Worker. "${name}" is not supported in this environment. Please, consider using jsdom or happy-dom environment or upgrade your Node.js version.`) + throw new Error(`[@vitest/web-worker] Cannot initiate a custom Web Worker. "${name}" is not supported in this environment. Please, consider using jsdom or happy-dom environment.`) } function createClonedMessageEvent(data: any, transferOrOptions: StructuredSerializeOptions | Transferable[] | undefined, clone: CloneOption) { @@ -64,7 +64,8 @@ function createClonedMessageEvent(data: any, transferOrOptions: StructuredSerial transfer?.length && console.warn( '[@vitest/web-worker] `structuredClone` is not supported in this environment. ' + 'Falling back to polyfill, your transferable options will be lost. ' - + 'Set `VITEST_WEB_WORKER_CLONE` environmental variable to "none", if you don\'t want to loose it.', + + 'Set `VITEST_WEB_WORKER_CLONE` environmental variable to "none", if you don\'t want to loose it,' + + 'or update to Node 17+.', ) return new MessageEvent('message', { data: ponyfillStructuredClone(data, { lossy: true }), @@ -96,6 +97,7 @@ export function defineWebWorker(options?: DefineWorkerOptions) { assertGlobalExists('window') assertGlobalExists('EventTarget') assertGlobalExists('MessageEvent') + assertGlobalExists('ErrorEvent') const { config, rpc, mockMap, moduleCache } = getWorkerState() @@ -199,7 +201,7 @@ export function defineWebWorker(options?: DefineWorkerOptions) { addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void { if (callback) this.outsideListeners.set(type, callback) - return this.addEventListener(type, callback, options) + return super.addEventListener(type, callback, options) } postMessage(...args: [any, StructuredSerializeOptions | Transferable[] | undefined]): void { From b6052780498a82ed208ac4ccb777a4e9dedaf8d7 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 5 Dec 2022 13:09:33 +0100 Subject: [PATCH 03/11] test: add more tests for web-worker --- packages/web-worker/README.md | 1 + packages/web-worker/package.json | 2 + packages/web-worker/rollup.config.js | 2 + packages/web-worker/src/pure.ts | 67 ++++++----- pnpm-lock.yaml | 82 ++++++++------ test/web-worker/src/objectWorker.ts | 3 + test/web-worker/test/clone.test.ts | 136 +++++++++++++++++++++++ test/web-worker/test/postMessage.test.ts | 11 ++ 8 files changed, 248 insertions(+), 56 deletions(-) create mode 100644 test/web-worker/src/objectWorker.ts create mode 100644 test/web-worker/test/clone.test.ts create mode 100644 test/web-worker/test/postMessage.test.ts diff --git a/packages/web-worker/README.md b/packages/web-worker/README.md index f80408fa66c6..aa61e8a75721 100644 --- a/packages/web-worker/README.md +++ b/packages/web-worker/README.md @@ -63,3 +63,4 @@ worker.onmessage = (e) => { - Transferring Buffer will not change its `byteLength`. - You have access to shared global space as your tests. - Requires Node 17, if you want to use native `structuredClone`. Otherwise, it fallbacks to [polyfill](https://github.com/ungap/structured-clone). You can configure this behavior by passing down `clone` option (`'native' | 'ponyfill' | 'none'`) to `defineWebWorker` or using `VITEST_WEB_WORKER_CLONE` environmental variable. +- If something is wrong, you can debug your worker, using `DEBUG=vitest:web-worker` environmental variable. diff --git a/packages/web-worker/package.json b/packages/web-worker/package.json index 8f75275a9bfa..9d354054b207 100644 --- a/packages/web-worker/package.json +++ b/packages/web-worker/package.json @@ -37,9 +37,11 @@ "vitest": "*" }, "dependencies": { + "debug": "^4.3.4", "vite-node": "workspace:*" }, "devDependencies": { + "@types/debug": "^4.1.7", "@types/ungap__structured-clone": "^0.3.0", "@ungap/structured-clone": "^1.0.1", "rollup": "^2.79.1" diff --git a/packages/web-worker/rollup.config.js b/packages/web-worker/rollup.config.js index 92cd65b7e548..92aee2ef6e24 100644 --- a/packages/web-worker/rollup.config.js +++ b/packages/web-worker/rollup.config.js @@ -1,6 +1,7 @@ import esbuild from 'rollup-plugin-esbuild' import dts from 'rollup-plugin-dts' import commonjs from '@rollup/plugin-commonjs' +import nodeResolve from '@rollup/plugin-node-resolve' import json from '@rollup/plugin-json' import alias from '@rollup/plugin-alias' import pkg from './package.json' @@ -25,6 +26,7 @@ const plugins = [ ], }), json(), + nodeResolve(), commonjs(), esbuild({ target: 'node14', diff --git a/packages/web-worker/src/pure.ts b/packages/web-worker/src/pure.ts index 2884d69631f6..9e35b38be969 100644 --- a/packages/web-worker/src/pure.ts +++ b/packages/web-worker/src/pure.ts @@ -2,6 +2,10 @@ import { VitestRunner } from 'vitest/node' import type { WorkerGlobalState } from 'vitest' import ponyfillStructuredClone from '@ungap/structured-clone' +import { toFilePath } from 'vite-node/utils' +import createDebug from 'debug' + +const debug = createDebug('vitest:web-worker') function getWorkerState(): WorkerGlobalState { // @ts-expect-error untyped global @@ -9,7 +13,6 @@ function getWorkerState(): WorkerGlobalState { } type Procedure = (...args: any[]) => void - type CloneOption = 'native' | 'ponyfill' | 'none' interface DefineWorkerOptions { @@ -54,13 +57,17 @@ function assertGlobalExists(name: string) { function createClonedMessageEvent(data: any, transferOrOptions: StructuredSerializeOptions | Transferable[] | undefined, clone: CloneOption) { const transfer = Array.isArray(transferOrOptions) ? transferOrOptions : transferOrOptions?.transfer + debug('clone worker message %o', data) + if (typeof structuredClone === 'function' && clone === 'native') { + debug('create message event, using native structured clone') return new MessageEvent('message', { data: structuredClone(data, { transfer }), origin: window.location.origin, }) } if (clone !== 'none') { + debug('create message event, using polifylled structured clone') transfer?.length && console.warn( '[@vitest/web-worker] `structuredClone` is not supported in this environment. ' + 'Falling back to polyfill, your transferable options will be lost. ' @@ -72,6 +79,7 @@ function createClonedMessageEvent(data: any, transferOrOptions: StructuredSerial origin: window.location.origin, }) } + debug('create message event without cloning an object') return new MessageEvent('message', { data, origin: window.location.origin, @@ -83,9 +91,8 @@ function createMessageEvent(data: any, transferOrOptions: StructuredSerializeOpt return createClonedMessageEvent(data, transferOrOptions, clone) } catch (error) { - return new ErrorEvent('messageerror', { - error, - message: error instanceof Error ? error.message : undefined, + return new MessageEvent('messageerror', { + data: error, }) } } @@ -115,16 +122,16 @@ export function defineWebWorker(options?: DefineWorkerOptions) { base: config.base, } - const cloneType = (options?.clone ?? process.env.VITEST_WEB_WORKER_CLONE ?? 'native') as CloneOption + const cloneType = () => (options?.clone ?? process.env.VITEST_WEB_WORKER_CLONE ?? 'native') as CloneOption globalThis.Worker = class Worker extends EventTarget { static __VITEST_WEB_WORKER__ = true - private inside = new EventTarget() - private insideListeners = new Map() - private outsideListeners = new Map() - - private messageQueue: any[] | null = [] + private _vw_workerTarget = new EventTarget() + private _vw_insideListeners = new Map() + private _vw_outsideListeners = new Map() + private _vw_name: string + private _vw_messageQueue: any[] | null = [] public onmessage: null | Procedure = null public onmessageerror: null | Procedure = null @@ -133,25 +140,26 @@ export function defineWebWorker(options?: DefineWorkerOptions) { constructor(url: URL | string, options?: WorkerOptions) { super() - // should equal to DedicatedWorkerGlobalScope + // should be equal to DedicatedWorkerGlobalScope const context: InlineWorkerContext = { onmessage: null, name: options?.name, close: () => this.terminate(), dispatchEvent: (event: Event) => { - return this.inside.dispatchEvent(event) + return this._vw_workerTarget.dispatchEvent(event) }, addEventListener: (...args) => { if (args[1]) - this.insideListeners.set(args[0], args[1]) - return this.inside.addEventListener(...args) + this._vw_insideListeners.set(args[0], args[1]) + return this._vw_workerTarget.addEventListener(...args) }, - removeEventListener: this.inside.removeEventListener, + removeEventListener: this._vw_workerTarget.removeEventListener, postMessage: (...args) => { if (!args.length) throw new SyntaxError('"postMessage" requires at least one argument.') - const event = createMessageEvent(args[0], args[1], cloneType) + debug('posting message %o from the worker %s to the main thread', args[0], this._vw_name) + const event = createMessageEvent(args[0], args[1], cloneType()) this.dispatchEvent(event) }, get self() { @@ -162,7 +170,7 @@ export function defineWebWorker(options?: DefineWorkerOptions) { }, } - this.inside.addEventListener('message', (e) => { + this._vw_workerTarget.addEventListener('message', (e) => { context.onmessage?.(e) }) @@ -186,7 +194,9 @@ export function defineWebWorker(options?: DefineWorkerOptions) { this.messageQueue = null if (q) q.forEach(([data, transfer]) => this.postMessage(data, transfer), this) + debug('worker %s successfully initialized', this._vw_name) }).catch((e) => { + debug('worker %s failed to initialize: %o', this._vw_name, e) const error = new ErrorEvent('error', { error: e, message: e.message, @@ -200,7 +210,7 @@ export function defineWebWorker(options?: DefineWorkerOptions) { addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void { if (callback) - this.outsideListeners.set(type, callback) + this._vw_outsideListeners.set(type, callback) return super.addEventListener(type, callback, options) } @@ -209,21 +219,28 @@ export function defineWebWorker(options?: DefineWorkerOptions) { throw new SyntaxError('"postMessage" requires at least one argument.') const [data, transferOrOptions] = args - if (this.messageQueue != null) { - this.messageQueue.push([data, transferOrOptions]) + if (this._vw_messageQueue != null) { + debug('worker %s is not yet initialized, queue message %s', this._vw_name, data) + this._vw_messageQueue.push([data, transferOrOptions]) return } - const event = createMessageEvent(data, transferOrOptions, cloneType) - this.inside.dispatchEvent(event) + debug('posting message %o from the main thread to the worker %s', data, this._vw_name) + + const event = createMessageEvent(data, transferOrOptions, cloneType()) + if (event.type === 'messageerror') + this.dispatchEvent(event) + else + this._vw_workerTarget.dispatchEvent(event) } terminate() { - this.outsideListeners.forEach((fn, type) => { + debug('terminating worker %s', this._vw_name) + this._vw_outsideListeners.forEach((fn, type) => { this.removeEventListener(type, fn) }) - this.insideListeners.forEach((fn, type) => { - this.inside.removeEventListener(type, fn) + this._vw_insideListeners.forEach((fn, type) => { + this._vw_workerTarget.removeEventListener(type, fn) }) } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bd69bd3ee29..79fe39b8de14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -236,7 +236,7 @@ importers: react-dom: 18.0.0_react@18.0.0 devDependencies: '@testing-library/react': 13.3.0_zpnidt7m3osuk7shl3s4oenomq - '@types/node': 18.11.15 + '@types/node': 18.11.10 '@types/react': 18.0.26 '@vitejs/plugin-react': 3.0.0 jsdom: 20.0.3 @@ -288,7 +288,7 @@ importers: '@types/react-test-renderer': 17.0.2 '@vitejs/plugin-react': 3.0.0_vite@4.0.0 '@vitest/ui': link:../../packages/ui - happy-dom: 8.1.0 + happy-dom: 7.7.2 jsdom: 20.0.3 react-test-renderer: 17.0.2_react@17.0.2 vite: 4.0.0 @@ -895,13 +895,17 @@ importers: packages/web-worker: specifiers: + '@types/debug': ^4.1.7 '@types/ungap__structured-clone': ^0.3.0 '@ungap/structured-clone': ^1.0.1 + debug: ^4.3.4 rollup: ^2.79.1 vite-node: workspace:* dependencies: + debug: 4.3.4 vite-node: link:../vite-node devDependencies: + '@types/debug': 4.1.7 '@types/ungap__structured-clone': 0.3.0 '@ungap/structured-clone': 1.0.1 rollup: 2.79.1 @@ -986,8 +990,9 @@ importers: devDependencies: '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.45 '@vue/test-utils': 2.2.6_vue@3.2.45 - happy-dom: 8.1.0 - vite: 4.0.0 + execa: 6.1.0 + happy-dom: 7.7.2 + vite: 3.2.3 vitest: link:../../packages/vitest vue: 3.2.45 @@ -4842,7 +4847,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.11.15 + '@types/node': 18.11.10 '@types/yargs': 15.0.14 chalk: 4.1.2 dev: true @@ -4854,7 +4859,7 @@ packages: '@jest/schemas': 29.0.0 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.11.15 + '@types/node': 18.11.10 '@types/yargs': 17.0.12 chalk: 4.1.2 dev: true @@ -7193,7 +7198,7 @@ packages: /@types/cheerio/0.22.31: resolution: {integrity: sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw==} dependencies: - '@types/node': 18.11.15 + '@types/node': 18.11.10 dev: true /@types/codemirror/5.60.5: @@ -7202,6 +7207,12 @@ packages: '@types/tern': 0.23.4 dev: true + /@types/concat-stream/1.6.1: + resolution: {integrity: sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==} + dependencies: + '@types/node': 18.11.10 + dev: true + /@types/cookie/0.4.1: resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} dev: true @@ -7256,30 +7267,36 @@ packages: resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} dev: true + /@types/form-data/0.0.33: + resolution: {integrity: sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==} + dependencies: + '@types/node': 18.11.10 + dev: true + /@types/fs-extra/9.0.13: resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} dependencies: - '@types/node': 18.11.15 + '@types/node': 18.11.10 dev: true /@types/glob/7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.1 - '@types/node': 18.11.15 + '@types/node': 18.11.10 dev: true /@types/glob/8.0.0: resolution: {integrity: sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==} dependencies: '@types/minimatch': 5.1.1 - '@types/node': 18.11.15 + '@types/node': 18.11.10 dev: true /@types/graceful-fs/4.1.5: resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} dependencies: - '@types/node': 18.11.15 + '@types/node': 18.11.10 dev: true /@types/hast/2.3.4: @@ -7340,7 +7357,7 @@ packages: /@types/jsdom/20.0.1: resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} dependencies: - '@types/node': 18.11.15 + '@types/node': 18.11.10 '@types/tough-cookie': 4.0.2 parse5: 7.1.1 dev: true @@ -7384,7 +7401,7 @@ packages: /@types/node-fetch/2.6.2: resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} dependencies: - '@types/node': 18.11.15 + '@types/node': 18.11.10 form-data: 3.0.1 dev: true @@ -7396,8 +7413,12 @@ packages: resolution: {integrity: sha512-aFcUkv7EddxxOa/9f74DINReQ/celqH8DiB3fRYgVDM2Xm5QJL8sl80QKuAnGvwAsMn+H3IFA6WCrQh1CY7m1A==} dev: true - /@types/node/18.11.14: - resolution: {integrity: sha512-0KXV57tENYmmJMl+FekeW9V3O/rlcqGQQJ/hNh9r8pKIj304pskWuEd8fCyNT86g/TpO0gcOTiLzsHLEURFMIQ==} + /@types/node/18.11.10: + resolution: {integrity: sha512-juG3RWMBOqcOuXC643OAdSA525V44cVgGV6dUDuiFtss+8Fk5x1hI93Rsld43VeJVIeqlP9I7Fn9/qaVqoEAuQ==} + dev: true + + /@types/node/18.11.9: + resolution: {integrity: sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==} dev: true /@types/node/18.11.15: @@ -7434,8 +7455,7 @@ packages: /@types/prompts/2.4.2: resolution: {integrity: sha512-TwNx7qsjvRIUv/BCx583tqF5IINEVjCNqg9ofKHRlSoUHE62WBHrem4B1HGXcIrG511v29d1kJ9a/t2Esz7MIg==} dependencies: - '@types/node': 18.11.15 - kleur: 3.0.3 + '@types/node': 18.11.10 dev: true /@types/prop-types/15.7.5: @@ -7507,7 +7527,7 @@ packages: /@types/resolve/1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: - '@types/node': 18.11.15 + '@types/node': 18.11.10 dev: true /@types/resolve/1.20.2: @@ -7524,7 +7544,7 @@ packages: /@types/set-cookie-parser/2.4.2: resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==} dependencies: - '@types/node': 18.11.15 + '@types/node': 18.11.10 dev: true /@types/sinonjs__fake-timers/8.1.1: @@ -7611,7 +7631,7 @@ packages: /@types/webpack-sources/3.2.0: resolution: {integrity: sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==} dependencies: - '@types/node': 18.11.15 + '@types/node': 18.11.10 '@types/source-list-map': 0.1.2 source-map: 0.7.4 dev: true @@ -7619,7 +7639,7 @@ packages: /@types/webpack/4.41.32: resolution: {integrity: sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==} dependencies: - '@types/node': 18.11.15 + '@types/node': 18.11.10 '@types/tapable': 1.0.8 '@types/uglify-js': 3.17.0 '@types/webpack-sources': 3.2.0 @@ -7630,7 +7650,7 @@ packages: /@types/ws/8.5.3: resolution: {integrity: sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==} dependencies: - '@types/node': 18.11.15 + '@types/node': 18.11.10 dev: true /@types/yargs-parser/21.0.0: @@ -7653,7 +7673,7 @@ packages: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: - '@types/node': 18.11.15 + '@types/node': 18.11.10 dev: true optional: true @@ -13454,8 +13474,8 @@ packages: - encoding dev: true - /happy-dom/8.1.0: - resolution: {integrity: sha512-R9NVb5815gpMFsldZF0wVSHw2uSobx6yitSkCdQPda1kwfAmVw4Ut8ZspxGHXkK6OA93SznldkcvrbhKFr6JcA==} + /happy-dom/7.7.2: + resolution: {integrity: sha512-xJhDLvS7jCie2sgU00HzyNFfdRSUOxm/ndE1gT++aNDo4ffXtn6/WI/Vf3IooDEC770AQ3J8fJvnakPZFEsLpg==} dependencies: css.escape: 1.5.1 he: 1.2.0 @@ -14585,7 +14605,7 @@ packages: dependencies: '@jest/types': 26.6.2 '@types/graceful-fs': 4.1.5 - '@types/node': 18.11.15 + '@types/node': 18.11.10 anymatch: 3.1.2 fb-watchman: 2.0.1 graceful-fs: 4.2.10 @@ -14653,7 +14673,7 @@ packages: resolution: {integrity: sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==} engines: {node: '>= 10.14.2'} dependencies: - '@types/node': 18.11.15 + '@types/node': 18.11.10 graceful-fs: 4.2.10 dev: true @@ -14662,7 +14682,7 @@ packages: engines: {node: '>= 10.14.2'} dependencies: '@jest/types': 26.6.2 - '@types/node': 18.11.15 + '@types/node': 18.11.10 chalk: 4.1.2 graceful-fs: 4.2.10 is-ci: 2.0.0 @@ -14674,7 +14694,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.0.1 - '@types/node': 18.11.15 + '@types/node': 18.11.10 chalk: 4.1.2 ci-info: 3.7.0 graceful-fs: 4.2.10 @@ -14685,7 +14705,7 @@ packages: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.11.15 + '@types/node': 18.11.10 merge-stream: 2.0.0 supports-color: 7.2.0 dev: true @@ -14694,7 +14714,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.11.15 + '@types/node': 18.11.10 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true diff --git a/test/web-worker/src/objectWorker.ts b/test/web-worker/src/objectWorker.ts new file mode 100644 index 000000000000..706b6350bcaf --- /dev/null +++ b/test/web-worker/src/objectWorker.ts @@ -0,0 +1,3 @@ +self.onmessage = (e) => { + self.postMessage(e.data) +} diff --git a/test/web-worker/test/clone.test.ts b/test/web-worker/test/clone.test.ts new file mode 100644 index 000000000000..90b239dc80ce --- /dev/null +++ b/test/web-worker/test/clone.test.ts @@ -0,0 +1,136 @@ +import { version } from 'process' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import MyWorker from '../src/objectWorker?worker' + +const major = Number(version.split('.')[0].slice(1)) + +describe.runIf(major >= 17)('when node supports structuredClone', () => { + it('uses native structure clone', () => { + expect.assertions(4) + + expect(structuredClone).toBeDefined() + + const worker = new MyWorker() + const buffer = new ArrayBuffer(1) + const obj = { hello: 'world', buffer } + worker.postMessage(obj, [buffer]) + + return new Promise((resolve, reject) => { + worker.onmessage = (e) => { + try { + expect(e).toBeInstanceOf(MessageEvent) + expect(e.data, 'doesn\'t keep reference').not.toBe(obj) + expect(e.data, 'shape is equal').toEqual(obj) + resolve() + } + catch (err) { + reject(err) + } + finally { + worker.terminate() + } + } + }) + }) + + it('throws error, if passing down unserializable data', () => { + expect.assertions(4) + + expect(structuredClone).toBeDefined() + + const worker = new MyWorker() + const obj = { hello: 'world', name() {} } + worker.postMessage(obj) + + return new Promise((resolve, reject) => { + worker.onmessageerror = (e) => { + try { + expect(e.type).toBe('messageerror') + expect(e).toBeInstanceOf(MessageEvent) + expect(e.data.message).toContain( + 'could not be cloned.', + ) + resolve() + } + catch (err) { + reject(err) + } + } + }) + }) +}) + +describe('when passing down custom clone', () => { + const { warn } = console + + beforeEach(() => { + console.warn = warn + process.env.VITEST_WEB_WORKER_CLONE = undefined + }) + + it('uses ponyfill clone', () => { + expect.assertions(4) + + console.warn = vi.fn() + process.env.VITEST_WEB_WORKER_CLONE = 'ponyfill' + + const worker = new MyWorker() + const buffer = new ArrayBuffer(1) + const obj = { hello: 'world' } + worker.postMessage(obj, [buffer]) + + return new Promise((resolve, reject) => { + worker.onmessageerror = (e) => { + reject(e.data) + } + worker.onmessage = (e) => { + try { + expect(e).toBeInstanceOf(MessageEvent) + expect(e.data, 'doesn\'t keep reference').not.toBe(obj) + expect(e.data, 'shape is not equal, don\'t transfer buffer').toEqual({ hello: 'world' }) + expect(console.warn).toBeCalledWith(expect.stringContaining('[@vitest/web-worker] `structuredClone` is not supported in this')) + resolve() + } + catch (err) { + reject(err) + } + finally { + worker.terminate() + } + } + }) + }) + + it('doesn\'t clone, if asked to', () => { + expect.assertions(3) + + console.warn = vi.fn() + process.env.VITEST_WEB_WORKER_CLONE = 'none' + + const worker = new MyWorker() + const buffer = new ArrayBuffer(1) + const obj = { hello: 'world', buffer } + worker.postMessage(obj, [buffer]) + + return new Promise((resolve, reject) => { + worker.onmessageerror = (e) => { + reject(e.data) + } + worker.onmessage = (e) => { + try { + expect(e).toBeInstanceOf(MessageEvent) + expect(e.data, 'keeps reference').toBe(obj) + expect(console.warn).not.toHaveBeenCalled() + resolve() + } + catch (err) { + reject(err) + } + finally { + worker.terminate() + } + } + }) + }) +}) diff --git a/test/web-worker/test/postMessage.test.ts b/test/web-worker/test/postMessage.test.ts new file mode 100644 index 000000000000..a4b082009012 --- /dev/null +++ b/test/web-worker/test/postMessage.test.ts @@ -0,0 +1,11 @@ +import { expect, it } from 'vitest' +import MyWorker from '../src/worker?worker' + +it('throws syntax errorm if no arguments are provided', () => { + const worker = new MyWorker() + + // @ts-expect-error requires at least one argument + expect(() => worker.postMessage()).toThrowError(SyntaxError) + expect(() => worker.postMessage(undefined)).not.toThrowError() + expect(() => worker.postMessage(null)).not.toThrowError() +}) From 7ab0490ea5cc543919e45f7d8549961ba6af6b02 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 5 Dec 2022 13:14:39 +0100 Subject: [PATCH 04/11] chore: debug messageerror in web-worker --- packages/web-worker/src/pure.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web-worker/src/pure.ts b/packages/web-worker/src/pure.ts index 9e35b38be969..42da3487195f 100644 --- a/packages/web-worker/src/pure.ts +++ b/packages/web-worker/src/pure.ts @@ -91,6 +91,7 @@ function createMessageEvent(data: any, transferOrOptions: StructuredSerializeOpt return createClonedMessageEvent(data, transferOrOptions, clone) } catch (error) { + debug('failed to clone message, dispatch "messageerror" event: %o', error) return new MessageEvent('messageerror', { data: error, }) From 491549b9fd9b5f3e15c01092140233a780c04681 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 5 Dec 2022 20:02:24 +0100 Subject: [PATCH 05/11] chore: relax requirements for web worker --- packages/web-worker/src/pure.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/web-worker/src/pure.ts b/packages/web-worker/src/pure.ts index 42da3487195f..1cb66ea49445 100644 --- a/packages/web-worker/src/pure.ts +++ b/packages/web-worker/src/pure.ts @@ -58,12 +58,13 @@ function createClonedMessageEvent(data: any, transferOrOptions: StructuredSerial const transfer = Array.isArray(transferOrOptions) ? transferOrOptions : transferOrOptions?.transfer debug('clone worker message %o', data) + const origin = typeof location === 'undefined' ? undefined : location.origin if (typeof structuredClone === 'function' && clone === 'native') { debug('create message event, using native structured clone') return new MessageEvent('message', { data: structuredClone(data, { transfer }), - origin: window.location.origin, + origin, }) } if (clone !== 'none') { @@ -76,13 +77,13 @@ function createClonedMessageEvent(data: any, transferOrOptions: StructuredSerial ) return new MessageEvent('message', { data: ponyfillStructuredClone(data, { lossy: true }), - origin: window.location.origin, + origin, }) } debug('create message event without cloning an object') return new MessageEvent('message', { data, - origin: window.location.origin, + origin, }) } @@ -102,10 +103,8 @@ export function defineWebWorker(options?: DefineWorkerOptions) { if (typeof Worker !== 'undefined' && '__VITEST_WEB_WORKER__' in globalThis.Worker) return - assertGlobalExists('window') assertGlobalExists('EventTarget') assertGlobalExists('MessageEvent') - assertGlobalExists('ErrorEvent') const { config, rpc, mockMap, moduleCache } = getWorkerState() @@ -198,7 +197,8 @@ export function defineWebWorker(options?: DefineWorkerOptions) { debug('worker %s successfully initialized', this._vw_name) }).catch((e) => { debug('worker %s failed to initialize: %o', this._vw_name, e) - const error = new ErrorEvent('error', { + const EventConstructor = globalThis.ErrorEvent || globalThis.Event + const error = new EventConstructor('error', { error: e, message: e.message, }) From bdf40368a2bd6cfa8dc479b8065cda321cb21059 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 8 Dec 2022 18:01:09 +0100 Subject: [PATCH 06/11] chore: update lockfile --- pnpm-lock.yaml | 62 +++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79fe39b8de14..c28772d9b66b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -236,7 +236,7 @@ importers: react-dom: 18.0.0_react@18.0.0 devDependencies: '@testing-library/react': 13.3.0_zpnidt7m3osuk7shl3s4oenomq - '@types/node': 18.11.10 + '@types/node': 18.11.11 '@types/react': 18.0.26 '@vitejs/plugin-react': 3.0.0 jsdom: 20.0.3 @@ -288,7 +288,7 @@ importers: '@types/react-test-renderer': 17.0.2 '@vitejs/plugin-react': 3.0.0_vite@4.0.0 '@vitest/ui': link:../../packages/ui - happy-dom: 7.7.2 + happy-dom: 8.1.0 jsdom: 20.0.3 react-test-renderer: 17.0.2_react@17.0.2 vite: 4.0.0 @@ -991,7 +991,7 @@ importers: '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.45 '@vue/test-utils': 2.2.6_vue@3.2.45 execa: 6.1.0 - happy-dom: 7.7.2 + happy-dom: 8.1.0 vite: 3.2.3 vitest: link:../../packages/vitest vue: 3.2.45 @@ -4847,7 +4847,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.11.10 + '@types/node': 18.11.11 '@types/yargs': 15.0.14 chalk: 4.1.2 dev: true @@ -4859,7 +4859,7 @@ packages: '@jest/schemas': 29.0.0 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.11.10 + '@types/node': 18.11.11 '@types/yargs': 17.0.12 chalk: 4.1.2 dev: true @@ -7198,7 +7198,7 @@ packages: /@types/cheerio/0.22.31: resolution: {integrity: sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw==} dependencies: - '@types/node': 18.11.10 + '@types/node': 18.11.11 dev: true /@types/codemirror/5.60.5: @@ -7210,7 +7210,7 @@ packages: /@types/concat-stream/1.6.1: resolution: {integrity: sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==} dependencies: - '@types/node': 18.11.10 + '@types/node': 18.11.11 dev: true /@types/cookie/0.4.1: @@ -7270,33 +7270,33 @@ packages: /@types/form-data/0.0.33: resolution: {integrity: sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==} dependencies: - '@types/node': 18.11.10 + '@types/node': 18.11.11 dev: true /@types/fs-extra/9.0.13: resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} dependencies: - '@types/node': 18.11.10 + '@types/node': 18.11.11 dev: true /@types/glob/7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.1 - '@types/node': 18.11.10 + '@types/node': 18.11.11 dev: true /@types/glob/8.0.0: resolution: {integrity: sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==} dependencies: '@types/minimatch': 5.1.1 - '@types/node': 18.11.10 + '@types/node': 18.11.11 dev: true /@types/graceful-fs/4.1.5: resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} dependencies: - '@types/node': 18.11.10 + '@types/node': 18.11.11 dev: true /@types/hast/2.3.4: @@ -7357,7 +7357,7 @@ packages: /@types/jsdom/20.0.1: resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} dependencies: - '@types/node': 18.11.10 + '@types/node': 18.11.11 '@types/tough-cookie': 4.0.2 parse5: 7.1.1 dev: true @@ -7401,7 +7401,7 @@ packages: /@types/node-fetch/2.6.2: resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} dependencies: - '@types/node': 18.11.10 + '@types/node': 18.11.11 form-data: 3.0.1 dev: true @@ -7413,8 +7413,8 @@ packages: resolution: {integrity: sha512-aFcUkv7EddxxOa/9f74DINReQ/celqH8DiB3fRYgVDM2Xm5QJL8sl80QKuAnGvwAsMn+H3IFA6WCrQh1CY7m1A==} dev: true - /@types/node/18.11.10: - resolution: {integrity: sha512-juG3RWMBOqcOuXC643OAdSA525V44cVgGV6dUDuiFtss+8Fk5x1hI93Rsld43VeJVIeqlP9I7Fn9/qaVqoEAuQ==} + /@types/node/18.11.11: + resolution: {integrity: sha512-KJ021B1nlQUBLopzZmPBVuGU9un7WJd/W4ya7Ih02B4Uwky5Nja0yGYav2EfYIk0RR2Q9oVhf60S2XR1BCWJ2g==} dev: true /@types/node/18.11.9: @@ -7455,7 +7455,7 @@ packages: /@types/prompts/2.4.2: resolution: {integrity: sha512-TwNx7qsjvRIUv/BCx583tqF5IINEVjCNqg9ofKHRlSoUHE62WBHrem4B1HGXcIrG511v29d1kJ9a/t2Esz7MIg==} dependencies: - '@types/node': 18.11.10 + '@types/node': 18.11.11 dev: true /@types/prop-types/15.7.5: @@ -7527,7 +7527,7 @@ packages: /@types/resolve/1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: - '@types/node': 18.11.10 + '@types/node': 18.11.11 dev: true /@types/resolve/1.20.2: @@ -7544,7 +7544,7 @@ packages: /@types/set-cookie-parser/2.4.2: resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==} dependencies: - '@types/node': 18.11.10 + '@types/node': 18.11.11 dev: true /@types/sinonjs__fake-timers/8.1.1: @@ -7631,7 +7631,7 @@ packages: /@types/webpack-sources/3.2.0: resolution: {integrity: sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==} dependencies: - '@types/node': 18.11.10 + '@types/node': 18.11.11 '@types/source-list-map': 0.1.2 source-map: 0.7.4 dev: true @@ -7639,7 +7639,7 @@ packages: /@types/webpack/4.41.32: resolution: {integrity: sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==} dependencies: - '@types/node': 18.11.10 + '@types/node': 18.11.11 '@types/tapable': 1.0.8 '@types/uglify-js': 3.17.0 '@types/webpack-sources': 3.2.0 @@ -7650,7 +7650,7 @@ packages: /@types/ws/8.5.3: resolution: {integrity: sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==} dependencies: - '@types/node': 18.11.10 + '@types/node': 18.11.11 dev: true /@types/yargs-parser/21.0.0: @@ -7673,7 +7673,7 @@ packages: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: - '@types/node': 18.11.10 + '@types/node': 18.11.11 dev: true optional: true @@ -13474,8 +13474,8 @@ packages: - encoding dev: true - /happy-dom/7.7.2: - resolution: {integrity: sha512-xJhDLvS7jCie2sgU00HzyNFfdRSUOxm/ndE1gT++aNDo4ffXtn6/WI/Vf3IooDEC770AQ3J8fJvnakPZFEsLpg==} + /happy-dom/8.1.0: + resolution: {integrity: sha512-R9NVb5815gpMFsldZF0wVSHw2uSobx6yitSkCdQPda1kwfAmVw4Ut8ZspxGHXkK6OA93SznldkcvrbhKFr6JcA==} dependencies: css.escape: 1.5.1 he: 1.2.0 @@ -14605,7 +14605,7 @@ packages: dependencies: '@jest/types': 26.6.2 '@types/graceful-fs': 4.1.5 - '@types/node': 18.11.10 + '@types/node': 18.11.11 anymatch: 3.1.2 fb-watchman: 2.0.1 graceful-fs: 4.2.10 @@ -14673,7 +14673,7 @@ packages: resolution: {integrity: sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==} engines: {node: '>= 10.14.2'} dependencies: - '@types/node': 18.11.10 + '@types/node': 18.11.11 graceful-fs: 4.2.10 dev: true @@ -14682,7 +14682,7 @@ packages: engines: {node: '>= 10.14.2'} dependencies: '@jest/types': 26.6.2 - '@types/node': 18.11.10 + '@types/node': 18.11.11 chalk: 4.1.2 graceful-fs: 4.2.10 is-ci: 2.0.0 @@ -14694,7 +14694,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.0.1 - '@types/node': 18.11.10 + '@types/node': 18.11.11 chalk: 4.1.2 ci-info: 3.7.0 graceful-fs: 4.2.10 @@ -14705,7 +14705,7 @@ packages: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.11.10 + '@types/node': 18.11.11 merge-stream: 2.0.0 supports-color: 7.2.0 dev: true @@ -14714,7 +14714,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.11.10 + '@types/node': 18.11.11 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true From 98ba8d5f502e12749c18600aafa4eca6f5d5aed4 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 8 Dec 2022 19:40:52 +0100 Subject: [PATCH 07/11] feat(web-worker): refactor into small peaces, add SharedWorker support --- packages/web-worker/README.md | 31 ++- packages/web-worker/pure.d.ts | 9 +- packages/web-worker/src/index.ts | 4 +- packages/web-worker/src/pure.ts | 253 +--------------------- packages/web-worker/src/runner.ts | 18 ++ packages/web-worker/src/shared-worker.ts | 136 ++++++++++++ packages/web-worker/src/types.ts | 19 ++ packages/web-worker/src/utils.ts | 80 +++++++ packages/web-worker/src/worker.ts | 136 ++++++++++++ test/web-worker/src/sharedWorker.ts | 10 + test/web-worker/test/init.test.ts | 13 ++ test/web-worker/test/sharedWorker.spec.ts | 66 ++++++ test/web-worker/vitest.config.ts | 4 + 13 files changed, 530 insertions(+), 249 deletions(-) create mode 100644 packages/web-worker/src/runner.ts create mode 100644 packages/web-worker/src/shared-worker.ts create mode 100644 packages/web-worker/src/types.ts create mode 100644 packages/web-worker/src/utils.ts create mode 100644 packages/web-worker/src/worker.ts create mode 100644 test/web-worker/src/sharedWorker.ts create mode 100644 test/web-worker/test/sharedWorker.spec.ts diff --git a/packages/web-worker/README.md b/packages/web-worker/README.md index aa61e8a75721..7ab25887c67b 100644 --- a/packages/web-worker/README.md +++ b/packages/web-worker/README.md @@ -2,7 +2,14 @@ > Web Worker support for Vitest testing. Doesn't require JSDom. -Simulates Web Worker, but in the same thread. Supports both `new Worker(url)` and `import from './worker?worker`. +Simulates Web Worker, but in the same thread. + +Supported: + +- `new Worker(path)` +- `new SharedWorker(path)` +- `import MyWorker from './worker?worker'` +- `import MySharedWorker from './worker?sharedworker'` ## Installing @@ -33,6 +40,22 @@ export default defineConfig({ }) ``` +You can also import `defineWebWorkers` from `@vitest/web-worker/pure` to defined workers, whenever you need: + +```js +import { defineWebWorkers } from '@vitest/web-worker/pure' + +if (process.env.SUPPORT_WORKERS) + defineWebWorkers({ clone: 'none' }) +``` + +It accepts options: + +- `clone`: `'native' | 'ponyfill' | 'none'`. Defines how should `Worker` clone message, when transferring data. Applies only to `Worker` communication. `SharedWorker` uses `MessageChannel` from Node's `worker_threads` module, and is not configurable. + +> **Note** +> Requires Node 17, if you want to use native `structuredClone`. Otherwise, it fallbacks to [polyfill](https://github.com/ungap/structured-clone), if not specified as `none`. You can also configure this option with `VITEST_WEB_WORKER_CLONE` environmental variable. + ## Examples ```ts @@ -59,8 +82,8 @@ worker.onmessage = (e) => { ## Notes -- Does not support `onmessage = () => {}`. Please, use `self.onmessage = () => {}`. +- Worker does not support `onmessage = () => {}`. Please, use `self.onmessage = () => {}`. +- Shared worker does not support `onconnect = () => {}`. Please, use `self.onconnect = () => {}`. - Transferring Buffer will not change its `byteLength`. - You have access to shared global space as your tests. -- Requires Node 17, if you want to use native `structuredClone`. Otherwise, it fallbacks to [polyfill](https://github.com/ungap/structured-clone). You can configure this behavior by passing down `clone` option (`'native' | 'ponyfill' | 'none'`) to `defineWebWorker` or using `VITEST_WEB_WORKER_CLONE` environmental variable. -- If something is wrong, you can debug your worker, using `DEBUG=vitest:web-worker` environmental variable. +- You can debug your worker, using `DEBUG=vitest:web-worker` environmental variable. diff --git a/packages/web-worker/pure.d.ts b/packages/web-worker/pure.d.ts index b54eae9e47bf..ea13ccfd6c5b 100644 --- a/packages/web-worker/pure.d.ts +++ b/packages/web-worker/pure.d.ts @@ -1,3 +1,8 @@ -declare function defineWebWorker(): void; +type CloneOption = 'native' | 'ponyfill' | 'none'; +interface DefineWorkerOptions { + clone: CloneOption; +} -export { defineWebWorker }; +declare function defineWebWorkers(options?: DefineWorkerOptions): void; + +export { defineWebWorkers }; diff --git a/packages/web-worker/src/index.ts b/packages/web-worker/src/index.ts index 0b401ea47e3f..6f7f32506ab5 100644 --- a/packages/web-worker/src/index.ts +++ b/packages/web-worker/src/index.ts @@ -1,3 +1,3 @@ -import { defineWebWorker } from './pure' +import { defineWebWorkers } from './pure' -defineWebWorker() +defineWebWorkers() diff --git a/packages/web-worker/src/pure.ts b/packages/web-worker/src/pure.ts index 1cb66ea49445..5c47233ab2aa 100644 --- a/packages/web-worker/src/pure.ts +++ b/packages/web-worker/src/pure.ts @@ -1,248 +1,19 @@ -/* eslint-disable no-restricted-imports */ -import { VitestRunner } from 'vitest/node' -import type { WorkerGlobalState } from 'vitest' -import ponyfillStructuredClone from '@ungap/structured-clone' -import { toFilePath } from 'vite-node/utils' -import createDebug from 'debug' +import { createWorkerConstructor } from './worker' +import type { DefineWorkerOptions } from './types' +import { assertGlobalExists } from './utils' +import { createSharedWorkerConstructor } from './shared-worker' -const debug = createDebug('vitest:web-worker') +export function defineWebWorkers(options?: DefineWorkerOptions) { + if (typeof Worker === 'undefined' || !('__VITEST_WEB_WORKER__' in globalThis.Worker)) { + assertGlobalExists('EventTarget') + assertGlobalExists('MessageEvent') -function getWorkerState(): WorkerGlobalState { - // @ts-expect-error untyped global - return globalThis.__vitest_worker__ -} - -type Procedure = (...args: any[]) => void -type CloneOption = 'native' | 'ponyfill' | 'none' - -interface DefineWorkerOptions { - clone: CloneOption -} - -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 -} - -class InlineWorkerRunner extends VitestRunner { - constructor(options: any, private context: InlineWorkerContext) { - super(options) - } - - prepareContext(context: Record) { - const ctx = super.prepareContext(context) - // not supported for now, we can't synchronously load modules - const importScripts = () => { - throw new Error('[vitest] `importScripts` is not supported in Vite workers. Please, consider using `import` instead.') - } - return Object.assign(ctx, this.context, { - importScripts, - }) - } -} - -function assertGlobalExists(name: string) { - if (!(name in globalThis)) - throw new Error(`[@vitest/web-worker] Cannot initiate a custom Web Worker. "${name}" is not supported in this environment. Please, consider using jsdom or happy-dom environment.`) -} - -function createClonedMessageEvent(data: any, transferOrOptions: StructuredSerializeOptions | Transferable[] | undefined, clone: CloneOption) { - const transfer = Array.isArray(transferOrOptions) ? transferOrOptions : transferOrOptions?.transfer - - debug('clone worker message %o', data) - const origin = typeof location === 'undefined' ? undefined : location.origin - - if (typeof structuredClone === 'function' && clone === 'native') { - debug('create message event, using native structured clone') - return new MessageEvent('message', { - data: structuredClone(data, { transfer }), - origin, - }) - } - if (clone !== 'none') { - debug('create message event, using polifylled structured clone') - transfer?.length && console.warn( - '[@vitest/web-worker] `structuredClone` is not supported in this environment. ' - + 'Falling back to polyfill, your transferable options will be lost. ' - + 'Set `VITEST_WEB_WORKER_CLONE` environmental variable to "none", if you don\'t want to loose it,' - + 'or update to Node 17+.', - ) - return new MessageEvent('message', { - data: ponyfillStructuredClone(data, { lossy: true }), - origin, - }) - } - debug('create message event without cloning an object') - return new MessageEvent('message', { - data, - origin, - }) -} - -function createMessageEvent(data: any, transferOrOptions: StructuredSerializeOptions | Transferable[] | undefined, clone: CloneOption) { - try { - return createClonedMessageEvent(data, transferOrOptions, clone) - } - catch (error) { - debug('failed to clone message, dispatch "messageerror" event: %o', error) - return new MessageEvent('messageerror', { - data: error, - }) - } -} - -export function defineWebWorker(options?: DefineWorkerOptions) { - if (typeof Worker !== 'undefined' && '__VITEST_WEB_WORKER__' in globalThis.Worker) - return - - assertGlobalExists('EventTarget') - assertGlobalExists('MessageEvent') - - const { config, rpc, mockMap, moduleCache } = getWorkerState() - - const runnerOptions = { - fetchModule(id: string) { - return rpc.fetch(id) - }, - resolveId(id: string, importer?: string) { - return rpc.resolveId(id, importer) - }, - moduleCache, - mockMap, - interopDefault: config.deps.interopDefault ?? true, - root: config.root, - base: config.base, + globalThis.Worker = createWorkerConstructor(options) } - const cloneType = () => (options?.clone ?? process.env.VITEST_WEB_WORKER_CLONE ?? 'native') as CloneOption - - globalThis.Worker = class Worker extends EventTarget { - static __VITEST_WEB_WORKER__ = true - - private _vw_workerTarget = new EventTarget() - private _vw_insideListeners = new Map() - private _vw_outsideListeners = new Map() - private _vw_name: string - private _vw_messageQueue: any[] | null = [] - - public onmessage: null | Procedure = null - public onmessageerror: null | Procedure = null - public onerror: null | Procedure = null - - constructor(url: URL | string, options?: WorkerOptions) { - super() - - // should be equal to DedicatedWorkerGlobalScope - const context: InlineWorkerContext = { - onmessage: null, - name: options?.name, - close: () => this.terminate(), - dispatchEvent: (event: Event) => { - return this._vw_workerTarget.dispatchEvent(event) - }, - addEventListener: (...args) => { - if (args[1]) - this._vw_insideListeners.set(args[0], args[1]) - return this._vw_workerTarget.addEventListener(...args) - }, - removeEventListener: this._vw_workerTarget.removeEventListener, - postMessage: (...args) => { - if (!args.length) - throw new SyntaxError('"postMessage" requires at least one argument.') - - debug('posting message %o from the worker %s to the main thread', args[0], this._vw_name) - const event = createMessageEvent(args[0], args[1], cloneType()) - this.dispatchEvent(event) - }, - get self() { - return context - }, - get global() { - return context - }, - } - - this._vw_workerTarget.addEventListener('message', (e) => { - context.onmessage?.(e) - }) - - this.addEventListener('message', (e) => { - this.onmessage?.(e) - }) - - this.addEventListener('messageerror', (e) => { - this.onmessageerror?.(e) - }) - - const runner = new InlineWorkerRunner(runnerOptions, context) - - const id = (url instanceof URL ? url.toString() : url).replace(/^file:\/+/, '/') - - runner.resolveUrl(id).then(([, fsPath]) => { - runner.executeFile(fsPath).then(() => { - // worker should be new every time, invalidate its sub dependency - moduleCache.invalidateSubDepTree([fsPath, runner.mocker.getMockPath(fsPath)]) - const q = this.messageQueue - this.messageQueue = null - if (q) - q.forEach(([data, transfer]) => this.postMessage(data, transfer), this) - debug('worker %s successfully initialized', this._vw_name) - }).catch((e) => { - debug('worker %s failed to initialize: %o', this._vw_name, e) - const EventConstructor = globalThis.ErrorEvent || globalThis.Event - const error = new EventConstructor('error', { - error: e, - message: e.message, - }) - this.dispatchEvent(error) - this.onerror?.(e) - console.error(e) - }) - }) - } - - addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void { - if (callback) - this._vw_outsideListeners.set(type, callback) - return super.addEventListener(type, callback, options) - } - - postMessage(...args: [any, StructuredSerializeOptions | Transferable[] | undefined]): void { - if (!args.length) - throw new SyntaxError('"postMessage" requires at least one argument.') - - const [data, transferOrOptions] = args - if (this._vw_messageQueue != null) { - debug('worker %s is not yet initialized, queue message %s', this._vw_name, data) - this._vw_messageQueue.push([data, transferOrOptions]) - return - } - - debug('posting message %o from the main thread to the worker %s', data, this._vw_name) - - const event = createMessageEvent(data, transferOrOptions, cloneType()) - if (event.type === 'messageerror') - this.dispatchEvent(event) - else - this._vw_workerTarget.dispatchEvent(event) - } + if (typeof SharedWorker === 'undefined' || !('__VITEST_WEB_WORKER__' in globalThis.SharedWorker)) { + assertGlobalExists('EventTarget') - terminate() { - debug('terminating worker %s', this._vw_name) - this._vw_outsideListeners.forEach((fn, type) => { - this.removeEventListener(type, fn) - }) - this._vw_insideListeners.forEach((fn, type) => { - this._vw_workerTarget.removeEventListener(type, fn) - }) - } + globalThis.SharedWorker = createSharedWorkerConstructor() } } diff --git a/packages/web-worker/src/runner.ts b/packages/web-worker/src/runner.ts new file mode 100644 index 000000000000..d7e7be3373a9 --- /dev/null +++ b/packages/web-worker/src/runner.ts @@ -0,0 +1,18 @@ +import { VitestRunner } from 'vitest/node' + +export class InlineWorkerRunner extends VitestRunner { + constructor(options: any, private context: any) { + super(options) + } + + prepareContext(context: Record) { + const ctx = super.prepareContext(context) + // not supported for now, we can't synchronously load modules + const importScripts = () => { + throw new Error('[vitest] `importScripts` is not supported in Vite workers. Please, consider using `import` instead.') + } + return Object.assign(ctx, this.context, { + importScripts, + }) + } +} diff --git a/packages/web-worker/src/shared-worker.ts b/packages/web-worker/src/shared-worker.ts new file mode 100644 index 000000000000..49028bbb5e17 --- /dev/null +++ b/packages/web-worker/src/shared-worker.ts @@ -0,0 +1,136 @@ +import { MessageChannel, type MessagePort as NodeMessagePort } from 'worker_threads' +import { toFilePath } from 'vite-node/utils' +import type { InlineWorkerContext, Procedure } from './types' +import { InlineWorkerRunner } from './runner' +import { debug, getRunnerOptions } from './utils' + +interface SharedInlineWorkerContext extends Omit { + onconnect: Procedure | null + self: SharedInlineWorkerContext + global: SharedInlineWorkerContext +} + +const convertNodePortToWebPort = (port: NodeMessagePort): MessagePort => { + if (!('addEventListener' in port)) { + Object.defineProperty(port, 'addEventListener', { + value(...args: any[]) { + return this.addListener(...args) + }, + configurable: true, + enumerable: true, + }) + } + if (!('removeEventListener' in port)) { + Object.defineProperty(port, 'removeEventListener', { + value(...args: any[]) { + return this.removeListener(...args) + }, + configurable: true, + enumerable: true, + }) + } + if (!('dispatchEvent' in port)) { + const emit = port.emit.bind(port) + Object.defineProperty(port, 'emit', { + value(event: any) { + if (event.name === 'message') + (port as any).onmessage?.(event) + if (event.name === 'messageerror') + (port as any).onmessageerror?.(event) + return emit(event) + }, + configurable: true, + enumerable: true, + }) + Object.defineProperty(port, 'dispatchEvent', { + value(event: any) { + return this.emit(event) + }, + configurable: true, + enumerable: true, + }) + } + return port as any as MessagePort +} + +export function createSharedWorkerConstructor(): typeof SharedWorker { + const runnerOptions = getRunnerOptions() + + return class SharedWorker extends EventTarget { + static __VITEST_WEB_WORKER__ = true + + private _vw_workerTarget = new EventTarget() + private _vw_name: string + private _vw_workerPort: MessagePort + + public onerror: null | Procedure = null + + public port: MessagePort + + constructor(url: URL | string, options?: WorkerOptions | string) { + super() + + const name = typeof options === 'string' ? options : options?.name + + // should be equal to SharedWorkerGlobalScope + const context: SharedInlineWorkerContext = { + onconnect: null, + name, + close: () => this.port.close(), + dispatchEvent: (event: Event) => { + return this._vw_workerTarget.dispatchEvent(event) + }, + addEventListener: (...args) => { + return this._vw_workerTarget.addEventListener(...args) + }, + removeEventListener: this._vw_workerTarget.removeEventListener, + get self() { + return context + }, + get global() { + return context + }, + } + + const channel = new MessageChannel() + this.port = convertNodePortToWebPort(channel.port1) + this._vw_workerPort = convertNodePortToWebPort(channel.port2) + + this._vw_workerTarget.addEventListener('connect', (e) => { + context.onconnect?.(e) + }) + + const runner = new InlineWorkerRunner(runnerOptions, context) + + const id = (url instanceof URL ? url.toString() : url).replace(/^file:\/+/, '/') + + const fsPath = toFilePath(id, runnerOptions.root) + + this._vw_name = name ?? fsPath + + debug('initialize shared worker %s', this._vw_name) + + runner.executeFile(fsPath) + .then(() => { + // worker should be new every time, invalidate its sub dependency + runnerOptions.moduleCache.invalidateSubDepTree([fsPath, `mock:${fsPath}`]) + this._vw_workerTarget.dispatchEvent( + new MessageEvent('connect', { + ports: [this._vw_workerPort], + }), + ) + debug('shared worker %s successfully initialized', this._vw_name) + }).catch((e) => { + debug('shared worker %s failed to initialize: %o', this._vw_name, e) + const EventConstructor = globalThis.ErrorEvent || globalThis.Event + const error = new EventConstructor('error', { + error: e, + message: e.message, + }) + this.dispatchEvent(error) + this.onerror?.(error) + console.error(e) + }) + } + } +} diff --git a/packages/web-worker/src/types.ts b/packages/web-worker/src/types.ts new file mode 100644 index 000000000000..7c665f5e8222 --- /dev/null +++ b/packages/web-worker/src/types.ts @@ -0,0 +1,19 @@ +export type Procedure = (...args: any[]) => void +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 +} diff --git a/packages/web-worker/src/utils.ts b/packages/web-worker/src/utils.ts new file mode 100644 index 000000000000..5209412aed48 --- /dev/null +++ b/packages/web-worker/src/utils.ts @@ -0,0 +1,80 @@ +/* eslint-disable no-restricted-imports */ +import type { WorkerGlobalState } from 'vitest' +import ponyfillStructuredClone from '@ungap/structured-clone' +import createDebug from 'debug' +import type { CloneOption } from './types' + +export const debug = createDebug('vitest:web-worker') + +export function getWorkerState(): WorkerGlobalState { + // @ts-expect-error untyped global + return globalThis.__vitest_worker__ +} + +export function assertGlobalExists(name: string) { + if (!(name in globalThis)) + throw new Error(`[@vitest/web-worker] Cannot initiate a custom Web Worker. "${name}" is not supported in this environment. Please, consider using jsdom or happy-dom environment.`) +} + +function createClonedMessageEvent(data: any, transferOrOptions: StructuredSerializeOptions | Transferable[] | undefined, clone: CloneOption) { + const transfer = Array.isArray(transferOrOptions) ? transferOrOptions : transferOrOptions?.transfer + + debug('clone worker message %o', data) + const origin = typeof location === 'undefined' ? undefined : location.origin + + if (typeof structuredClone === 'function' && clone === 'native') { + debug('create message event, using native structured clone') + return new MessageEvent('message', { + data: structuredClone(data, { transfer }), + origin, + }) + } + if (clone !== 'none') { + debug('create message event, using polifylled structured clone') + transfer?.length && console.warn( + '[@vitest/web-worker] `structuredClone` is not supported in this environment. ' + + 'Falling back to polyfill, your transferable options will be lost. ' + + 'Set `VITEST_WEB_WORKER_CLONE` environmental variable to "none", if you don\'t want to loose it,' + + 'or update to Node 17+.', + ) + return new MessageEvent('message', { + data: ponyfillStructuredClone(data, { lossy: true }), + origin, + }) + } + debug('create message event without cloning an object') + return new MessageEvent('message', { + data, + origin, + }) +} + +export function createMessageEvent(data: any, transferOrOptions: StructuredSerializeOptions | Transferable[] | undefined, clone: CloneOption) { + try { + return createClonedMessageEvent(data, transferOrOptions, clone) + } + catch (error) { + debug('failed to clone message, dispatch "messageerror" event: %o', error) + return new MessageEvent('messageerror', { + data: error, + }) + } +} + +export function getRunnerOptions() { + const { config, rpc, mockMap, moduleCache } = getWorkerState() + + return { + fetchModule(id: string) { + return rpc.fetch(id) + }, + resolveId(id: string, importer?: string) { + return rpc.resolveId(id, importer) + }, + moduleCache, + mockMap, + interopDefault: config.deps.interopDefault ?? true, + root: config.root, + base: config.base, + } +} diff --git a/packages/web-worker/src/worker.ts b/packages/web-worker/src/worker.ts new file mode 100644 index 000000000000..d77c9bf8f9be --- /dev/null +++ b/packages/web-worker/src/worker.ts @@ -0,0 +1,136 @@ +import { toFilePath } from 'vite-node/utils' +import type { CloneOption, DefineWorkerOptions, InlineWorkerContext, Procedure } from './types' +import { InlineWorkerRunner } from './runner' +import { createMessageEvent, debug, getRunnerOptions } from './utils' + +export function createWorkerConstructor(options?: DefineWorkerOptions): typeof Worker { + const runnerOptions = getRunnerOptions() + const cloneType = () => (options?.clone ?? process.env.VITEST_WEB_WORKER_CLONE ?? 'native') as CloneOption + + return class Worker extends EventTarget { + static __VITEST_WEB_WORKER__ = true + + private _vw_workerTarget = new EventTarget() + private _vw_insideListeners = new Map() + private _vw_outsideListeners = new Map() + private _vw_name: string + private _vw_messageQueue: any[] | null = [] + + public onmessage: null | Procedure = null + public onmessageerror: null | Procedure = null + public onerror: null | Procedure = null + + constructor(url: URL | string, options?: WorkerOptions) { + super() + + // should be equal to DedicatedWorkerGlobalScope + const context: InlineWorkerContext = { + onmessage: null, + name: options?.name, + close: () => this.terminate(), + dispatchEvent: (event: Event) => { + return this._vw_workerTarget.dispatchEvent(event) + }, + addEventListener: (...args) => { + if (args[1]) + this._vw_insideListeners.set(args[0], args[1]) + return this._vw_workerTarget.addEventListener(...args) + }, + removeEventListener: this._vw_workerTarget.removeEventListener, + postMessage: (...args) => { + if (!args.length) + throw new SyntaxError('"postMessage" requires at least one argument.') + + debug('posting message %o from the worker %s to the main thread', args[0], this._vw_name) + const event = createMessageEvent(args[0], args[1], cloneType()) + this.dispatchEvent(event) + }, + get self() { + return context + }, + get global() { + return context + }, + } + + this._vw_workerTarget.addEventListener('message', (e) => { + context.onmessage?.(e) + }) + + this.addEventListener('message', (e) => { + this.onmessage?.(e) + }) + + this.addEventListener('messageerror', (e) => { + this.onmessageerror?.(e) + }) + + const runner = new InlineWorkerRunner(runnerOptions, context) + + const id = (url instanceof URL ? url.toString() : url).replace(/^file:\/+/, '/') + + const fsPath = toFilePath(id, runnerOptions.root) + + this._vw_name = options?.name ?? fsPath + + debug('initialize worker %s', this._vw_name) + + runner.executeFile(fsPath) + .then(() => { + // worker should be new every time, invalidate its sub dependency + runnerOptions.moduleCache.invalidateSubDepTree([fsPath, `mock:${fsPath}`]) + const q = this._vw_messageQueue + this._vw_messageQueue = null + if (q) + q.forEach(([data, transfer]) => this.postMessage(data, transfer), this) + debug('worker %s successfully initialized', this._vw_name) + }).catch((e) => { + debug('worker %s failed to initialize: %o', this._vw_name, e) + const EventConstructor = globalThis.ErrorEvent || globalThis.Event + const error = new EventConstructor('error', { + error: e, + message: e.message, + }) + this.dispatchEvent(error) + this.onerror?.(error) + console.error(e) + }) + } + + addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void { + if (callback) + this._vw_outsideListeners.set(type, callback) + return super.addEventListener(type, callback, options) + } + + postMessage(...args: [any, StructuredSerializeOptions | Transferable[] | undefined]): void { + if (!args.length) + throw new SyntaxError('"postMessage" requires at least one argument.') + + const [data, transferOrOptions] = args + if (this._vw_messageQueue != null) { + debug('worker %s is not yet initialized, queue message %s', this._vw_name, data) + this._vw_messageQueue.push([data, transferOrOptions]) + return + } + + debug('posting message %o from the main thread to the worker %s', data, this._vw_name) + + const event = createMessageEvent(data, transferOrOptions, cloneType()) + if (event.type === 'messageerror') + this.dispatchEvent(event) + else + this._vw_workerTarget.dispatchEvent(event) + } + + terminate() { + debug('terminating worker %s', this._vw_name) + this._vw_outsideListeners.forEach((fn, type) => { + this.removeEventListener(type, fn) + }) + this._vw_insideListeners.forEach((fn, type) => { + this._vw_workerTarget.removeEventListener(type, fn) + }) + } + } +} diff --git a/test/web-worker/src/sharedWorker.ts b/test/web-worker/src/sharedWorker.ts new file mode 100644 index 000000000000..cbe67ecf637c --- /dev/null +++ b/test/web-worker/src/sharedWorker.ts @@ -0,0 +1,10 @@ +self.addEventListener('connect', (event) => { + const e = event as MessageEvent + const port = e.ports[0] + + port.onmessage = (e) => { + port.postMessage(e.data) + } + + port.start() +}) diff --git a/test/web-worker/test/init.test.ts b/test/web-worker/test/init.test.ts index a22872f0c9c7..e6c1bbfd88e8 100644 --- a/test/web-worker/test/init.test.ts +++ b/test/web-worker/test/init.test.ts @@ -54,6 +54,19 @@ it('worker with url', async () => { await testWorker(new Worker(new URL('../src/worker.ts', url))) }) +it('worker with invalid url throws an error', async () => { + const url = import.meta.url + const worker = new Worker(new URL('../src/workerInvalid-path.ts', url)) + const event = await new Promise((resolve) => { + worker.onerror = (e) => { + resolve(e) + } + }) + expect(event).toBeInstanceOf(ErrorEvent) + expect(event.error).toBeInstanceOf(Error) + expect(event.error.message).toContain('Failed to load') +}) + it('self injected into worker and its deps should be equal', async () => { expect.assertions(4) expect(await testSelfWorker(new MySelfWorker())).toBeTruthy() diff --git a/test/web-worker/test/sharedWorker.spec.ts b/test/web-worker/test/sharedWorker.spec.ts new file mode 100644 index 000000000000..993afbf50d8f --- /dev/null +++ b/test/web-worker/test/sharedWorker.spec.ts @@ -0,0 +1,66 @@ +import { expect, it } from 'vitest' +import MySharedWorker from './src/sharedWorker?sharedworker' + +const sendEventMessage = (worker: SharedWorker, msg: any) => { + worker.port.postMessage(msg) + return new Promise((resolve) => { + worker.port.addEventListener('message', function onmessage(e) { + worker.port.removeEventListener('message', onmessage) + resolve(e.data as string) + }) + }) +} + +const sendOnMessage = (worker: SharedWorker, msg: any) => { + worker.port.postMessage(msg) + return new Promise((resolve) => { + worker.port.onmessage = function onmessage(e) { + worker.port.onmessage = null + resolve(e.data as string) + } + }) +} + +it('vite shared worker works', async () => { + expect(MySharedWorker).toBeDefined() + expect(SharedWorker).toBeDefined() + const worker = new MySharedWorker() + expect(worker).toBeInstanceOf(SharedWorker) + + await expect(sendEventMessage(worker, 'event')).resolves.toBe('event') + await expect(sendOnMessage(worker, 'event')).resolves.toBe('event') +}) + +it('shared worker with path works', async () => { + expect(SharedWorker).toBeDefined() + const worker = new SharedWorker(new URL('../src/sharedWorker.ts', import.meta.url)) + expect(worker).toBeTruthy() + + await expect(sendEventMessage(worker, 'event')).resolves.toBe('event') + await expect(sendOnMessage(worker, 'event')).resolves.toBe('event') +}) + +it('throws an error on invalid path', async () => { + expect(SharedWorker).toBeDefined() + const worker = new SharedWorker('./some-invalid-path') + const event = await new Promise((resolve) => { + worker.onerror = (e) => { + resolve(e) + } + }) + expect(event).toBeInstanceOf(ErrorEvent) + expect(event.error).toBeInstanceOf(Error) + expect(event.error.message).toContain('Failed to load') +}) + +it('doesn\'t trigger events, if closed', async () => { + const worker = new MySharedWorker() + worker.port.close() + await new Promise((resolve) => { + worker.port.addEventListener('message', () => { + expect.fail('should not trigger message') + }) + worker.port.postMessage('event') + setTimeout(resolve, 100) + }) +}) diff --git a/test/web-worker/vitest.config.ts b/test/web-worker/vitest.config.ts index d5daed349443..69f70c696d4d 100644 --- a/test/web-worker/vitest.config.ts +++ b/test/web-worker/vitest.config.ts @@ -11,5 +11,9 @@ export default defineConfig({ /packages\/web-worker/, ], }, + onConsoleLog(log) { + if (log.includes('Failed to load')) + return false + }, }, }) From b302e8541ced9aca729817bf0a090336e85af4e4 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 15 Dec 2022 19:29:06 +0100 Subject: [PATCH 08/11] chore: update lockfile --- pnpm-lock.yaml | 90 +++++++++++++++++++++----------------------------- 1 file changed, 37 insertions(+), 53 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c28772d9b66b..33d86c1d73e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,7 +58,7 @@ importers: '@rollup/plugin-node-resolve': 15.0.1_rollup@2.79.1 '@types/fs-extra': 9.0.13 '@types/lodash': 4.14.191 - '@types/node': 18.11.14 + '@types/node': 18.11.15 '@types/ws': 8.5.3 '@vitest/browser': link:packages/browser '@vitest/coverage-c8': link:packages/coverage-c8 @@ -84,10 +84,10 @@ importers: rollup-plugin-esbuild: 4.10.2_itddgjfly6okm3gulavqgvimcq rollup-plugin-license: 2.9.1_rollup@2.79.1 simple-git-hooks: 2.8.1 - ts-node: 10.9.1_jxffkbksrxcpjftzfp475m3gy4 + ts-node: 10.9.1_ewfw2lwfc3dwdvz7r6yz2ssqyi tsup: 6.5.0_z6wznmtyb6ovnulj6iujpct7um typescript: 4.9.4 - vite: 4.0.0_@types+node@18.11.14 + vite: 4.0.0_@types+node@18.11.15 vitest: link:packages/vitest docs: @@ -236,7 +236,7 @@ importers: react-dom: 18.0.0_react@18.0.0 devDependencies: '@testing-library/react': 13.3.0_zpnidt7m3osuk7shl3s4oenomq - '@types/node': 18.11.11 + '@types/node': 18.11.15 '@types/react': 18.0.26 '@vitejs/plugin-react': 3.0.0 jsdom: 20.0.3 @@ -990,9 +990,8 @@ importers: devDependencies: '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.45 '@vue/test-utils': 2.2.6_vue@3.2.45 - execa: 6.1.0 happy-dom: 8.1.0 - vite: 3.2.3 + vite: 4.0.0 vitest: link:../../packages/vitest vue: 3.2.45 @@ -4847,7 +4846,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.11.11 + '@types/node': 18.11.15 '@types/yargs': 15.0.14 chalk: 4.1.2 dev: true @@ -4859,7 +4858,7 @@ packages: '@jest/schemas': 29.0.0 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.11.11 + '@types/node': 18.11.15 '@types/yargs': 17.0.12 chalk: 4.1.2 dev: true @@ -7198,7 +7197,7 @@ packages: /@types/cheerio/0.22.31: resolution: {integrity: sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw==} dependencies: - '@types/node': 18.11.11 + '@types/node': 18.11.15 dev: true /@types/codemirror/5.60.5: @@ -7207,12 +7206,6 @@ packages: '@types/tern': 0.23.4 dev: true - /@types/concat-stream/1.6.1: - resolution: {integrity: sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==} - dependencies: - '@types/node': 18.11.11 - dev: true - /@types/cookie/0.4.1: resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} dev: true @@ -7267,36 +7260,30 @@ packages: resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} dev: true - /@types/form-data/0.0.33: - resolution: {integrity: sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==} - dependencies: - '@types/node': 18.11.11 - dev: true - /@types/fs-extra/9.0.13: resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} dependencies: - '@types/node': 18.11.11 + '@types/node': 18.11.15 dev: true /@types/glob/7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.1 - '@types/node': 18.11.11 + '@types/node': 18.11.15 dev: true /@types/glob/8.0.0: resolution: {integrity: sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==} dependencies: '@types/minimatch': 5.1.1 - '@types/node': 18.11.11 + '@types/node': 18.11.15 dev: true /@types/graceful-fs/4.1.5: resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} dependencies: - '@types/node': 18.11.11 + '@types/node': 18.11.15 dev: true /@types/hast/2.3.4: @@ -7357,7 +7344,7 @@ packages: /@types/jsdom/20.0.1: resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} dependencies: - '@types/node': 18.11.11 + '@types/node': 18.11.15 '@types/tough-cookie': 4.0.2 parse5: 7.1.1 dev: true @@ -7401,7 +7388,7 @@ packages: /@types/node-fetch/2.6.2: resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} dependencies: - '@types/node': 18.11.11 + '@types/node': 18.11.15 form-data: 3.0.1 dev: true @@ -7413,12 +7400,8 @@ packages: resolution: {integrity: sha512-aFcUkv7EddxxOa/9f74DINReQ/celqH8DiB3fRYgVDM2Xm5QJL8sl80QKuAnGvwAsMn+H3IFA6WCrQh1CY7m1A==} dev: true - /@types/node/18.11.11: - resolution: {integrity: sha512-KJ021B1nlQUBLopzZmPBVuGU9un7WJd/W4ya7Ih02B4Uwky5Nja0yGYav2EfYIk0RR2Q9oVhf60S2XR1BCWJ2g==} - dev: true - - /@types/node/18.11.9: - resolution: {integrity: sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==} + /@types/node/18.11.15: + resolution: {integrity: sha512-VkhBbVo2+2oozlkdHXLrb3zjsRkpdnaU2bXmX8Wgle3PUi569eLRaHGlgETQHR7lLL1w7GiG3h9SnePhxNDecw==} dev: true /@types/node/18.11.15: @@ -7455,7 +7438,8 @@ packages: /@types/prompts/2.4.2: resolution: {integrity: sha512-TwNx7qsjvRIUv/BCx583tqF5IINEVjCNqg9ofKHRlSoUHE62WBHrem4B1HGXcIrG511v29d1kJ9a/t2Esz7MIg==} dependencies: - '@types/node': 18.11.11 + '@types/node': 18.11.15 + kleur: 3.0.3 dev: true /@types/prop-types/15.7.5: @@ -7527,7 +7511,7 @@ packages: /@types/resolve/1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: - '@types/node': 18.11.11 + '@types/node': 18.11.15 dev: true /@types/resolve/1.20.2: @@ -7544,7 +7528,7 @@ packages: /@types/set-cookie-parser/2.4.2: resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==} dependencies: - '@types/node': 18.11.11 + '@types/node': 18.11.15 dev: true /@types/sinonjs__fake-timers/8.1.1: @@ -7631,7 +7615,7 @@ packages: /@types/webpack-sources/3.2.0: resolution: {integrity: sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==} dependencies: - '@types/node': 18.11.11 + '@types/node': 18.11.15 '@types/source-list-map': 0.1.2 source-map: 0.7.4 dev: true @@ -7639,7 +7623,7 @@ packages: /@types/webpack/4.41.32: resolution: {integrity: sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==} dependencies: - '@types/node': 18.11.11 + '@types/node': 18.11.15 '@types/tapable': 1.0.8 '@types/uglify-js': 3.17.0 '@types/webpack-sources': 3.2.0 @@ -7650,7 +7634,7 @@ packages: /@types/ws/8.5.3: resolution: {integrity: sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==} dependencies: - '@types/node': 18.11.11 + '@types/node': 18.11.15 dev: true /@types/yargs-parser/21.0.0: @@ -7673,7 +7657,7 @@ packages: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: - '@types/node': 18.11.11 + '@types/node': 18.11.15 dev: true optional: true @@ -7810,8 +7794,8 @@ packages: resolution: {integrity: sha512-zKVyTt6rELvPXYwcVPTJcPFtY0AckN5A7xWuc7owBqR0FdtuDYhE9MZZUi6IY1kZUQFSXV1B3UOOIyLkVHYd2w==} dev: true - /@unocss/astro/0.46.0_vite@3.2.3: - resolution: {integrity: sha512-IHUQ5JpNjc2szW4Y+Vau6QpoZLc+4109R6QMFwjOXwFa88GVmh510GKKmNTIP0f3V/knPdlhu5TWzORNhQUhMw==} + /@unocss/astro/0.47.6_rollup@2.79.1+vite@4.0.0: + resolution: {integrity: sha512-8lR4KwuCeVxOTKk6g6hx6VUHhW1u+hki8oRsJaKEB0s5iUPmY6rCNtb/iaBJdceY11bZMMy5LZHJFTkod/T/zg==} dependencies: '@unocss/core': 0.47.6 '@unocss/reset': 0.47.6 @@ -14605,7 +14589,7 @@ packages: dependencies: '@jest/types': 26.6.2 '@types/graceful-fs': 4.1.5 - '@types/node': 18.11.11 + '@types/node': 18.11.15 anymatch: 3.1.2 fb-watchman: 2.0.1 graceful-fs: 4.2.10 @@ -14673,7 +14657,7 @@ packages: resolution: {integrity: sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==} engines: {node: '>= 10.14.2'} dependencies: - '@types/node': 18.11.11 + '@types/node': 18.11.15 graceful-fs: 4.2.10 dev: true @@ -14682,7 +14666,7 @@ packages: engines: {node: '>= 10.14.2'} dependencies: '@jest/types': 26.6.2 - '@types/node': 18.11.11 + '@types/node': 18.11.15 chalk: 4.1.2 graceful-fs: 4.2.10 is-ci: 2.0.0 @@ -14694,7 +14678,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.0.1 - '@types/node': 18.11.11 + '@types/node': 18.11.15 chalk: 4.1.2 ci-info: 3.7.0 graceful-fs: 4.2.10 @@ -14705,7 +14689,7 @@ packages: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.11.11 + '@types/node': 18.11.15 merge-stream: 2.0.0 supports-color: 7.2.0 dev: true @@ -14714,7 +14698,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.11.11 + '@types/node': 18.11.15 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -16986,7 +16970,7 @@ packages: optional: true dependencies: lilconfig: 2.0.6 - ts-node: 10.9.1_jxffkbksrxcpjftzfp475m3gy4 + ts-node: 10.9.1_ewfw2lwfc3dwdvz7r6yz2ssqyi yaml: 1.10.2 dev: true @@ -19708,7 +19692,7 @@ packages: tslib: 2.4.1 dev: false - /ts-node/10.9.1_jxffkbksrxcpjftzfp475m3gy4: + /ts-node/10.9.1_ewfw2lwfc3dwdvz7r6yz2ssqyi: resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -19727,7 +19711,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.3 - '@types/node': 18.11.14 + '@types/node': 18.11.15 acorn: 8.8.1 acorn-walk: 8.2.0 arg: 4.1.3 @@ -20685,7 +20669,7 @@ packages: optionalDependencies: fsevents: 2.3.2 - /vite/4.0.0_@types+node@18.11.14: + /vite/4.0.0_@types+node@18.11.15: resolution: {integrity: sha512-ynad+4kYs8Jcnn8J7SacS9vAbk7eMy0xWg6E7bAhS1s79TK+D7tVFGXVZ55S7RNLRROU1rxoKlvZ/qjaB41DGA==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -20710,7 +20694,7 @@ packages: terser: optional: true dependencies: - '@types/node': 18.11.14 + '@types/node': 18.11.15 esbuild: 0.16.3 postcss: 8.4.19 resolve: 1.22.1 From 76e6a0f7fc4a218856635130eb18e9696ed88d05 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 16 Dec 2022 15:34:49 +0100 Subject: [PATCH 09/11] chore: cleanup --- packages/vitest/src/runtime/error.ts | 6 +++--- packages/vitest/src/utils/base.ts | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/vitest/src/runtime/error.ts b/packages/vitest/src/runtime/error.ts index 6417ca86c7cf..8ea6a4c7f802 100644 --- a/packages/vitest/src/runtime/error.ts +++ b/packages/vitest/src/runtime/error.ts @@ -136,13 +136,13 @@ function isReplaceable(obj1: any, obj2: any) { return obj1Type === obj2Type && obj1Type === 'Object' } -export function replaceAsymmetricMatcher(actual: any, expected: any, actualReplaced = new WeakMap(), expectedReplaced = new WeakMap()) { +export function replaceAsymmetricMatcher(actual: any, expected: any, actualReplaced = new WeakSet(), expectedReplaced = new WeakSet()) { if (!isReplaceable(actual, expected)) return { replacedActual: actual, replacedExpected: expected } if (actualReplaced.has(actual) || expectedReplaced.has(expected)) return { replacedActual: actual, replacedExpected: expected } - actualReplaced.set(actual, true) - expectedReplaced.set(expected, true) + actualReplaced.add(actual) + expectedReplaced.add(expected) ChaiUtil.getOwnEnumerableProperties(expected).forEach((key) => { const expectedValue = expected[key] const actualValue = actual[key] diff --git a/packages/vitest/src/utils/base.ts b/packages/vitest/src/utils/base.ts index bfe92430b3de..fe8765cbffb8 100644 --- a/packages/vitest/src/utils/base.ts +++ b/packages/vitest/src/utils/base.ts @@ -37,10 +37,6 @@ export function slash(str: string) { return str.replace(/\\/g, '/') } -export function mergeSlashes(str: string) { - return str.replace(/\/\//g, '/') -} - export const noop = () => { } export function getType(value: unknown): string { From fd1df0cbbd059782593fffaaff800d610f8e11bc Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 16 Dec 2022 15:41:16 +0100 Subject: [PATCH 10/11] chore: merge with main --- packages/web-worker/src/shared-worker.ts | 12 ++++---- packages/web-worker/src/worker.ts | 12 ++++---- pnpm-lock.yaml | 35 ++++++++++-------------- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/packages/web-worker/src/shared-worker.ts b/packages/web-worker/src/shared-worker.ts index 49028bbb5e17..2de074d21a41 100644 --- a/packages/web-worker/src/shared-worker.ts +++ b/packages/web-worker/src/shared-worker.ts @@ -1,5 +1,4 @@ import { MessageChannel, type MessagePort as NodeMessagePort } from 'worker_threads' -import { toFilePath } from 'vite-node/utils' import type { InlineWorkerContext, Procedure } from './types' import { InlineWorkerRunner } from './runner' import { debug, getRunnerOptions } from './utils' @@ -104,14 +103,14 @@ export function createSharedWorkerConstructor(): typeof SharedWorker { const id = (url instanceof URL ? url.toString() : url).replace(/^file:\/+/, '/') - const fsPath = toFilePath(id, runnerOptions.root) + this._vw_name = id - this._vw_name = name ?? fsPath + runner.resolveUrl(id).then(([, fsPath]) => { + this._vw_name = name ?? fsPath - debug('initialize shared worker %s', this._vw_name) + debug('initialize shared worker %s', this._vw_name) - runner.executeFile(fsPath) - .then(() => { + runner.executeFile(fsPath).then(() => { // worker should be new every time, invalidate its sub dependency runnerOptions.moduleCache.invalidateSubDepTree([fsPath, `mock:${fsPath}`]) this._vw_workerTarget.dispatchEvent( @@ -131,6 +130,7 @@ export function createSharedWorkerConstructor(): typeof SharedWorker { this.onerror?.(error) console.error(e) }) + }) } } } diff --git a/packages/web-worker/src/worker.ts b/packages/web-worker/src/worker.ts index d77c9bf8f9be..07f38812d90f 100644 --- a/packages/web-worker/src/worker.ts +++ b/packages/web-worker/src/worker.ts @@ -1,4 +1,3 @@ -import { toFilePath } from 'vite-node/utils' import type { CloneOption, DefineWorkerOptions, InlineWorkerContext, Procedure } from './types' import { InlineWorkerRunner } from './runner' import { createMessageEvent, debug, getRunnerOptions } from './utils' @@ -69,14 +68,14 @@ export function createWorkerConstructor(options?: DefineWorkerOptions): typeof W const id = (url instanceof URL ? url.toString() : url).replace(/^file:\/+/, '/') - const fsPath = toFilePath(id, runnerOptions.root) + this._vw_name = id - this._vw_name = options?.name ?? fsPath + runner.resolveUrl(id).then(([, fsPath]) => { + this._vw_name = options?.name ?? fsPath - debug('initialize worker %s', this._vw_name) + debug('initialize worker %s', this._vw_name) - runner.executeFile(fsPath) - .then(() => { + runner.executeFile(fsPath).then(() => { // worker should be new every time, invalidate its sub dependency runnerOptions.moduleCache.invalidateSubDepTree([fsPath, `mock:${fsPath}`]) const q = this._vw_messageQueue @@ -95,6 +94,7 @@ export function createWorkerConstructor(options?: DefineWorkerOptions): typeof W this.onerror?.(error) console.error(e) }) + }) } addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33d86c1d73e4..686767d8ccf7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,7 +58,7 @@ importers: '@rollup/plugin-node-resolve': 15.0.1_rollup@2.79.1 '@types/fs-extra': 9.0.13 '@types/lodash': 4.14.191 - '@types/node': 18.11.15 + '@types/node': 18.11.14 '@types/ws': 8.5.3 '@vitest/browser': link:packages/browser '@vitest/coverage-c8': link:packages/coverage-c8 @@ -84,10 +84,10 @@ importers: rollup-plugin-esbuild: 4.10.2_itddgjfly6okm3gulavqgvimcq rollup-plugin-license: 2.9.1_rollup@2.79.1 simple-git-hooks: 2.8.1 - ts-node: 10.9.1_ewfw2lwfc3dwdvz7r6yz2ssqyi + ts-node: 10.9.1_jxffkbksrxcpjftzfp475m3gy4 tsup: 6.5.0_z6wznmtyb6ovnulj6iujpct7um typescript: 4.9.4 - vite: 4.0.0_@types+node@18.11.15 + vite: 4.0.0_@types+node@18.11.14 vitest: link:packages/vitest docs: @@ -1850,11 +1850,6 @@ packages: resolution: {integrity: sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==} dev: true - /@babel/helper-plugin-utils/7.19.0: - resolution: {integrity: sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==} - engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-plugin-utils/7.20.2: resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==} engines: {node: '>=6.9.0'} @@ -3484,7 +3479,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.18.13 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.20.2 dev: true /@babel/plugin-transform-react-jsx-source/7.19.6_@babel+core@7.19.6: @@ -3516,7 +3511,7 @@ packages: '@babel/core': 7.18.13 '@babel/helper-annotate-as-pure': 7.18.6 '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.20.2 '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.18.13 '@babel/types': 7.20.0 dev: true @@ -3530,7 +3525,7 @@ packages: '@babel/core': 7.18.13 '@babel/helper-annotate-as-pure': 7.18.6 '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.20.2 '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.18.13 '@babel/types': 7.20.0 dev: true @@ -3544,7 +3539,7 @@ packages: '@babel/core': 7.19.6 '@babel/helper-annotate-as-pure': 7.18.6 '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.20.2 '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.19.6 '@babel/types': 7.20.0 dev: true @@ -3558,7 +3553,7 @@ packages: '@babel/core': 7.20.5 '@babel/helper-annotate-as-pure': 7.18.6 '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.20.2 '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.20.5 '@babel/types': 7.20.0 dev: true @@ -7400,8 +7395,8 @@ packages: resolution: {integrity: sha512-aFcUkv7EddxxOa/9f74DINReQ/celqH8DiB3fRYgVDM2Xm5QJL8sl80QKuAnGvwAsMn+H3IFA6WCrQh1CY7m1A==} dev: true - /@types/node/18.11.15: - resolution: {integrity: sha512-VkhBbVo2+2oozlkdHXLrb3zjsRkpdnaU2bXmX8Wgle3PUi569eLRaHGlgETQHR7lLL1w7GiG3h9SnePhxNDecw==} + /@types/node/18.11.14: + resolution: {integrity: sha512-0KXV57tENYmmJMl+FekeW9V3O/rlcqGQQJ/hNh9r8pKIj304pskWuEd8fCyNT86g/TpO0gcOTiLzsHLEURFMIQ==} dev: true /@types/node/18.11.15: @@ -16970,7 +16965,7 @@ packages: optional: true dependencies: lilconfig: 2.0.6 - ts-node: 10.9.1_ewfw2lwfc3dwdvz7r6yz2ssqyi + ts-node: 10.9.1_jxffkbksrxcpjftzfp475m3gy4 yaml: 1.10.2 dev: true @@ -19692,7 +19687,7 @@ packages: tslib: 2.4.1 dev: false - /ts-node/10.9.1_ewfw2lwfc3dwdvz7r6yz2ssqyi: + /ts-node/10.9.1_jxffkbksrxcpjftzfp475m3gy4: resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -19711,7 +19706,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.3 - '@types/node': 18.11.15 + '@types/node': 18.11.14 acorn: 8.8.1 acorn-walk: 8.2.0 arg: 4.1.3 @@ -20669,7 +20664,7 @@ packages: optionalDependencies: fsevents: 2.3.2 - /vite/4.0.0_@types+node@18.11.15: + /vite/4.0.0_@types+node@18.11.14: resolution: {integrity: sha512-ynad+4kYs8Jcnn8J7SacS9vAbk7eMy0xWg6E7bAhS1s79TK+D7tVFGXVZ55S7RNLRROU1rxoKlvZ/qjaB41DGA==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -20694,7 +20689,7 @@ packages: terser: optional: true dependencies: - '@types/node': 18.11.15 + '@types/node': 18.11.14 esbuild: 0.16.3 postcss: 8.4.19 resolve: 1.22.1 From 749dff9460413dd8757da0dc8e470e86cffb6fe3 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 16 Dec 2022 15:47:58 +0100 Subject: [PATCH 11/11] chore: fix reference error --- packages/vite-node/src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite-node/src/client.ts b/packages/vite-node/src/client.ts index d43dc93c116d..00a2e1176271 100644 --- a/packages/vite-node/src/client.ts +++ b/packages/vite-node/src/client.ts @@ -278,7 +278,7 @@ export class ViteNodeRunner { set: (_, p, value) => { // treat "module.exports =" the same as "exports.default =" to not have nested "default.default", // so "exports.default" becomes the actual module - if (p === 'default' && this.shouldInterop(url, { default: value })) { + if (p === 'default' && this.shouldInterop(modulePath, { default: value })) { exportAll(cjsExports, value) exports.default = value return true