From 8d4f278ebae6ad794568cb3e4fd40f8d554917bc Mon Sep 17 00:00:00 2001 From: Aniketiitk21 Date: Wed, 6 May 2026 08:15:42 +0530 Subject: [PATCH] fix wasm-webp node initialization --- plugins/wasm-webp/package.json | 1 + plugins/wasm-webp/src/index.node.test.ts | 45 +++++++++++ plugins/wasm-webp/src/index.ts | 95 +++++++++++++++++++++++- 3 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 plugins/wasm-webp/src/index.node.test.ts diff --git a/plugins/wasm-webp/package.json b/plugins/wasm-webp/package.json index 02ce411a..bdbe5004 100644 --- a/plugins/wasm-webp/package.json +++ b/plugins/wasm-webp/package.json @@ -7,6 +7,7 @@ }, "scripts": { "lint": "eslint .", + "test": "vitest", "build": "tshy", "dev": "tshy --watch", "clean": "rm -rf node_modules .tshy .tshy-build dist .turbo" diff --git a/plugins/wasm-webp/src/index.node.test.ts b/plugins/wasm-webp/src/index.node.test.ts new file mode 100644 index 00000000..41d3cdc3 --- /dev/null +++ b/plugins/wasm-webp/src/index.node.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from "vitest"; +import { readFile } from "node:fs/promises"; + +import webp from "./index.js"; + +const format = webp(); + +describe("WASM WebP (Node)", () => { + test("loads WebP fixtures in Node", async () => { + const image = await format.decode( + await readFile(new URL("./images/test.webp", import.meta.url)) + ); + + expect(image.width).toBeGreaterThan(0); + expect(image.height).toBeGreaterThan(0); + expect(image.data.length).toBe(image.width * image.height * 4); + expect(Buffer.from(image.data).some((value) => value !== 0)).toBe(true); + }); + + test("exports WebP buffers in Node", async () => { + const bitmap = { + width: 3, + height: 3, + data: Buffer.from([ + 255, 0, 0, 255, + 255, 0, 128, 255, + 255, 0, 255, 255, + 255, 0, 128, 255, + 255, 0, 255, 255, + 128, 0, 255, 255, + 255, 0, 255, 255, + 128, 0, 255, 255, + 0, 0, 255, 255, + ]), + }; + + const buffer = await format.encode(bitmap); + expect(buffer.toString("ascii", 0, 4)).toBe("RIFF"); + expect(buffer.toString("ascii", 8, 12)).toBe("WEBP"); + + const roundTrip = await format.decode(buffer); + expect(roundTrip.width).toBe(3); + expect(roundTrip.height).toBe(3); + }); +}); diff --git a/plugins/wasm-webp/src/index.ts b/plugins/wasm-webp/src/index.ts index 95b86fca..f2c19298 100644 --- a/plugins/wasm-webp/src/index.ts +++ b/plugins/wasm-webp/src/index.ts @@ -101,7 +101,96 @@ const WebpOptionsSchema = z.object({ type WebpOptions = z.infer; -export default function png() { +const isNodeRuntime = + typeof process === "object" && + typeof process.versions === "object" && + typeof process.versions.node === "string"; + +const nodeWasmModuleCache = new Map>(); +let decoderInitPromise: Promise | undefined; +let encoderInitPromise: Promise | undefined; + +async function supportsSimd() { + return WebAssembly.validate( + new Uint8Array([ + 0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, + 10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11, + ]) + ); +} + +function resetPromiseOnError( + promise: Promise, + reset: () => void +): Promise { + return promise.catch((error) => { + reset(); + throw error; + }); +} + +async function loadNodeWasmModule(specifier: string): Promise { + const cachedModule = nodeWasmModuleCache.get(specifier); + if (cachedModule) { + return cachedModule; + } + + const modulePromise = (async () => { + try { + const { readFile } = await import("node:fs/promises"); + const { default: Module } = await import("node:module"); + + const require = Module.createRequire(import.meta.url); + const wasmPath = require.resolve(specifier); + return WebAssembly.compile(await readFile(wasmPath)); + } catch (error) { + nodeWasmModuleCache.delete(specifier); + throw error; + } + })(); + + nodeWasmModuleCache.set(specifier, modulePromise); + return modulePromise; +} + +async function ensureDecoderInitialized() { + if (!decoderInitPromise) { + decoderInitPromise = resetPromiseOnError( + isNodeRuntime + ? initDecoder( + await loadNodeWasmModule("@jsquash/webp/codec/dec/webp_dec.wasm") + ) + : initDecoder(), + () => { + decoderInitPromise = undefined; + } + ); + } + + return decoderInitPromise; +} + +async function ensureEncoderInitialized() { + if (!encoderInitPromise) { + const encoderWasmSpecifier = + isNodeRuntime && (await supportsSimd()) + ? "@jsquash/webp/codec/enc/webp_enc_simd.wasm" + : "@jsquash/webp/codec/enc/webp_enc.wasm"; + + encoderInitPromise = resetPromiseOnError( + isNodeRuntime + ? initEncoder(await loadNodeWasmModule(encoderWasmSpecifier)) + : initEncoder(), + () => { + encoderInitPromise = undefined; + } + ); + } + + return encoderInitPromise; +} + +export default function webp() { return { mime: "image/webp", hasAlpha: true, @@ -135,7 +224,7 @@ export default function png() { targetSize, colorSpace = "srgb", } = WebpOptionsSchema.parse(options); - await initEncoder(); + await ensureEncoderInitialized(); const arrayBuffer = await encode( { ...bitmap, @@ -175,7 +264,7 @@ export default function png() { return Buffer.from(arrayBuffer); }, decode: async (data) => { - await initDecoder(); + await ensureDecoderInitialized(); const result = await decode(new Uint8Array(data).buffer); return {