From 8c6af6e5daf132429e9ad88758a1645e0ce4965e Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Tue, 3 Dec 2024 00:31:22 +0000 Subject: [PATCH] test: test `ModuleRunnerTransport` `invoke` API --- .../__tests__/fixtures/worker.invoke.mjs | 57 +++++++ .../server-worker-runner.invoke.spec.ts | 140 ++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 packages/vite/src/node/ssr/runtime/__tests__/fixtures/worker.invoke.mjs create mode 100644 packages/vite/src/node/ssr/runtime/__tests__/server-worker-runner.invoke.spec.ts diff --git a/packages/vite/src/node/ssr/runtime/__tests__/fixtures/worker.invoke.mjs b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/worker.invoke.mjs new file mode 100644 index 00000000000000..38b34d09bd89b3 --- /dev/null +++ b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/worker.invoke.mjs @@ -0,0 +1,57 @@ +// @ts-check + +import { BroadcastChannel, parentPort } from 'node:worker_threads' +import { fileURLToPath } from 'node:url' +import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner' + +if (!parentPort) { + throw new Error('File "worker.js" must be run in a worker thread') +} + +/** @type {import('vite/module-runner').ModuleRunnerTransport} */ +const transport = { + async invoke(/** @type {import('vite').HotPayload} */ event) { + const hotPayloadData = event['data'] + + const id = hotPayloadData['data'][0] + + if (id === 'test_invalid_error') { + return { + error: 'a string instead of an error' + } + } + + if (id !== 'virtual:invoke-default-string') { + return { + error: new Error(`error, module not found: ${id}`) + } + } + + return { + result: { + "code": "__vite_ssr_exports__.default = 'hello invoke world'", + "id": "\0virtual:invoke-default-string", + } + }; + }, +} + +const runner = new ModuleRunner( + { + root: fileURLToPath(new URL('./', import.meta.url)), + transport, + hmr: false, + }, + new ESModulesEvaluator(), +) + +const channel = new BroadcastChannel('vite-worker') +channel.onmessage = async (message) => { + try { + const mod = await runner.import(message.data.id) + channel.postMessage({ result: mod.default }) + } catch (e) { + channel.postMessage({ error: e.stack }) + } +} +parentPort.postMessage('ready') diff --git a/packages/vite/src/node/ssr/runtime/__tests__/server-worker-runner.invoke.spec.ts b/packages/vite/src/node/ssr/runtime/__tests__/server-worker-runner.invoke.spec.ts new file mode 100644 index 00000000000000..801e6116b0f461 --- /dev/null +++ b/packages/vite/src/node/ssr/runtime/__tests__/server-worker-runner.invoke.spec.ts @@ -0,0 +1,140 @@ +import { BroadcastChannel, Worker } from 'node:worker_threads' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import type { HotChannel, HotChannelListener, HotPayload } from 'vite' +import { DevEnvironment } from '../../..' +import { type ViteDevServer, createServer } from '../../../server' + +const createWorkerTransport = (w: Worker): HotChannel => { + const handlerToWorkerListener = new WeakMap< + HotChannelListener, + (value: HotPayload) => void + >() + + return { + send: (data) => w.postMessage(data), + on: (event: string, handler: HotChannelListener) => { + if (event === 'connection') return + + const listener = (value: HotPayload) => { + if (value.type === 'custom' && value.event === event) { + const client = { + send(payload: HotPayload) { + w.postMessage(payload) + }, + } + handler(value.data, client) + } + } + handlerToWorkerListener.set(handler, listener) + w.on('message', listener) + }, + off: (event, handler: HotChannelListener) => { + if (event === 'connection') return + const listener = handlerToWorkerListener.get(handler) + if (listener) { + w.off('message', listener) + handlerToWorkerListener.delete(handler) + } + }, + } +} + +describe('running module runner inside a worker and using the ModuleRunnerTransport#invoke API', () => { + let worker: Worker + let server: ViteDevServer + + beforeAll(async () => { + worker = new Worker( + new URL('./fixtures/worker.invoke.mjs', import.meta.url), + { + stdout: true, + }, + ) + await new Promise((resolve, reject) => { + worker.on('message', () => resolve()) + worker.on('error', reject) + }) + server = await createServer({ + root: __dirname, + logLevel: 'error', + server: { + middlewareMode: true, + watch: null, + hmr: { + port: 9610, + }, + }, + environments: { + worker: { + dev: { + createEnvironment: (name, config) => { + return new DevEnvironment(name, config, { + hot: false, + transport: createWorkerTransport(worker), + }) + }, + }, + }, + }, + }) + }) + + afterAll(() => { + server.close() + worker.terminate() + }) + + it('correctly runs ssr code', async () => { + const channel = new BroadcastChannel('vite-worker') + return new Promise((resolve, reject) => { + channel.onmessage = (event) => { + try { + expect((event as MessageEvent).data).toEqual({ + result: 'hello invoke world', + }) + } catch (e) { + reject(e) + } finally { + resolve() + } + } + channel.postMessage({ id: 'virtual:invoke-default-string' }) + }) + }) + + it('correctly triggers an error', async () => { + const channel = new BroadcastChannel('vite-worker') + return new Promise((resolve, reject) => { + channel.onmessage = (event) => { + try { + expect((event as MessageEvent).data.error).toContain( + 'Error: error, module not found: test_error', + ) + } catch (e) { + reject(e) + } finally { + resolve() + } + } + channel.postMessage({ id: 'test_error' }) + }) + }) + + it('correctly triggers an unknown error', async () => { + const channel = new BroadcastChannel('vite-worker') + return new Promise((resolve, reject) => { + channel.onmessage = (event) => { + try { + expect((event as MessageEvent).data.error).toContain( + 'Error: Unknown invoke error', + ) + } catch (e) { + reject(e) + } finally { + resolve() + } + } + channel.postMessage({ id: 'test_invalid_error' }) + }) + }) +})