Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/vite/src/node/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1404,7 +1404,7 @@ export function toOutputFilePathInJS(
return result
}
}
if (relative && !ssr) {
if (relative || ssr) {
return toRelative(filename, hostId)
}
return joinUrlSegments(decodedBase, filename)
Expand Down
78 changes: 59 additions & 19 deletions packages/vite/src/node/plugins/wasm.ts
Original file line number Diff line number Diff line change
@@ -1,5 +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'
Expand All @@ -26,28 +30,47 @@ 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()
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 (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)
}

const instantiateFromFileCode = instantiateFromFile.toString()

export const wasmHelperPlugin = (): Plugin => {
return {
name: 'vite:wasm-helper',
Expand All @@ -62,12 +85,29 @@ 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}`
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}
`
}
}

const url = await fileToUrl(this, id)

id = id.split('?')[0]
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)})
Expand Down
42 changes: 42 additions & 0 deletions playground/ssr-wasm/__tests__/serve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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 { 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<void> }> {
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)
}
})
}
15 changes: 15 additions & 0 deletions playground/ssr-wasm/__tests__/ssr-wasm.spec.ts
Original file line number Diff line number Diff line change
@@ -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')
})
15 changes: 15 additions & 0 deletions playground/ssr-wasm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@vitejs/test-ssr-wasm",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "node server",
"build": "vite build",
"preview": "NODE_ENV=production node server"
},
"dependencies": {},
"devDependencies": {
"express": "^5.1.0"
}
}
60 changes: 60 additions & 0 deletions playground/ssr-wasm/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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} */
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,
},
},
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 = 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?.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')
}),
)
}
8 changes: 8 additions & 0 deletions playground/ssr-wasm/src/app.js
Original file line number Diff line number Diff line change
@@ -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()
}
}
1 change: 1 addition & 0 deletions playground/ssr-wasm/src/heavy.wasm
1 change: 1 addition & 0 deletions playground/ssr-wasm/src/light.wasm
12 changes: 12 additions & 0 deletions playground/ssr-wasm/src/static-heavy.js
Original file line number Diff line number Diff line change
@@ -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 `<div class="static-heavy">${result}</div>`
}
12 changes: 12 additions & 0 deletions playground/ssr-wasm/src/static-light.js
Original file line number Diff line number Diff line change
@@ -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 `<div class="static-light">${result}</div>`
}
9 changes: 9 additions & 0 deletions playground/ssr-wasm/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from 'vite'
export default defineConfig({
build: {
// make cannot emit light.wasm
assetsInlineLimit: 80,
ssr: './src/app.js',
ssrEmitAssets: true,
},
})
2 changes: 2 additions & 0 deletions playground/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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,
Expand Down
Loading