diff --git a/www/dev.mjs b/www/dev.mjs new file mode 100755 index 0000000..c87d33b --- /dev/null +++ b/www/dev.mjs @@ -0,0 +1,18 @@ +#!/usr/bin/env node +import * as esbuild from 'esbuild' +import wasmPlugin from './wasmPlugin.mjs' + +const ctx = await esbuild.context({ + entryPoints: ['./src/index.jsx'], + bundle: true, + outfile: './public/bundle/index.js', + plugins: [wasmPlugin], +}) + +const { host, port } = await ctx.serve({ + servedir: 'public', + onRequest: () => { + console.log('Got request') + }, +}) +console.log(`http://${host}:${port}`) diff --git a/www/package.json b/www/package.json index 848cc8a..1f08a79 100644 --- a/www/package.json +++ b/www/package.json @@ -4,7 +4,7 @@ "description": "", "main": "dist/index.js", "scripts": { - "dev": "esbuild ./src/index.tsx --bundle --outdir=public/bundle --watch --servedir=public", + "dev": "./dev.mjs", "build": "./build.mjs", "test": "echo \"Error: no test specified\" && exit 1" }, @@ -15,6 +15,7 @@ }, "dependencies": { "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "wasm-game-of-life": "file:../pkg" } } diff --git a/www/pnpm-lock.yaml b/www/pnpm-lock.yaml index 5282246..a5ef658 100644 --- a/www/pnpm-lock.yaml +++ b/www/pnpm-lock.yaml @@ -1,236 +1,266 @@ -lockfileVersion: '6.0' +lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false -dependencies: - react: - specifier: ^18.2.0 - version: 18.2.0 - react-dom: - specifier: ^18.2.0 - version: 18.2.0(react@18.2.0) +importers: -devDependencies: - esbuild: - specifier: 0.20.2 - version: 0.20.2 + .: + dependencies: + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + wasm-game-of-life: + specifier: file:../pkg + version: file:../pkg + devDependencies: + esbuild: + specifier: 0.20.2 + version: 0.20.2 packages: - /@esbuild/aix-ppc64@0.20.2: + '@esbuild/aix-ppc64@0.20.2': resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-arm64@0.20.2: + '@esbuild/android-arm64@0.20.2': resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} engines: {node: '>=12'} cpu: [arm64] os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-arm@0.20.2: + '@esbuild/android-arm@0.20.2': resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} engines: {node: '>=12'} cpu: [arm] os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-x64@0.20.2: + '@esbuild/android-x64@0.20.2': resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} engines: {node: '>=12'} cpu: [x64] os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/darwin-arm64@0.20.2: + '@esbuild/darwin-arm64@0.20.2': resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] - requiresBuild: true - dev: true - optional: true - /@esbuild/darwin-x64@0.20.2: + '@esbuild/darwin-x64@0.20.2': resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} engines: {node: '>=12'} cpu: [x64] os: [darwin] - requiresBuild: true - dev: true - optional: true - /@esbuild/freebsd-arm64@0.20.2: + '@esbuild/freebsd-arm64@0.20.2': resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/freebsd-x64@0.20.2: + '@esbuild/freebsd-x64@0.20.2': resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-arm64@0.20.2: + '@esbuild/linux-arm64@0.20.2': resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} engines: {node: '>=12'} cpu: [arm64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-arm@0.20.2: + '@esbuild/linux-arm@0.20.2': resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} engines: {node: '>=12'} cpu: [arm] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-ia32@0.20.2: + '@esbuild/linux-ia32@0.20.2': resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} engines: {node: '>=12'} cpu: [ia32] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-loong64@0.20.2: + '@esbuild/linux-loong64@0.20.2': resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} engines: {node: '>=12'} cpu: [loong64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-mips64el@0.20.2: + '@esbuild/linux-mips64el@0.20.2': resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-ppc64@0.20.2: + '@esbuild/linux-ppc64@0.20.2': resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-riscv64@0.20.2: + '@esbuild/linux-riscv64@0.20.2': resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-s390x@0.20.2: + '@esbuild/linux-s390x@0.20.2': resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} engines: {node: '>=12'} cpu: [s390x] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-x64@0.20.2: + '@esbuild/linux-x64@0.20.2': resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} engines: {node: '>=12'} cpu: [x64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/netbsd-x64@0.20.2: + '@esbuild/netbsd-x64@0.20.2': resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/openbsd-x64@0.20.2: + '@esbuild/openbsd-x64@0.20.2': resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/sunos-x64@0.20.2: + '@esbuild/sunos-x64@0.20.2': resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} engines: {node: '>=12'} cpu: [x64] os: [sunos] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-arm64@0.20.2: + '@esbuild/win32-arm64@0.20.2': resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} engines: {node: '>=12'} cpu: [arm64] os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-ia32@0.20.2: + '@esbuild/win32-ia32@0.20.2': resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} engines: {node: '>=12'} cpu: [ia32] os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-x64@0.20.2: + '@esbuild/win32-x64@0.20.2': resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} engines: {node: '>=12'} cpu: [x64] os: [win32] - requiresBuild: true - dev: true - optional: true - /esbuild@0.20.2: + esbuild@0.20.2: resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} engines: {node: '>=12'} hasBin: true - requiresBuild: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + react-dom@18.2.0: + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + + react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + + scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + + wasm-game-of-life@file:../pkg: + resolution: {directory: ../pkg, type: directory} + +snapshots: + + '@esbuild/aix-ppc64@0.20.2': + optional: true + + '@esbuild/android-arm64@0.20.2': + optional: true + + '@esbuild/android-arm@0.20.2': + optional: true + + '@esbuild/android-x64@0.20.2': + optional: true + + '@esbuild/darwin-arm64@0.20.2': + optional: true + + '@esbuild/darwin-x64@0.20.2': + optional: true + + '@esbuild/freebsd-arm64@0.20.2': + optional: true + + '@esbuild/freebsd-x64@0.20.2': + optional: true + + '@esbuild/linux-arm64@0.20.2': + optional: true + + '@esbuild/linux-arm@0.20.2': + optional: true + + '@esbuild/linux-ia32@0.20.2': + optional: true + + '@esbuild/linux-loong64@0.20.2': + optional: true + + '@esbuild/linux-mips64el@0.20.2': + optional: true + + '@esbuild/linux-ppc64@0.20.2': + optional: true + + '@esbuild/linux-riscv64@0.20.2': + optional: true + + '@esbuild/linux-s390x@0.20.2': + optional: true + + '@esbuild/linux-x64@0.20.2': + optional: true + + '@esbuild/netbsd-x64@0.20.2': + optional: true + + '@esbuild/openbsd-x64@0.20.2': + optional: true + + '@esbuild/sunos-x64@0.20.2': + optional: true + + '@esbuild/win32-arm64@0.20.2': + optional: true + + '@esbuild/win32-ia32@0.20.2': + optional: true + + '@esbuild/win32-x64@0.20.2': + optional: true + + esbuild@0.20.2: optionalDependencies: '@esbuild/aix-ppc64': 0.20.2 '@esbuild/android-arm': 0.20.2 @@ -255,38 +285,25 @@ packages: '@esbuild/win32-arm64': 0.20.2 '@esbuild/win32-ia32': 0.20.2 '@esbuild/win32-x64': 0.20.2 - dev: true - /js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: false + js-tokens@4.0.0: {} - /loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 - dev: false - /react-dom@18.2.0(react@18.2.0): - resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} - peerDependencies: - react: ^18.2.0 + react-dom@18.2.0(react@18.2.0): dependencies: loose-envify: 1.4.0 react: 18.2.0 scheduler: 0.23.0 - dev: false - /react@18.2.0: - resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} - engines: {node: '>=0.10.0'} + react@18.2.0: dependencies: loose-envify: 1.4.0 - dev: false - /scheduler@0.23.0: - resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + scheduler@0.23.0: dependencies: loose-envify: 1.4.0 - dev: false + + wasm-game-of-life@file:../pkg: {} diff --git a/www/src/app.tsx b/www/src/app.tsx index 3625ff4..66d3860 100644 --- a/www/src/app.tsx +++ b/www/src/app.tsx @@ -1,5 +1,32 @@ import React from 'react'; +import run from './game-of-life' + +const bodyStyle = { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', +}; +const fpsStyles = { + whiteSpace: 'pre', + fontFamily: 'monospace', +} export default function App() { - return

