From 41f6bc368cc24c06277a611adfc9983fd0a71d99 Mon Sep 17 00:00:00 2001 From: Xidorn Quan Date: Mon, 10 Nov 2025 15:38:24 +1100 Subject: [PATCH 1/9] Add wasm support for test and SSR --- packages/vite/src/node/plugins/wasm.ts | 41 +++++---- playground/ssr-wasm/__tests__/serve.ts | 35 ++++++++ .../ssr-wasm/__tests__/ssr-wasm.spec.ts | 15 ++++ playground/ssr-wasm/package.json | 15 ++++ playground/ssr-wasm/server.js | 56 +++++++++++++ playground/ssr-wasm/src/app.js | 8 ++ playground/ssr-wasm/src/heavy.wasm | 1 + playground/ssr-wasm/src/light.wasm | 1 + playground/ssr-wasm/src/static-heavy.js | 12 +++ playground/ssr-wasm/src/static-light.js | 12 +++ playground/ssr-wasm/vite.config.ts | 7 ++ playground/test-utils.ts | 2 + playground/wasm/__tests__/wasm.spec.ts | 84 +++++++++++++------ pnpm-lock.yaml | 6 ++ 14 files changed, 255 insertions(+), 40 deletions(-) create mode 100644 playground/ssr-wasm/__tests__/serve.ts create mode 100644 playground/ssr-wasm/__tests__/ssr-wasm.spec.ts create mode 100644 playground/ssr-wasm/package.json create mode 100644 playground/ssr-wasm/server.js create mode 100644 playground/ssr-wasm/src/app.js create mode 120000 playground/ssr-wasm/src/heavy.wasm create mode 120000 playground/ssr-wasm/src/light.wasm create mode 100644 playground/ssr-wasm/src/static-heavy.js create mode 100644 playground/ssr-wasm/src/static-light.js create mode 100644 playground/ssr-wasm/vite.config.ts diff --git a/packages/vite/src/node/plugins/wasm.ts b/packages/vite/src/node/plugins/wasm.ts index 98cd50309aed59..bce374b6c287b1 100644 --- a/packages/vite/src/node/plugins/wasm.ts +++ b/packages/vite/src/node/plugins/wasm.ts @@ -26,21 +26,32 @@ const wasmHelper = async (opts = {}, url: string) => { } result = await WebAssembly.instantiate(bytes, opts) } else { - // https://github.com/mdn/webassembly-examples/issues/5 - // WebAssembly.instantiateStreaming requires the server to provide the - // correct MIME type for .wasm files, which unfortunately doesn't work for - // a lot of static file servers, so we just work around it by getting the - // raw buffer. - const response = await fetch(url) - const contentType = response.headers.get('Content-Type') || '' - if ( - 'instantiateStreaming' in WebAssembly && - contentType.startsWith('application/wasm') - ) { - result = await WebAssembly.instantiateStreaming(response, opts) - } else { - const buffer = await response.arrayBuffer() + if (typeof process !== 'undefined' && process.versions?.node) { + const fs = await import('node:fs/promises') + if (url.startsWith('/@fs/')) { + url = url.slice(4) + } else if (url.startsWith('/')) { + url = url.slice(1) + } + const buffer = await fs.readFile(url) result = await WebAssembly.instantiate(buffer, opts) + } else { + // https://github.com/mdn/webassembly-examples/issues/5 + // WebAssembly.instantiateStreaming requires the server to provide the + // correct MIME type for .wasm files, which unfortunately doesn't work for + // a lot of static file servers, so we just work around it by getting the + // raw buffer. + const response = await fetch(url) + const contentType = response.headers.get('Content-Type') || '' + if ( + 'instantiateStreaming' in WebAssembly && + contentType.startsWith('application/wasm') + ) { + result = await WebAssembly.instantiateStreaming(response, opts) + } else { + const buffer = await response.arrayBuffer() + result = await WebAssembly.instantiate(buffer, opts) + } } } return result.instance @@ -66,7 +77,7 @@ export const wasmHelperPlugin = (): Plugin => { return `export default ${wasmHelperCode}` } - const url = await fileToUrl(this, id) + const url = (await fileToUrl(this, id)).split('?')[0] return ` import initWasm from "${wasmHelperId}" diff --git a/playground/ssr-wasm/__tests__/serve.ts b/playground/ssr-wasm/__tests__/serve.ts new file mode 100644 index 00000000000000..b4425c609e8f6f --- /dev/null +++ b/playground/ssr-wasm/__tests__/serve.ts @@ -0,0 +1,35 @@ +// this is automatically detected by playground/vitestSetup.ts and will replace +// the default e2e test serve behavior + +import path from 'node:path' +import kill from 'kill-port' +import { hmrPorts, ports, rootDir } from '~utils' + +export const port = ports['ssr-wasm'] + +export async function serve(): Promise<{ close(): Promise }> { + await kill(port) + + const { createServer } = await import(path.resolve(rootDir, 'server.js')) + const { app, vite } = await createServer(rootDir, hmrPorts['ssr-wasm']) + + return new Promise((resolve, reject) => { + try { + const server = app.listen(port, () => { + resolve({ + // for test teardown + async close() { + await new Promise((resolve) => { + server.close(resolve) + }) + if (vite) { + await vite.close() + } + }, + }) + }) + } catch (e) { + reject(e) + } + }) +} diff --git a/playground/ssr-wasm/__tests__/ssr-wasm.spec.ts b/playground/ssr-wasm/__tests__/ssr-wasm.spec.ts new file mode 100644 index 00000000000000..601f627c3fc62d --- /dev/null +++ b/playground/ssr-wasm/__tests__/ssr-wasm.spec.ts @@ -0,0 +1,15 @@ +import { expect, test } from 'vitest' +import { port } from './serve' +import { page } from '~utils' + +const url = `http://localhost:${port}` + +test('should work when inlined', async () => { + await page.goto(`${url}/static-light`) + expect(await page.textContent('.static-light')).toMatch('42') +}) + +test('should work when output', async () => { + await page.goto(`${url}/static-heavy`) + expect(await page.textContent('.static-heavy')).toMatch('24') +}) diff --git a/playground/ssr-wasm/package.json b/playground/ssr-wasm/package.json new file mode 100644 index 00000000000000..c2a599d4d55cd8 --- /dev/null +++ b/playground/ssr-wasm/package.json @@ -0,0 +1,15 @@ +{ + "name": "@vitejs/test-ssr-wasm", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "node server", + "serve": "NODE_ENV=production node server", + "debug": "node --inspect-brk server" + }, + "dependencies": {}, + "devDependencies": { + "express": "^5.1.0" + } +} diff --git a/playground/ssr-wasm/server.js b/playground/ssr-wasm/server.js new file mode 100644 index 00000000000000..4508300693e825 --- /dev/null +++ b/playground/ssr-wasm/server.js @@ -0,0 +1,56 @@ +import express from 'express' + +const isTest = process.env.VITEST + +export async function createServer(root = process.cwd(), hmrPort) { + const app = express() + + /** + * @type {import('vite').ViteDevServer} + */ + const vite = await ( + await import('vite') + ).createServer({ + root, + logLevel: isTest ? 'error' : 'info', + server: { + middlewareMode: true, + watch: { + // During tests we edit the files too fast and sometimes chokidar + // misses change events, so enforce polling for consistency + usePolling: true, + interval: 100, + }, + hmr: { + port: hmrPort, + }, + }, + appType: 'custom', + }) + // use vite's connect instance as middleware + app.use(vite.middlewares) + + app.use('*all', async (req, res, next) => { + try { + const url = req.originalUrl + const render = (await vite.ssrLoadModule('/src/app.js')).render + const html = await render(url) + res.status(200).set({ 'Content-Type': 'text/html' }).end(html) + } catch (e) { + vite && vite.ssrFixStacktrace(e) + if (isTest) throw e + console.log(e.stack) + res.status(500).end(e.stack) + } + }) + + return { app, vite } +} + +if (!isTest) { + createServer().then(({ app }) => + app.listen(5173, () => { + console.log('http://localhost:5173') + }), + ) +} diff --git a/playground/ssr-wasm/src/app.js b/playground/ssr-wasm/src/app.js new file mode 100644 index 00000000000000..a1c9b3336bc8cc --- /dev/null +++ b/playground/ssr-wasm/src/app.js @@ -0,0 +1,8 @@ +export async function render(url) { + switch (url) { + case '/static-light': + return (await import('./static-light')).render() + case '/static-heavy': + return (await import('./static-heavy')).render() + } +} diff --git a/playground/ssr-wasm/src/heavy.wasm b/playground/ssr-wasm/src/heavy.wasm new file mode 120000 index 00000000000000..e606d782a550fd --- /dev/null +++ b/playground/ssr-wasm/src/heavy.wasm @@ -0,0 +1 @@ +../../wasm/heavy.wasm \ No newline at end of file diff --git a/playground/ssr-wasm/src/light.wasm b/playground/ssr-wasm/src/light.wasm new file mode 120000 index 00000000000000..24ab4b13e7fd1c --- /dev/null +++ b/playground/ssr-wasm/src/light.wasm @@ -0,0 +1 @@ +../../wasm/light.wasm \ No newline at end of file diff --git a/playground/ssr-wasm/src/static-heavy.js b/playground/ssr-wasm/src/static-heavy.js new file mode 100644 index 00000000000000..49d1772dd30473 --- /dev/null +++ b/playground/ssr-wasm/src/static-heavy.js @@ -0,0 +1,12 @@ +import heavy from './heavy.wasm?init' + +export async function render() { + let result + const { exported_func } = await heavy({ + imports: { + imported_func: (res) => (result = res), + }, + }).then((i) => i.exports) + exported_func() + return `
${result}
` +} diff --git a/playground/ssr-wasm/src/static-light.js b/playground/ssr-wasm/src/static-light.js new file mode 100644 index 00000000000000..18a1ee291b0899 --- /dev/null +++ b/playground/ssr-wasm/src/static-light.js @@ -0,0 +1,12 @@ +import light from './light.wasm?init' + +export async function render() { + let result + const { exported_func } = await light({ + imports: { + imported_func: (res) => (result = res), + }, + }).then((i) => i.exports) + exported_func() + return `
${result}
` +} diff --git a/playground/ssr-wasm/vite.config.ts b/playground/ssr-wasm/vite.config.ts new file mode 100644 index 00000000000000..f70301f75c43bb --- /dev/null +++ b/playground/ssr-wasm/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +export default defineConfig({ + build: { + // make cannot emit light.wasm + assetsInlineLimit: 80, + }, +}) diff --git a/playground/test-utils.ts b/playground/test-utils.ts index 97e4c57455f7b2..5e084e636d61d6 100644 --- a/playground/test-utils.ts +++ b/playground/test-utils.ts @@ -34,6 +34,7 @@ export const ports = { 'ssr-html': 9602, 'ssr-noexternal': 9603, 'ssr-pug': 9604, + 'ssr-wasm': 9608, 'ssr-webworker': 9605, 'proxy-bypass': 9606, // not imported but used in `proxy-hmr/vite.config.js` 'proxy-bypass/non-existent-app': 9607, // not imported but used in `proxy-hmr/other-app/vite.config.js` @@ -57,6 +58,7 @@ export const hmrPorts = { 'ssr-html': 24683, 'ssr-noexternal': 24684, 'ssr-pug': 24685, + 'ssr-wasm': 24691, 'css/lightningcss-proxy': 24686, json: 24687, 'ssr-conditions': 24688, diff --git a/playground/wasm/__tests__/wasm.spec.ts b/playground/wasm/__tests__/wasm.spec.ts index 0f23851fe97ffc..653c3927cb33bd 100644 --- a/playground/wasm/__tests__/wasm.spec.ts +++ b/playground/wasm/__tests__/wasm.spec.ts @@ -1,33 +1,67 @@ -import { expect, test } from 'vitest' +import { describe, expect, test } from 'vitest' import { isBuild, page } from '~utils' -test('should work when inlined', async () => { - await page.click('.inline-wasm .run') - await expect - .poll(() => page.textContent('.inline-wasm .result')) - .toMatch('42') -}) +describe('WASM in browser', () => { + test('should work when inlined', async () => { + await page.click('.inline-wasm .run') + await expect + .poll(() => page.textContent('.inline-wasm .result')) + .toMatch('42') + }) -test('should work when output', async () => { - await page.click('.output-wasm .run') - await expect - .poll(() => page.textContent('.output-wasm .result')) - .toMatch('24') -}) + test('should work when output', async () => { + await page.click('.output-wasm .run') + await expect + .poll(() => page.textContent('.output-wasm .result')) + .toMatch('24') + }) -test('init function returns WebAssembly.Instance', async () => { - await page.click('.init-returns-instance .run') - await expect - .poll(() => page.textContent('.init-returns-instance .result')) - .toMatch('true') -}) + test('init function returns WebAssembly.Instance', async () => { + await page.click('.init-returns-instance .run') + await expect + .poll(() => page.textContent('.init-returns-instance .result')) + .toMatch('true') + }) -test('?url', async () => { - expect(await page.textContent('.url')).toMatch( - isBuild ? 'data:application/wasm' : '/light.wasm', - ) + test('?url', async () => { + expect(await page.textContent('.url')).toMatch( + isBuild ? 'data:application/wasm' : '/light.wasm', + ) + }) + + test('should work when wasm in worker', async () => { + await expect + .poll(() => page.textContent('.worker-wasm .result')) + .toMatch('3') + }) }) -test('should work when wasm in worker', async () => { - await expect.poll(() => page.textContent('.worker-wasm .result')).toMatch('3') +describe('WASM in vitest', () => { + test('should work when inlined', async () => { + const { default: light } = await import('../light.wasm?init') + let result + const instance = await light({ + imports: { + imported_func: (res) => (result = res), + }, + }) + expect(instance instanceof WebAssembly.Instance).toBe(true) + // @ts-ignore + instance.exports.exported_func() + expect(result).toBe(42) + }) + + test('should work when output', async () => { + const { default: heavy } = await import('../heavy.wasm?init') + let result + const instance = await heavy({ + imports: { + imported_func: (res) => (result = res), + }, + }) + expect(instance instanceof WebAssembly.Instance).toBe(true) + // @ts-ignore + instance.exports.exported_func() + expect(result).toBe(24) + }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd03a2a6817b50..9001a2bb51d26a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1675,6 +1675,12 @@ importers: playground/ssr-resolve/pkg-module-sync: {} + playground/ssr-wasm: + devDependencies: + express: + specifier: ^5.1.0 + version: 5.1.0 + playground/ssr-webworker: dependencies: '@vitejs/test-browser-exports': From 791ca1bf5df097b14ae2246dee093e0b7c690a89 Mon Sep 17 00:00:00 2001 From: Xidorn Quan Date: Mon, 10 Nov 2025 16:27:00 +1100 Subject: [PATCH 2/9] fixes --- packages/vite/src/node/plugins/wasm.ts | 2 +- playground/ssr-wasm/server.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/plugins/wasm.ts b/packages/vite/src/node/plugins/wasm.ts index bce374b6c287b1..8c7e8e7d37e472 100644 --- a/packages/vite/src/node/plugins/wasm.ts +++ b/packages/vite/src/node/plugins/wasm.ts @@ -29,7 +29,7 @@ const wasmHelper = async (opts = {}, url: string) => { if (typeof process !== 'undefined' && process.versions?.node) { const fs = await import('node:fs/promises') if (url.startsWith('/@fs/')) { - url = url.slice(4) + url = url.slice(5) } else if (url.startsWith('/')) { url = url.slice(1) } diff --git a/playground/ssr-wasm/server.js b/playground/ssr-wasm/server.js index 4508300693e825..4d103ed67cc5d9 100644 --- a/playground/ssr-wasm/server.js +++ b/playground/ssr-wasm/server.js @@ -30,7 +30,7 @@ export async function createServer(root = process.cwd(), hmrPort) { // use vite's connect instance as middleware app.use(vite.middlewares) - app.use('*all', async (req, res, next) => { + app.use('*', async (req, res, next) => { try { const url = req.originalUrl const render = (await vite.ssrLoadModule('/src/app.js')).render From 2a9b355881825563734cbed8cf5ac9996e2c9249 Mon Sep 17 00:00:00 2001 From: Xidorn Quan Date: Mon, 10 Nov 2025 16:32:00 +1100 Subject: [PATCH 3/9] fix --- playground/ssr-wasm/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/ssr-wasm/server.js b/playground/ssr-wasm/server.js index 4d103ed67cc5d9..4508300693e825 100644 --- a/playground/ssr-wasm/server.js +++ b/playground/ssr-wasm/server.js @@ -30,7 +30,7 @@ export async function createServer(root = process.cwd(), hmrPort) { // use vite's connect instance as middleware app.use(vite.middlewares) - app.use('*', async (req, res, next) => { + app.use('*all', async (req, res, next) => { try { const url = req.originalUrl const render = (await vite.ssrLoadModule('/src/app.js')).render From b971467e8e6f04e7eb6c694de37647f32e3ef24e Mon Sep 17 00:00:00 2001 From: Xidorn Quan Date: Mon, 10 Nov 2025 16:42:05 +1100 Subject: [PATCH 4/9] handle windows and unix path --- packages/vite/src/node/plugins/wasm.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/vite/src/node/plugins/wasm.ts b/packages/vite/src/node/plugins/wasm.ts index 8c7e8e7d37e472..0e631254fbbbc5 100644 --- a/packages/vite/src/node/plugins/wasm.ts +++ b/packages/vite/src/node/plugins/wasm.ts @@ -30,6 +30,9 @@ const wasmHelper = async (opts = {}, url: string) => { const fs = await import('node:fs/promises') if (url.startsWith('/@fs/')) { url = url.slice(5) + if (!/^[A-Z]:/i.test(url)) { + url = `/${url}` + } } else if (url.startsWith('/')) { url = url.slice(1) } From a68efaffcb2d4df334d636dfc4e24cce6e961d11 Mon Sep 17 00:00:00 2001 From: Xidorn Quan Date: Mon, 10 Nov 2025 16:50:55 +1100 Subject: [PATCH 5/9] trying --- packages/vite/src/node/plugins/wasm.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/plugins/wasm.ts b/packages/vite/src/node/plugins/wasm.ts index 0e631254fbbbc5..aa0f33146934fe 100644 --- a/packages/vite/src/node/plugins/wasm.ts +++ b/packages/vite/src/node/plugins/wasm.ts @@ -26,7 +26,11 @@ const wasmHelper = async (opts = {}, url: string) => { } result = await WebAssembly.instantiate(bytes, opts) } else { - if (typeof process !== 'undefined' && process.versions?.node) { + if ( + typeof process !== 'undefined' && + process.versions && + process.versions.node + ) { const fs = await import('node:fs/promises') if (url.startsWith('/@fs/')) { url = url.slice(5) From 642cfce3fe4ee1ebe59595d5184a3d67898770d6 Mon Sep 17 00:00:00 2001 From: Xidorn Quan Date: Mon, 10 Nov 2025 17:06:37 +1100 Subject: [PATCH 6/9] fix --- packages/vite/src/node/plugins/wasm.ts | 81 ++++++++++++++------------ 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/packages/vite/src/node/plugins/wasm.ts b/packages/vite/src/node/plugins/wasm.ts index aa0f33146934fe..25691217703ad1 100644 --- a/packages/vite/src/node/plugins/wasm.ts +++ b/packages/vite/src/node/plugins/wasm.ts @@ -1,5 +1,6 @@ import { exactRegex } from '@rolldown/pluginutils' import type { Plugin } from '../plugin' +import { fsPathFromId } from '../utils' import { fileToUrl } from './asset' const wasmHelperId = '\0vite/wasm-helper.js' @@ -26,46 +27,45 @@ const wasmHelper = async (opts = {}, url: string) => { } result = await WebAssembly.instantiate(bytes, opts) } else { - if ( - typeof process !== 'undefined' && - process.versions && - process.versions.node - ) { - const fs = await import('node:fs/promises') - if (url.startsWith('/@fs/')) { - url = url.slice(5) - if (!/^[A-Z]:/i.test(url)) { - url = `/${url}` - } - } else if (url.startsWith('/')) { - url = url.slice(1) - } - const buffer = await fs.readFile(url) - result = await WebAssembly.instantiate(buffer, opts) - } else { - // https://github.com/mdn/webassembly-examples/issues/5 - // WebAssembly.instantiateStreaming requires the server to provide the - // correct MIME type for .wasm files, which unfortunately doesn't work for - // a lot of static file servers, so we just work around it by getting the - // raw buffer. - const response = await fetch(url) - const contentType = response.headers.get('Content-Type') || '' - if ( - 'instantiateStreaming' in WebAssembly && - contentType.startsWith('application/wasm') - ) { - result = await WebAssembly.instantiateStreaming(response, opts) - } else { - const buffer = await response.arrayBuffer() - result = await WebAssembly.instantiate(buffer, opts) - } - } + result = await instantiateFromUrl(url, opts) } return result.instance } const wasmHelperCode = wasmHelper.toString() +const instantiateFromUrl = async (url: string, opts?: WebAssembly.Imports) => { + // https://github.com/mdn/webassembly-examples/issues/5 + // WebAssembly.instantiateStreaming requires the server to provide the + // correct MIME type for .wasm files, which unfortunately doesn't work for + // a lot of static file servers, so we just work around it by getting the + // raw buffer. + const response = await fetch(url) + const contentType = response.headers.get('Content-Type') || '' + if ( + 'instantiateStreaming' in WebAssembly && + contentType.startsWith('application/wasm') + ) { + return WebAssembly.instantiateStreaming(response, opts) + } else { + const buffer = await response.arrayBuffer() + return WebAssembly.instantiate(buffer, opts) + } +} + +const instantiateFromUrlCode = instantiateFromUrl.toString() + +const instantiateFromFile = async ( + fsPath: string, + opts?: WebAssembly.Imports, +) => { + const fs = await import('node:fs/promises') + const buffer = await fs.readFile(fsPath) + return WebAssembly.instantiate(buffer, opts) +} + +const instantiateFromFileCode = instantiateFromFile.toString() + export const wasmHelperPlugin = (): Plugin => { return { name: 'vite:wasm-helper', @@ -80,11 +80,20 @@ export const wasmHelperPlugin = (): Plugin => { load: { filter: { id: [exactRegex(wasmHelperId), wasmInitRE] }, async handler(id) { + const isServer = this.environment.config.consumer === 'server' + if (id === wasmHelperId) { - return `export default ${wasmHelperCode}` + const instantiateFromUrl = isServer + ? instantiateFromFileCode + : instantiateFromUrlCode + return ` +const instantiateFromUrl = ${instantiateFromUrl} +export default ${wasmHelperCode} +` } - const url = (await fileToUrl(this, id)).split('?')[0] + id = id.split('?')[0] + const url = isServer ? fsPathFromId(id) : await fileToUrl(this, id) return ` import initWasm from "${wasmHelperId}" From 109d41cb5da4b2a36f2132bb3ccf5147f97e787e Mon Sep 17 00:00:00 2001 From: Xidorn Quan Date: Wed, 12 Nov 2025 12:09:23 +1100 Subject: [PATCH 7/9] correctly handle ssr build --- packages/vite/src/node/build.ts | 2 +- packages/vite/src/node/plugins/wasm.ts | 39 ++++++++++++------- playground/ssr-wasm/__tests__/serve.ts | 9 ++++- playground/ssr-wasm/package.json | 4 +- playground/ssr-wasm/server.js | 54 ++++++++++++++------------ playground/ssr-wasm/vite.config.ts | 2 + 6 files changed, 68 insertions(+), 42 deletions(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 844a45c14656a2..301a69de1d3d47 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -1404,7 +1404,7 @@ export function toOutputFilePathInJS( return result } } - if (relative && !ssr) { + if (relative || ssr) { return toRelative(filename, hostId) } return joinUrlSegments(decodedBase, filename) diff --git a/packages/vite/src/node/plugins/wasm.ts b/packages/vite/src/node/plugins/wasm.ts index 25691217703ad1..73d05dcd0966af 100644 --- a/packages/vite/src/node/plugins/wasm.ts +++ b/packages/vite/src/node/plugins/wasm.ts @@ -1,6 +1,9 @@ +import { fileURLToPath, pathToFileURL } from 'node:url' +import { readFile } from 'node:fs/promises' import { exactRegex } from '@rolldown/pluginutils' import type { Plugin } from '../plugin' import { fsPathFromId } from '../utils' +import { FS_PREFIX } from '../constants' import { fileToUrl } from './asset' const wasmHelperId = '\0vite/wasm-helper.js' @@ -55,12 +58,14 @@ const instantiateFromUrl = async (url: string, opts?: WebAssembly.Imports) => { const instantiateFromUrlCode = instantiateFromUrl.toString() -const instantiateFromFile = async ( - fsPath: string, - opts?: WebAssembly.Imports, -) => { - const fs = await import('node:fs/promises') - const buffer = await fs.readFile(fsPath) +const instantiateFromFile = async (url: string, opts?: WebAssembly.Imports) => { + let fsPath = url + if (url.startsWith('file:')) { + fsPath = fileURLToPath(url) + } else if (url.startsWith('/')) { + fsPath = url.slice(1) + } + const buffer = await readFile(fsPath) return WebAssembly.instantiate(buffer, opts) } @@ -83,18 +88,26 @@ export const wasmHelperPlugin = (): Plugin => { const isServer = this.environment.config.consumer === 'server' if (id === wasmHelperId) { - const instantiateFromUrl = isServer - ? instantiateFromFileCode - : instantiateFromUrlCode - return ` -const instantiateFromUrl = ${instantiateFromUrl} + if (isServer) { + return ` +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +const instantiateFromUrl = ${instantiateFromFileCode} +export default ${wasmHelperCode} +` + } else { + return ` +const instantiateFromUrl = ${instantiateFromUrlCode} export default ${wasmHelperCode} ` + } } id = id.split('?')[0] - const url = isServer ? fsPathFromId(id) : await fileToUrl(this, id) - + let url = await fileToUrl(this, id) + if (isServer && url.startsWith(FS_PREFIX)) { + url = pathToFileURL(fsPathFromId(id)).href + } return ` import initWasm from "${wasmHelperId}" export default opts => initWasm(opts, ${JSON.stringify(url)}) diff --git a/playground/ssr-wasm/__tests__/serve.ts b/playground/ssr-wasm/__tests__/serve.ts index b4425c609e8f6f..95a3e0552cb17a 100644 --- a/playground/ssr-wasm/__tests__/serve.ts +++ b/playground/ssr-wasm/__tests__/serve.ts @@ -3,10 +3,17 @@ import path from 'node:path' import kill from 'kill-port' -import { hmrPorts, ports, rootDir } from '~utils' +import { build } from 'vite' +import { hmrPorts, isBuild, ports, rootDir } from '~utils' export const port = ports['ssr-wasm'] +export async function preServe() { + if (isBuild) { + await build({ root: rootDir }) + } +} + export async function serve(): Promise<{ close(): Promise }> { await kill(port) diff --git a/playground/ssr-wasm/package.json b/playground/ssr-wasm/package.json index c2a599d4d55cd8..4530b1bf29de5a 100644 --- a/playground/ssr-wasm/package.json +++ b/playground/ssr-wasm/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "dev": "node server", - "serve": "NODE_ENV=production node server", - "debug": "node --inspect-brk server" + "build": "vite build", + "preview": "NODE_ENV=production node server" }, "dependencies": {}, "devDependencies": { diff --git a/playground/ssr-wasm/server.js b/playground/ssr-wasm/server.js index 4508300693e825..14f594694411bc 100644 --- a/playground/ssr-wasm/server.js +++ b/playground/ssr-wasm/server.js @@ -1,43 +1,47 @@ import express from 'express' const isTest = process.env.VITEST +const isProduction = process.env.NODE_ENV === 'production' export async function createServer(root = process.cwd(), hmrPort) { const app = express() - /** - * @type {import('vite').ViteDevServer} - */ - const vite = await ( - await import('vite') - ).createServer({ - root, - logLevel: isTest ? 'error' : 'info', - server: { - middlewareMode: true, - watch: { - // During tests we edit the files too fast and sometimes chokidar - // misses change events, so enforce polling for consistency - usePolling: true, - interval: 100, + /** @type {import('vite').ViteDevServer} */ + let vite + if (!isProduction) { + vite = await ( + await import('vite') + ).createServer({ + root, + logLevel: isTest ? 'error' : 'info', + server: { + middlewareMode: true, + watch: { + // During tests we edit the files too fast and sometimes chokidar + // misses change events, so enforce polling for consistency + usePolling: true, + interval: 100, + }, + hmr: { + port: hmrPort, + }, }, - hmr: { - port: hmrPort, - }, - }, - appType: 'custom', - }) - // use vite's connect instance as middleware - app.use(vite.middlewares) + appType: 'custom', + }) + // use vite's connect instance as middleware + app.use(vite.middlewares) + } app.use('*all', async (req, res, next) => { try { const url = req.originalUrl - const render = (await vite.ssrLoadModule('/src/app.js')).render + const render = isProduction + ? (await import('./dist/app.js')).render + : (await vite.ssrLoadModule('/src/app.js')).render const html = await render(url) res.status(200).set({ 'Content-Type': 'text/html' }).end(html) } catch (e) { - vite && vite.ssrFixStacktrace(e) + vite?.ssrFixStacktrace(e) if (isTest) throw e console.log(e.stack) res.status(500).end(e.stack) diff --git a/playground/ssr-wasm/vite.config.ts b/playground/ssr-wasm/vite.config.ts index f70301f75c43bb..e02cb3a969afa5 100644 --- a/playground/ssr-wasm/vite.config.ts +++ b/playground/ssr-wasm/vite.config.ts @@ -3,5 +3,7 @@ export default defineConfig({ build: { // make cannot emit light.wasm assetsInlineLimit: 80, + ssr: './src/app.js', + ssrEmitAssets: true, }, }) From 11375b18596a0ac5b288b722626df1bd137f98e0 Mon Sep 17 00:00:00 2001 From: Xidorn Quan Date: Wed, 12 Nov 2025 13:34:16 +1100 Subject: [PATCH 8/9] add build tests to verify assets --- .../ssr-wasm/__tests__/ssr-wasm.spec.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/playground/ssr-wasm/__tests__/ssr-wasm.spec.ts b/playground/ssr-wasm/__tests__/ssr-wasm.spec.ts index 601f627c3fc62d..5aba47e37dae80 100644 --- a/playground/ssr-wasm/__tests__/ssr-wasm.spec.ts +++ b/playground/ssr-wasm/__tests__/ssr-wasm.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from 'vitest' import { port } from './serve' -import { page } from '~utils' +import { findAssetFile, isBuild, listAssets, page } from '~utils' const url = `http://localhost:${port}` @@ -13,3 +13,25 @@ test('should work when output', async () => { await page.goto(`${url}/static-heavy`) expect(await page.textContent('.static-heavy')).toMatch('24') }) + +test.runIf(isBuild)('should not contain wasm file when inlined', async () => { + const assets = await listAssets() + const lightWasm = assets.find((f) => /light-.+\.wasm$/.test(f)) + expect(lightWasm).toBeUndefined() + + const staticLight = await findAssetFile(/^static-light-.+\.js$/) + expect(staticLight).toContain('data:application/wasm;base64,') +}) + +test.runIf(isBuild)( + 'should contain and reference wasm file when output', + async () => { + const assets = await listAssets() + const heavyWasm = assets.find((f) => /heavy-.+\.wasm$/.test(f)) + expect(heavyWasm).toBeDefined() + + const staticHeavy = await findAssetFile(/^static-heavy-.+\.js$/) + expect(staticHeavy).toContain(heavyWasm) + expect(staticHeavy).not.toContain('data:application/wasm;base64,') + }, +) From 9f6cdad5aaeacc39b8ee9141a4ae5f0219c1b49e Mon Sep 17 00:00:00 2001 From: Xidorn Quan Date: Wed, 26 Nov 2025 09:53:30 +1100 Subject: [PATCH 9/9] handle wasm init specially --- packages/vite/src/node/build.ts | 2 +- packages/vite/src/node/plugins/wasm.ts | 50 ++++++++++++++++++++++++-- pnpm-lock.yaml | 2 +- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 301a69de1d3d47..844a45c14656a2 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -1404,7 +1404,7 @@ export function toOutputFilePathInJS( return result } } - if (relative || ssr) { + if (relative && !ssr) { return toRelative(filename, hostId) } return joinUrlSegments(decodedBase, filename) diff --git a/packages/vite/src/node/plugins/wasm.ts b/packages/vite/src/node/plugins/wasm.ts index 73d05dcd0966af..320dd7632c782a 100644 --- a/packages/vite/src/node/plugins/wasm.ts +++ b/packages/vite/src/node/plugins/wasm.ts @@ -1,15 +1,20 @@ import { fileURLToPath, pathToFileURL } from 'node:url' import { readFile } from 'node:fs/promises' +import MagicString from 'magic-string' import { exactRegex } from '@rolldown/pluginutils' +import { createToImportMetaURLBasedRelativeRuntime } from '../build' import type { Plugin } from '../plugin' import { fsPathFromId } from '../utils' import { FS_PREFIX } from '../constants' -import { fileToUrl } from './asset' +import { cleanUrl } from '../../shared/utils' +import { assetUrlRE, fileToUrl } from './asset' const wasmHelperId = '\0vite/wasm-helper.js' const wasmInitRE = /(? { let result if (url.startsWith('data:')) { @@ -105,8 +110,12 @@ export default ${wasmHelperCode} id = id.split('?')[0] let url = await fileToUrl(this, id) - if (isServer && url.startsWith(FS_PREFIX)) { - url = pathToFileURL(fsPathFromId(id)).href + if (isServer) { + if (url.startsWith(FS_PREFIX)) { + url = pathToFileURL(fsPathFromId(id)).href + } else if (assetUrlRE.test(url)) { + url = url.replace('__VITE_ASSET__', '__VITE_WASM_INIT__') + } } return ` import initWasm from "${wasmHelperId}" @@ -114,6 +123,41 @@ export default ${wasmHelperCode} ` }, }, + + renderChunk(code, chunk, opts) { + if (this.environment.config.consumer !== 'server') { + return null + } + + const toRelativeRuntime = createToImportMetaURLBasedRelativeRuntime( + opts.format, + this.environment.config.isWorker, + ) + + let match: RegExpExecArray | null + let s: MagicString | undefined + + wasmInitUrlRE.lastIndex = 0 + while ((match = wasmInitUrlRE.exec(code))) { + const [full, referenceId] = match + const file = this.getFileName(referenceId) + chunk.viteMetadata!.importedAssets.add(cleanUrl(file)) + const { runtime } = toRelativeRuntime(file, chunk.fileName) + s ||= new MagicString(code) + s.update(match.index, match.index + full.length, `"+${runtime}+"`) + } + + if (s) { + return { + code: s.toString(), + map: this.environment.config.build.sourcemap + ? s.generateMap({ hires: 'boundary' }) + : null, + } + } else { + return null + } + }, } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a753132cba2aac..7d35087c24b367 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1677,7 +1677,7 @@ importers: devDependencies: express: specifier: ^5.1.0 - version: 5.1.0 + version: 5.1.0(ms@2.1.3) playground/ssr-webworker: dependencies: