From e89f76a673e20286625096b0a802ac3178bf6fed Mon Sep 17 00:00:00 2001 From: nobswebdev Date: Tue, 13 Oct 2026 21:07:04 +0200 Subject: [PATCH] Pin @uswriting/exiftool and patch exit-0 stderr guard on install. Postinstall removes upstream guards that mark parseMetadata failed when stderr is non-empty after success; Docker dev image copies patches before npm ci. --- frontend/Dockerfile.dev | 1 + frontend/package-lock.json | 34 ++++++++++++ frontend/package.json | 4 +- frontend/patches/README.md | 16 ++++++ frontend/patches/patch-exiftool-stderr.mjs | 61 ++++++++++++++++++++++ 5 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 frontend/patches/README.md create mode 100644 frontend/patches/patch-exiftool-stderr.mjs diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev index 553bfe0..92b8c9e 100644 --- a/frontend/Dockerfile.dev +++ b/frontend/Dockerfile.dev @@ -3,6 +3,7 @@ FROM node:22-alpine WORKDIR /frontend COPY package*.json . +COPY patches ./patches RUN npm ci diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0b4d43e..39e1685 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7,8 +7,10 @@ "": { "name": "frontend", "version": "0.0.0", + "hasInstallScript": true, "dependencies": { "@tailwindcss/vite": "^4.3.3", + "@uswriting/exiftool": "1.0.9", "axios": "^1.20.0", "pinia": "^4.0.3", "tailwindcss": "^4.3.3", @@ -24,6 +26,12 @@ "vue-tsc": "^3.2.6" } }, + "node_modules/@6over3/zeroperl-ts": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@6over3/zeroperl-ts/-/zeroperl-ts-1.0.10.tgz", + "integrity": "sha512-d7fBgN1UaP9BR6kZMyNsjIrxPd0QLqqx29VSprhK66m1csm/7E/anr+rEfp3gJDSne5FfDpM6aKtDGHyXG/vYQ==", + "license": "Apache-2.0" + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -886,6 +894,15 @@ "undici-types": "~6.21.0" } }, + "node_modules/@uswriting/exiftool": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@uswriting/exiftool/-/exiftool-1.0.9.tgz", + "integrity": "sha512-tziE5oH/Wf4gJ/g/ZVGBxCjnoHz27SxXRZ4KoFa4c0qN/6uBfvBFEQ272dq7LxbzhXkvGqHdmDyNJe/WUZCV5A==", + "license": "Apache-2.0", + "dependencies": { + "@6over3/zeroperl-ts": "1.0.10" + } + }, "node_modules/@vitejs/plugin-vue": { "version": "6.0.9", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.9.tgz", @@ -2156,6 +2173,23 @@ "peerDependencies": { "typescript": ">=5.0.0" } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 7cc412d..66aa4f9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,10 +6,12 @@ "scripts": { "dev": "vite --host 0.0.0.0", "build": "vue-tsc -b && vite build", - "preview": "vite preview" + "preview": "vite preview", + "postinstall": "node patches/patch-exiftool-stderr.mjs" }, "dependencies": { "@tailwindcss/vite": "^4.3.3", + "@uswriting/exiftool": "1.0.9", "axios": "^1.20.0", "pinia": "^4.0.3", "tailwindcss": "^4.3.3", diff --git a/frontend/patches/README.md b/frontend/patches/README.md new file mode 100644 index 0000000..dee9429 --- /dev/null +++ b/frontend/patches/README.md @@ -0,0 +1,16 @@ +# Dependency patches + +## `@uswriting/exiftool@1.0.9` — stderr vs warnings + +We do **not** use a `patch-package` diff here: the published `dist/*.js` files are one minified line each, so unified diffs are huge and hard to review. + +Instead, `patches/patch-exiftool-stderr.mjs` runs on `postinstall` and removes two **documented** guard strings: + +| Guard | Role | +| ----- | ---- | +| `READ_STDERR_GUARD` | After exit 0 on **read**, fail if stderr is non-empty | +| `WRITE_STDERR_GUARD` | After exit 0 on **write**, fail if stderr is non-empty | + +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). diff --git a/frontend/patches/patch-exiftool-stderr.mjs b/frontend/patches/patch-exiftool-stderr.mjs new file mode 100644 index 0000000..b105307 --- /dev/null +++ b/frontend/patches/patch-exiftool-stderr.mjs @@ -0,0 +1,61 @@ +/** + * Patch @uswriting/exiftool@1.0.9 after install. + * + * Problem: parseMetadata / writeMetadata fail when ExifTool prints warnings to stderr + * even if exit code is 0 (no stdout / no strip bytes returned). + * + * Fix: remove the two stderr early-return checks in the published bundle. + * Upload safety still relies on post-strip re-read + isCleanAfterStrip (app code). + * + * Upstream: https://github.com/6over3/exiftool (issue: warnings incorrectly treated as errors) + * + * Re-verify when bumping @uswriting/exiftool: search dist for these exact snippets. + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const READ_STDERR_GUARD = + 'if(o&&o.trim())return{success:!1,data:void 0,error:o,exitCode:0};'; + +const WRITE_STDERR_GUARD = + 'if(d&&d.trim())return{success:!1,data:void 0,error:d,exitCode:0};'; + +const packageRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../node_modules/@uswriting/exiftool/dist' +); + +const targets = ['esm/index.js', 'cjs/index.cjs']; + +const patchFile = (relativePath) => { + const filePath = path.join(packageRoot, relativePath); + + if (!fs.existsSync(filePath)) { + throw new Error(`patch-exiftool-stderr: missing ${filePath} (is @uswriting/exiftool installed?)`); + } + + const original = fs.readFileSync(filePath, 'utf8'); + const alreadyPatched = + !original.includes(READ_STDERR_GUARD) && !original.includes(WRITE_STDERR_GUARD); + + if (alreadyPatched) { + return; + } + + if (!original.includes(READ_STDERR_GUARD) || !original.includes(WRITE_STDERR_GUARD)) { + throw new Error( + `patch-exiftool-stderr: ${relativePath} does not match @uswriting/exiftool@1.0.9; update or remove this script` + ); + } + + const patched = original + .replaceAll(READ_STDERR_GUARD, '') + .replaceAll(WRITE_STDERR_GUARD, ''); + + fs.writeFileSync(filePath, patched); +}; + +for (const relativePath of targets) { + patchFile(relativePath); +}