diff --git a/frontend/package.json b/frontend/package.json index 66aa4f9..297625e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,7 @@ "dev": "vite --host 0.0.0.0", "build": "vue-tsc -b && vite build", "preview": "vite preview", - "postinstall": "node patches/patch-exiftool-stderr.mjs" + "postinstall": "node patches/patch-exiftool-stderr.mjs && node patches/patch-zeroperl-worker-fetch.mjs" }, "dependencies": { "@tailwindcss/vite": "^4.3.3", diff --git a/frontend/patches/README.md b/frontend/patches/README.md index dee9429..1965824 100644 --- a/frontend/patches/README.md +++ b/frontend/patches/README.md @@ -14,3 +14,16 @@ Instead, `patches/patch-exiftool-stderr.mjs` runs on `postinstall` and removes t See the script for exact literals and upstream context. When upgrading `@uswriting/exiftool`, run `npm install` and confirm the script still finds both guards (or update the script for the new bundle). + +## `@6over3/zeroperl-ts@1.0.10` — WASM load in module workers + +`patches/patch-zeroperl-worker-fetch.mjs` changes the WASM loader so a custom `fetch` (from `@uswriting/exiftool` options) is honored even when `document` is missing (Web Workers). + +| Snippet | Role | +| ----- | ---- | +| `if(Ee())a=await...` → `if(Ee()\|\|e)a=await...` (ESM) | Use fetch path when `options.fetch` is set | +| `if(pe())a=await...` → `if(pe()\|\|e)a=await...` (CJS) | Same for CJS bundle | + +Workers still pass Vite’s WASM URL via `zeroperl.wasm?url` and `exiftoolParseOptions.fetch` in app code. + +When upgrading `@6over3/zeroperl-ts`, re-run `npm install` and confirm the script still matches (or update literals). diff --git a/frontend/patches/patch-zeroperl-worker-fetch.mjs b/frontend/patches/patch-zeroperl-worker-fetch.mjs new file mode 100644 index 0000000..9a2dc72 --- /dev/null +++ b/frontend/patches/patch-zeroperl-worker-fetch.mjs @@ -0,0 +1,60 @@ +/** + * Patch @6over3/zeroperl-ts@1.0.10 after install. + * + * Problem: WASM loader treats only `window` + `document` as browser. Module workers have + * neither, so it dynamic-imports `node:fs/promises` and fails under Vite. + * Custom `fetch` from @uswriting/exiftool is ignored on that branch. + * + * Fix: use the fetch path when a custom fetch is provided (`Ee() || e`). + * + * Upstream: https://github.com/6over3/zeroperl-ts + * + * Re-verify when bumping @6over3/zeroperl-ts: search dist for the ORIGINAL snippets below. + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const patches = [ + { + relativePath: 'esm/index.js', + original: 'if(Ee())a=await(await(e??fetch)(q)).arrayBuffer();', + patched: 'if(Ee()||e)a=await(await(e??fetch)(q)).arrayBuffer();' + }, + { + relativePath: 'cjs/index.cjs', + original: 'if(pe())a=await(await(e??fetch)(w)).arrayBuffer();', + patched: 'if(pe()||e)a=await(await(e??fetch)(w)).arrayBuffer();' + } +]; + +const packageRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../node_modules/@6over3/zeroperl-ts/dist' +); + +const patchFile = ({ relativePath, original, patched }) => { + const filePath = path.join(packageRoot, relativePath); + + if (!fs.existsSync(filePath)) { + throw new Error(`patch-zeroperl-worker-fetch: missing ${filePath} (is @6over3/zeroperl-ts installed?)`); + } + + const content = fs.readFileSync(filePath, 'utf8'); + + if (content.includes(patched)) { + return; + } + + if (!content.includes(original)) { + throw new Error( + `patch-zeroperl-worker-fetch: ${relativePath} does not match @6over3/zeroperl-ts@1.0.10; update or remove this script` + ); + } + + fs.writeFileSync(filePath, content.replaceAll(original, patched)); +}; + +for (const entry of patches) { + patchFile(entry); +} diff --git a/frontend/src/workers/exiftoolWorker.ts b/frontend/src/workers/exiftoolWorker.ts index ee42806..603a4e3 100644 --- a/frontend/src/workers/exiftoolWorker.ts +++ b/frontend/src/workers/exiftoolWorker.ts @@ -1,8 +1,12 @@ +import zeroperlWasmUrl from '@6over3/zeroperl-ts/zeroperl.wasm?url'; import { parseMetadata } from '@uswriting/exiftool'; import { EXIFTOOL_WORKER_INIT_JPEG } from '@/consts/exiftoolWorkerInitJpeg'; import type { ExiftoolWorkerInMessage } from '@/types/workers/exiftoolWorkerInMessage'; import { resolveErrorMessage } from '@/utils/resolveErrorMessage'; +/** Custom fetch function for `@uswriting/exiftool` to load `zeroperl.wasm` from the worker. */ +const exiftoolParseOptions = { fetch: () => fetch(zeroperlWasmUrl) }; + let initialized = false; let initializeWorkerPromise: Promise | null = null; @@ -19,7 +23,7 @@ let initializeWorkerPromise: Promise | null = null; * Concurrent callers (init message, read-metadata, future write) share one in-flight warmup. * * @throws When warmup `parseMetadata` fails (network, WASM, or ExifTool error). - * @see Main thread `initialize()` posts `{ type: 'init' }` when idle. + * @see Main thread `initializeExiftool()` posts `{ type: 'init' }` when idle. */ const initializeWorker = async (): Promise => { if (initialized) { @@ -29,7 +33,10 @@ const initializeWorker = async (): Promise => { if (!initializeWorkerPromise) { initializeWorkerPromise = (async () => { try { - const result = await parseMetadata({ name: 'init.jpg', data: EXIFTOOL_WORKER_INIT_JPEG }); + const result = await parseMetadata( + { name: 'init.jpg', data: EXIFTOOL_WORKER_INIT_JPEG }, + exiftoolParseOptions + ); if (!result.success) { throw new Error(result.error); @@ -69,7 +76,7 @@ self.addEventListener('message', async (event: MessageEvent