fix(coding-agent): load photon wasm in compiled binaries

This commit is contained in:
Mario Zechner 2026-01-18 17:11:31 +01:00
parent 61833b3dc9
commit 964f17b8d1
3 changed files with 99 additions and 4 deletions

View file

@ -33,7 +33,7 @@
"build": "tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js && npm run copy-assets",
"build:binary": "npm run build && bun build --compile ./dist/cli.js --outfile dist/pi && npm run copy-binary-assets",
"copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/",
"copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/",
"copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/",
"test": "vitest --run",
"prepublishOnly": "npm run clean && npm run build"
},

View file

@ -8,17 +8,107 @@
* The challenge: photon-node's CJS entry uses fs.readFileSync(__dirname + '/photon_rs_bg.wasm')
* which bakes the build machine's absolute path into Bun compiled binaries.
*
* Solution: Lazy-load photon via dynamic import and gracefully handle failures.
* Image processing functions have fallbacks that return original images when photon isn't available.
* Solution:
* 1. Patch fs.readFileSync to redirect missing photon_rs_bg.wasm reads
* 2. Copy photon_rs_bg.wasm next to the executable in build:binary
*/
import type { PathOrFileDescriptor } from "fs";
import { createRequire } from "module";
import * as path from "path";
import { fileURLToPath } from "url";
const require = createRequire(import.meta.url);
const fs = require("fs") as typeof import("fs");
// Re-export types from the main package
export type { PhotonImage as PhotonImageType } from "@silvia-odwyer/photon-node";
type ReadFileSync = typeof fs.readFileSync;
const WASM_FILENAME = "photon_rs_bg.wasm";
// Lazy-loaded photon module
let photonModule: typeof import("@silvia-odwyer/photon-node") | null = null;
let loadPromise: Promise<typeof import("@silvia-odwyer/photon-node") | null> | null = null;
function pathOrNull(file: PathOrFileDescriptor): string | null {
if (typeof file === "string") {
return file;
}
if (file instanceof URL) {
return fileURLToPath(file);
}
return null;
}
function getFallbackWasmPaths(): string[] {
const execDir = path.dirname(process.execPath);
return [
path.join(execDir, WASM_FILENAME),
path.join(execDir, "photon", WASM_FILENAME),
path.join(process.cwd(), WASM_FILENAME),
];
}
function patchPhotonWasmRead(): () => void {
const originalReadFileSync: ReadFileSync = fs.readFileSync.bind(fs);
const fallbackPaths = getFallbackWasmPaths();
const mutableFs = fs as { readFileSync: ReadFileSync };
const patchedReadFileSync: ReadFileSync = ((...args: Parameters<ReadFileSync>) => {
const [file, options] = args;
const resolvedPath = pathOrNull(file);
if (resolvedPath?.endsWith(WASM_FILENAME)) {
try {
return originalReadFileSync(...args);
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err?.code && err.code !== "ENOENT") {
throw error;
}
for (const fallbackPath of fallbackPaths) {
if (!fs.existsSync(fallbackPath)) {
continue;
}
if (options === undefined) {
return originalReadFileSync(fallbackPath);
}
return originalReadFileSync(fallbackPath, options);
}
throw error;
}
}
return originalReadFileSync(...args);
}) as ReadFileSync;
try {
mutableFs.readFileSync = patchedReadFileSync;
} catch {
Object.defineProperty(fs, "readFileSync", {
value: patchedReadFileSync,
writable: true,
configurable: true,
});
}
return () => {
try {
mutableFs.readFileSync = originalReadFileSync;
} catch {
Object.defineProperty(fs, "readFileSync", {
value: originalReadFileSync,
writable: true,
configurable: true,
});
}
};
}
/**
* Load the photon module asynchronously.
* Returns cached module on subsequent calls.
@ -33,12 +123,16 @@ export async function loadPhoton(): Promise<typeof import("@silvia-odwyer/photon
}
loadPromise = (async () => {
const restoreReadFileSync = patchPhotonWasmRead();
try {
photonModule = await import("@silvia-odwyer/photon-node");
return photonModule;
} catch {
photonModule = null;
return photonModule;
} finally {
restoreReadFileSync();
}
return photonModule;
})();
return loadPromise;

View file

@ -116,6 +116,7 @@ for platform in "${PLATFORMS[@]}"; do
cp package.json binaries/$platform/
cp README.md binaries/$platform/
cp CHANGELOG.md binaries/$platform/
cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm binaries/$platform/
mkdir -p binaries/$platform/theme
cp dist/modes/interactive/theme/*.json binaries/$platform/theme/
cp -r examples binaries/$platform/