Hello, world

; + const canvasRef = React.useRef() + React.useEffect(() => { + run(canvasRef.current) + }, []) + return ( +
+ +
+ +
+ ); } diff --git a/www/src/game-of-life.js b/www/src/game-of-life.js new file mode 100644 index 0000000..9d059b4 --- /dev/null +++ b/www/src/game-of-life.js @@ -0,0 +1,199 @@ +import wasm from "../node_modules/wasm-game-of-life/wasm_game_of_life_bg.wasm"; +import * as bg from "../node_modules/wasm-game-of-life/wasm_game_of_life_bg.js" + +export default function run(canvas) { + wasm({'./wasm_game_of_life_bg.js': bg}).then(wasm => { + bg.__wbg_set_wasm(wasm) + main(bg.Universe, bg.Cell, wasm.memory, canvas) + }) +} + +function main(Universe, Cell, memory, canvas) { + const CELL_SIZE = 5; // px + const GRID_COLOR = "#CCCCCC"; + const DEAD_COLOR = "#FFFFFF"; + const ALIVE_COLOR = "#000000"; + + // Construct the universe, and get its width and height. + const universe = Universe.new(); + const width = universe.width(); + const height = universe.height(); + + // Give the canvas room for all of our cells and a 1px border + // around each of them. + canvas.height = (CELL_SIZE + 1) * height + 1; + canvas.width = (CELL_SIZE + 1) * width + 1; + + const ctx = canvas.getContext('2d'); + + const fps = new class { + constructor() { + this.fps = document.getElementById("fps"); + this.frames = []; + this.lastFrameTimeStamp = performance.now(); + } + + render() { + // Convert the delta time since the last frame render into a measure + // of frames per second. + const now = performance.now(); + const delta = now - this.lastFrameTimeStamp; + this.lastFrameTimeStamp = now; + const fps = 1 / delta * 1000; + + // Save only the latest 100 timings. + this.frames.push(fps); + if (this.frames.length > 100) { + this.frames.shift(); + } + + // Find the max, min, and mean of our 100 latest timings. + let min = Infinity; + let max = -Infinity; + let sum = 0; + for (let i = 0; i < this.frames.length; i++) { + sum += this.frames[i]; + min = Math.min(this.frames[i], min); + max = Math.max(this.frames[i], max); + } + let mean = sum / this.frames.length; + + // Render the statistics. + this.fps.textContent = ` + Frames per Second: + latest = ${Math.round(fps)} + avg of last 100 = ${Math.round(mean)} + min of last 100 = ${Math.round(min)} + max of last 100 = ${Math.round(max)} + `.trim(); + } + }; + + let animationId = null; + + const renderLoop = () => { + fps.render(); + + drawGrid(); + drawCells(); + + for (let i = 0; i < 9; i++) { + universe.tick(); + } + + animationId = requestAnimationFrame(renderLoop); + }; + + const isPaused = () => { + return animationId === null; + }; + + const playPauseButton = document.getElementById("play-pause"); + + const play = () => { + playPauseButton.textContent = "⏸"; + renderLoop(); + }; + + const pause = () => { + playPauseButton.textContent = "▶"; + cancelAnimationFrame(animationId); + animationId = null; + }; + + playPauseButton.addEventListener("click", event => { + if (isPaused()) { + play(); + } else { + pause(); + } + }); + + const drawGrid = () => { + ctx.beginPath(); + ctx.strokeStyle = GRID_COLOR; + + // Vertical lines. + for (let i = 0; i <= width; i++) { + ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0); + ctx.lineTo(i * (CELL_SIZE + 1) + 1, (CELL_SIZE + 1) * height + 1); + } + + // Horizontal lines. + for (let j = 0; j <= height; j++) { + ctx.moveTo(0, j * (CELL_SIZE + 1) + 1); + ctx.lineTo((CELL_SIZE + 1) * width + 1, j * (CELL_SIZE + 1) + 1); + } + + ctx.stroke(); + }; + + const getIndex = (row, column) => { + return row * width + column; + }; + + const drawCells = () => { + const cellsPtr = universe.cells(); + const cells = new Uint8Array(memory.buffer, cellsPtr, width * height); + + ctx.beginPath(); + + // Alive cells. + ctx.fillStyle = ALIVE_COLOR; + for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const idx = getIndex(row, col); + if (cells[idx] !== Cell.Alive) { + continue; + } + + ctx.fillRect( + col * (CELL_SIZE + 1) + 1, + row * (CELL_SIZE + 1) + 1, + CELL_SIZE, + CELL_SIZE + ); + } + } + + // Dead cells. + ctx.fillStyle = DEAD_COLOR; + for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const idx = getIndex(row, col); + if (cells[idx] !== Cell.Dead) { + continue; + } + + ctx.fillRect( + col * (CELL_SIZE + 1) + 1, + row * (CELL_SIZE + 1) + 1, + CELL_SIZE, + CELL_SIZE + ); + } + } + + ctx.stroke(); + }; + + canvas.addEventListener("click", event => { + const boundingRect = canvas.getBoundingClientRect(); + + const scaleX = canvas.width / boundingRect.width; + const scaleY = canvas.height / boundingRect.height; + + const canvasLeft = (event.clientX - boundingRect.left) * scaleX; + const canvasTop = (event.clientY - boundingRect.top) * scaleY; + + const row = Math.min(Math.floor(canvasTop / (CELL_SIZE + 1)), height - 1); + const col = Math.min(Math.floor(canvasLeft / (CELL_SIZE + 1)), width - 1); + + universe.toggle_cell(row, col); + + drawCells(); + drawGrid(); + }); + + play(); +} diff --git a/www/wasmPlugin.mjs b/www/wasmPlugin.mjs new file mode 100644 index 0000000..4a60fdc --- /dev/null +++ b/www/wasmPlugin.mjs @@ -0,0 +1,55 @@ +import path from 'path' +import fs from 'fs' + +const wasmPlugin = { + name: 'wasm', + setup(build) { + // Resolve ".wasm" files to a path with a namespace + build.onResolve({ filter: /\.wasm$/ }, args => { + // If this is the import inside the stub module, import the + // binary itself. Put the path in the "wasm-binary" namespace + // to tell our binary load callback to load the binary file. + if (args.namespace === 'wasm-stub') { + return { + path: args.path, + namespace: 'wasm-binary', + } + } + + // Otherwise, generate the JavaScript stub module for this + // ".wasm" file. Put it in the "wasm-stub" namespace to tell + // our stub load callback to fill it with JavaScript. + // + // Resolve relative paths to absolute paths here since this + // resolve callback is given "resolveDir", the directory to + // resolve imports against. + if (args.resolveDir === '') { + return // Ignore unresolvable paths + } + return { + path: path.isAbsolute(args.path) ? args.path : path.join(args.resolveDir, args.path), + namespace: 'wasm-stub', + } + }) + + // Virtual modules in the "wasm-stub" namespace are filled with + // the JavaScript code for compiling the WebAssembly binary. The + // binary itself is imported from a second virtual module. + build.onLoad({ filter: /.*/, namespace: 'wasm-stub' }, async (args) => ({ + contents: `import wasm from ${JSON.stringify(args.path)} + export default (imports) => + WebAssembly.instantiate(wasm, imports).then( + result => result.instance.exports)`, + })) + + // Virtual modules in the "wasm-binary" namespace contain the + // actual bytes of the WebAssembly file. This uses esbuild's + // built-in "binary" loader instead of manually embedding the + // binary data inside JavaScript code ourselves. + build.onLoad({ filter: /.*/, namespace: 'wasm-binary' }, async (args) => ({ + contents: await fs.promises.readFile(args.path), + loader: 'binary', + })) + }, +} +export default wasmPlugin