Skip to content

Instantly share code, notes, and snippets.

@relative
Created August 25, 2023 21:38
Show Gist options
  • Save relative/15910175a9022459c6834244a4ccf0c9 to your computer and use it in GitHub Desktop.
Save relative/15910175a9022459c6834244a4ccf0c9 to your computer and use it in GitHub Desktop.
esbuild .wat->wasm loader
// Loosely based upon example WebAssembly plugin in esbuild docs
// https://esbuild.github.io/plugins/#webassembly-plugin
const childProcess = require('child_process'),
path = require('path'),
util = require('util')
const execFile = util.promisify(childProcess.execFile)
const ErrorMatchRegex = /(?<path>.+?):(?<line>\d+):(?<col>\d+): (?<type>error|warning): (?<message>.+)/i,
DefaultOptions = {
filter: /\.wat$/i,
namespace: 'wat',
}
/**
* @param {string} watPath
* @throws {Error}
*/
async function compileWasm(watPath) {
const { stdout } = await execFile('wat2wasm', [watPath, '--output=-'], {
encoding: 'buffer',
})
return `data:application/wasm;base64,${stdout.toString('base64')}`
}
/**
* @param {{
* filter: RegExp
* namespace: string
* }} opts
* @returns {import('esbuild').Plugin}
*/
function esbuildPluginWat({ filter, namespace } = DefaultOptions) {
return {
name: 'wat-loader',
setup(build) {
const nsWatStub = `${namespace}-stub`,
nsWatFile = `${namespace}-binary`
build.onResolve(
{
filter,
},
args => {
if (args.namespace === nsWatStub)
return {
path: args.path,
namespace: nsWatFile,
watchFiles: [args.path],
}
if (args.resolveDir === '') return // Ignore unresolveable paths
return {
path: path.isAbsolute(args.path) ? args.path : path.join(args.resolveDir, args.path),
namespace: nsWatStub,
}
}
)
build.onLoad(
{
filter,
namespace: nsWatStub,
},
async args => {
return {
contents: `import url from ${JSON.stringify(
args.path
)};export default (imports) => WebAssembly.instantiateStreaming(fetch(url), imports)`,
loader: 'js',
}
}
)
build.onLoad(
{
filter,
namespace: nsWatFile,
},
async args => {
try {
return {
contents: await compileWasm(args.path),
loader: 'text',
}
} catch (err) {
const { stderr } = err
if (!Buffer.isBuffer(stderr)) throw err
const t = stderr.toString('utf8').split(/\r?\n/gi)
/**
* @type {import('esbuild').PartialMessage[]}
*/
const warnings = []
/**
* @type {import('esbuild').PartialMessage[]}
*/
const errors = []
for (const [_idx, line] of Object.entries(t)) {
const match = line.match(ErrorMatchRegex),
i = parseInt(_idx)
if (!match) continue
/**
* @type {{
* path: string
* line: string
* col: string
* type: 'error' | 'warning'
* message: string
* }}
*/
const groups = match.groups
;(groups.type === 'error' ? errors : warnings).push({
location: {
file: groups.path,
line: parseInt(groups.line),
column: parseInt(groups.col) - 1, // esbuild expects 0-based column
lineText: t[i + 1],
},
text: groups.message,
detail: err,
})
}
return {
warnings,
errors,
}
}
}
)
},
}
}
module.exports = esbuildPluginWat
declare module '*.wat' {
const wat: (imports?: WebAssembly.Imports) => Promise<WebAssembly.WebAssemblyInstantiatedSource>
export default wat
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment