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..3e843293a513f6 --- /dev/null +++ b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/worker.invoke.mjs @@ -0,0 +1,41 @@ +// @ts-check + +import { BroadcastChannel, parentPort } from 'node:worker_threads' +import { fileURLToPath } from 'node:url' +import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner' +import { createBirpc } from 'birpc' + +if (!parentPort) { + throw new Error('File "worker.js" must be run in a worker thread') +} + +/** @type {import('worker_threads').MessagePort} */ +const pPort = parentPort + +/** @type {import('birpc').BirpcReturn<{ invoke: (data: any) => any }>} */ +const rpc = createBirpc({}, { + post: (data) => pPort.postMessage(data), + on: (data) => pPort.on('message', data), +}) + +const runner = new ModuleRunner( + { + root: fileURLToPath(new URL('./', import.meta.url)), + transport: { + invoke(data) { return rpc.invoke(data) } + }, + hmr: false, + }, + new ESModulesEvaluator(), +) + +const channel = new BroadcastChannel('vite-worker:invoke') +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__/package.json b/packages/vite/src/node/ssr/runtime/__tests__/package.json index cb8754df8a41cd..7634431fba86e1 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/package.json +++ b/packages/vite/src/node/ssr/runtime/__tests__/package.json @@ -8,6 +8,7 @@ "dependencies": { "@vitejs/cjs-external": "link:./fixtures/cjs-external", "@vitejs/esm-external": "link:./fixtures/esm-external", - "tinyspy": "2.2.0" + "tinyspy": "2.2.0", + "birpc": "^0.2.19" } } 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..cdca15c695afc6 --- /dev/null +++ b/packages/vite/src/node/ssr/runtime/__tests__/server-worker-runner.invoke.spec.ts @@ -0,0 +1,102 @@ +import { BroadcastChannel, Worker } from 'node:worker_threads' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import type { BirpcReturn } from 'birpc' +import { createBirpc } from 'birpc' +import { DevEnvironment } from '../../..' +import { type ViteDevServer, createServer } from '../../../server' + +describe('running module runner inside a worker and using the ModuleRunnerTransport#invoke API', () => { + let worker: Worker + let server: ViteDevServer + let rpc: BirpcReturn< + unknown, + { invoke: (data: any) => Promise<{ result: any } | { error: any }> } + > + let handleInvoke: (data: any) => Promise<{ result: any } | { error: any }> + + 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, + }) + }, + }, + }, + }, + }) + handleInvoke = (data: any) => server.environments.ssr.hot.handleInvoke(data) + rpc = createBirpc( + { + invoke: (data: any) => handleInvoke(data), + }, + { + post: (data) => worker.postMessage(data), + on: (data) => worker.on('message', data), + }, + ) + }) + + afterAll(() => { + server.close() + worker.terminate() + rpc.$close() + }) + + async function run(id: string) { + const channel = new BroadcastChannel('vite-worker:invoke') + return new Promise((resolve, reject) => { + channel.onmessage = (event) => { + try { + resolve((event as MessageEvent).data) + } catch (e) { + reject(e) + } + } + channel.postMessage({ id }) + }) + } + + it('correctly runs ssr code', async () => { + const output = await run('./fixtures/default-string.ts') + expect(output).toStrictEqual({ + result: 'hello world', + }) + }) + + it('triggers an error', async () => { + handleInvoke = async () => ({ error: new Error('This is an Invoke Error') }) + const output = await run('dummy') + expect(output).not.toHaveProperty('result') + expect(output.error).toContain('Error: This is an Invoke Error') + }) + + it('triggers an unknown error', async () => { + handleInvoke = async () => ({ error: 'a string instead of an error' }) + const output = await run('dummy') + expect(output).not.toHaveProperty('result') + expect(output.error).toContain('Error: Unknown invoke error') + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c84cf1803d203..ee05329c3e0aaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -455,6 +455,9 @@ importers: '@vitejs/esm-external': specifier: link:./fixtures/esm-external version: link:fixtures/esm-external + birpc: + specifier: ^0.2.19 + version: 0.2.19 tinyspy: specifier: 2.2.0 version: 2.2.0